Skip to content
This repository has been archived by the owner on Nov 29, 2023. It is now read-only.

feat: add supports for video and audio #4

Merged
merged 2 commits into from
Jan 31, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions example/src/App.vue
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,8 @@ import {
ExtensionHighlight,
ExtensionCommands,
ExtensionIframe,
ExtensionVideo,
ExtensionAudio,
CommandsSuggestion,
CommandHeader1,
CommandHeader2,
Expand All @@ -46,6 +48,8 @@ import {
CommandHeader6,
CommandCodeBlock,
CommandIframe,
CommandVideo,
CommandAudio,
CommandTable,
CommandBulletList,
CommandOrderedList,
Expand Down Expand Up @@ -124,6 +128,8 @@ const editor = useEditor({
placeholder: "输入 / 以选择输入类型",
}),
ExtensionHighlight,
ExtensionVideo,
ExtensionAudio,
ExtensionCommands.configure({
suggestion: {
...CommandsSuggestion,
Expand All @@ -141,6 +147,8 @@ const editor = useEditor({
CommandOrderedList,
CommandTaskList,
CommandIframe,
CommandVideo,
CommandAudio,
].filter((item) =>
[...item.keywords, item.title].some((keyword) =>
keyword.includes(query)
Expand Down
130 changes: 130 additions & 0 deletions packages/editor/src/extensions/audio/AudioView.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,130 @@
<script lang="ts" setup>
import type { Node as ProseMirrorNode } from "prosemirror-model";
import type { Decoration } from "prosemirror-view";
import { Editor, NodeViewWrapper, Node } from "@tiptap/vue-3";
import { computed } from "vue";
import BlockCard from "@/components/block/BlockCard.vue";
import BlockActionButton from "@/components/block/BlockActionButton.vue";
import BlockActionSeparator from "@/components/block/BlockActionSeparator.vue";
import MdiLinkVariant from "~icons/mdi/link-variant";
import MdiPlayCircle from "~icons/mdi/play-circle";
import MdiPlayCircleOutline from "~icons/mdi/play-circle-outline";
import MdiMotionPlayOutline from "~icons/mdi/motion-play-outline";
import MdiMotionPlay from "~icons/mdi/motion-play";

const props = defineProps<{
editor: Editor;
node: ProseMirrorNode;
decorations: Decoration[];
selected: boolean;
extension: Node<any, any>;
getPos: () => number;
updateAttributes: (attributes: Record<string, any>) => void;
deleteNode: () => void;
}>();

const src = computed({
get: () => {
return props.node?.attrs.src;
},
set: (src: string) => {
props.updateAttributes({ src: src });
},
});

const autoplay = computed(() => {
return props.node.attrs.autoplay;
});

const loop = computed(() => {
return props.node.attrs.loop;
});

function handleSetFocus() {
props.editor.commands.setNodeSelection(props.getPos());
}

function handleToggleAutoplay() {
props.updateAttributes({
autoplay: props.node.attrs.autoplay ? null : true,
});
props.editor.chain().focus().setNodeSelection(props.getPos()).run();
}

function handleToggleLoop() {
props.updateAttributes({
loop: props.node.attrs.loop ? null : true,
});
props.editor.chain().focus().setNodeSelection(props.getPos()).run();
}

function handleOpenLink() {
window.open(src.value, "_blank");
}
</script>

<template>
<node-view-wrapper as="div">
<block-card
:editor="editor"
:get-pos="getPos"
:delete-node="deleteNode"
:selected="selected"
>
<template #content>
<div
class="inline-block overflow-hidden transition-all text-center relative h-full w-full"
>
<div class="py-1.5">
<input
v-model.lazy="src"
class="block px-2 w-full py-1.5 text-sm text-gray-900 border border-gray-300 rounded-md bg-gray-50 focus:ring-blue-500 focus:border-blue-500"
placeholder="输入链接,按回车确定"
tabindex="-1"
@focus="handleSetFocus"
/>
</div>
<audio
v-if="src"
controls
:autoplay="autoplay"
:loop="loop"
:src="node!.attrs.src"
@mouseenter="handleSetFocus"
></audio>
</div>
</template>
<template #actions>
<BlockActionButton
:selected="autoplay"
:tooltip="`${autoplay ? '关闭自动播放' : '开启自动播放'}`"
@click="handleToggleAutoplay"
>
<template #icon>
<MdiPlayCircle v-if="autoplay" />
<MdiPlayCircleOutline v-else />
</template>
</BlockActionButton>

<BlockActionButton
:selected="loop"
:tooltip="`${loop ? '关闭循环播放' : '开启循环播放'}`"
@click="handleToggleLoop"
>
<template #icon>
<MdiMotionPlay v-if="loop" />
<MdiMotionPlayOutline v-else />
</template>
</BlockActionButton>

<BlockActionSeparator />

<BlockActionButton tooltip="打开链接" @click="handleOpenLink">
<template #icon>
<MdiLinkVariant />
</template>
</BlockActionButton>
</template>
</block-card>
</node-view-wrapper>
</template>
114 changes: 114 additions & 0 deletions packages/editor/src/extensions/audio/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,114 @@
import { mergeAttributes, Node, nodeInputRule } from "@tiptap/core";
import { VueNodeViewRenderer } from "@tiptap/vue-3";
import AudioView from "./AudioView.vue";

declare module "@tiptap/core" {
interface Commands<ReturnType> {
audio: {
setAudio: (options: { src: string }) => ReturnType;
};
}
}

const Audio = Node.create({
name: "audio",

inline() {
return true;
},

group() {
return "inline";
},

addAttributes() {
return {
...this.parent?.(),
src: {
default: null,
parseHTML: (element) => {
return element.getAttribute("src");
},
},
autoplay: {
default: null,
parseHTML: (element) => {
return element.getAttribute("autoplay");
},
renderHTML: (attributes) => {
return {
autoplay: attributes.autoplay,
};
},
},
controls: {
default: true,
parseHTML: (element) => {
return element.getAttribute("controls");
},
renderHTML: (attributes) => {
return {
controls: attributes.controls,
};
},
},
loop: {
default: null,
parseHTML: (element) => {
return element.getAttribute("loop");
},
renderHTML: (attributes) => {
return {
loop: attributes.loop,
};
},
},
};
},

parseHTML() {
return [
{
tag: "audio",
},
];
},

renderHTML({ HTMLAttributes }) {
return [
"audio",
mergeAttributes(this.options.HTMLAttributes, HTMLAttributes),
];
},

addCommands() {
return {
setAudio:
(options) =>
({ commands }) => {
return commands.insertContent({
type: this.name,
attrs: options,
});
},
};
},

addInputRules() {
return [
nodeInputRule({
find: /^\$audio\$$/,
type: this.type,
getAttributes: () => {
return { width: "100%" };
},
}),
];
},

addNodeView() {
return VueNodeViewRenderer(AudioView);
},
});

export default Audio;
38 changes: 38 additions & 0 deletions packages/editor/src/extensions/commands-menu/suggestion.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,8 @@ import MdiFormatListCheckbox from "~icons/mdi/format-list-checkbox";
import MdiFormatListNumbered from "~icons/mdi/format-list-numbered";
import MdiTable from "~icons/mdi/table";
import MdiWeb from "~icons/mdi/web";
import MdiVideo from "~icons/mdi/video";
import MdiMusicCircleOutline from "~icons/mdi/music-circle-outline";
import { markRaw, type Component } from "vue";
import type { SuggestionOptions } from "@tiptap/suggestion";

Expand Down Expand Up @@ -147,6 +149,40 @@ export const CommandIframe: Item = {
},
};

export const CommandVideo: Item = {
icon: markRaw(MdiVideo),
title: "视频",
keywords: ["video", "shipin"],
command: ({ editor, range }: { editor: Editor; range: Range }) => {
editor
.chain()
.focus()
.deleteRange(range)
.insertContent([
{ type: "video", attrs: { src: "" } },
{ type: "paragraph", content: "" },
])
.run();
},
};

export const CommandAudio: Item = {
icon: markRaw(MdiMusicCircleOutline),
title: "音频",
keywords: ["audio", "yinpin"],
command: ({ editor, range }: { editor: Editor; range: Range }) => {
editor
.chain()
.focus()
.deleteRange(range)
.insertContent([
{ type: "audio", attrs: { src: "" } },
{ type: "paragraph", content: "" },
])
.run();
},
};

export const CommandTable: Item = {
icon: markRaw(MdiTable),
title: "表格",
Expand Down Expand Up @@ -204,6 +240,8 @@ export default {
CommandOrderedList,
CommandTaskList,
CommandIframe,
CommandVideo,
CommandAudio,
]
.filter((item) =>
[...item.keywords, item.title].some((keyword) =>
Expand Down
6 changes: 6 additions & 0 deletions packages/editor/src/extensions/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,8 @@ import {
} from "../extensions/commands-menu";
import { ExtensionCodeBlock, lowlight } from "@/extensions/code-block";
import ExtensionIframe from "./iframe";
import ExtensionVideo from "./video";
import ExtensionAudio from "./audio";
import ExtensionImage from "./image";
import ExtensionTable from "./table";

Expand Down Expand Up @@ -81,6 +83,8 @@ const allExtensions = [
lowlight,
}),
ExtensionIframe,
ExtensionVideo,
ExtensionAudio,
];

export {
Expand Down Expand Up @@ -118,4 +122,6 @@ export {
ExtensionCodeBlock,
lowlight,
ExtensionIframe,
ExtensionVideo,
ExtensionAudio,
};
Loading