UNPKG

@portabletext/editor

Version:

Portable Text Editor made in React

635 lines (634 loc) 18.8 kB
import { isSelectionCollapsed, getCaretWordSelection, isSelectionExpanded, getFocusBlockObject, getPreviousBlock, getNextBlock, getFocusTextBlock, isOverlappingSelection, isAtTheEndOfBlock, getFocusSpan, isAtTheStartOfBlock, getFocusListBlock, getSelectedBlocks, createGuards } from "./selector.is-overlapping-selection.js"; import { isPortableTextTextBlock } from "@sanity/types"; import { isEmptyTextBlock } from "./util.slice-blocks.js"; 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 (!isSelectionCollapsed(snapshot)) return !1; const caretWordSelection = getCaretWordSelection(snapshot); return !caretWordSelection || !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) || !isSelectionCollapsed(snapshot)) return !1; const focusBlockObject = getFocusBlockObject(snapshot), nextBlock = 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) || !isSelectionCollapsed(snapshot)) return !1; const focusBlockObject = getFocusBlockObject(snapshot), previousBlock = 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 = getFocusBlockObject(snapshot); return 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 && !isSelectionCollapsed(snapshot)) return !1; const focusBlockObject = getFocusBlockObject({ context: { ...snapshot.context, selection: event.position.selection } }), previousBlock = 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 && !isSelectionCollapsed(snapshot)) return !1; const focusBlockObject = getFocusBlockObject({ context: { ...snapshot.context, selection: event.position.selection } }), nextBlock = 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 = getFocusTextBlock(snapshot), selectionCollapsed = isSelectionCollapsed(snapshot), previousBlock = getPreviousBlock(snapshot); return !focusTextBlock || !selectionCollapsed || !previousBlock ? !1 : isEmptyTextBlock(focusTextBlock.node) && !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 = getFocusTextBlock(snapshot), selectionCollapsed = isSelectionCollapsed(snapshot), nextBlock = getNextBlock(snapshot); return !focusTextBlock || !selectionCollapsed || !nextBlock ? !1 : isEmptyTextBlock(focusTextBlock.node) && !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 ? isOverlappingSelection(event.position.selection)({ ...snapshot, context: { ...snapshot.context, selection: dragOrigin.selection } }) : !1; }, actions: [() => [{ type: "noop" }]] } ], breakingAtTheEndOfTextBlock = { on: "insert.break", guard: ({ snapshot }) => { const focusTextBlock = getFocusTextBlock(snapshot), selectionCollapsed = isSelectionCollapsed(snapshot); if (!snapshot.context.selection || !focusTextBlock || !selectionCollapsed) return !1; const atTheEndOfBlock = 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 = getFocusTextBlock(snapshot), selectionCollapsed = isSelectionCollapsed(snapshot); if (!snapshot.context.selection || !focusTextBlock || !selectionCollapsed) return !1; const focusSpan = 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 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 = isSelectionCollapsed(snapshot), focusTextBlock = getFocusTextBlock(snapshot), focusSpan = 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 = isSelectionCollapsed(snapshot), focusTextBlock = getFocusTextBlock(snapshot), focusSpan = 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 = isSelectionCollapsed(snapshot), focusListBlock = getFocusListBlock(snapshot); return !selectionCollapsed || !focusListBlock || !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 = getSelectedBlocks(snapshot), guards = 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 = getSelectedBlocks(snapshot), guards = 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]; export { coreBehaviors, defineBehavior, effect, execute, isHotkey, noop, raise }; //# sourceMappingURL=behavior.core.js.map