UNPKG

@moontra/moonui-pro

Version:

Premium React components for MoonUI - Advanced UI library with 50+ pro components including performance, interactive, and gesture components

220 lines (190 loc) 7.08 kB
import { Extension } from '@tiptap/core'; import { Plugin, PluginKey } from '@tiptap/pm/state'; import { Decoration, DecorationSet } from '@tiptap/pm/view'; import type { SlashCommand } from '@/lib/ai-providers'; declare module '@tiptap/core' { interface Commands<ReturnType> { slashCommands: { selectSlashCommand: (index: number) => ReturnType; }; } } interface SlashCommandsOptions { commands: SlashCommand[]; onSelectCommand: (command: SlashCommand) => void; } const slashCommandsPluginKey = new PluginKey('slashCommands'); export const SlashCommandsExtension = Extension.create<SlashCommandsOptions>({ name: 'slashCommands', addOptions() { return { commands: [], onSelectCommand: () => {}, }; }, addCommands() { return { selectSlashCommand: (index: number) => ({ editor }) => { const state = slashCommandsPluginKey.getState(editor.state); if (state && state.active && state.filteredCommands[index]) { const command = state.filteredCommands[index]; // Clear the slash command const { from, to } = state.range; editor.chain() .deleteRange({ from, to }) .focus() .run(); // Execute the command this.options.onSelectCommand(command); return true; } return false; }, }; }, addKeyboardShortcuts() { return { ArrowUp: ({ editor }) => { const state = slashCommandsPluginKey.getState(editor.state); if (state && state.active) { const newIndex = state.selectedIndex > 0 ? state.selectedIndex - 1 : state.filteredCommands.length - 1; editor.view.dispatch( editor.state.tr.setMeta(slashCommandsPluginKey, { selectedIndex: newIndex }) ); return true; } return false; }, ArrowDown: ({ editor }) => { const state = slashCommandsPluginKey.getState(editor.state); if (state && state.active) { const newIndex = state.selectedIndex < state.filteredCommands.length - 1 ? state.selectedIndex + 1 : 0; editor.view.dispatch( editor.state.tr.setMeta(slashCommandsPluginKey, { selectedIndex: newIndex }) ); return true; } return false; }, Enter: ({ editor }) => { const state = slashCommandsPluginKey.getState(editor.state); if (state && state.active) { editor.commands.selectSlashCommand(state.selectedIndex); return true; } return false; }, Escape: ({ editor }) => { const state = slashCommandsPluginKey.getState(editor.state); if (state && state.active) { editor.view.dispatch( editor.state.tr.setMeta(slashCommandsPluginKey, { active: false }) ); return true; } return false; }, }; }, addProseMirrorPlugins() { return [ new Plugin({ key: slashCommandsPluginKey, state: { init() { return { active: false, range: { from: 0, to: 0 }, query: '', filteredCommands: [], selectedIndex: 0, }; }, apply(transaction, value, oldState, newState) { const meta = transaction.getMeta(slashCommandsPluginKey); if (meta) { return { ...value, ...meta }; } // Check for slash character const { selection } = newState; const { empty, $from } = selection; if (!empty) { return { ...value, active: false }; } const textBefore = $from.parent.textBetween( Math.max(0, $from.parentOffset - 50), $from.parentOffset, null, '\ufffc' ); const match = textBefore.match(/\/(\w*)$/); if (match) { const query = match[1].toLowerCase(); const filteredCommands = this.options.commands.filter(cmd => cmd.command.toLowerCase().includes(query) || cmd.description.toLowerCase().includes(query) ); return { active: true, range: { from: $from.pos - match[0].length, to: $from.pos, }, query, filteredCommands, selectedIndex: 0, }; } return { ...value, active: false }; }, }, props: { decorations(state) { const pluginState = this.getState(state); if (!pluginState.active) { return DecorationSet.empty; } // Slash menu decoration const { from } = pluginState.range; const decoration = Decoration.widget(from, () => { const container = document.createElement('div'); container.className = 'slash-commands-menu'; pluginState.filteredCommands.forEach((cmd, index) => { const item = document.createElement('div'); item.className = `slash-command-item ${index === pluginState.selectedIndex ? 'selected' : ''}`; if (cmd.icon) { const iconWrapper = document.createElement('div'); iconWrapper.innerHTML = cmd.icon; item.appendChild(iconWrapper); } const textWrapper = document.createElement('div'); const commandName = document.createElement('div'); commandName.className = 'command-name'; commandName.textContent = `/${cmd.command}`; textWrapper.appendChild(commandName); const commandDesc = document.createElement('div'); commandDesc.className = 'command-description'; commandDesc.textContent = cmd.description; textWrapper.appendChild(commandDesc); item.appendChild(textWrapper); item.addEventListener('click', () => { const editor = (state as any).editor; editor.commands.selectSlashCommand(index); }); container.appendChild(item); }); if (pluginState.filteredCommands.length === 0) { const empty = document.createElement('div'); empty.className = 'text-gray-500 p-3 text-sm'; empty.textContent = 'No commands found'; container.appendChild(empty); } return container; }); return DecorationSet.create(state.doc, [decoration]); }, }, }), ]; }, });