@portabletext/editor
Version:
Portable Text Editor made in React
510 lines (509 loc) • 18.5 kB
JavaScript
;
Object.defineProperty(exports, "__esModule", { value: !0 });
var reactCompilerRuntime = require("react-compiler-runtime"), React = require("react"), editorProvider = require("../_chunks-cjs/editor-provider.cjs"), jsxRuntime = require("react/jsx-runtime"), behavior_core = require("../_chunks-cjs/behavior.core.cjs"), react = require("@xstate/react"), isEqual = require("lodash/isEqual.js"), xstate = require("xstate"), selector_isOverlappingSelection = require("../_chunks-cjs/selector.is-overlapping-selection.cjs"), util_sliceBlocks = require("../_chunks-cjs/util.slice-blocks.cjs"), util_selectionPointToBlockOffset = require("../_chunks-cjs/util.selection-point-to-block-offset.cjs"), selector_getTextBefore = require("../_chunks-cjs/selector.get-text-before.cjs"), useEffectEvent = require("use-effect-event"), behavior_markdown = require("../_chunks-cjs/behavior.markdown.cjs"), util_mergeTextBlocks = require("../_chunks-cjs/util.merge-text-blocks.cjs");
function _interopDefaultCompat(e) {
return e && typeof e == "object" && "default" in e ? e : { default: e };
}
var React__default = /* @__PURE__ */ _interopDefaultCompat(React), isEqual__default = /* @__PURE__ */ _interopDefaultCompat(isEqual);
function BehaviorPlugin(props) {
const $ = reactCompilerRuntime.c(4), editor = editorProvider.useEditor();
let t0, t1;
return $[0] !== editor || $[1] !== props.behaviors ? (t0 = () => {
const unregisterBehaviors = props.behaviors.map((behavior) => editor.registerBehavior({
behavior
}));
return () => {
unregisterBehaviors.forEach(_temp);
};
}, t1 = [editor, props.behaviors], $[0] = editor, $[1] = props.behaviors, $[2] = t0, $[3] = t1) : (t0 = $[2], t1 = $[3]), React.useEffect(t0, t1), null;
}
function _temp(unregister) {
return unregister();
}
function CoreBehaviorsPlugin() {
const $ = reactCompilerRuntime.c(1);
let t0;
return $[0] === Symbol.for("react.memo_cache_sentinel") ? (t0 = /* @__PURE__ */ jsxRuntime.jsx(BehaviorPlugin, { behaviors: behavior_core.coreBehaviors }), $[0] = t0) : t0 = $[0], t0;
}
function createPairRegex(char, amount) {
const prePrefix = `(?<!\\${char})`, prefix = `\\${char}`.repeat(Math.max(amount, 1)), postPrefix = "(?!\\s)", content = `([^${char}\\n]+?)`, preSuffix = "(?<!\\s)", suffix = `\\${char}`.repeat(Math.max(amount, 1)), postSuffix = `(?!\\${char})`;
return `${prePrefix}${prefix}${postPrefix}${content}${preSuffix}${suffix}${postSuffix}`;
}
function createDecoratorPairBehavior(config) {
config.pair.amount < 1 && console.warn("The amount of characters in the pair should be greater than 0");
const pairRegex = createPairRegex(config.pair.char, config.pair.amount), regEx = new RegExp(`(${pairRegex})$`);
return behavior_core.defineBehavior({
on: "insert.text",
guard: ({
snapshot,
event
}) => {
if (config.pair.amount < 1)
return !1;
const decorator = config.decorator({
schema: snapshot.context.schema
});
if (decorator === void 0)
return !1;
const focusTextBlock = selector_isOverlappingSelection.getFocusTextBlock(snapshot), selectionStartPoint = selector_isOverlappingSelection.getSelectionStartPoint(snapshot), selectionStartOffset = selectionStartPoint ? util_sliceBlocks.spanSelectionPointToBlockOffset({
value: snapshot.context.value,
selectionPoint: selectionStartPoint
}) : void 0;
if (!focusTextBlock || !selectionStartOffset)
return !1;
const newText = `${selector_getTextBefore.getBlockTextBefore(snapshot)}${event.text}`, textToDecorate = newText.match(regEx)?.at(0);
if (textToDecorate === void 0)
return !1;
const prefixOffsets = {
anchor: {
path: focusTextBlock.path,
// Example: "foo **bar**".length - "**bar**".length = 4
offset: newText.length - textToDecorate.length
},
focus: {
path: focusTextBlock.path,
// Example: "foo **bar**".length - "**bar**".length + "*".length * 2 = 6
offset: newText.length - textToDecorate.length + config.pair.char.length * config.pair.amount
}
}, suffixOffsets = {
anchor: {
path: focusTextBlock.path,
// Example: "foo **bar*|" (10) + "*".length - 2 = 9
offset: selectionStartOffset.offset + event.text.length - config.pair.char.length * config.pair.amount
},
focus: {
path: focusTextBlock.path,
// Example: "foo **bar*|" (10) + "*".length = 11
offset: selectionStartOffset.offset + event.text.length
}
};
if (prefixOffsets.focus.offset - prefixOffsets.anchor.offset > 1) {
const prefixSelection = util_selectionPointToBlockOffset.blockOffsetsToSelection({
value: snapshot.context.value,
offsets: prefixOffsets
}), inlineObjectBeforePrefixFocus = selector_isOverlappingSelection.getPreviousInlineObject({
context: {
...snapshot.context,
selection: prefixSelection ? {
anchor: prefixSelection.focus,
focus: prefixSelection.focus
} : null
}
}), inlineObjectBeforePrefixFocusOffset = inlineObjectBeforePrefixFocus ? util_selectionPointToBlockOffset.childSelectionPointToBlockOffset({
value: snapshot.context.value,
selectionPoint: {
path: inlineObjectBeforePrefixFocus.path,
offset: 0
}
}) : void 0;
if (inlineObjectBeforePrefixFocusOffset && inlineObjectBeforePrefixFocusOffset.offset > prefixOffsets.anchor.offset && inlineObjectBeforePrefixFocusOffset.offset < prefixOffsets.focus.offset)
return !1;
}
if (suffixOffsets.focus.offset - suffixOffsets.anchor.offset > 1) {
const previousInlineObject = selector_isOverlappingSelection.getPreviousInlineObject(snapshot), previousInlineObjectOffset = previousInlineObject ? util_selectionPointToBlockOffset.childSelectionPointToBlockOffset({
value: snapshot.context.value,
selectionPoint: {
path: previousInlineObject.path,
offset: 0
}
}) : void 0;
if (previousInlineObjectOffset && previousInlineObjectOffset.offset > suffixOffsets.anchor.offset && previousInlineObjectOffset.offset < suffixOffsets.focus.offset)
return !1;
}
return {
prefixOffsets,
suffixOffsets,
decorator
};
},
actions: [
// Insert the text as usual in its own undo step
({
event
}) => [behavior_core.execute(event)],
(_, {
prefixOffsets,
suffixOffsets,
decorator
}) => [
// Decorate the text between the prefix and suffix
behavior_core.execute({
type: "decorator.add",
decorator,
at: {
anchor: prefixOffsets.focus,
focus: suffixOffsets.anchor
}
}),
// Delete the suffix
behavior_core.execute({
type: "delete.text",
at: suffixOffsets
}),
// Delete the prefix
behavior_core.execute({
type: "delete.text",
at: prefixOffsets
}),
// Toggle the decorator off so the next inserted text isn't emphasized
behavior_core.execute({
type: "decorator.remove",
decorator
}),
behavior_core.effect(() => {
config.onDecorate({
...suffixOffsets.anchor,
offset: suffixOffsets.anchor.offset - (prefixOffsets.focus.offset - prefixOffsets.anchor.offset)
});
})
]
]
});
}
function DecoratorShortcutPlugin(config) {
const $ = reactCompilerRuntime.c(4), editor = editorProvider.useEditor();
let t0;
return $[0] !== config.decorator || $[1] !== config.pair || $[2] !== editor ? (t0 = {
input: {
editor,
decorator: config.decorator,
pair: config.pair
}
}, $[0] = config.decorator, $[1] = config.pair, $[2] = editor, $[3] = t0) : t0 = $[3], react.useActorRef(decoratorPairMachine, t0), null;
}
const emphasisListener = ({
sendBack,
input
}) => input.editor.registerBehavior({
behavior: createDecoratorPairBehavior({
decorator: input.decorator,
pair: input.pair,
onDecorate: (offset) => {
sendBack({
type: "emphasis.add",
blockOffset: offset
});
}
})
}), selectionListenerCallback = ({
sendBack,
input
}) => input.editor.registerBehavior({
behavior: behavior_core.defineBehavior({
on: "select",
guard: ({
snapshot,
event
}) => {
if (!event.at)
return {
blockOffsets: void 0
};
const anchor = util_sliceBlocks.spanSelectionPointToBlockOffset({
value: snapshot.context.value,
selectionPoint: event.at.anchor
}), focus = util_sliceBlocks.spanSelectionPointToBlockOffset({
value: snapshot.context.value,
selectionPoint: event.at.focus
});
return !anchor || !focus ? {
blockOffsets: void 0
} : {
blockOffsets: {
anchor,
focus
}
};
},
actions: [(_, {
blockOffsets
}) => [{
type: "effect",
effect: () => {
sendBack({
type: "selection",
blockOffsets
});
}
}]]
})
}), deleteBackwardListenerCallback = ({
sendBack,
input
}) => input.editor.registerBehavior({
behavior: behavior_core.defineBehavior({
on: "delete.backward",
actions: [() => [behavior_core.execute({
type: "history.undo"
}), behavior_core.effect(() => {
sendBack({
type: "delete.backward"
});
})]]
})
}), decoratorPairMachine = xstate.setup({
types: {
context: {},
input: {},
events: {}
},
actors: {
"emphasis listener": xstate.fromCallback(emphasisListener),
"delete.backward listener": xstate.fromCallback(deleteBackwardListenerCallback),
"selection listener": xstate.fromCallback(selectionListenerCallback)
}
}).createMachine({
id: "decorator pair",
context: ({
input
}) => ({
decorator: input.decorator,
editor: input.editor,
pair: input.pair
}),
initial: "idle",
states: {
idle: {
invoke: [{
src: "emphasis listener",
input: ({
context
}) => ({
decorator: context.decorator,
editor: context.editor,
pair: context.pair
})
}],
on: {
"emphasis.add": {
target: "emphasis added",
actions: xstate.assign({
offsetAfterEmphasis: ({
event
}) => event.blockOffset
})
}
}
},
"emphasis added": {
exit: [xstate.assign({
offsetAfterEmphasis: void 0
})],
invoke: [{
src: "selection listener",
input: ({
context
}) => ({
editor: context.editor
})
}, {
src: "delete.backward listener",
input: ({
context
}) => ({
editor: context.editor
})
}],
on: {
selection: {
target: "idle",
guard: ({
context,
event
}) => !isEqual__default.default({
anchor: context.offsetAfterEmphasis,
focus: context.offsetAfterEmphasis
}, event.blockOffsets)
},
"delete.backward": {
target: "idle"
}
}
}
}
}), EditorRefPlugin = React__default.default.forwardRef((_, ref) => {
const $ = reactCompilerRuntime.c(2), editor = editorProvider.useEditor(), portableTextEditorRef = React__default.default.useRef(editor);
let t0, t1;
return $[0] === Symbol.for("react.memo_cache_sentinel") ? (t0 = () => portableTextEditorRef.current, t1 = [], $[0] = t0, $[1] = t1) : (t0 = $[0], t1 = $[1]), React__default.default.useImperativeHandle(ref, t0, t1), null;
});
EditorRefPlugin.displayName = "EditorRefPlugin";
function EventListenerPlugin(props) {
const $ = reactCompilerRuntime.c(5), editor = editorProvider.useEditor(), on = useEffectEvent.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], React.useEffect(t0, t1), null;
}
function MarkdownPlugin(props) {
const $ = reactCompilerRuntime.c(17), editor = editorProvider.useEditor();
let t0, t1;
$[0] !== editor || $[1] !== props.config ? (t0 = () => {
const unregisterBehaviors = behavior_markdown.createMarkdownBehaviors(props.config).map((behavior) => editor.registerBehavior({
behavior
}));
return () => {
for (const unregisterBehavior of unregisterBehaviors)
unregisterBehavior();
};
}, t1 = [editor, props.config], $[0] = editor, $[1] = props.config, $[2] = t0, $[3] = t1) : (t0 = $[2], t1 = $[3]), React.useEffect(t0, t1);
let t2;
$[4] !== props.config.boldDecorator ? (t2 = props.config.boldDecorator ? /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
/* @__PURE__ */ jsxRuntime.jsx(DecoratorShortcutPlugin, { decorator: props.config.boldDecorator, pair: {
char: "*",
amount: 2
} }),
/* @__PURE__ */ jsxRuntime.jsx(DecoratorShortcutPlugin, { decorator: props.config.boldDecorator, pair: {
char: "_",
amount: 2
} })
] }) : null, $[4] = props.config.boldDecorator, $[5] = t2) : t2 = $[5];
let t3;
$[6] !== props.config.codeDecorator ? (t3 = props.config.codeDecorator ? /* @__PURE__ */ jsxRuntime.jsx(DecoratorShortcutPlugin, { decorator: props.config.codeDecorator, pair: {
char: "`",
amount: 1
} }) : null, $[6] = props.config.codeDecorator, $[7] = t3) : t3 = $[7];
let t4;
$[8] !== props.config.italicDecorator ? (t4 = props.config.italicDecorator ? /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
/* @__PURE__ */ jsxRuntime.jsx(DecoratorShortcutPlugin, { decorator: props.config.italicDecorator, pair: {
char: "*",
amount: 1
} }),
/* @__PURE__ */ jsxRuntime.jsx(DecoratorShortcutPlugin, { decorator: props.config.italicDecorator, pair: {
char: "_",
amount: 1
} })
] }) : null, $[8] = props.config.italicDecorator, $[9] = t4) : t4 = $[9];
let t5;
$[10] !== props.config.strikeThroughDecorator ? (t5 = props.config.strikeThroughDecorator ? /* @__PURE__ */ jsxRuntime.jsx(DecoratorShortcutPlugin, { decorator: props.config.strikeThroughDecorator, pair: {
char: "~",
amount: 2
} }) : null, $[10] = props.config.strikeThroughDecorator, $[11] = t5) : t5 = $[11];
let t6;
return $[12] !== t2 || $[13] !== t3 || $[14] !== t4 || $[15] !== t5 ? (t6 = /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
t2,
t3,
t4,
t5
] }), $[12] = t2, $[13] = t3, $[14] = t4, $[15] = t5, $[16] = t6) : t6 = $[16], t6;
}
const oneLineBehaviors = [
/**
* Hitting Enter on an expanded selection should just delete that selection
* without causing a line break.
*/
behavior_core.defineBehavior({
on: "insert.break",
guard: ({
snapshot
}) => snapshot.context.selection && selector_isOverlappingSelection.isSelectionExpanded(snapshot) ? {
selection: snapshot.context.selection
} : !1,
actions: [(_, {
selection
}) => [behavior_core.execute({
type: "delete",
at: selection
})]]
}),
/**
* All other cases of `insert.break` should be aborted.
*/
behavior_core.defineBehavior({
on: "insert.break",
actions: [() => [{
type: "noop"
}]]
}),
/**
* `split.block`s as well.
*/
behavior_core.defineBehavior({
on: "split.block",
actions: [() => [{
type: "noop"
}]]
}),
/**
* `insert.block` `before` or `after` is not allowed in a one-line editor.
*/
behavior_core.defineBehavior({
on: "insert.block",
guard: ({
event
}) => event.placement === "before" || event.placement === "after",
actions: [() => [{
type: "noop"
}]]
}),
/**
* An ordinary `insert.block` is acceptable if it's a text block. In that
* case it will get merged into the existing text block.
*/
behavior_core.defineBehavior({
on: "insert.block",
guard: ({
snapshot,
event
}) => !(!selector_isOverlappingSelection.getFocusTextBlock(snapshot) || !util_mergeTextBlocks.isTextBlock(snapshot.context, event.block)),
actions: [({
event
}) => [behavior_core.execute({
type: "insert.block",
block: event.block,
placement: "auto",
select: "end"
})]]
}),
/**
* Fallback Behavior to avoid `insert.block` in case the Behaviors above all
* end up with a falsy guard.
*/
behavior_core.defineBehavior({
on: "insert.block",
actions: [() => [{
type: "noop"
}]]
}),
/**
* If multiple blocks are inserted, then the non-text blocks are filtered out
* and the text blocks are merged into one block
*/
behavior_core.defineBehavior({
on: "insert.blocks",
guard: ({
snapshot,
event
}) => event.blocks.filter((block) => util_mergeTextBlocks.isTextBlock(snapshot.context, block)).reduce((targetBlock, incomingBlock) => util_mergeTextBlocks.mergeTextBlocks({
context: snapshot.context,
targetBlock,
incomingBlock
})),
actions: [
// `insert.block` is raised so the Behavior above can handle the
// insertion
(_, block) => [behavior_core.raise({
type: "insert.block",
block,
placement: "auto"
})]
]
})
];
function OneLinePlugin() {
const $ = reactCompilerRuntime.c(1);
let t0;
return $[0] === Symbol.for("react.memo_cache_sentinel") ? (t0 = /* @__PURE__ */ jsxRuntime.jsx(BehaviorPlugin, { behaviors: oneLineBehaviors }), $[0] = t0) : t0 = $[0], t0;
}
exports.BehaviorPlugin = BehaviorPlugin;
exports.CoreBehaviorsPlugin = CoreBehaviorsPlugin;
exports.DecoratorShortcutPlugin = DecoratorShortcutPlugin;
exports.EditorRefPlugin = EditorRefPlugin;
exports.EventListenerPlugin = EventListenerPlugin;
exports.MarkdownPlugin = MarkdownPlugin;
exports.OneLinePlugin = OneLinePlugin;
//# sourceMappingURL=index.cjs.map