UNPKG

@portabletext/plugin-markdown-shortcuts

Version:

Adds helpful Markdown shortcuts to the editor

383 lines (382 loc) 13.3 kB
import { jsxs, Fragment, jsx } from "react/jsx-runtime"; import { useEditor } from "@portabletext/editor"; import { CharacterPairDecoratorPlugin } from "@portabletext/plugin-character-pair-decorator"; import { useEffect } from "react"; import { defineBehavior, execute } from "@portabletext/editor/behaviors"; import * as selectors from "@portabletext/editor/selectors"; import * as utils from "@portabletext/editor/utils"; function createMarkdownBehaviors(config) { const automaticBlockquoteOnSpace = defineBehavior({ on: "insert.text", guard: ({ snapshot, event }) => { if (event.text !== " ") return !1; const selectionCollapsed = selectors.isSelectionCollapsed(snapshot), focusTextBlock = selectors.getFocusTextBlock(snapshot), focusSpan = selectors.getFocusSpan(snapshot); if (!selectionCollapsed || !focusTextBlock || !focusSpan) return !1; const previousInlineObject = selectors.getPreviousInlineObject(snapshot), blockOffset = utils.spanSelectionPointToBlockOffset({ context: snapshot.context, selectionPoint: { path: [ { _key: focusTextBlock.node._key }, "children", { _key: focusSpan.node._key } ], offset: snapshot.context.selection?.focus.offset ?? 0 } }); if (previousInlineObject || !blockOffset) return !1; const blockText = utils.getTextBlockText(focusTextBlock.node), caretAtTheEndOfQuote = blockOffset.offset === 1, looksLikeMarkdownQuote = /^>/.test(blockText), blockquoteStyle = config.blockquoteStyle?.({ schema: snapshot.context.schema }); return caretAtTheEndOfQuote && looksLikeMarkdownQuote && blockquoteStyle !== void 0 ? { focusTextBlock, style: blockquoteStyle } : !1; }, actions: [ () => [ execute({ type: "insert.text", text: " " }) ], (_, { focusTextBlock, style }) => [ execute({ type: "block.unset", props: ["listItem", "level"], at: focusTextBlock.path }), execute({ type: "block.set", props: { style }, at: focusTextBlock.path }), execute({ type: "delete.text", at: { anchor: { path: focusTextBlock.path, offset: 0 }, focus: { path: focusTextBlock.path, offset: 2 } } }) ] ] }), automaticHr = defineBehavior({ on: "insert.text", guard: ({ snapshot, event }) => { const hrCharacter = event.text === "-" ? "-" : event.text === "*" ? "*" : event.text === "_" ? "_" : void 0; if (hrCharacter === void 0) return !1; const hrObject = config.horizontalRuleObject?.({ schema: snapshot.context.schema }), focusBlock = selectors.getFocusTextBlock(snapshot), selectionCollapsed = selectors.isSelectionCollapsed(snapshot); if (!hrObject || !focusBlock || !selectionCollapsed) return !1; const previousInlineObject = selectors.getPreviousInlineObject(snapshot), textBefore = selectors.getBlockTextBefore(snapshot), hrBlockOffsets = { anchor: { path: focusBlock.path, offset: 0 }, focus: { path: focusBlock.path, offset: 3 } }; return !previousInlineObject && textBefore === `${hrCharacter}${hrCharacter}` ? { hrObject, focusBlock, hrCharacter, hrBlockOffsets } : !1; }, actions: [ (_, { hrCharacter }) => [ execute({ type: "insert.text", text: hrCharacter }) ], (_, { hrObject, hrBlockOffsets }) => [ execute({ type: "insert.block", block: { _type: hrObject.name, ...hrObject.value ?? {} }, placement: "before", select: "none" }), execute({ type: "delete.text", at: hrBlockOffsets }) ] ] }), automaticHrOnPaste = defineBehavior({ on: "clipboard.paste", guard: ({ snapshot, event }) => { const text = event.originEvent.dataTransfer.getData("text/plain"), hrRegExp = /^(---)$|(___)$|(\*\*\*)$/, hrCharacters = text.match(hrRegExp)?.[0], hrObject = config.horizontalRuleObject?.({ schema: snapshot.context.schema }), focusBlock = selectors.getFocusBlock(snapshot), focusTextBlock = selectors.getFocusTextBlock(snapshot); return !hrCharacters || !hrObject || !focusBlock ? !1 : { hrCharacters, hrObject, focusBlock, focusTextBlock }; }, actions: [ (_, { hrCharacters }) => [ execute({ type: "insert.text", text: hrCharacters }) ], ({ snapshot }, { hrObject, focusBlock, focusTextBlock }) => focusTextBlock ? [ execute({ type: "insert.block", block: { _type: snapshot.context.schema.block.name, children: focusTextBlock.node.children }, placement: "after" }), execute({ type: "insert.block", block: { _type: hrObject.name, ...hrObject.value ?? {} }, placement: "after" }), execute({ type: "delete.block", at: focusBlock.path }) ] : [ execute({ type: "insert.block", block: { _type: hrObject.name, ...hrObject.value ?? {} }, placement: "after" }) ] ] }), automaticHeadingOnSpace = defineBehavior({ on: "insert.text", guard: ({ snapshot, event }) => { if (event.text !== " ") return !1; const selectionCollapsed = selectors.isSelectionCollapsed(snapshot), focusTextBlock = selectors.getFocusTextBlock(snapshot), focusSpan = selectors.getFocusSpan(snapshot); if (!selectionCollapsed || !focusTextBlock || !focusSpan) return !1; const blockOffset = utils.spanSelectionPointToBlockOffset({ context: snapshot.context, selectionPoint: { path: [ { _key: focusTextBlock.node._key }, "children", { _key: focusSpan.node._key } ], offset: snapshot.context.selection?.focus.offset ?? 0 } }); if (!blockOffset) return !1; const previousInlineObject = selectors.getPreviousInlineObject(snapshot), blockText = utils.getTextBlockText(focusTextBlock.node), markdownHeadingSearch = /^#+/.exec(blockText), level = markdownHeadingSearch ? markdownHeadingSearch[0].length : void 0, caretAtTheEndOfHeading = blockOffset.offset === level; if (previousInlineObject || !caretAtTheEndOfHeading) return !1; const style = level !== void 0 ? config.headingStyle?.({ schema: snapshot.context.schema, level }) : void 0; return level !== void 0 && style !== void 0 ? { focusTextBlock, style, level } : !1; }, actions: [ ({ event }) => [execute(event)], (_, { focusTextBlock, style, level }) => [ execute({ type: "block.unset", props: ["listItem", "level"], at: focusTextBlock.path }), execute({ type: "block.set", props: { style }, at: focusTextBlock.path }), execute({ type: "delete.text", at: { anchor: { path: focusTextBlock.path, offset: 0 }, focus: { path: focusTextBlock.path, offset: level + 1 } } }) ] ] }), clearStyleOnBackspace = defineBehavior({ on: "delete.backward", guard: ({ snapshot }) => { const selectionCollapsed = selectors.isSelectionCollapsed(snapshot), focusTextBlock = selectors.getFocusTextBlock(snapshot), focusSpan = selectors.getFocusSpan(snapshot); if (!selectionCollapsed || !focusTextBlock || !focusSpan) return !1; const atTheBeginningOfBLock = focusTextBlock.node.children[0]._key === focusSpan.node._key && snapshot.context.selection?.focus.offset === 0, defaultStyle = config.defaultStyle?.({ schema: snapshot.context.schema }); return atTheBeginningOfBLock && defaultStyle && focusTextBlock.node.style !== defaultStyle ? { defaultStyle, focusTextBlock } : !1; }, actions: [ (_, { defaultStyle, focusTextBlock }) => [ execute({ type: "block.set", props: { style: defaultStyle }, at: focusTextBlock.path }) ] ] }), automaticListOnSpace = defineBehavior({ on: "insert.text", guard: ({ snapshot, event }) => { if (event.text !== " ") return !1; const selectionCollapsed = selectors.isSelectionCollapsed(snapshot), focusTextBlock = selectors.getFocusTextBlock(snapshot), focusSpan = selectors.getFocusSpan(snapshot); if (!selectionCollapsed || !focusTextBlock || !focusSpan) return !1; const previousInlineObject = selectors.getPreviousInlineObject(snapshot), blockOffset = utils.spanSelectionPointToBlockOffset({ context: snapshot.context, selectionPoint: { path: [ { _key: focusTextBlock.node._key }, "children", { _key: focusSpan.node._key } ], offset: snapshot.context.selection?.focus.offset ?? 0 } }); if (previousInlineObject || !blockOffset) return !1; const blockText = utils.getTextBlockText(focusTextBlock.node), defaultStyle = config.defaultStyle?.({ schema: snapshot.context.schema }), looksLikeUnorderedList = /^(-|\*)/.test(blockText), unorderedList = config.unorderedList?.({ schema: snapshot.context.schema }), caretAtTheEndOfUnorderedList = blockOffset.offset === 1; if (defaultStyle && caretAtTheEndOfUnorderedList && looksLikeUnorderedList && unorderedList !== void 0) return { focusTextBlock, listItem: unorderedList, listItemLength: 1, style: defaultStyle }; const looksLikeOrderedList = /^1\./.test(blockText), orderedList = config.orderedList?.({ schema: snapshot.context.schema }), caretAtTheEndOfOrderedList = blockOffset.offset === 2; return defaultStyle && caretAtTheEndOfOrderedList && looksLikeOrderedList && orderedList !== void 0 ? { focusTextBlock, listItem: orderedList, listItemLength: 2, style: defaultStyle } : !1; }, actions: [ ({ event }) => [execute(event)], (_, { focusTextBlock, style, listItem, listItemLength }) => [ execute({ type: "block.set", props: { listItem, level: 1, style }, at: focusTextBlock.path }), execute({ type: "delete.text", at: { anchor: { path: focusTextBlock.path, offset: 0 }, focus: { path: focusTextBlock.path, offset: listItemLength + 1 } } }) ] ] }); return [ automaticBlockquoteOnSpace, automaticHeadingOnSpace, automaticHr, automaticHrOnPaste, clearStyleOnBackspace, automaticListOnSpace ]; } function MarkdownShortcutsPlugin(props) { const editor = useEditor(); return useEffect(() => { const unregisterBehaviors = createMarkdownBehaviors(props).map( (behavior) => editor.registerBehavior({ behavior }) ); return () => { for (const unregisterBehavior of unregisterBehaviors) unregisterBehavior(); }; }, [editor, props]), /* @__PURE__ */ jsxs(Fragment, { children: [ props.boldDecorator ? /* @__PURE__ */ jsxs(Fragment, { children: [ /* @__PURE__ */ jsx( CharacterPairDecoratorPlugin, { decorator: props.boldDecorator, pair: { char: "*", amount: 2 } } ), /* @__PURE__ */ jsx( CharacterPairDecoratorPlugin, { decorator: props.boldDecorator, pair: { char: "_", amount: 2 } } ) ] }) : null, props.codeDecorator ? /* @__PURE__ */ jsx( CharacterPairDecoratorPlugin, { decorator: props.codeDecorator, pair: { char: "`", amount: 1 } } ) : null, props.italicDecorator ? /* @__PURE__ */ jsxs(Fragment, { children: [ /* @__PURE__ */ jsx( CharacterPairDecoratorPlugin, { decorator: props.italicDecorator, pair: { char: "*", amount: 1 } } ), /* @__PURE__ */ jsx( CharacterPairDecoratorPlugin, { decorator: props.italicDecorator, pair: { char: "_", amount: 1 } } ) ] }) : null, props.strikeThroughDecorator ? /* @__PURE__ */ jsx( CharacterPairDecoratorPlugin, { decorator: props.strikeThroughDecorator, pair: { char: "~", amount: 2 } } ) : null ] }); } export { MarkdownShortcutsPlugin }; //# sourceMappingURL=index.js.map