UNPKG

@portabletext/editor

Version:

Portable Text Editor made in React

510 lines (509 loc) 18.5 kB
"use strict"; 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