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 VideoOptions {
|
|
HTMLAttributes: Record<string, unknown>;
|
|
}
|
|
|
|
declare module '@tiptap/core' {
|
|
interface Commands<ReturnType> {
|
|
video: {
|
|
/**
|
|
* Set a video node
|
|
*/
|
|
setVideo: (src: string) => ReturnType;
|
|
/**
|
|
* Toggle a video
|
|
*/
|
|
toggleVideo: (src: string) => ReturnType;
|
|
/**
|
|
* Remove a video
|
|
*/
|
|
removeVideo: () => ReturnType;
|
|
};
|
|
}
|
|
}
|
|
|
|
const VIDEO_INPUT_REGEX = /!\[(.+|:?)]\((\S+)(?:(?:\s+)["'](\S+)["'])?\)/;
|
|
|
|
export const Video = Node.create<VideoOptions>({
|
|
name: 'video',
|
|
group: 'block',
|
|
content: 'inline*',
|
|
draggable: true,
|
|
isolating: true,
|
|
addOptions() {
|
|
return {
|
|
HTMLAttributes: {}
|
|
};
|
|
},
|
|
addAttributes() {
|
|
return {
|
|
src: {
|
|
default: null,
|
|
parseHTML: (el) => (el as HTMLSpanElement).getAttribute('src'),
|
|
renderHTML: (attrs) => ({ src: attrs.src })
|
|
}
|
|
};
|
|
},
|
|
parseHTML() {
|
|
return [
|
|
{
|
|
tag: 'video',
|
|
getAttrs: (el) => ({ src: (el as HTMLVideoElement).getAttribute('src') })
|
|
}
|
|
];
|
|
},
|
|
|
|
renderHTML({ HTMLAttributes }) {
|
|
return [
|
|
'video',
|
|
{ controls: 'true', style: 'width: fit-content;', ...HTMLAttributes },
|
|
['source', HTMLAttributes]
|
|
];
|
|
},
|
|
addCommands() {
|
|
return {
|
|
setVideo:
|
|
(src: string) =>
|
|
({ commands }) =>
|
|
commands.insertContent(
|
|
`<video controls="true" autoplay="false" style="width: fit-content" src="${src}" />`
|
|
),
|
|
|
|
toggleVideo:
|
|
() =>
|
|
({ commands }) =>
|
|
commands.toggleNode(this.name, 'paragraph'),
|
|
removeVideo:
|
|
() =>
|
|
({ commands }) =>
|
|
commands.deleteNode(this.name)
|
|
};
|
|
},
|
|
addInputRules() {
|
|
return [
|
|
nodeInputRule({
|
|
find: VIDEO_INPUT_REGEX,
|
|
type: this.type,
|
|
getAttributes: (match) => {
|
|
const [, , src] = match;
|
|
|
|
return { src };
|
|
}
|
|
})
|
|
];
|
|
},
|
|
addProseMirrorPlugins() {
|
|
return [
|
|
new Plugin({
|
|
key: new PluginKey('videoDropPlugin'),
|
|
|
|
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 videos = Array.from(event.dataTransfer.files).filter((file) =>
|
|
/video/i.test(file.type)
|
|
);
|
|
|
|
if (videos.length === 0) return false;
|
|
|
|
event.preventDefault();
|
|
|
|
const coordinates = view.posAtCoords({ left: event.clientX, top: event.clientY });
|
|
|
|
videos.forEach((video) => {
|
|
const reader = new FileReader();
|
|
|
|
reader.onload = (readerEvent) => {
|
|
const node = schema.nodes.video.create({ src: readerEvent.target?.result });
|
|
|
|
if (coordinates && typeof coordinates.pos === 'number') {
|
|
const transaction = tr.insert(coordinates?.pos, node);
|
|
|
|
dispatch(transaction);
|
|
}
|
|
};
|
|
|
|
reader.readAsDataURL(video);
|
|
});
|
|
|
|
return true;
|
|
}
|
|
}
|
|
}
|
|
})
|
|
];
|
|
}
|
|
});
|