UNPKG

@limetech/lime-elements

Version:
242 lines (241 loc) • 8.45 kB
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