@limetech/lime-elements
Version:
242 lines (241 loc) • 8.45 kB
JavaScript
import { toggleMark, setBlockType, wrapIn, lift } from 'prosemirror-commands';
import { findWrapping, liftTarget } from 'prosemirror-transform';
import { TextSelection } from 'prosemirror-state';
import { EditorMenuTypes, LevelMapping } from './types';
import { getLinkAttributes } from '../plugins/link/utils';
const setActiveMethodForMark = (command, markType) => {
command.active = (state) => {
const { from, $from, to, empty } = state.selection;
if (empty) {
return !!markType.isInSet(state.storedMarks || $from.marks());
}
else {
return state.doc.rangeHasMark(from, to, markType);
}
};
};
const setActiveMethodForNode = (command, nodeType, level) => {
command.active = (state) => {
const { $from } = state.selection;
const node = $from.node($from.depth);
if (node && node.type.name === nodeType.name) {
if (nodeType.name === LevelMapping.Heading && level) {
return node.attrs.level === level;
}
return true;
}
return false;
};
};
const setActiveMethodForWrap = (command, nodeType) => {
command.active = (state) => {
const { from, to } = state.selection;
for (let pos = from; pos <= to; pos++) {
const resolvedPos = state.doc.resolve(pos);
for (let i = resolvedPos.depth; i > 0; i--) {
const node = resolvedPos.node(i);
if (node && node.type.name === nodeType.name) {
return true;
}
}
}
return false;
};
};
const createInsertLinkCommand = (schema, _, link) => {
const command = (state, dispatch) => {
const { from, to } = state.selection;
const linkMark = schema.marks.link.create(getLinkAttributes(link.href, link.href));
if (from === to) {
// If no text is selected, insert new text with link
const linkText = link.text || link.href;
const newLink = schema.text(linkText, [linkMark]);
dispatch(state.tr.insert(from, newLink));
}
else {
// If text is selected, replace selected text with link text
const selectedText = state.doc.textBetween(from, to, ' ');
const newLink = schema.text(link.text || selectedText, [linkMark]);
dispatch(state.tr.replaceWith(from, to, newLink));
}
return true;
};
setActiveMethodForMark(command, schema.marks.link);
return command;
};
const createToggleMarkCommand = (schema, markName, link) => {
const markType = schema.marks[markName];
if (!markType) {
throw new Error(`Mark "${markName}" not found in schema`);
}
const attrs = getAttributes(markName, link);
const command = toggleMark(markType, attrs);
setActiveMethodForMark(command, markType);
return command;
};
const getAttributes = (markName, link) => {
if (markName === EditorMenuTypes.Link && link.href) {
return {
href: link.href,
target: isExternalLink(link.href) ? '_blank' : null,
};
}
return undefined;
};
export const isExternalLink = (url) => {
return !url.startsWith(window.location.origin);
};
/**
* Toggles or wraps a node type based on the selection and parameters.
* - Toggles to paragraph if the selection is of the specified type.
* - Lifts content out if already wrapped in the specified type.
* - Wraps or sets the selection to the specified type based on `shouldWrap`.
* @param schema - ProseMirror schema.
* @param type - Block type name to toggle.
* @param attrs - Attributes for the block type.
* @param shouldWrap - Wrap selection if true, otherwise directly set the block type for the selection.
* @returns A command based on selection and action needed.
*/
const toggleNodeType = (schema, type, attrs = {}, shouldWrap = false) => {
const nodeType = schema.nodes[type];
const paragraphType = schema.nodes.paragraph;
return (state, dispatch) => {
const { $from, $to } = state.selection;
const hasActiveWrap = $from.node($from.depth - 1).type === nodeType;
if (state.selection instanceof TextSelection &&
// Ensure selection is within the same parent block
// We don't want toggling block types across multiple blocks
$from.sameParent($from.doc.resolve($to.pos))) {
if ($from.parent.type === nodeType) {
if (dispatch) {
dispatch(state.tr.setBlockType($from.pos, $to.pos, paragraphType));
}
return true;
}
else {
if (hasActiveWrap) {
return lift(state, dispatch);
}
if (shouldWrap) {
return wrapIn(nodeType, attrs)(state, dispatch);
}
else {
return setBlockType(nodeType, attrs)(state, dispatch);
}
}
}
return false;
};
};
const createSetNodeTypeCommand = (schema, nodeType, level) => {
const type = schema.nodes[nodeType];
if (!type) {
throw new Error(`Node type "${nodeType}" not found in schema`);
}
let command;
if (nodeType === LevelMapping.Heading && level) {
command = toggleNodeType(schema, LevelMapping.Heading, {
level: level,
});
}
else if (nodeType === EditorMenuTypes.CodeBlock) {
command = toggleNodeType(schema, EditorMenuTypes.CodeBlock);
}
else {
command = setBlockType(type);
}
setActiveMethodForNode(command, type, level);
return command;
};
const createWrapInCommand = (schema, nodeType) => {
const type = schema.nodes[nodeType];
if (!type) {
throw new Error(`Node type "${nodeType}" not found in schema`);
}
let command;
if (nodeType === EditorMenuTypes.Blockquote) {
command = toggleNodeType(schema, EditorMenuTypes.Blockquote, {}, true);
}
else {
command = wrapIn(type);
}
setActiveMethodForWrap(command, type);
return command;
};
const toggleList = (listType) => {
return (state, dispatch) => {
const { $from, $to } = state.selection;
const range = $from.blockRange($to);
if (!range) {
return false;
}
const wrapping = range && findWrapping(range, listType);
if (wrapping) {
// Wrap the selection in a list
if (dispatch) {
dispatch(state.tr.wrap(range, wrapping).scrollIntoView());
}
return true;
}
else {
// Check if we are in a list item and lift out of the list
const liftRange = range && liftTarget(range);
if (liftRange !== null) {
if (dispatch) {
dispatch(state.tr.lift(range, liftRange).scrollIntoView());
}
return true;
}
return false;
}
};
};
const createListCommand = (schema, listType) => {
const type = schema.nodes[listType];
if (!type) {
throw new Error(`List type "${listType}" not found in schema`);
}
const command = toggleList(type);
setActiveMethodForWrap(command, type);
return command;
};
const commandMapping = {
strong: createToggleMarkCommand,
em: createToggleMarkCommand,
underline: createToggleMarkCommand,
strikethrough: createToggleMarkCommand,
code: createToggleMarkCommand,
link: createInsertLinkCommand,
headerlevel1: (schema) => createSetNodeTypeCommand(schema, LevelMapping.Heading, LevelMapping.one),
headerlevel2: (schema) => createSetNodeTypeCommand(schema, LevelMapping.Heading, LevelMapping.two),
headerlevel3: (schema) => createSetNodeTypeCommand(schema, LevelMapping.Heading, LevelMapping.three),
blockquote: (schema) => createWrapInCommand(schema, EditorMenuTypes.Blockquote),
code_block: (schema) => createSetNodeTypeCommand(schema, EditorMenuTypes.CodeBlock),
ordered_list: (schema) => createListCommand(schema, EditorMenuTypes.OrderedList),
bullet_list: (schema) => createListCommand(schema, EditorMenuTypes.BulletList),
};
export class MenuCommandFactory {
constructor(schema) {
this.schema = schema;
}
getCommand(mark, link) {
const commandFunc = commandMapping[mark];
if (!commandFunc) {
throw new Error(`The Mark "${mark}" is not supported`);
}
return commandFunc(this.schema, mark, link);
}
buildKeymap() {
return {
'Mod-B': this.getCommand(EditorMenuTypes.Bold),
'Mod-I': this.getCommand(EditorMenuTypes.Italic),
'Mod-Shift-1': this.getCommand(EditorMenuTypes.HeaderLevel1),
'Mod-Shift-2': this.getCommand(EditorMenuTypes.HeaderLevel2),
'Mod-Shift-3': this.getCommand(EditorMenuTypes.HeaderLevel3),
'Mod-Shift-X': this.getCommand(EditorMenuTypes.Strikethrough),
'Mod-`': this.getCommand(EditorMenuTypes.Code),
'Mod-Shift-C': this.getCommand(EditorMenuTypes.CodeBlock),
};
}
}
//# sourceMappingURL=menu-commands.js.map