147 lines
3 KiB
TypeScript
147 lines
3 KiB
TypeScript
import { Node, nodeInputRule } from '@tiptap/core';
|
|
import { Plugin, PluginKey } from '@tiptap/pm/state';
|
|
|
|
export interface AudioOptions {
|
|
HTMLAttributes: Record<string, unknown>;
|
|
}
|
|
|
|
declare module '@tiptap/core' {
|
|
interface Commands<ReturnType> {
|
|
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<AudioOptions>({
|
|
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(
|
|
`<audio controls autoplay="false" style="width: 100%;" src="${src}"></audio>`
|
|
),
|
|
|
|
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;
|
|
}
|
|
}
|
|
}
|
|
})
|
|
];
|
|
}
|
|
});
|