UNPKG

tldraw

Version:

A tiny little drawing editor.

241 lines (240 loc) • 8.6 kB
"use strict"; var __create = Object.create; var __defProp = Object.defineProperty; var __getOwnPropDesc = Object.getOwnPropertyDescriptor; var __getOwnPropNames = Object.getOwnPropertyNames; var __getProtoOf = Object.getPrototypeOf; var __hasOwnProp = Object.prototype.hasOwnProperty; var __export = (target, all) => { for (var name in all) __defProp(target, name, { get: all[name], enumerable: true }); }; var __copyProps = (to, from, except, desc) => { if (from && typeof from === "object" || typeof from === "function") { for (let key of __getOwnPropNames(from)) if (!__hasOwnProp.call(to, key) && key !== except) __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable }); } return to; }; var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps( // If the importer is in node compatibility mode or this is not an ESM // file that has been converted to a CommonJS file using a Babel- // compatible transform (i.e. "__esModule" has not been set), then set // "default" to the CommonJS "module.exports" for node compatibility. isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target, mod )); var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod); var RichTextArea_exports = {}; __export(RichTextArea_exports, { RichTextArea: () => RichTextArea }); module.exports = __toCommonJS(RichTextArea_exports); var import_jsx_runtime = require("react/jsx-runtime"); var import_react = require("@tiptap/react"); var import_editor = require("@tldraw/editor"); var import_react2 = __toESM(require("react"), 1); const RichTextArea = import_react2.default.forwardRef(function RichTextArea2({ shapeId, isEditing, richText, handleFocus, handleChange, handleBlur, handleKeyDown, handleDoubleClick, hasCustomTabBehavior, handlePaste }, ref) { const editor = (0, import_editor.useEditor)(); const tipTapId = (0, import_editor.useUniqueSafeId)("tip-tap-editor"); const tipTapConfig = editor.getTextOptions().tipTapConfig; const rInitialRichText = (0, import_react2.useRef)(richText); const rTextEditor = (0, import_react2.useRef)(null); const rTextEditorEl = (0, import_react2.useRef)(null); (0, import_react2.useLayoutEffect)(() => { if (!rTextEditor.current) { rInitialRichText.current = richText; } else if (!(0, import_editor.isEqual)(rInitialRichText.current, richText)) { rTextEditor.current.commands.setContent(richText); } }, [richText]); const rCreateInfo = (0, import_react2.useRef)({ selectAll: false, caretPosition: null }); (0, import_react2.useLayoutEffect)(() => { function selectAllIfEditing(event) { if (event.shapeId === editor.getEditingShapeId()) { rCreateInfo.current.selectAll = true; } } function placeCaret(event) { if (event.shapeId === editor.getEditingShapeId()) { rCreateInfo.current.caretPosition = event.point; } } editor.on("select-all-text", selectAllIfEditing); editor.on("place-caret", placeCaret); return () => { editor.off("select-all-text", selectAllIfEditing); editor.off("place-caret", placeCaret); }; }, [editor, isEditing]); const onChange = (0, import_editor.useEvent)(handleChange); const onKeyDown = (0, import_editor.useEvent)(handleKeyDown); const onFocus = (0, import_editor.useEvent)(handleFocus); const onBlur = (0, import_editor.useEvent)(handleBlur); const onDoubleClick = (0, import_editor.useEvent)(handleDoubleClick); const onPaste = (0, import_editor.useEvent)(handlePaste); (0, import_react2.useLayoutEffect)(() => { if (!isEditing || !tipTapConfig || !rTextEditorEl.current) return; const { editorProps, ...restOfTipTapConfig } = tipTapConfig; const textEditorInstance = new import_react.Editor({ element: rTextEditorEl.current, autofocus: true, editable: isEditing, onUpdate: (props) => { const content = props.editor.state.doc.toJSON(); rInitialRichText.current = content; onChange({ richText: content }); }, onFocus, onBlur, // onCreate is called after a `setTimeout(0)` onCreate: (props) => { if (editor.getEditingShapeId() !== shapeId) return; const textEditor = props.editor; editor.setRichTextEditor(textEditor); const { selectAll, caretPosition } = rCreateInfo.current; if (selectAll) { textEditor.chain().focus().selectAll().run(); } else if (caretPosition) { const pos = textEditor.view.posAtCoords({ left: caretPosition.x, top: caretPosition.y })?.pos; if (pos) { textEditor.chain().focus().setTextSelection(pos).run(); } else { textEditor.chain().focus().selectAll().run(); } } }, editorProps: { handleKeyDown: (view, event) => { if (!hasCustomTabBehavior && event.key === "Tab") { handleTab(editor, view, event); } onKeyDown(event); }, handlePaste: (view, event) => { onPaste(event); if (event.defaultPrevented) return true; return false; }, handleDoubleClick: (_view, _pos, event) => onDoubleClick(event), ...editorProps }, coreExtensionOptions: { clipboardTextSerializer: { blockSeparator: "\n" } }, // N.B. We disable the text direction in the core list here, // but we add it back in again in our own extensions list so that // people can omit/override it if they want to. enableCoreExtensions: { textDirection: false }, textDirection: "auto", ...restOfTipTapConfig, content: rInitialRichText.current }); const timeout = editor.timers.setTimeout(() => { if (rCreateInfo.current.caretPosition || rCreateInfo.current.selectAll) { textEditorInstance.commands.focus(); } else { textEditorInstance.commands.focus("end"); } rCreateInfo.current.selectAll = false; rCreateInfo.current.caretPosition = null; }, 100); rTextEditor.current = textEditorInstance; return () => { rTextEditor.current = null; clearTimeout(timeout); textEditorInstance.destroy(); }; }, [ isEditing, tipTapConfig, onFocus, onBlur, onDoubleClick, onChange, onPaste, onKeyDown, editor, shapeId, hasCustomTabBehavior ]); if (!isEditing || !tipTapConfig) { return null; } return /* @__PURE__ */ (0, import_jsx_runtime.jsx)( "div", { id: tipTapId, ref, tabIndex: -1, "data-testid": "rich-text-area", className: "tl-rich-text tl-text tl-text-input", onContextMenu: isEditing ? (e) => e.stopPropagation() : void 0, onPointerDownCapture: (e) => e.stopPropagation(), onTouchEnd: (e) => e.stopPropagation(), onDragStart: import_editor.preventDefault, children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { className: "tl-rich-text", ref: rTextEditorEl }) } ); }); function handleTab(editor, view, event) { event.preventDefault(); const textEditor = editor.getRichTextEditor(); if (textEditor?.isActive("bulletList") || textEditor?.isActive("orderedList")) return; const { state, dispatch } = view; const { $from, $to } = state.selection; const isShift = event.shiftKey; let tr = state.tr; let pos = $to.end(); while (pos >= $from.start()) { const line = state.doc.resolve(pos).blockRange(); if (!line) break; const lineStart = line.start; const lineEnd = line.end; const lineText = state.doc.textBetween(lineStart, lineEnd, "\n"); let isInList = false; state.doc.nodesBetween(lineStart, lineEnd, (node) => { if (node.type.name === "bulletList" || node.type.name === "orderedList") { isInList = true; return false; } return true; }); if (!isInList) { if (!isShift) { tr = tr.insertText(" ", lineStart + 1); } else { if (lineText.startsWith(" ")) { tr = tr.delete(lineStart + 1, lineStart + 2); } } } pos = lineStart - 1; } const mappedSelection = state.selection.map(tr.doc, tr.mapping); tr.setSelection(mappedSelection); if (tr.docChanged) { dispatch(tr); } } //# sourceMappingURL=RichTextArea.js.map