@portabletext/editor
Version:
Portable Text Editor made in React
1,341 lines • 60.4 kB
JavaScript
import { c } from "react-compiler-runtime";
import { useEffect, useContext, useRef, useState, useMemo, startTransition, useCallback, forwardRef, useImperativeHandle } from "react";
import { useEffectEvent } from "use-effect-event";
import { useEditor, getNodeBlock, getFirstBlock, getLastBlock, slateRangeToSelection, toSlateRange, debugWithName, EditorActorContext, fromSlateValue, KEY_TO_VALUE_ELEMENT, usePortableTextEditor, PortableTextEditor, moveRangeByOperation, isEqualToEmptyEditor, getEditorSnapshot } from "./_chunks-es/editor-provider.js";
import { EditorProvider, defineSchema, defaultKeyGenerator, useEditorSelector, usePortableTextEditorSelection } from "./_chunks-es/editor-provider.js";
import { jsxs, jsx, Fragment } from "react/jsx-runtime";
import { useSelector, useActorRef } from "@xstate/react";
import noop from "lodash/noop.js";
import { Editor, Range, Element as Element$2, Text, Path, Transforms } from "slate";
import { useSlateStatic, useSelected, ReactEditor, useSlate, Editable } from "slate-react";
import { isSelectionCollapsed, getFocusTextBlock, getFocusSpan, getSelectedBlocks, isSelectionExpanded, getSelectionStartBlock, getSelectionEndBlock, isOverlappingSelection, getFocusBlock } from "./_chunks-es/selector.is-overlapping-selection.js";
import { getBlockEndPoint, getBlockStartPoint, isKeyedSegment } from "./_chunks-es/util.slice-blocks.js";
import { getFocusInlineObject } from "./_chunks-es/selector.get-focus-inline-object.js";
import { DOMEditor, isDOMNode } from "slate-dom";
import { isSelectionCollapsed as isSelectionCollapsed$1, getSelectionEndPoint } from "./_chunks-es/util.is-selection-collapsed.js";
import { parseBlocks } from "./_chunks-es/parse-blocks.js";
import isEqual from "lodash/isEqual.js";
import { isSelectingEntireBlocks } from "./_chunks-es/selector.is-selecting-entire-blocks.js";
import { defineBehavior, isHotkey } from "./_chunks-es/behavior.core.js";
import uniq from "lodash/uniq.js";
import { setup, fromCallback, assign, assertEvent, and } from "xstate";
function EditorEventListener(props) {
const $ = c(5), editor = useEditor(), on = useEffectEvent(props.on);
let t0;
$[0] !== editor || $[1] !== on ? (t0 = () => {
const subscription = editor.on("*", on);
return () => {
subscription.unsubscribe();
};
}, $[0] = editor, $[1] = on, $[2] = t0) : t0 = $[2];
let t1;
return $[3] !== editor ? (t1 = [editor], $[3] = editor, $[4] = t1) : t1 = $[4], useEffect(t0, t1), null;
}
function getCompoundClientRect(nodes) {
if (nodes.length === 0)
return new DOMRect(0, 0, 0, 0);
const elements = nodes.filter((node) => node instanceof Element), firstRect = elements.at(0)?.getBoundingClientRect();
if (!firstRect)
return new DOMRect(0, 0, 0, 0);
let left = firstRect.left, top = firstRect.top, right = firstRect.right, bottom = firstRect.bottom;
for (let i = 1; i < elements.length; i++) {
const rect = elements[i].getBoundingClientRect();
left = Math.min(left, rect.left), top = Math.min(top, rect.top), right = Math.max(right, rect.right), bottom = Math.max(bottom, rect.bottom);
}
return new DOMRect(left, top, right - left, bottom - top);
}
function getDragSelection({
eventSelection,
snapshot
}) {
let dragSelection = eventSelection;
if (getFocusInlineObject({
context: {
...snapshot.context,
selection: eventSelection
}
}))
return dragSelection;
const draggingCollapsedSelection = isSelectionCollapsed({
context: {
...snapshot.context,
selection: eventSelection
}
}), draggedTextBlock = getFocusTextBlock({
context: {
...snapshot.context,
selection: eventSelection
}
}), draggedSpan = getFocusSpan({
context: {
...snapshot.context,
selection: eventSelection
}
});
draggingCollapsedSelection && draggedTextBlock && draggedSpan && (dragSelection = {
anchor: getBlockStartPoint(draggedTextBlock),
focus: getBlockEndPoint(draggedTextBlock)
});
const selectedBlocks = getSelectedBlocks(snapshot);
if (snapshot.context.selection && isSelectionExpanded(snapshot) && selectedBlocks.length > 1) {
const selectionStartBlock = getSelectionStartBlock(snapshot), selectionEndBlock = getSelectionEndBlock(snapshot);
if (!selectionStartBlock || !selectionEndBlock)
return dragSelection;
const selectionStartPoint = getBlockStartPoint(selectionStartBlock), selectionEndPoint = getBlockEndPoint(selectionEndBlock);
isOverlappingSelection(eventSelection)({
...snapshot,
context: {
...snapshot.context,
selection: {
anchor: selectionStartPoint,
focus: selectionEndPoint
}
}
}) && (dragSelection = {
anchor: selectionStartPoint,
focus: selectionEndPoint
});
}
return dragSelection;
}
function getEventPosition({
editorActor,
slateEditor,
event
}) {
if (editorActor.getSnapshot().matches({
setup: "setting up"
}))
return;
const node = getEventNode({
slateEditor,
event
});
if (!node)
return;
const block = getNodeBlock({
editor: slateEditor,
schema: editorActor.getSnapshot().context.schema,
node
}), positionBlock = getEventPositionBlock({
node,
slateEditor,
event
}), selection = getEventSelection({
schema: editorActor.getSnapshot().context.schema,
slateEditor,
event
});
if (block && positionBlock && !selection && !Editor.isEditor(node))
return {
block: positionBlock,
isEditor: !1,
selection: {
anchor: getBlockStartPoint({
node: block,
path: [{
_key: block._key
}]
}),
focus: getBlockEndPoint({
node: block,
path: [{
_key: block._key
}]
})
}
};
if (!positionBlock || !selection)
return;
const focusBlockPath = selection.focus.path.at(0), focusBlockKey = isKeyedSegment(focusBlockPath) ? focusBlockPath._key : void 0;
if (focusBlockKey)
return isSelectionCollapsed$1(selection) && block && focusBlockKey !== block._key ? {
block: positionBlock,
isEditor: !1,
selection: {
anchor: getBlockStartPoint({
node: block,
path: [{
_key: block._key
}]
}),
focus: getBlockEndPoint({
node: block,
path: [{
_key: block._key
}]
})
}
} : {
block: positionBlock,
isEditor: Editor.isEditor(node),
selection
};
}
function getEventNode({
slateEditor,
event
}) {
return DOMEditor.hasTarget(slateEditor, event.target) ? DOMEditor.toSlateNode(slateEditor, event.target) : void 0;
}
function getEventPositionBlock({
node,
slateEditor,
event
}) {
const [firstBlock] = getFirstBlock({
editor: slateEditor
});
if (!firstBlock)
return;
const firstBlockRect = DOMEditor.toDOMNode(slateEditor, firstBlock).getBoundingClientRect();
if (event.pageY < firstBlockRect.top)
return "start";
const [lastBlock] = getLastBlock({
editor: slateEditor
});
if (!lastBlock)
return;
const lastBlockRef = DOMEditor.toDOMNode(slateEditor, lastBlock).getBoundingClientRect();
if (event.pageY > lastBlockRef.bottom)
return "end";
const elementRect = DOMEditor.toDOMNode(slateEditor, node).getBoundingClientRect(), top = elementRect.top, height = elementRect.height;
return Math.abs(top - event.pageY) < height / 2 ? "start" : "end";
}
function getEventSelection({
schema,
slateEditor,
event
}) {
const range = getSlateRangeFromEvent(slateEditor, event);
return range ? slateRangeToSelection({
schema,
editor: slateEditor,
range
}) : null;
}
function getSlateRangeFromEvent(editor, event) {
if (!event.target || !isDOMNode(event.target))
return;
const window2 = DOMEditor.getWindow(editor);
let domRange;
if (window2.document.caretPositionFromPoint !== void 0) {
const position = window2.document.caretPositionFromPoint(event.clientX, event.clientY);
if (position)
try {
domRange = window2.document.createRange(), domRange.setStart(position.offsetNode, position.offset), domRange.setEnd(position.offsetNode, position.offset);
} catch {
}
} else if (window2.document.caretRangeFromPoint !== void 0)
domRange = window2.document.caretRangeFromPoint(event.clientX, event.clientY) ?? void 0;
else {
console.warn("Neither caretPositionFromPoint nor caretRangeFromPoint is supported");
return;
}
if (!domRange)
return;
let range;
try {
range = DOMEditor.toSlateRange(editor, domRange, {
exactMatch: !1,
// It can still throw even with this option set to true
suppressThrow: !1
});
} catch {
}
return range;
}
function normalizePoint(point, value) {
if (!point || !value)
return null;
const newPath = [];
let newOffset = point.offset || 0;
const blockKey = typeof point.path[0] == "object" && "_key" in point.path[0] && point.path[0]._key, childKey = typeof point.path[2] == "object" && "_key" in point.path[2] && point.path[2]._key, block = value.find((blk) => blk._key === blockKey);
if (block)
newPath.push({
_key: block._key
});
else
return null;
if (block && point.path[1] === "children") {
if (!block.children || Array.isArray(block.children) && block.children.length === 0)
return null;
const child = Array.isArray(block.children) && block.children.find((cld) => cld._key === childKey);
if (child)
newPath.push("children"), newPath.push({
_key: child._key
}), newOffset = child.text && child.text.length >= point.offset ? point.offset : child.text && child.text.length || 0;
else
return null;
}
return {
path: newPath,
offset: newOffset
};
}
function normalizeSelection(selection, value) {
if (!selection || !value || value.length === 0)
return null;
let newAnchor = null, newFocus = null;
const {
anchor,
focus
} = selection;
return anchor && value.find((blk) => isEqual({
_key: blk._key
}, anchor.path[0])) && (newAnchor = normalizePoint(anchor, value)), focus && value.find((blk) => isEqual({
_key: blk._key
}, focus.path[0])) && (newFocus = normalizePoint(focus, value)), newAnchor && newFocus ? {
anchor: newAnchor,
focus: newFocus,
backward: selection.backward
} : null;
}
function getSelectionDomNodes({
slateEditor,
snapshot
}) {
if (!snapshot.context.selection)
return {
blockNodes: [],
childNodes: []
};
const range = toSlateRange(snapshot.context.selection, slateEditor);
if (!range)
return {
blockNodes: [],
childNodes: []
};
const blockEntries = Array.from(Editor.nodes(slateEditor, {
at: range,
mode: "highest",
match: (n) => !Editor.isEditor(n)
})), childEntries = Array.from(Editor.nodes(slateEditor, {
at: range,
mode: "lowest",
match: (n) => !Editor.isEditor(n) && slateEditor.isTextSpan(n) || !slateEditor.isBlock(n)
}));
return {
blockNodes: blockEntries.map(([blockNode]) => DOMEditor.toDOMNode(slateEditor, blockNode)),
childNodes: childEntries.map(([childNode]) => DOMEditor.toDOMNode(slateEditor, childNode))
};
}
function DefaultBlockObject(props) {
const $ = c(4);
let t0;
$[0] === Symbol.for("react.memo_cache_sentinel") ? (t0 = {
userSelect: "none"
}, $[0] = t0) : t0 = $[0];
let t1;
return $[1] !== props.value._key || $[2] !== props.value._type ? (t1 = /* @__PURE__ */ jsxs("div", { style: t0, children: [
"[",
props.value._type,
": ",
props.value._key,
"]"
] }), $[1] = props.value._key, $[2] = props.value._type, $[3] = t1) : t1 = $[3], t1;
}
function DefaultInlineObject(props) {
const $ = c(4);
let t0;
$[0] === Symbol.for("react.memo_cache_sentinel") ? (t0 = {
userSelect: "none"
}, $[0] = t0) : t0 = $[0];
let t1;
return $[1] !== props.value._key || $[2] !== props.value._type ? (t1 = /* @__PURE__ */ jsxs("span", { style: t0, children: [
"[",
props.value._type,
": ",
props.value._key,
"]"
] }), $[1] = props.value._key, $[2] = props.value._type, $[3] = t1) : t1 = $[3], t1;
}
function DropIndicator() {
const $ = c(1);
let t0;
return $[0] === Symbol.for("react.memo_cache_sentinel") ? (t0 = /* @__PURE__ */ jsx("div", { contentEditable: !1, className: "pt-drop-indicator", style: {
position: "absolute",
width: "100%",
height: 1,
borderBottom: "1px solid currentColor",
zIndex: 5
}, children: /* @__PURE__ */ jsx("span", {}) }), $[0] = t0) : t0 = $[0], t0;
}
debugWithName("components:Element");
const EMPTY_ANNOTATIONS = [], inlineBlockStyle = {
display: "inline-block"
}, Element$1 = ({
attributes,
children,
element,
schemaTypes,
readOnly,
renderBlock,
renderChild,
renderListItem,
renderStyle,
spellCheck
}) => {
const editorActor = useContext(EditorActorContext), slateEditor = useSlateStatic(), selected = useSelected(), blockRef = useRef(null), inlineBlockObjectRef = useRef(null), focused = selected && slateEditor.selection && Range.isCollapsed(slateEditor.selection) || !1, [dragPositionBlock, setDragPositionBlock] = useState();
useEffect(() => {
const behavior = defineBehavior({
on: "drag.dragover",
guard: ({
snapshot,
event
}) => {
const dropFocusBlock = getFocusBlock({
context: {
...snapshot.context,
selection: event.position.selection
}
});
if (!dropFocusBlock || dropFocusBlock.node._key !== element._key)
return !1;
const dragOrigin = snapshot.beta.internalDrag?.origin;
return !dragOrigin || getSelectedBlocks({
context: {
...snapshot.context,
selection: dragOrigin.selection
}
}).some((draggedBlock) => draggedBlock.node._key === element._key) ? !1 : isSelectingEntireBlocks({
context: {
...snapshot.context,
selection: dragOrigin.selection
}
});
},
actions: [({
event: event_0
}) => [{
type: "effect",
effect: () => {
setDragPositionBlock(event_0.position.block);
}
}, {
type: "noop"
}]]
});
return editorActor.send({
type: "add behavior",
behavior
}), () => {
editorActor.send({
type: "remove behavior",
behavior
});
};
}, [editorActor, element._key]), useEffect(() => {
const behavior_0 = defineBehavior({
on: "drag.*",
guard: ({
event: event_1
}) => event_1.type !== "drag.dragover",
actions: [() => [{
type: "effect",
effect: () => {
setDragPositionBlock(void 0);
}
}]]
});
return editorActor.send({
type: "add behavior",
behavior: behavior_0
}), () => {
editorActor.send({
type: "remove behavior",
behavior: behavior_0
});
};
}, [editorActor]);
const value = useMemo(() => fromSlateValue([element], schemaTypes.block.name, KEY_TO_VALUE_ELEMENT.get(slateEditor))[0], [slateEditor, element, schemaTypes.block.name]);
let renderedBlock = children, className;
const blockPath = useMemo(() => [{
_key: element._key
}], [element]);
if (typeof element._type != "string")
throw new Error("Expected element to have a _type property");
if (typeof element._key != "string")
throw new Error("Expected element to have a _key property");
if (slateEditor.isInline(element)) {
const path = ReactEditor.findPath(slateEditor, element), [block] = Editor.node(slateEditor, path, {
depth: 1
}), schemaType = schemaTypes.inlineObjects.find((_type) => _type.name === element._type);
if (!schemaType)
throw new Error("Could not find type for inline block element");
if (Element$2.isElement(block)) {
const elmPath = [{
_key: block._key
}, "children", {
_key: element._key
}];
return /* @__PURE__ */ jsxs("span", { ...attributes, children: [
children,
/* @__PURE__ */ jsxs("span", { draggable: !readOnly, className: "pt-inline-object", "data-testid": "pt-inline-object", ref: inlineBlockObjectRef, style: inlineBlockStyle, contentEditable: !1, children: [
renderChild && renderChild({
annotations: EMPTY_ANNOTATIONS,
// These inline objects currently doesn't support annotations. This is a limitation of the current PT spec/model.
children: /* @__PURE__ */ jsx(DefaultInlineObject, { value }),
editorElementRef: inlineBlockObjectRef,
focused,
path: elmPath,
schemaType,
selected,
type: schemaType,
value
}),
!renderChild && /* @__PURE__ */ jsx(DefaultInlineObject, { value })
] }, element._key)
] });
}
throw new Error("Block not found!");
}
if (element._type === schemaTypes.block.name) {
className = "pt-block pt-text-block";
const isListItem = "listItem" in element, style = "style" in element && element.style || "normal";
className = `pt-block pt-text-block pt-text-block-style-${style}`;
const blockStyleType = schemaTypes.styles.find((item) => item.value === style);
renderStyle && blockStyleType && (renderedBlock = renderStyle({
block: element,
children,
focused,
selected,
value: style,
path: blockPath,
schemaType: blockStyleType,
editorElementRef: blockRef
}));
let level;
if (isListItem && (typeof element.level == "number" && (level = element.level), className += ` pt-list-item pt-list-item-${element.listItem} pt-list-item-level-${level || 1}`), slateEditor.isListBlock(value) && isListItem && element.listItem) {
const listType = schemaTypes.lists.find((item_0) => item_0.value === element.listItem);
renderListItem && listType && (renderedBlock = renderListItem({
block: value,
children: renderedBlock,
focused,
selected,
value: element.listItem,
path: blockPath,
schemaType: listType,
level: value.level || 1,
editorElementRef: blockRef
}));
}
const renderProps = Object.defineProperty({
children: renderedBlock,
editorElementRef: blockRef,
focused,
level,
listItem: isListItem ? element.listItem : void 0,
path: blockPath,
selected,
style,
schemaType: schemaTypes.block,
value
}, "type", {
enumerable: !1,
get() {
return console.warn("Property 'type' is deprecated, use 'schemaType' instead."), schemaTypes.block;
}
}), propsOrDefaultRendered = renderBlock ? renderBlock(renderProps) : children;
return /* @__PURE__ */ jsxs("div", { ...attributes, className, spellCheck, children: [
dragPositionBlock === "start" ? /* @__PURE__ */ jsx(DropIndicator, {}) : null,
/* @__PURE__ */ jsx("div", { ref: blockRef, children: propsOrDefaultRendered }),
dragPositionBlock === "end" ? /* @__PURE__ */ jsx(DropIndicator, {}) : null
] }, element._key);
}
const schemaType_0 = schemaTypes.blockObjects.find((_type_0) => _type_0.name === element._type);
if (!schemaType_0)
throw new Error(`Could not find schema type for block element of _type ${element._type}`);
className = "pt-block pt-object-block";
const block_0 = fromSlateValue([element], schemaTypes.block.name, KEY_TO_VALUE_ELEMENT.get(slateEditor))[0];
let renderedBlockFromProps;
if (renderBlock) {
const _props = Object.defineProperty({
children: /* @__PURE__ */ jsx(DefaultBlockObject, { value }),
editorElementRef: blockRef,
focused,
path: blockPath,
schemaType: schemaType_0,
selected,
value: block_0
}, "type", {
enumerable: !1,
get() {
return console.warn("Property 'type' is deprecated, use 'schemaType' instead."), schemaType_0;
}
});
renderedBlockFromProps = renderBlock(_props);
}
return /* @__PURE__ */ jsxs("div", { ...attributes, className, children: [
dragPositionBlock === "start" ? /* @__PURE__ */ jsx(DropIndicator, {}) : null,
children,
/* @__PURE__ */ jsx("div", { ref: blockRef, contentEditable: !1, draggable: !readOnly, children: renderedBlockFromProps || /* @__PURE__ */ jsx(DefaultBlockObject, { value }) }),
dragPositionBlock === "end" ? /* @__PURE__ */ jsx(DropIndicator, {}) : null
] }, element._key);
};
Element$1.displayName = "Element";
const debug$2 = debugWithName("components:Leaf"), EMPTY_MARKS = [], Leaf = (props) => {
const {
editorActor,
attributes,
children,
leaf,
schemaTypes,
renderChild,
renderDecorator,
renderAnnotation
} = props, spanRef = useRef(null), portableTextEditor = usePortableTextEditor(), blockSelected = useSelected(), [focused, setFocused] = useState(!1), [selected, setSelected] = useState(!1), block = children.props.parent, path = useMemo(() => block ? [{
_key: block?._key
}, "children", {
_key: leaf._key
}] : [], [block, leaf._key]), decoratorValues = useMemo(() => schemaTypes.decorators.map((dec) => dec.value), [schemaTypes.decorators]), marks = useMemo(() => uniq((leaf.marks || EMPTY_MARKS).filter((mark) => decoratorValues.includes(mark))), [decoratorValues, leaf.marks]), annotationMarks = Array.isArray(leaf.marks) ? leaf.marks : EMPTY_MARKS, annotations = useMemo(() => annotationMarks.map((mark_0) => !decoratorValues.includes(mark_0) && block?.markDefs?.find((def) => def._key === mark_0)).filter(Boolean), [annotationMarks, block, decoratorValues]), shouldTrackSelectionAndFocus = annotations.length > 0 && blockSelected;
useEffect(() => {
if (!shouldTrackSelectionAndFocus) {
setFocused(!1);
return;
}
const sel = PortableTextEditor.getSelection(portableTextEditor);
sel && isEqual(sel.focus.path, path) && PortableTextEditor.isCollapsedSelection(portableTextEditor) && startTransition(() => {
setFocused(!0);
});
}, [shouldTrackSelectionAndFocus, path, portableTextEditor]);
const setSelectedFromRange = useCallback(() => {
if (!shouldTrackSelectionAndFocus)
return;
debug$2("Setting selection and focus from range");
const winSelection = window.getSelection();
if (!winSelection) {
setSelected(!1);
return;
}
if (winSelection && winSelection.rangeCount > 0) {
const range = winSelection.getRangeAt(0);
spanRef.current && range.intersectsNode(spanRef.current) ? setSelected(!0) : setSelected(!1);
} else
setSelected(!1);
}, [shouldTrackSelectionAndFocus]);
useEffect(() => {
if (!shouldTrackSelectionAndFocus)
return;
const onBlur = editorActor.on("blurred", () => {
setFocused(!1), setSelected(!1);
}), onFocus = editorActor.on("focused", () => {
const sel_0 = PortableTextEditor.getSelection(portableTextEditor);
sel_0 && isEqual(sel_0.focus.path, path) && PortableTextEditor.isCollapsedSelection(portableTextEditor) && setFocused(!0), setSelectedFromRange();
}), onSelection = editorActor.on("selection", (event) => {
event.selection && isEqual(event.selection.focus.path, path) && PortableTextEditor.isCollapsedSelection(portableTextEditor) ? setFocused(!0) : setFocused(!1), setSelectedFromRange();
});
return () => {
onBlur.unsubscribe(), onFocus.unsubscribe(), onSelection.unsubscribe();
};
}, [editorActor, path, portableTextEditor, setSelectedFromRange, shouldTrackSelectionAndFocus]), useEffect(() => setSelectedFromRange(), [setSelectedFromRange]);
const content = useMemo(() => {
let returnedChildren = children;
if (Text.isText(leaf) && leaf._type === schemaTypes.span.name && (marks.forEach((mark_1) => {
const schemaType = schemaTypes.decorators.find((dec_0) => dec_0.value === mark_1);
if (schemaType && renderDecorator) {
const _props = Object.defineProperty({
children: returnedChildren,
editorElementRef: spanRef,
focused,
path,
selected,
schemaType,
value: mark_1
}, "type", {
enumerable: !1,
get() {
return console.warn("Property 'type' is deprecated, use 'schemaType' instead."), schemaType;
}
});
returnedChildren = renderDecorator(_props);
}
}), block && annotations.length > 0 && annotations.forEach((annotation) => {
const schemaType_0 = schemaTypes.annotations.find((t) => t.name === annotation._type);
if (schemaType_0)
if (renderAnnotation) {
const _props_0 = Object.defineProperty({
block,
children: returnedChildren,
editorElementRef: spanRef,
focused,
path,
selected,
schemaType: schemaType_0,
value: annotation
}, "type", {
enumerable: !1,
get() {
return console.warn("Property 'type' is deprecated, use 'schemaType' instead."), schemaType_0;
}
});
returnedChildren = /* @__PURE__ */ jsx("span", { ref: spanRef, children: renderAnnotation(_props_0) });
} else
returnedChildren = /* @__PURE__ */ jsx("span", { ref: spanRef, children: returnedChildren });
}), block && renderChild)) {
const child = block.children.find((_child) => _child._key === leaf._key);
if (child) {
const _props_1 = Object.defineProperty({
annotations,
children: /* @__PURE__ */ jsx(Fragment, { children: returnedChildren }),
editorElementRef: spanRef,
focused,
path,
schemaType: schemaTypes.span,
selected,
value: child
}, "type", {
enumerable: !1,
get() {
return console.warn("Property 'type' is deprecated, use 'schemaType' instead."), schemaTypes.span;
}
});
returnedChildren = renderChild(_props_1);
}
}
return returnedChildren;
}, [annotations, block, children, focused, leaf, marks, path, renderAnnotation, renderChild, renderDecorator, schemaTypes.annotations, schemaTypes.decorators, schemaTypes.span, selected]);
return useMemo(() => /* @__PURE__ */ jsx("span", { ...attributes, ref: spanRef, children: content }, leaf._key), [leaf, attributes, content]);
};
Leaf.displayName = "Leaf";
const debug$1 = debugWithName("plugin:withHotKeys");
function createWithHotkeys(editorActor, portableTextEditor, hotkeysFromOptions) {
const reservedHotkeys = ["enter", "tab", "shift", "delete", "end"], activeHotkeys = hotkeysFromOptions ?? {};
return function(editor) {
return editor.pteWithHotKeys = (event) => {
Object.keys(activeHotkeys).forEach((cat) => {
if (cat === "marks")
for (const hotkey in activeHotkeys[cat]) {
if (reservedHotkeys.includes(hotkey))
throw new Error(`The hotkey ${hotkey} is reserved!`);
if (isHotkey(hotkey, event.nativeEvent)) {
event.preventDefault();
const possibleMark = activeHotkeys[cat];
if (possibleMark) {
const mark = possibleMark[hotkey];
debug$1(`HotKey ${hotkey} to toggle ${mark}`), editorActor.send({
type: "behavior event",
behaviorEvent: {
type: "decorator.toggle",
decorator: mark
},
editor
});
}
}
}
if (cat === "custom")
for (const hotkey in activeHotkeys[cat]) {
if (reservedHotkeys.includes(hotkey))
throw new Error(`The hotkey ${hotkey} is reserved!`);
if (isHotkey(hotkey, event.nativeEvent)) {
const possibleCommand = activeHotkeys[cat];
if (possibleCommand) {
const command = possibleCommand[hotkey];
command(event, portableTextEditor);
}
}
}
});
}, editor;
};
}
const slateOperationCallback = ({
input,
sendBack
}) => {
const originalApply = input.slateEditor.apply;
return input.slateEditor.apply = (op) => {
op.type !== "set_selection" && sendBack({
type: "slate operation",
operation: op
}), originalApply(op);
}, () => {
input.slateEditor.apply = originalApply;
};
}, rangeDecorationsMachine = setup({
types: {
context: {},
input: {},
events: {}
},
actions: {
"update pending range decorations": assign({
pendingRangeDecorations: ({
event
}) => (assertEvent(event, "range decorations updated"), event.rangeDecorations)
}),
"set up initial range decorations": assign({
decoratedRanges: ({
context,
event
}) => {
assertEvent(event, "ready");
const rangeDecorationState = [];
for (const rangeDecoration of context.pendingRangeDecorations) {
const slateRange = toSlateRange(rangeDecoration.selection, context.slateEditor);
if (!Range.isRange(slateRange)) {
rangeDecoration.onMoved?.({
newSelection: null,
rangeDecoration,
origin: "local"
});
continue;
}
rangeDecorationState.push({
rangeDecoration,
...slateRange
});
}
return rangeDecorationState;
}
}),
"update range decorations": assign({
decoratedRanges: ({
context,
event
}) => {
assertEvent(event, "range decorations updated");
const rangeDecorationState = [];
for (const rangeDecoration of event.rangeDecorations) {
const slateRange = toSlateRange(rangeDecoration.selection, context.slateEditor);
if (!Range.isRange(slateRange)) {
rangeDecoration.onMoved?.({
newSelection: null,
rangeDecoration,
origin: "local"
});
continue;
}
rangeDecorationState.push({
rangeDecoration,
...slateRange
});
}
return rangeDecorationState;
}
}),
"move range decorations": assign({
decoratedRanges: ({
context,
event
}) => {
assertEvent(event, "slate operation");
const rangeDecorationState = [];
for (const decoratedRange of context.decoratedRanges) {
const slateRange = toSlateRange(decoratedRange.rangeDecoration.selection, context.slateEditor);
if (!Range.isRange(slateRange)) {
decoratedRange.rangeDecoration.onMoved?.({
newSelection: null,
rangeDecoration: decoratedRange.rangeDecoration,
origin: "local"
});
continue;
}
let newRange;
if (newRange = moveRangeByOperation(slateRange, event.operation), newRange && newRange !== slateRange || newRange === null && slateRange) {
const newRangeSelection = newRange ? slateRangeToSelection({
schema: context.schema,
editor: context.slateEditor,
range: newRange
}) : null;
decoratedRange.rangeDecoration.onMoved?.({
newSelection: newRangeSelection,
rangeDecoration: decoratedRange.rangeDecoration,
origin: "local"
});
}
newRange !== null && rangeDecorationState.push({
...newRange || slateRange,
rangeDecoration: {
...decoratedRange.rangeDecoration,
selection: slateRangeToSelection({
schema: context.schema,
editor: context.slateEditor,
range: newRange
})
}
});
}
return rangeDecorationState;
}
}),
"assign readOnly": assign({
readOnly: ({
event
}) => (assertEvent(event, "update read only"), event.readOnly)
}),
"increment update count": assign({
updateCount: ({
context
}) => context.updateCount + 1
})
},
actors: {
"slate operation listener": fromCallback(slateOperationCallback)
},
guards: {
"has pending range decorations": ({
context
}) => context.pendingRangeDecorations.length > 0,
"has range decorations": ({
context
}) => context.decoratedRanges.length > 0,
"has different decorations": ({
context,
event
}) => {
assertEvent(event, "range decorations updated");
const existingRangeDecorations = context.decoratedRanges.map((decoratedRange) => ({
anchor: decoratedRange.rangeDecoration.selection?.anchor,
focus: decoratedRange.rangeDecoration.selection?.focus
})), newRangeDecorations = event.rangeDecorations.map((rangeDecoration) => ({
anchor: rangeDecoration.selection?.anchor,
focus: rangeDecoration.selection?.focus
}));
return !isEqual(existingRangeDecorations, newRangeDecorations);
},
"not read only": ({
context
}) => !context.readOnly,
"should skip setup": ({
context
}) => context.skipSetup
}
}).createMachine({
id: "range decorations",
context: ({
input
}) => ({
readOnly: input.readOnly,
pendingRangeDecorations: input.rangeDecorations,
decoratedRanges: [],
skipSetup: input.skipSetup,
schema: input.schema,
slateEditor: input.slateEditor,
updateCount: 0
}),
invoke: {
src: "slate operation listener",
input: ({
context
}) => ({
slateEditor: context.slateEditor
})
},
on: {
"update read only": {
actions: ["assign readOnly"]
}
},
initial: "setting up",
states: {
"setting up": {
always: [{
guard: and(["should skip setup", "has pending range decorations"]),
target: "ready",
actions: ["set up initial range decorations", "increment update count"]
}, {
guard: "should skip setup",
target: "ready"
}],
on: {
"range decorations updated": {
actions: ["update pending range decorations"]
},
ready: [{
target: "ready",
guard: "has pending range decorations",
actions: ["set up initial range decorations", "increment update count"]
}, {
target: "ready"
}]
}
},
ready: {
initial: "idle",
on: {
"range decorations updated": {
target: ".idle",
guard: "has different decorations",
actions: ["update range decorations", "increment update count"]
}
},
states: {
idle: {
on: {
"slate operation": {
target: "moving range decorations",
guard: and(["has range decorations", "not read only"])
}
}
},
"moving range decorations": {
entry: ["move range decorations"],
always: {
target: "idle"
}
}
}
}
}
});
function createDecorate(rangeDecorationActor) {
return function([node, path]) {
if (isEqualToEmptyEditor(rangeDecorationActor.getSnapshot().context.slateEditor.children, rangeDecorationActor.getSnapshot().context.schema))
return [{
anchor: {
path: [0, 0],
offset: 0
},
focus: {
path: [0, 0],
offset: 0
},
placeholder: !0
}];
if (path.length === 0)
return [];
if (!Element$2.isElement(node) || node.children.length === 0)
return [];
const blockIndex = path.at(0);
return blockIndex === void 0 ? [] : rangeDecorationActor.getSnapshot().context.decoratedRanges.filter((decoratedRange) => Range.isCollapsed(decoratedRange) ? node.children.some((_, childIndex) => Path.equals(decoratedRange.anchor.path, [blockIndex, childIndex]) && Path.equals(decoratedRange.focus.path, [blockIndex, childIndex])) : Range.intersection(decoratedRange, {
anchor: {
path,
offset: 0
},
focus: {
path,
offset: 0
}
}) || Range.includes(decoratedRange, path));
};
}
const debug = debugWithName("component:Editable"), PLACEHOLDER_STYLE = {
position: "absolute",
userSelect: "none",
pointerEvents: "none",
left: 0,
right: 0
}, PortableTextEditable = forwardRef(function(props, forwardedRef) {
const {
hotkeys,
onBlur,
onFocus,
onBeforeInput,
onPaste,
onCopy,
onCut,
onClick,
onDragStart,
onDrag,
onDragEnd,
onDragEnter,
onDragOver,
onDrop,
onDragLeave,
rangeDecorations,
renderAnnotation,
renderBlock,
renderChild,
renderDecorator,
renderListItem,
renderPlaceholder,
renderStyle,
selection: propsSelection,
scrollSelectionIntoView,
spellCheck,
...restProps
} = props, portableTextEditor = usePortableTextEditor(), ref = useRef(null), [editableElement, setEditableElement] = useState(null), [hasInvalidValue, setHasInvalidValue] = useState(!1);
useImperativeHandle(forwardedRef, () => ref.current);
const editorActor = useContext(EditorActorContext), readOnly = useSelector(editorActor, (s) => s.matches({
"edit mode": "read only"
})), slateEditor = useSlate(), rangeDecorationsActor = useActorRef(rangeDecorationsMachine, {
input: {
rangeDecorations: rangeDecorations ?? [],
readOnly,
schema: editorActor.getSnapshot().context.schema,
slateEditor,
skipSetup: !editorActor.getSnapshot().matches({
setup: "setting up"
})
}
});
useSelector(rangeDecorationsActor, (s_0) => s_0.context.updateCount);
const decorate = useMemo(() => createDecorate(rangeDecorationsActor), [rangeDecorationsActor]);
useEffect(() => {
rangeDecorationsActor.send({
type: "update read only",
readOnly
});
}, [rangeDecorationsActor, readOnly]), useEffect(() => {
rangeDecorationsActor.send({
type: "range decorations updated",
rangeDecorations: rangeDecorations ?? []
});
}, [rangeDecorationsActor, rangeDecorations]), useMemo(() => {
if (readOnly)
return debug("Editable is in read only mode"), slateEditor;
const withHotKeys = createWithHotkeys(editorActor, portableTextEditor, hotkeys);
return debug("Editable is in edit mode"), withHotKeys(slateEditor);
}, [editorActor, hotkeys, portableTextEditor, readOnly, slateEditor]);
const renderElement = useCallback((eProps) => /* @__PURE__ */ jsx(Element$1, { ...eProps, readOnly, renderBlock, renderChild, renderListItem, renderStyle, schemaTypes: portableTextEditor.schemaTypes, spellCheck }), [portableTextEditor, spellCheck, readOnly, renderBlock, renderChild, renderListItem, renderStyle]), renderLeaf = useCallback((lProps) => {
if (lProps.leaf._type === "span") {
let rendered = /* @__PURE__ */ jsx(Leaf, { ...lProps, editorActor, schemaTypes: portableTextEditor.schemaTypes, renderAnnotation, renderChild, renderDecorator, readOnly });
if (renderPlaceholder && lProps.leaf.placeholder && lProps.text.text === "")
return /* @__PURE__ */ jsxs(Fragment, { children: [
/* @__PURE__ */ jsx("span", { style: PLACEHOLDER_STYLE, contentEditable: !1, children: renderPlaceholder() }),
rendered
] });
const decoration = lProps.leaf.rangeDecoration;
return decoration && (rendered = decoration.component({
children: rendered
})), rendered;
}
return lProps.children;
}, [editorActor, portableTextEditor, readOnly, renderAnnotation, renderChild, renderDecorator, renderPlaceholder]), restoreSelectionFromProps = useCallback(() => {
if (propsSelection) {
debug(`Selection from props ${JSON.stringify(propsSelection)}`);
const normalizedSelection = normalizeSelection(propsSelection, fromSlateValue(slateEditor.children, editorActor.getSnapshot().context.schema.block.name));
if (normalizedSelection !== null) {
debug(`Normalized selection from props ${JSON.stringify(normalizedSelection)}`);
const slateRange = toSlateRange(normalizedSelection, slateEditor);
slateRange && (Transforms.select(slateEditor, slateRange), slateEditor.operations.some((o) => o.type === "set_selection") || editorActor.send({
type: "notify.selection",
selection: normalizedSelection
}), slateEditor.onChange());
}
}
}, [editorActor, propsSelection, slateEditor]);
useEffect(() => {
const onReady = editorActor.on("ready", () => {
rangeDecorationsActor.send({
type: "ready"
}), restoreSelectionFromProps();
}), onInvalidValue = editorActor.on("invalid value", () => {
setHasInvalidValue(!0);
}), onValueChanged = editorActor.on("value changed", () => {
setHasInvalidValue(!1);
});
return () => {
onReady.unsubscribe(), onInvalidValue.unsubscribe(), onValueChanged.unsubscribe();
};
}, [rangeDecorationsActor, editorActor, restoreSelectionFromProps]), useEffect(() => {
propsSelection && !hasInvalidValue && restoreSelectionFromProps();
}, [hasInvalidValue, propsSelection, restoreSelectionFromProps]);
const handleCopy = useCallback((event) => {
if (onCopy)
onCopy(event) !== void 0 && event.preventDefault();
else if (event.nativeEvent.clipboardData) {
event.stopPropagation(), event.preventDefault();
const selection = slateEditor.selection ? slateRangeToSelection({
schema: editorActor.getSnapshot().context.schema,
editor: slateEditor,
range: slateEditor.selection
}) : void 0, position = selection ? {
selection
} : void 0;
if (!position) {
console.warn("Could not find position for copy event");
return;
}
editorActor.send({
type: "behavior event",
behaviorEvent: {
type: "clipboard.copy",
originEvent: {
dataTransfer: event.nativeEvent.clipboardData
},
position
},
editor: slateEditor,
nativeEvent: event
});
}
}, [onCopy, editorActor, slateEditor]), handleCut = useCallback((event_0) => {
if (onCut)
onCut(event_0) !== void 0 && event_0.preventDefault();
else if (event_0.nativeEvent.clipboardData) {
event_0.stopPropagation(), event_0.preventDefault();
const selection_0 = editorActor.getSnapshot().context.selection, position_0 = selection_0 ? {
selection: selection_0
} : void 0;
if (!position_0) {
console.warn("Could not find position for cut event");
return;
}
editorActor.send({
type: "behavior event",
behaviorEvent: {
type: "clipboard.cut",
originEvent: {
dataTransfer: event_0.nativeEvent.clipboardData
},
position: position_0
},
editor: slateEditor,
nativeEvent: event_0
});
}
}, [onCut, editorActor, slateEditor]), handlePaste = useCallback((event_1) => {
const value = fromSlateValue(slateEditor.children, editorActor.getSnapshot().context.schema.block.name, KEY_TO_VALUE_ELEMENT.get(slateEditor)), path = (slateEditor.selection ? slateRangeToSelection({
schema: editorActor.getSnapshot().context.schema,
editor: slateEditor,
range: slateEditor.selection
}) : null)?.focus.path || [], onPasteResult = onPaste?.({
event: event_1,
value,
path,
schemaTypes: portableTextEditor.schemaTypes
});
if (onPasteResult || !slateEditor.selection)
event_1.preventDefault(), editorActor.send({
type: "notify.loading"
}), Promise.resolve(onPasteResult).then((result_1) => {
if (debug("Custom paste function from client resolved", result_1), !result_1 || !result_1.insert) {
debug("No result from custom paste handler, pasting normally");
const selection_1 = editorActor.getSnapshot().context.selection, position_1 = selection_1 ? {
selection: selection_1
} : void 0;
if (!position_1) {
console.warn("Could not find position for paste event");
return;
}
editorActor.send({
type: "behavior event",
behaviorEvent: {
type: "clipboard.paste",
originEvent: {
dataTransfer: event_1.clipboardData
},
position: position_1
},
editor: slateEditor,
nativeEvent: event_1
});
} else result_1.insert ? editorActor.send({
type: "behavior event",
behaviorEvent: {
type: "insert.blocks",
blocks: parseBlocks({
context: {
keyGenerator: editorActor.getSnapshot().context.keyGenerator,
schema: editorActor.getSnapshot().context.schema
},
blocks: result_1.insert,
options: {
refreshKeys: !0
}
}),
placement: "auto"
},
editor: slateEditor
}) : console.warn("Your onPaste function returned something unexpected:", result_1);
}).catch((error) => (console.warn(error), error)).finally(() => {
editorActor.send({
type: "notify.done loading"
});
});
else if (event_1.nativeEvent.clipboardData) {
event_1.preventDefault(), event_1.stopPropagation();
const selection_2 = editorActor.getSnapshot().context.selection, position_2 = selection_2 ? {
selection: selection_2
} : void 0;
if (!position_2) {
console.warn("Could not find position for paste event");
return;
}
editorActor.send({
type: "behavior event",
behaviorEvent: {
type: "clipboard.paste",
originEvent: {
dataTransfer: event_1.nativeEvent.clipboardData
},
position: position_2
},
editor: slateEditor,
nativeEvent: event_1
});
}
debug("No result from custom paste handler, pasting normally");
}, [editorActor, onPaste, portableTextEditor, slateEditor]), handleOnFocus = useCallback((event_2) => {
if (onFocus && onFocus(event_2), !event_2.isDefaultPrevented()) {
const selection_3 = PortableTextEditor.getSelection(portableTextEditor);
selection_3 === null && (Transforms.select(slateEditor, Editor.start(slateEditor, [])), slateEditor.onChange()), editorActor.send({
type: "notify.focused",
event: event_2
});
const newSelection = PortableTextEditor.getSelection(portableTextEditor);
selection_3 === newSelection && editorActor.send({
type: "notify.selection",
selection: selection_3
});
}
}, [editorActor, onFocus, slateEditor, portableTextEditor]), handleClick = useCallback((event_3) => {
if (onClick && onClick(event_3), event_3.isDefaultPrevented() || event_3.isPropagationStopped())
return;
const position_3 = getEventPosition({
editorActor,
slateEditor,
event: event_3.nativeEvent
});
position_3 && editorActor.send({
type: "behavior event",
behaviorEvent: {
type: "mouse.click",
position: position_3
},
editor: slateEditor,
nativeEvent: event_3
});
}, [onClick, editorActor, slateEditor]), handleOnBlur = useCallback((event_4) => {
onBlur && onBlur(event_4), event_4.isPropagationStopped() || editorActor.send({
type: "notify.blurred",
event: event_4
});
}, [editorActor, onBlur]), handleOnBeforeInput = useCallback((event_5) => {
onBeforeInput && onBeforeInput(event_5);
}, [onBeforeInput]), validateSelection = useCallback(() => {
if (!slateEditor.selection)
return;
const root = ReactEditor.findDocumentOrShadowRoot(slateEditor), {
activeElement
} = root;
if (ref.current !== activeElement)
return;
const domSelection = ReactEditor.getWindow(slateEditor).getSelection();
if (!domSelection || domSelection.rangeCount === 0)
return;
const existingDOMRange = domSelection.getRangeAt(0);
try {
const newDOMRange = ReactEditor.toDOMRange(slateEditor, slateEditor.selection);
(newDOMRange.startOffset !== existingDOMRange.startOffset || newDOMRange.endOffset !== existingDOMRange.endOffset) && (debug("DOM range out of sync, validating selection"), domSelection?.removeAllRanges(), domSelection.addRange(newDOMRange));
} catch {
debug("Could not resolve selection, selecting top document"), Transforms.deselect(slateEditor), slateEditor.children.length > 0 && Transforms.select(slateEditor, [0, 0]), slateEditor.onChange();
}
}, [ref, slateEditor]);
useEffect(() => {
if (editableElement) {
const mutationObserver = new MutationObserver(validateSelection);
return mutationObserver.observe(editableElement, {
attributeOldValue: !1,
attributes: !1,
characterData: !1,
childList: !0,
subtree: !0
}), () => {
mutationObserver.disconnect();
};
}
}, [validateSelection, editableElement]);
const handleKeyDown = useCallback((event_6) => {
props.onKeyDown && props.onKeyDown(event_6), event_6.isDefaultPrevented() || slateEditor.pteWithHotKeys(event_6), event_6.isDefaultPrevented() || editorActor.send({
type: "behavior event",
behaviorEvent: {
type: "keyboard.keydown",
originEvent: {
key: event_6.key,
code: event_6.code,
altKey: event_6.altKey,
ctrlKey: event_6.ctrlKey,