UNPKG

@portabletext/editor

Version:

Portable Text Editor made in React

327 lines (326 loc) 10.7 kB
import { isPortableTextTextBlock } from "@sanity/types"; import { isSelectionCollapsed, getFocusTextBlock, getFocusSpan, getPreviousInlineObject, getFocusBlock } from "./selector.is-overlapping-selection.js"; import { spanSelectionPointToBlockOffset, getTextBlockText } from "./util.slice-blocks.js"; import { getBlockTextBefore } from "./selector.get-text-before.js"; import { defineBehavior, execute } from "./behavior.core.js"; function createMarkdownBehaviors(config) { const automaticBlockquoteOnSpace = defineBehavior({ on: "insert.text", guard: ({ snapshot, event }) => { if (event.text !== " ") return !1; const selectionCollapsed = isSelectionCollapsed(snapshot), focusTextBlock = getFocusTextBlock(snapshot), focusSpan = getFocusSpan(snapshot); if (!selectionCollapsed || !focusTextBlock || !focusSpan) return !1; const previousInlineObject = getPreviousInlineObject(snapshot), blockOffset = spanSelectionPointToBlockOffset({ value: snapshot.context.value, 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 = getTextBlockText(focusTextBlock.node), caretAtTheEndOfQuote = blockOffset.offset === 1, looksLikeMarkdownQuote = /^>/.test(blockText), blockquoteStyle = config.blockquoteStyle?.(snapshot.context); 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?.(snapshot.context), focusBlock = getFocusTextBlock(snapshot), selectionCollapsed = isSelectionCollapsed(snapshot); if (!hrObject || !focusBlock || !selectionCollapsed) return !1; const previousInlineObject = getPreviousInlineObject(snapshot), textBefore = 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", placement: "before", block: { _type: hrObject.name, ...hrObject.value ?? {} } }), 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?.(snapshot.context), focusBlock = getFocusBlock(snapshot); return !hrCharacters || !hrObject || !focusBlock ? !1 : { hrCharacters, hrObject, focusBlock }; }, actions: [(_, { hrCharacters }) => [execute({ type: "insert.text", text: hrCharacters })], ({ snapshot }, { hrObject, focusBlock }) => isPortableTextTextBlock(focusBlock.node) ? [execute({ type: "insert.block", block: { _type: snapshot.context.schema.block.name, children: focusBlock.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 = isSelectionCollapsed(snapshot), focusTextBlock = getFocusTextBlock(snapshot), focusSpan = getFocusSpan(snapshot); if (!selectionCollapsed || !focusTextBlock || !focusSpan) return !1; const blockOffset = spanSelectionPointToBlockOffset({ value: snapshot.context.value, selectionPoint: { path: [{ _key: focusTextBlock.node._key }, "children", { _key: focusSpan.node._key }], offset: snapshot.context.selection?.focus.offset ?? 0 } }); if (!blockOffset) return !1; const previousInlineObject = getPreviousInlineObject(snapshot), blockText = 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 = isSelectionCollapsed(snapshot), focusTextBlock = getFocusTextBlock(snapshot), focusSpan = 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?.(snapshot.context); 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 = isSelectionCollapsed(snapshot), focusTextBlock = getFocusTextBlock(snapshot), focusSpan = getFocusSpan(snapshot); if (!selectionCollapsed || !focusTextBlock || !focusSpan) return !1; const previousInlineObject = getPreviousInlineObject(snapshot), blockOffset = spanSelectionPointToBlockOffset({ value: snapshot.context.value, 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 = getTextBlockText(focusTextBlock.node), defaultStyle = config.defaultStyle?.(snapshot.context), looksLikeUnorderedList = /^(-|\*)/.test(blockText), unorderedListStyle = config.unorderedListStyle?.(snapshot.context), caretAtTheEndOfUnorderedList = blockOffset.offset === 1; if (defaultStyle && caretAtTheEndOfUnorderedList && looksLikeUnorderedList && unorderedListStyle !== void 0) return { focusTextBlock, listItem: unorderedListStyle, listItemLength: 1, style: defaultStyle }; const looksLikeOrderedList = /^1\./.test(blockText), orderedListStyle = config.orderedListStyle?.(snapshot.context), caretAtTheEndOfOrderedList = blockOffset.offset === 2; return defaultStyle && caretAtTheEndOfOrderedList && looksLikeOrderedList && orderedListStyle !== void 0 ? { focusTextBlock, listItem: orderedListStyle, 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]; } export { createMarkdownBehaviors }; //# sourceMappingURL=behavior.markdown.js.map