UNPKG

mertz-rich-text

Version:

A flexible rich text editor and viewer for React applications

304 lines (297 loc) 11.2 kB
import React, { useRef, useState, useEffect, useCallback } from 'react'; import PropTypes from 'prop-types'; function _arrayLikeToArray(r, a) { (null == a || a > r.length) && (a = r.length); for (var e = 0, n = Array(a); e < a; e++) n[e] = r[e]; return n; } function _arrayWithHoles(r) { if (Array.isArray(r)) return r; } function _iterableToArrayLimit(r, l) { var t = null == r ? null : "undefined" != typeof Symbol && r[Symbol.iterator] || r["@@iterator"]; if (null != t) { var e, n, i, u, a = [], f = !0, o = !1; try { if (i = (t = t.call(r)).next, 0 === l) { if (Object(t) !== t) return; f = !1; } else for (; !(f = (e = i.call(t)).done) && (a.push(e.value), a.length !== l); f = !0); } catch (r) { o = !0, n = r; } finally { try { if (!f && null != t.return && (u = t.return(), Object(u) !== u)) return; } finally { if (o) throw n; } } return a; } } function _nonIterableRest() { throw new TypeError("Invalid attempt to destructure non-iterable instance.\nIn order to be iterable, non-array objects must have a [Symbol.iterator]() method."); } function _slicedToArray(r, e) { return _arrayWithHoles(r) || _iterableToArrayLimit(r, e) || _unsupportedIterableToArray(r, e) || _nonIterableRest(); } function _unsupportedIterableToArray(r, a) { if (r) { if ("string" == typeof r) return _arrayLikeToArray(r, a); var t = {}.toString.call(r).slice(8, -1); return "Object" === t && r.constructor && (t = r.constructor.name), "Map" === t || "Set" === t ? Array.from(r) : "Arguments" === t || /^(?:Ui|I)nt(?:8|16|32)(?:Clamped)?Array$/.test(t) ? _arrayLikeToArray(r, a) : void 0; } } var RichTextEditor = function RichTextEditor(_ref) { var _ref$initialContent = _ref.initialContent, initialContent = _ref$initialContent === void 0 ? "" : _ref$initialContent, _ref$readOnly = _ref.readOnly, readOnly = _ref$readOnly === void 0 ? false : _ref$readOnly, onChange = _ref.onChange, _ref$className = _ref.className, className = _ref$className === void 0 ? "" : _ref$className, _ref$showToolbar = _ref.showToolbar, showToolbar = _ref$showToolbar === void 0 ? true : _ref$showToolbar, _ref$showPreview = _ref.showPreview, showPreview = _ref$showPreview === void 0 ? true : _ref$showPreview; var editorRef = useRef(null); var _useState = useState(initialContent), _useState2 = _slicedToArray(_useState, 2), content = _useState2[0], setContent = _useState2[1]; var _useState3 = useState({ bold: false, italic: false, underline: false, h1: false, h2: false }), _useState4 = _slicedToArray(_useState3, 2), styleStates = _useState4[0], setStyleStates = _useState4[1]; var _useState5 = useState(false), _useState6 = _slicedToArray(_useState5, 2), isTouchDevice = _useState6[0], setIsTouchDevice = _useState6[1]; // Detect touch device useEffect(function () { setIsTouchDevice('ontouchstart' in window || navigator.maxTouchPoints > 0); }, []); var checkStyleStates = useCallback(function () { if (readOnly) return; var selection = window.getSelection(); if (!selection.rangeCount) return; var range = selection.getRangeAt(0); var parentElement = range.commonAncestorContainer.nodeType === 3 ? range.commonAncestorContainer.parentNode : range.commonAncestorContainer; var isH1 = parentElement.closest("h1"); var isH2 = parentElement.closest("h2"); var styles = { bold: document.queryCommandState("bold"), italic: document.queryCommandState("italic"), underline: document.queryCommandState("underline"), h1: !!isH1, h2: !!isH2 }; setStyleStates(styles); }, [readOnly]); var debounce = function debounce(func, wait) { var timeout; return function () { for (var _len = arguments.length, args = new Array(_len), _key = 0; _key < _len; _key++) { args[_key] = arguments[_key]; } clearTimeout(timeout); timeout = setTimeout(function () { return func.apply(void 0, args); }, wait); }; }; var debouncedCheckStyleStates = useCallback(debounce(checkStyleStates, 100), [checkStyleStates]); var executeCommand = useCallback(function (command) { if (readOnly) return; // Ensure editor has focus on mobile if (isTouchDevice) { var _editorRef$current; (_editorRef$current = editorRef.current) === null || _editorRef$current === void 0 || _editorRef$current.focus(); } if (command === "h1" || command === "h2") { document.execCommand("formatBlock", false, command); } else if (command === "createLink") { var url = prompt("Enter the link URL:"); if (url) { document.execCommand(command, false, url); } } else if (command === "insertImage") { var _url = prompt("Enter the image URL:"); if (_url) { var alt = prompt("Enter alt text for the image:"); var caption = prompt("Enter a caption for the image (optional):"); var figure = document.createElement("figure"); figure.className = "editor-figure"; var img = document.createElement("img"); img.src = _url; img.alt = alt || ""; img.className = "editor-image"; figure.appendChild(img); if (caption) { var figcaption = document.createElement("figcaption"); figcaption.textContent = caption; figcaption.className = "editor-figcaption"; figure.appendChild(figcaption); } document.execCommand("insertHTML", false, figure.outerHTML + "<br>"); var selection = window.getSelection(); var range = selection.getRangeAt(0); range.setStartAfter(editorRef.current.lastChild); range.collapse(true); selection.removeAllRanges(); selection.addRange(range); } } else { document.execCommand(command, false, null); } setTimeout(function () { debouncedCheckStyleStates(); updateContent(); }, 10); }, [readOnly, isTouchDevice, debouncedCheckStyleStates]); var handleUndo = function handleUndo() { if (readOnly) return; document.execCommand("undo", false, null); updateContent(); }; var handleRedo = function handleRedo() { if (readOnly) return; document.execCommand("redo", false, null); updateContent(); }; var updateContent = function updateContent() { if (editorRef.current) { var newContent = editorRef.current.innerHTML; setContent(newContent); onChange === null || onChange === void 0 || onChange(newContent); } }; // Handle touch events for better mobile experience var handleTouchStart = useCallback(function (e) { if (!readOnly && isTouchDevice) { e.target.focus(); } }, [readOnly, isTouchDevice]); useEffect(function () { var editor = editorRef.current; if (!readOnly) { editor.addEventListener("input", updateContent); editor.addEventListener("keyup", debouncedCheckStyleStates); editor.addEventListener("touchstart", handleTouchStart); document.addEventListener("selectionchange", debouncedCheckStyleStates); } if (initialContent && editor.innerHTML !== initialContent) { editor.innerHTML = initialContent; setContent(initialContent); } return function () { if (!readOnly) { editor.removeEventListener("input", updateContent); editor.removeEventListener("keyup", debouncedCheckStyleStates); editor.removeEventListener("touchstart", handleTouchStart); document.removeEventListener("selectionchange", debouncedCheckStyleStates); } }; }, [debouncedCheckStyleStates, initialContent, readOnly, onChange, handleTouchStart]); return /*#__PURE__*/React.createElement("div", { className: "rich-text-container ".concat(className) }, showToolbar && !readOnly && /*#__PURE__*/React.createElement("div", { className: "toolbar", role: "toolbar", "aria-label": "Text formatting options" }, /*#__PURE__*/React.createElement("button", { onClick: function onClick() { return executeCommand("bold"); }, className: "toolbar-button ".concat(styleStates.bold ? "active" : ""), "aria-label": "Bold", "aria-pressed": styleStates.bold }, "B"), /*#__PURE__*/React.createElement("button", { onClick: function onClick() { return executeCommand("italic"); }, className: "toolbar-button ".concat(styleStates.italic ? "active" : ""), "aria-label": "Italic", "aria-pressed": styleStates.italic }, "I"), /*#__PURE__*/React.createElement("button", { onClick: function onClick() { return executeCommand("underline"); }, className: "toolbar-button ".concat(styleStates.underline ? "active" : ""), "aria-label": "Underline", "aria-pressed": styleStates.underline }, "U"), /*#__PURE__*/React.createElement("button", { onClick: function onClick() { return executeCommand("h1"); }, className: "toolbar-button ".concat(styleStates.h1 ? "active" : ""), "aria-label": "Heading 1", "aria-pressed": styleStates.h1 }, "H1"), /*#__PURE__*/React.createElement("button", { onClick: function onClick() { return executeCommand("h2"); }, className: "toolbar-button ".concat(styleStates.h2 ? "active" : ""), "aria-label": "Heading 2", "aria-pressed": styleStates.h2 }, "H2"), /*#__PURE__*/React.createElement("button", { onClick: function onClick() { return executeCommand("createLink"); }, className: "toolbar-button", "aria-label": "Insert Link" }, "\uD83D\uDD17"), /*#__PURE__*/React.createElement("button", { onClick: function onClick() { return executeCommand("insertImage"); }, className: "toolbar-button", "aria-label": "Insert Image" }, "\uD83D\uDCF7"), /*#__PURE__*/React.createElement("button", { onClick: handleUndo, className: "toolbar-button", "aria-label": "Undo" }, "\u21A9"), /*#__PURE__*/React.createElement("button", { onClick: handleRedo, className: "toolbar-button", "aria-label": "Redo" }, "\u21AA")), /*#__PURE__*/React.createElement("div", { className: "editor-container ".concat(!showPreview ? 'full-width' : '') }, /*#__PURE__*/React.createElement("div", { ref: editorRef, contentEditable: !readOnly, className: "editor", "aria-label": readOnly ? "Rich text viewer" : "Rich text editor", role: "textbox", "aria-multiline": "true" }), showPreview && /*#__PURE__*/React.createElement("div", { className: "preview", role: "region", "aria-label": "Content preview" }, /*#__PURE__*/React.createElement("h3", null, "Live Preview"), /*#__PURE__*/React.createElement("div", { dangerouslySetInnerHTML: { __html: content } })))); }; RichTextEditor.propTypes = { initialContent: PropTypes.string, readOnly: PropTypes.bool, onChange: PropTypes.func, className: PropTypes.string, showToolbar: PropTypes.bool, showPreview: PropTypes.bool }; export { RichTextEditor, RichTextEditor as default }; //# sourceMappingURL=index.esm.js.map