@portabletext/editor
Version:
Portable Text Editor made in React
386 lines (385 loc) • 13 kB
JavaScript
"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