import { Node, nodeInputRule } from '@tiptap/core' import { Plugin, PluginKey } from '@tiptap/pm/state' export interface AudioOptions { HTMLAttributes: Record } declare module '@tiptap/core' { interface Commands { audio: { /** * Set a audio node */ setAudio: (src: string) => ReturnType /** * Toggle a audio */ toggleAudio: (src: string) => ReturnType /** * Remove a audio */ removeAudio: () => ReturnType } } } const AUDIO_INPUT_REGEX = /!\[(.+|:?)]\((\S+)(?:(?:\s+)["'](\S+)["'])?\)/ export const Audio = Node.create({ name: 'audio', group: 'block', draggable: true, isolating: true, atom: true, addOptions() { return { HTMLAttributes: {} } }, addAttributes() { return { src: { default: null, parseHTML: (el) => (el as HTMLSpanElement).getAttribute('src'), renderHTML: (attrs) => ({ src: attrs.src }) } } }, parseHTML() { return [ { tag: 'audio', getAttrs: (el) => ({ src: (el as HTMLAudioElement).getAttribute('src') }) } ] }, renderHTML({ HTMLAttributes }) { return [ 'audio', { controls: 'true', style: 'width: 100%;', ...HTMLAttributes }, ['source', HTMLAttributes] ] }, addCommands() { return { setAudio: (src: string) => ({ commands }) => commands.insertContent( `` ), toggleAudio: () => ({ commands }) => commands.toggleNode(this.name, 'paragraph'), removeAudio: () => ({ commands }) => commands.deleteNode(this.name) } }, addInputRules() { return [ nodeInputRule({ find: AUDIO_INPUT_REGEX, type: this.type, getAttributes: (match) => { const [, , src] = match return { src } } }) ] }, addProseMirrorPlugins() { return [ new Plugin({ key: new PluginKey('audioDropPlugin'), props: { handleDOMEvents: { drop(view, event) { const { state: { schema, tr }, dispatch } = view const hasFiles = event.dataTransfer && event.dataTransfer.files && event.dataTransfer.files.length if (!hasFiles) return false const audios = Array.from(event.dataTransfer.files).filter((file) => /audio/i.test(file.type) ) if (audios.length === 0) return false event.preventDefault() const coordinates = view.posAtCoords({ left: event.clientX, top: event.clientY }) audios.forEach((audio) => { const reader = new FileReader() reader.onload = (readerEvent) => { const node = schema.nodes.audio.create({ src: readerEvent.target?.result }) if (coordinates && typeof coordinates.pos === 'number') { const transaction = tr.insert(coordinates?.pos, node) dispatch(transaction) } } reader.readAsDataURL(audio) }) return true } } } }) ] } })