@portabletext/editor
Version:
Portable Text Editor made in React
327 lines (326 loc) • 10.7 kB
JavaScript
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