@portabletext/editor
Version:
Portable Text Editor made in React
632 lines (631 loc) • 20.1 kB
JavaScript
;
var selector_isOverlappingSelection = require("./selector.is-overlapping-selection.cjs"), types = require("@sanity/types"), util_sliceBlocks = require("./util.slice-blocks.cjs");
function execute(event) {
return {
type: "execute",
event
};
}
function raise(event) {
return {
type: "raise",
event
};
}
function effect(effect2) {
return {
type: "effect",
effect: effect2
};
}
function noop() {
return {
type: "noop"
};
}
function defineBehavior(behavior) {
return behavior;
}
const addAnnotationOnCollapsedSelection = {
on: "annotation.add",
guard: ({
snapshot
}) => {
if (!selector_isOverlappingSelection.isSelectionCollapsed(snapshot))
return !1;
const caretWordSelection = selector_isOverlappingSelection.getCaretWordSelection(snapshot);
return !caretWordSelection || !selector_isOverlappingSelection.isSelectionExpanded({
context: {
...snapshot.context,
selection: caretWordSelection
}
}) ? !1 : {
caretWordSelection
};
},
actions: [({
event
}, {
caretWordSelection
}) => [raise({
type: "select",
at: caretWordSelection
}), raise({
type: "annotation.add",
annotation: event.annotation
})]]
}, coreAnnotationBehaviors = {
addAnnotationOnCollapsedSelection
}, IS_MAC = typeof window < "u" && /Mac|iPod|iPhone|iPad/.test(window.navigator.userAgent), modifiers = {
alt: "altKey",
control: "ctrlKey",
meta: "metaKey",
shift: "shiftKey"
}, aliases = {
add: "+",
break: "pause",
cmd: "meta",
command: "meta",
ctl: "control",
ctrl: "control",
del: "delete",
down: "arrowdown",
esc: "escape",
ins: "insert",
left: "arrowleft",
mod: IS_MAC ? "meta" : "control",
opt: "alt",
option: "alt",
return: "enter",
right: "arrowright",
space: " ",
spacebar: " ",
up: "arrowup",
win: "meta",
windows: "meta"
}, keyCodes = {
backspace: 8,
tab: 9,
enter: 13,
shift: 16,
control: 17,
alt: 18,
pause: 19,
capslock: 20,
escape: 27,
" ": 32,
pageup: 33,
pagedown: 34,
end: 35,
home: 36,
arrowleft: 37,
arrowup: 38,
arrowright: 39,
arrowdown: 40,
insert: 45,
delete: 46,
meta: 91,
numlock: 144,
scrolllock: 145,
";": 186,
"=": 187,
",": 188,
"-": 189,
".": 190,
"/": 191,
"`": 192,
"[": 219,
"\\": 220,
"]": 221,
"'": 222,
f1: 112,
f2: 113,
f3: 114,
f4: 115,
f5: 116,
f6: 117,
f7: 118,
f8: 119,
f9: 120,
f10: 121,
f11: 122,
f12: 123,
f13: 124,
f14: 125,
f15: 126,
f16: 127,
f17: 128,
f18: 129,
f19: 130,
f20: 131
};
function isHotkey(hotkey, event) {
return compareHotkey(parseHotkey(hotkey), event);
}
function parseHotkey(hotkey) {
const parsedHotkey = {
altKey: !1,
ctrlKey: !1,
metaKey: !1,
shiftKey: !1
}, hotkeySegments = hotkey.replace("++", "+add").split("+");
for (const rawHotkeySegment of hotkeySegments) {
const optional = rawHotkeySegment.endsWith("?") && rawHotkeySegment.length > 1, hotkeySegment = optional ? rawHotkeySegment.slice(0, -1) : rawHotkeySegment, keyName = toKeyName(hotkeySegment), modifier = modifiers[keyName], alias = aliases[hotkeySegment], code = keyCodes[keyName];
if (hotkeySegment.length > 1 && modifier === void 0 && alias === void 0 && code === void 0)
throw new TypeError(`Unknown modifier: "${hotkeySegment}"`);
(hotkeySegments.length === 1 || modifier === void 0) && (parsedHotkey.key = keyName, parsedHotkey.keyCode = toKeyCode(hotkeySegment)), modifier !== void 0 && (parsedHotkey[modifier] = optional ? null : !0);
}
return parsedHotkey;
}
function compareHotkey(parsedHotkey, event) {
return (parsedHotkey.altKey == null || parsedHotkey.altKey === event.altKey) && (parsedHotkey.ctrlKey == null || parsedHotkey.ctrlKey === event.ctrlKey) && (parsedHotkey.metaKey == null || parsedHotkey.metaKey === event.metaKey) && (parsedHotkey.shiftKey == null || parsedHotkey.shiftKey === event.shiftKey) ? parsedHotkey.keyCode !== void 0 && event.keyCode !== void 0 ? parsedHotkey.keyCode === 91 && event.keyCode === 93 ? !0 : parsedHotkey.keyCode === event.keyCode : parsedHotkey.keyCode === event.keyCode || parsedHotkey.key === event.key.toLowerCase() : !1;
}
function toKeyCode(name) {
const keyName = toKeyName(name);
return keyCodes[keyName] ?? keyName.toUpperCase().charCodeAt(0);
}
function toKeyName(name) {
const keyName = name.toLowerCase();
return aliases[keyName] ?? keyName;
}
const arrowDownOnLonelyBlockObject = {
on: "keyboard.keydown",
guard: ({
snapshot,
event
}) => {
if (!isHotkey("ArrowDown", event.originEvent) || !selector_isOverlappingSelection.isSelectionCollapsed(snapshot))
return !1;
const focusBlockObject = selector_isOverlappingSelection.getFocusBlockObject(snapshot), nextBlock = selector_isOverlappingSelection.getNextBlock(snapshot);
return focusBlockObject && !nextBlock;
},
actions: [({
snapshot
}) => [raise({
type: "insert.block",
block: {
_type: snapshot.context.schema.block.name
},
placement: "after"
})]]
}, arrowUpOnLonelyBlockObject = {
on: "keyboard.keydown",
guard: ({
snapshot,
event
}) => {
if (!isHotkey("ArrowUp", event.originEvent) || !selector_isOverlappingSelection.isSelectionCollapsed(snapshot))
return !1;
const focusBlockObject = selector_isOverlappingSelection.getFocusBlockObject(snapshot), previousBlock = selector_isOverlappingSelection.getPreviousBlock(snapshot);
return focusBlockObject && !previousBlock;
},
actions: [({
snapshot
}) => [raise({
type: "insert.block",
block: {
_type: snapshot.context.schema.block.name
},
placement: "before"
})]]
}, breakingBlockObject = {
on: "insert.break",
guard: ({
snapshot
}) => {
const focusBlockObject = selector_isOverlappingSelection.getFocusBlockObject(snapshot);
return selector_isOverlappingSelection.isSelectionCollapsed(snapshot) && focusBlockObject !== void 0;
},
actions: [({
snapshot
}) => [raise({
type: "insert.block",
block: {
_type: snapshot.context.schema.block.name
},
placement: "after"
})]]
}, clickingAboveLonelyBlockObject = {
on: "mouse.click",
guard: ({
snapshot,
event
}) => {
if (snapshot.context.readOnly || snapshot.context.selection && !selector_isOverlappingSelection.isSelectionCollapsed(snapshot))
return !1;
const focusBlockObject = selector_isOverlappingSelection.getFocusBlockObject({
context: {
...snapshot.context,
selection: event.position.selection
}
}), previousBlock = selector_isOverlappingSelection.getPreviousBlock({
context: {
...snapshot.context,
selection: event.position.selection
}
});
return event.position.isEditor && event.position.block === "start" && focusBlockObject && !previousBlock;
},
actions: [({
snapshot,
event
}) => [raise({
type: "select",
at: event.position.selection
}), raise({
type: "insert.block",
block: {
_type: snapshot.context.schema.block.name
},
placement: "before",
select: "start"
})]]
}, clickingBelowLonelyBlockObject = {
on: "mouse.click",
guard: ({
snapshot,
event
}) => {
if (snapshot.context.readOnly || snapshot.context.selection && !selector_isOverlappingSelection.isSelectionCollapsed(snapshot))
return !1;
const focusBlockObject = selector_isOverlappingSelection.getFocusBlockObject({
context: {
...snapshot.context,
selection: event.position.selection
}
}), nextBlock = selector_isOverlappingSelection.getNextBlock({
context: {
...snapshot.context,
selection: event.position.selection
}
});
return event.position.isEditor && event.position.block === "end" && focusBlockObject && !nextBlock;
},
actions: [({
snapshot,
event
}) => [raise({
type: "select",
at: event.position.selection
}), raise({
type: "insert.block",
block: {
_type: snapshot.context.schema.block.name
},
placement: "after",
select: "start"
})]]
}, deletingEmptyTextBlockAfterBlockObject = {
on: "delete.backward",
guard: ({
snapshot
}) => {
const focusTextBlock = selector_isOverlappingSelection.getFocusTextBlock(snapshot), selectionCollapsed = selector_isOverlappingSelection.isSelectionCollapsed(snapshot), previousBlock = selector_isOverlappingSelection.getPreviousBlock(snapshot);
return !focusTextBlock || !selectionCollapsed || !previousBlock ? !1 : util_sliceBlocks.isEmptyTextBlock(focusTextBlock.node) && !types.isPortableTextTextBlock(previousBlock.node) ? {
focusTextBlock,
previousBlock
} : !1;
},
actions: [(_, {
focusTextBlock,
previousBlock
}) => [raise({
type: "delete.block",
at: focusTextBlock.path
}), raise({
type: "select",
at: {
anchor: {
path: previousBlock.path,
offset: 0
},
focus: {
path: previousBlock.path,
offset: 0
}
}
})]]
}, deletingEmptyTextBlockBeforeBlockObject = {
on: "delete.forward",
guard: ({
snapshot
}) => {
const focusTextBlock = selector_isOverlappingSelection.getFocusTextBlock(snapshot), selectionCollapsed = selector_isOverlappingSelection.isSelectionCollapsed(snapshot), nextBlock = selector_isOverlappingSelection.getNextBlock(snapshot);
return !focusTextBlock || !selectionCollapsed || !nextBlock ? !1 : util_sliceBlocks.isEmptyTextBlock(focusTextBlock.node) && !types.isPortableTextTextBlock(nextBlock.node) ? {
focusTextBlock,
nextBlock
} : !1;
},
actions: [(_, {
focusTextBlock,
nextBlock
}) => [raise({
type: "delete.block",
at: focusTextBlock.path
}), raise({
type: "select",
at: {
anchor: {
path: nextBlock.path,
offset: 0
},
focus: {
path: nextBlock.path,
offset: 0
}
}
})]]
}, coreBlockObjectBehaviors = {
arrowDownOnLonelyBlockObject,
arrowUpOnLonelyBlockObject,
breakingBlockObject,
clickingAboveLonelyBlockObject,
clickingBelowLonelyBlockObject,
deletingEmptyTextBlockAfterBlockObject,
deletingEmptyTextBlockBeforeBlockObject
}, coreDecoratorBehaviors = {
strongShortcut: {
on: "keyboard.keydown",
guard: ({
snapshot,
event
}) => isHotkey("mod+b", event.originEvent) && snapshot.context.schema.decorators.some((decorator) => decorator.name === "strong"),
actions: [() => [raise({
type: "decorator.toggle",
decorator: "strong"
})]]
},
emShortcut: {
on: "keyboard.keydown",
guard: ({
snapshot,
event
}) => isHotkey("mod+i", event.originEvent) && snapshot.context.schema.decorators.some((decorator) => decorator.name === "em"),
actions: [() => [raise({
type: "decorator.toggle",
decorator: "em"
})]]
},
underlineShortcut: {
on: "keyboard.keydown",
guard: ({
snapshot,
event
}) => isHotkey("mod+u", event.originEvent) && snapshot.context.schema.decorators.some((decorator) => decorator.name === "underline"),
actions: [() => [raise({
type: "decorator.toggle",
decorator: "underline"
})]]
},
codeShortcut: {
on: "keyboard.keydown",
guard: ({
snapshot,
event
}) => isHotkey("mod+'", event.originEvent) && snapshot.context.schema.decorators.some((decorator) => decorator.name === "code"),
actions: [() => [raise({
type: "decorator.toggle",
decorator: "code"
})]]
}
}, coreDndBehaviors = [
/**
* When dragging over the drag origin, we don't want to show the caret in the
* text.
*/
{
on: "drag.dragover",
guard: ({
snapshot,
event
}) => {
const dragOrigin = snapshot.beta.internalDrag?.origin;
return dragOrigin ? selector_isOverlappingSelection.isOverlappingSelection(event.position.selection)({
...snapshot,
context: {
...snapshot.context,
selection: dragOrigin.selection
}
}) : !1;
},
actions: [() => [{
type: "noop"
}]]
}
], breakingAtTheEndOfTextBlock = {
on: "insert.break",
guard: ({
snapshot
}) => {
const focusTextBlock = selector_isOverlappingSelection.getFocusTextBlock(snapshot), selectionCollapsed = selector_isOverlappingSelection.isSelectionCollapsed(snapshot);
if (!snapshot.context.selection || !focusTextBlock || !selectionCollapsed)
return !1;
const atTheEndOfBlock = selector_isOverlappingSelection.isAtTheEndOfBlock(focusTextBlock)(snapshot), focusListItem = focusTextBlock.node.listItem, focusLevel = focusTextBlock.node.level;
return atTheEndOfBlock ? {
focusListItem,
focusLevel
} : !1;
},
actions: [({
snapshot
}, {
focusListItem,
focusLevel
}) => [raise({
type: "insert.block",
block: {
_type: snapshot.context.schema.block.name,
children: [{
_type: snapshot.context.schema.span.name,
text: "",
marks: []
}],
markDefs: [],
listItem: focusListItem,
level: focusLevel,
style: snapshot.context.schema.styles[0]?.name
},
placement: "after"
})]]
}, breakingAtTheStartOfTextBlock = {
on: "insert.break",
guard: ({
snapshot
}) => {
const focusTextBlock = selector_isOverlappingSelection.getFocusTextBlock(snapshot), selectionCollapsed = selector_isOverlappingSelection.isSelectionCollapsed(snapshot);
if (!snapshot.context.selection || !focusTextBlock || !selectionCollapsed)
return !1;
const focusSpan = selector_isOverlappingSelection.getFocusSpan(snapshot), focusDecorators = focusSpan?.node.marks?.filter((mark) => snapshot.context.schema.decorators.some((decorator) => decorator.name === mark) ?? []), focusAnnotations = focusSpan?.node.marks?.filter((mark) => !snapshot.context.schema.decorators.some((decorator) => decorator.name === mark)) ?? [], focusListItem = focusTextBlock.node.listItem, focusLevel = focusTextBlock.node.level;
return selector_isOverlappingSelection.isAtTheStartOfBlock(focusTextBlock)(snapshot) ? {
focusAnnotations,
focusDecorators,
focusListItem,
focusLevel
} : !1;
},
actions: [({
snapshot
}, {
focusAnnotations,
focusDecorators,
focusListItem,
focusLevel
}) => [raise({
type: "insert.block",
block: {
_type: snapshot.context.schema.block.name,
children: [{
_type: snapshot.context.schema.span.name,
marks: focusAnnotations.length === 0 ? focusDecorators : [],
text: ""
}],
listItem: focusListItem,
level: focusLevel,
style: snapshot.context.schema.styles[0]?.name
},
placement: "before",
select: "none"
})]]
}, coreInsertBreakBehaviors = {
breakingAtTheEndOfTextBlock,
breakingAtTheStartOfTextBlock
}, MAX_LIST_LEVEL = 10, clearListOnBackspace = {
on: "delete.backward",
guard: ({
snapshot
}) => {
const selectionCollapsed = selector_isOverlappingSelection.isSelectionCollapsed(snapshot), focusTextBlock = selector_isOverlappingSelection.getFocusTextBlock(snapshot), focusSpan = selector_isOverlappingSelection.getFocusSpan(snapshot);
return !selectionCollapsed || !focusTextBlock || !focusSpan ? !1 : focusTextBlock.node.children[0]._key === focusSpan.node._key && snapshot.context.selection?.focus.offset === 0 && focusTextBlock.node.level === 1 ? {
focusTextBlock
} : !1;
},
actions: [(_, {
focusTextBlock
}) => [raise({
type: "block.unset",
props: ["listItem", "level"],
at: focusTextBlock.path
})]]
}, unindentListOnBackspace = {
on: "delete.backward",
guard: ({
snapshot
}) => {
const selectionCollapsed = selector_isOverlappingSelection.isSelectionCollapsed(snapshot), focusTextBlock = selector_isOverlappingSelection.getFocusTextBlock(snapshot), focusSpan = selector_isOverlappingSelection.getFocusSpan(snapshot);
return !selectionCollapsed || !focusTextBlock || !focusSpan ? !1 : focusTextBlock.node.children[0]._key === focusSpan.node._key && snapshot.context.selection?.focus.offset === 0 && focusTextBlock.node.level !== void 0 && focusTextBlock.node.level > 1 ? {
focusTextBlock,
level: focusTextBlock.node.level - 1
} : !1;
},
actions: [(_, {
focusTextBlock,
level
}) => [raise({
type: "block.set",
props: {
level
},
at: focusTextBlock.path
})]]
}, clearListOnEnter = {
on: "insert.break",
guard: ({
snapshot
}) => {
const selectionCollapsed = selector_isOverlappingSelection.isSelectionCollapsed(snapshot), focusListBlock = selector_isOverlappingSelection.getFocusListBlock(snapshot);
return !selectionCollapsed || !focusListBlock || !util_sliceBlocks.isEmptyTextBlock(focusListBlock.node) ? !1 : {
focusListBlock
};
},
actions: [(_, {
focusListBlock
}) => [raise({
type: "block.unset",
props: ["listItem", "level"],
at: focusListBlock.path
})]]
}, indentListOnTab = {
on: "keyboard.keydown",
guard: ({
snapshot,
event
}) => {
if (!isHotkey("Tab", event.originEvent))
return !1;
const selectedBlocks = selector_isOverlappingSelection.getSelectedBlocks(snapshot), guards = selector_isOverlappingSelection.createGuards(snapshot.context), selectedListBlocks = selectedBlocks.flatMap((block) => guards.isListBlock(block.node) ? [{
node: block.node,
path: block.path
}] : []);
return selectedListBlocks.length === selectedBlocks.length ? {
selectedListBlocks
} : !1;
},
actions: [(_, {
selectedListBlocks
}) => selectedListBlocks.map((selectedListBlock) => raise({
type: "block.set",
props: {
level: Math.min(MAX_LIST_LEVEL, Math.max(1, selectedListBlock.node.level + 1))
},
at: selectedListBlock.path
}))]
}, unindentListOnShiftTab = {
on: "keyboard.keydown",
guard: ({
snapshot,
event
}) => {
if (!isHotkey("Shift+Tab", event.originEvent))
return !1;
const selectedBlocks = selector_isOverlappingSelection.getSelectedBlocks(snapshot), guards = selector_isOverlappingSelection.createGuards(snapshot.context), selectedListBlocks = selectedBlocks.flatMap((block) => guards.isListBlock(block.node) ? [{
node: block.node,
path: block.path
}] : []);
return selectedListBlocks.length === selectedBlocks.length ? {
selectedListBlocks
} : !1;
},
actions: [(_, {
selectedListBlocks
}) => selectedListBlocks.map((selectedListBlock) => raise({
type: "block.set",
props: {
level: Math.min(MAX_LIST_LEVEL, Math.max(1, selectedListBlock.node.level - 1))
},
at: selectedListBlock.path
}))]
}, coreListBehaviors = {
clearListOnBackspace,
unindentListOnBackspace,
clearListOnEnter,
indentListOnTab,
unindentListOnShiftTab
}, coreBehaviors = [coreAnnotationBehaviors.addAnnotationOnCollapsedSelection, coreDecoratorBehaviors.strongShortcut, coreDecoratorBehaviors.emShortcut, coreDecoratorBehaviors.underlineShortcut, coreDecoratorBehaviors.codeShortcut, ...coreDndBehaviors, coreBlockObjectBehaviors.clickingAboveLonelyBlockObject, coreBlockObjectBehaviors.clickingBelowLonelyBlockObject, coreBlockObjectBehaviors.arrowDownOnLonelyBlockObject, coreBlockObjectBehaviors.arrowUpOnLonelyBlockObject, coreBlockObjectBehaviors.breakingBlockObject, coreBlockObjectBehaviors.deletingEmptyTextBlockAfterBlockObject, coreBlockObjectBehaviors.deletingEmptyTextBlockBeforeBlockObject, coreListBehaviors.clearListOnBackspace, coreListBehaviors.unindentListOnBackspace, coreListBehaviors.clearListOnEnter, coreListBehaviors.indentListOnTab, coreListBehaviors.unindentListOnShiftTab, coreInsertBreakBehaviors.breakingAtTheEndOfTextBlock, coreInsertBreakBehaviors.breakingAtTheStartOfTextBlock];
exports.coreBehaviors = coreBehaviors;
exports.defineBehavior = defineBehavior;
exports.effect = effect;
exports.execute = execute;
exports.isHotkey = isHotkey;
exports.noop = noop;
exports.raise = raise;
//# sourceMappingURL=behavior.core.cjs.map