tldraw
Version:
A tiny little drawing editor.
241 lines (240 loc) • 8.6 kB
JavaScript
;
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