mertz-rich-text
Version:
A flexible rich text editor and viewer for React applications
304 lines (297 loc) • 11.2 kB
JavaScript
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