UNPKG

@portabletext/editor

Version:

Portable Text Editor made in React

386 lines (385 loc) 13 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: !0 }); var behavior_core = require("../_chunks-cjs/behavior.core.cjs"), selector_isOverlappingSelection = require("../_chunks-cjs/selector.is-overlapping-selection.cjs"), xstate = require("xstate"), selector_getTextBefore = require("../_chunks-cjs/selector.get-text-before.cjs"), behavior_markdown = require("../_chunks-cjs/behavior.markdown.cjs"); function createCodeEditorBehaviors(config) { return [behavior_core.defineBehavior({ on: "keyboard.keydown", guard: ({ snapshot, event }) => { const isMoveUpShortcut = behavior_core.isHotkey(config.moveBlockUpShortcut, event.originEvent), firstBlock = selector_isOverlappingSelection.getFirstBlock(snapshot), selectedBlocks = selector_isOverlappingSelection.getSelectedBlocks(snapshot), blocksAbove = firstBlock?.node._key !== selectedBlocks[0]?.node._key; return !isMoveUpShortcut || !blocksAbove ? !1 : { paths: selectedBlocks.map((block) => block.path) }; }, actions: [(_, { paths }) => paths.map((at) => behavior_core.raise({ type: "move.block up", at }))] }), behavior_core.defineBehavior({ on: "keyboard.keydown", guard: ({ snapshot, event }) => { const isMoveDownShortcut = behavior_core.isHotkey(config.moveBlockDownShortcut, event.originEvent), lastBlock = selector_isOverlappingSelection.getLastBlock(snapshot), selectedBlocks = selector_isOverlappingSelection.getSelectedBlocks(snapshot), blocksBelow = lastBlock?.node._key !== selectedBlocks[selectedBlocks.length - 1]?.node._key; return !isMoveDownShortcut || !blocksBelow ? !1 : { paths: selectedBlocks.map((block) => block.path).reverse() }; }, actions: [(_, { paths }) => paths.map((at) => behavior_core.raise({ type: "move.block down", at }))] })]; } const emojiCharRegEx = /^[a-zA-Z-_0-9]{1}$/, incompleteEmojiRegEx = /:([a-zA-Z-_0-9]+)$/, emojiRegEx = /:([a-zA-Z-_0-9]+):$/; function createEmojiPickerBehaviors(config) { const emojiPickerActor = xstate.createActor(createEmojiPickerMachine()); return emojiPickerActor.start(), emojiPickerActor.subscribe((state) => { config.onMatchesChanged({ matches: state.context.matches }), config.onSelectedIndexChanged({ selectedIndex: state.context.selectedIndex }); }), [behavior_core.defineBehavior({ on: "insert.text", guard: ({ snapshot, event }) => { if (event.text === ":") return !1; if (!emojiCharRegEx.test(event.text)) return { emojis: [] }; const focusBlock = selector_isOverlappingSelection.getFocusTextBlock(snapshot), emojiKeyword = `${selector_getTextBefore.getBlockTextBefore(snapshot)}${event.text}`.match(incompleteEmojiRegEx)?.[1]; return !focusBlock || emojiKeyword === void 0 ? { emojis: [] } : { emojis: config.matchEmojis({ keyword: emojiKeyword }) }; }, actions: [(_, params) => [{ type: "effect", effect: () => { emojiPickerActor.send({ type: "emojis found", matches: params.emojis }); } }]] }), behavior_core.defineBehavior({ on: "insert.text", guard: ({ snapshot, event }) => { if (event.text !== ":") return !1; const matches = emojiPickerActor.getSnapshot().context.matches, selectedIndex = emojiPickerActor.getSnapshot().context.selectedIndex, emoji = matches[selectedIndex] ? config.parseMatch({ match: matches[selectedIndex] }) : void 0, focusBlock = selector_isOverlappingSelection.getFocusTextBlock(snapshot), textBefore = selector_getTextBefore.getBlockTextBefore(snapshot), emojiKeyword = `${textBefore}:`.match(emojiRegEx)?.[1]; if (!focusBlock || emojiKeyword === void 0) return !1; const emojiStringLength = emojiKeyword.length + 2; return emoji ? { focusBlock, emoji, emojiStringLength, textBeforeLength: textBefore.length + 1 } : !1; }, actions: [() => [behavior_core.execute({ type: "insert.text", text: ":" })], (_, params) => [behavior_core.effect(() => { emojiPickerActor.send({ type: "select" }); }), behavior_core.execute({ type: "delete.text", at: { anchor: { path: params.focusBlock.path, offset: params.textBeforeLength - params.emojiStringLength }, focus: { path: params.focusBlock.path, offset: params.textBeforeLength } } }), behavior_core.execute({ type: "insert.text", text: params.emoji })]] }), behavior_core.defineBehavior({ on: "keyboard.keydown", guard: ({ snapshot, event }) => { const matches = emojiPickerActor.getSnapshot().context.matches; if (matches.length === 0) return !1; if (behavior_core.isHotkey("Escape", event.originEvent)) return { action: "reset" }; const isEnter = behavior_core.isHotkey("Enter", event.originEvent), isTab = behavior_core.isHotkey("Tab", event.originEvent); if (isEnter || isTab) { const selectedIndex = emojiPickerActor.getSnapshot().context.selectedIndex, emoji = matches[selectedIndex] ? config.parseMatch({ match: matches[selectedIndex] }) : void 0; if (!emoji) return !1; const focusBlock = selector_isOverlappingSelection.getFocusTextBlock(snapshot), textBefore = selector_getTextBefore.getBlockTextBefore(snapshot), emojiKeyword = textBefore.match(incompleteEmojiRegEx)?.[1]; if (!focusBlock || emojiKeyword === void 0) return !1; const emojiStringLength = emojiKeyword.length + 1; return emoji ? { action: "select", focusBlock, emoji, emojiStringLength, textBeforeLength: textBefore.length } : !1; } const isArrowDown = behavior_core.isHotkey("ArrowDown", event.originEvent), isArrowUp = behavior_core.isHotkey("ArrowUp", event.originEvent); return isArrowDown && matches.length > 0 ? { action: "navigate down" } : isArrowUp && matches.length > 0 ? { action: "navigate up" } : !1; }, actions: [(_, params) => params.action === "select" ? [behavior_core.effect(() => { emojiPickerActor.send({ type: "select" }); }), behavior_core.execute({ type: "delete.text", at: { anchor: { path: params.focusBlock.path, offset: params.textBeforeLength - params.emojiStringLength }, focus: { path: params.focusBlock.path, offset: params.textBeforeLength } } }), behavior_core.execute({ type: "insert.text", text: params.emoji })] : params.action === "navigate up" ? [ // If we are navigating then we want to hijack the key event and // turn it into a noop. behavior_core.noop(), behavior_core.effect(() => { emojiPickerActor.send({ type: "navigate up" }); }) ] : params.action === "navigate down" ? [ // If we are navigating then we want to hijack the key event and // turn it into a noop. behavior_core.noop(), behavior_core.effect(() => { emojiPickerActor.send({ type: "navigate down" }); }) ] : [behavior_core.effect(() => { emojiPickerActor.send({ type: "reset" }); })]] }), behavior_core.defineBehavior({ on: "delete.backward", guard: ({ snapshot, event }) => { if (event.unit !== "character" || emojiPickerActor.getSnapshot().context.matches.length === 0) return !1; const focusBlock = selector_isOverlappingSelection.getFocusTextBlock(snapshot), textBefore = selector_getTextBefore.getBlockTextBefore(snapshot), emojiKeyword = textBefore.slice(0, textBefore.length - 1).match(incompleteEmojiRegEx)?.[1]; return !focusBlock || emojiKeyword === void 0 ? { emojis: [] } : { emojis: config.matchEmojis({ keyword: emojiKeyword }) }; }, actions: [(_, params) => [{ type: "effect", effect: () => { emojiPickerActor.send({ type: "emojis found", matches: params.emojis }); } }]] })]; } function createEmojiPickerMachine() { return xstate.setup({ types: { context: {}, events: {} }, actions: { "assign matches": xstate.assign({ matches: ({ event }) => (xstate.assertEvent(event, "emojis found"), event.matches) }), "reset matches": xstate.assign({ matches: [] }), "reset selected index": xstate.assign({ selectedIndex: 0 }), "increment selected index": xstate.assign({ selectedIndex: ({ context }) => context.selectedIndex === context.matches.length - 1 ? 0 : context.selectedIndex + 1 }), "decrement selected index": xstate.assign({ selectedIndex: ({ context }) => context.selectedIndex === 0 ? context.matches.length - 1 : context.selectedIndex - 1 }) }, guards: { "no matches": ({ context }) => context.matches.length === 0 } }).createMachine({ id: "emoji picker", context: { matches: [], selectedIndex: 0 }, initial: "idle", states: { idle: { on: { "emojis found": { actions: "assign matches", target: "showing matches" } } }, "showing matches": { always: { guard: "no matches", target: "idle" }, exit: ["reset selected index"], on: { "emojis found": { actions: "assign matches" }, "navigate down": { actions: "increment selected index" }, "navigate up": { actions: "decrement selected index" }, reset: { target: "idle", actions: ["reset selected index", "reset matches"] }, select: { target: "idle", actions: ["reset selected index", "reset matches"] } } } } }); } function looksLikeUrl(text) { let looksLikeUrl2 = !1; try { const url = new URL(text); if (!sensibleProtocols.includes(url.protocol)) return !1; looksLikeUrl2 = !0; } catch { } return looksLikeUrl2; } const sensibleProtocols = ["http:", "https:", "mailto:", "tel:"]; function createLinkBehaviors(config) { const pasteLinkOnSelection = behavior_core.defineBehavior({ on: "clipboard.paste", guard: ({ snapshot, event }) => { const selectionCollapsed = selector_isOverlappingSelection.isSelectionCollapsed(snapshot), text = event.originEvent.dataTransfer.getData("text/plain"), url = looksLikeUrl(text) ? text : void 0, annotation = url !== void 0 ? config.linkAnnotation?.({ url, schema: snapshot.context.schema }) : void 0; return annotation && !selectionCollapsed ? { annotation } : !1; }, actions: [(_, { annotation }) => [behavior_core.execute({ type: "annotation.add", annotation })]] }), pasteLinkAtCaret = behavior_core.defineBehavior({ on: "clipboard.paste", guard: ({ snapshot, event }) => { const focusSpan = selector_isOverlappingSelection.getFocusSpan(snapshot), selectionCollapsed = selector_isOverlappingSelection.isSelectionCollapsed(snapshot); if (!focusSpan || !selectionCollapsed) return !1; const text = event.originEvent.dataTransfer.getData("text/plain"), url = looksLikeUrl(text) ? text : void 0, annotation = url !== void 0 ? config.linkAnnotation?.({ url, schema: snapshot.context.schema }) : void 0; return url && annotation && selectionCollapsed ? { focusSpan, annotation, url } : !1; }, actions: [(_, { annotation, url }) => [behavior_core.execute({ type: "insert.span", text: url, annotations: [annotation] })]] }); return [pasteLinkOnSelection, pasteLinkAtCaret]; } exports.coreBehaviors = behavior_core.coreBehaviors; exports.defineBehavior = behavior_core.defineBehavior; exports.effect = behavior_core.effect; exports.execute = behavior_core.execute; exports.noop = behavior_core.noop; exports.raise = behavior_core.raise; exports.createMarkdownBehaviors = behavior_markdown.createMarkdownBehaviors; exports.createCodeEditorBehaviors = createCodeEditorBehaviors; exports.createEmojiPickerBehaviors = createEmojiPickerBehaviors; exports.createLinkBehaviors = createLinkBehaviors; //# sourceMappingURL=index.cjs.map