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

Commit

Permalink
feat: add supports for video and audio (#4)
Browse files Browse the repository at this point in the history
支持添加音视频内容。

Fixes halo-dev/halo#2684

<img width="863" alt="image" src="https://user-images.githubusercontent.com/21301288/215723575-ec961dad-af71-4d21-851b-a929d6023d6e.png">
<img width="847" alt="image" src="https://user-images.githubusercontent.com/21301288/215723637-c717bb02-61a6-4113-9c3b-b7e5b1fc3d52.png">

/kind feature

```release-note
支持添加音视频内容
```
  • Loading branch information
ruibaby authored Jan 31, 2023
1 parent 5aef10f commit 1056a58
Show file tree
Hide file tree
Showing 7 changed files with 665 additions and 0 deletions.
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

0 comments on commit 1056a58

Please sign in to comment.