2026-02-12 20:20:17 +08:00
|
|
|
import '@tiptap/extension-text-style'
|
|
|
|
|
import { Extension, Node } from '@tiptap/core'
|
|
|
|
|
import Image from '@tiptap/extension-image'
|
|
|
|
|
import { VueNodeViewRenderer } from '@tiptap/vue-3'
|
|
|
|
|
import ImageResizeComponent from '../components/ImageResizeComponent.vue'
|
|
|
|
|
import VideoPlayerComponent from '../components/VideoPlayerComponent.vue'
|
|
|
|
|
|
|
|
|
|
declare module '@tiptap/core' {
|
|
|
|
|
interface Commands<ReturnType> {
|
|
|
|
|
fontSize: {
|
|
|
|
|
setFontSize: (size: string) => ReturnType
|
|
|
|
|
unsetFontSize: () => ReturnType
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export const FontSize = Extension.create({
|
|
|
|
|
name: 'fontSize',
|
|
|
|
|
|
|
|
|
|
addOptions() {
|
|
|
|
|
return {
|
|
|
|
|
types: ['textStyle'],
|
|
|
|
|
}
|
|
|
|
|
},
|
|
|
|
|
|
|
|
|
|
addGlobalAttributes() {
|
|
|
|
|
return [
|
|
|
|
|
{
|
|
|
|
|
types: this.options.types,
|
|
|
|
|
attributes: {
|
|
|
|
|
fontSize: {
|
|
|
|
|
default: null,
|
|
|
|
|
parseHTML: element => element.style.fontSize.replace(/['"]+/g, ''),
|
|
|
|
|
renderHTML: attributes => {
|
|
|
|
|
if (!attributes.fontSize) {
|
|
|
|
|
return {}
|
|
|
|
|
}
|
|
|
|
|
return {
|
|
|
|
|
style: `font-size: ${attributes.fontSize}`,
|
|
|
|
|
}
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
]
|
|
|
|
|
},
|
|
|
|
|
|
|
|
|
|
addCommands() {
|
|
|
|
|
return {
|
|
|
|
|
setFontSize: fontSize => ({ chain }) => {
|
|
|
|
|
return chain()
|
|
|
|
|
.setMark('textStyle', { fontSize })
|
|
|
|
|
.run()
|
|
|
|
|
},
|
|
|
|
|
unsetFontSize: () => ({ chain }) => {
|
|
|
|
|
return chain()
|
|
|
|
|
.setMark('textStyle', { fontSize: null })
|
|
|
|
|
.removeEmptyTextStyle()
|
|
|
|
|
.run()
|
|
|
|
|
},
|
|
|
|
|
}
|
|
|
|
|
},
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
// ========== 自定义视频扩展 ==========
|
|
|
|
|
export const CustomVideo = Node.create({
|
|
|
|
|
name: 'customVideo',
|
|
|
|
|
group: 'block',
|
|
|
|
|
atom: true,
|
|
|
|
|
draggable: true,
|
|
|
|
|
selectable: true,
|
2026-02-25 14:43:32 +08:00
|
|
|
|
2026-02-12 20:20:17 +08:00
|
|
|
addAttributes() {
|
|
|
|
|
return {
|
|
|
|
|
src: { default: null },
|
|
|
|
|
width: { default: '100%' },
|
|
|
|
|
height: { default: 'auto' },
|
|
|
|
|
controls: { default: true },
|
|
|
|
|
}
|
|
|
|
|
},
|
2026-02-25 14:43:32 +08:00
|
|
|
|
2026-02-12 20:20:17 +08:00
|
|
|
parseHTML() {
|
|
|
|
|
return [
|
|
|
|
|
{
|
|
|
|
|
tag: 'div[data-custom-video]',
|
|
|
|
|
},
|
|
|
|
|
]
|
|
|
|
|
},
|
2026-02-25 14:43:32 +08:00
|
|
|
|
2026-02-12 20:20:17 +08:00
|
|
|
renderHTML({ HTMLAttributes }) {
|
|
|
|
|
return ['div', { 'data-custom-video': '', ...HTMLAttributes }]
|
|
|
|
|
},
|
2026-02-25 14:43:32 +08:00
|
|
|
|
2026-02-12 20:20:17 +08:00
|
|
|
addNodeView() {
|
|
|
|
|
return VueNodeViewRenderer(VideoPlayerComponent)
|
|
|
|
|
},
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
// ========== 自定义图片扩展(支持缩放) ==========
|
|
|
|
|
export const ResizableImage = Image.extend({
|
|
|
|
|
name: 'resizableImage',
|
2026-02-25 14:43:32 +08:00
|
|
|
|
2026-02-12 20:20:17 +08:00
|
|
|
addAttributes() {
|
|
|
|
|
return {
|
|
|
|
|
...this.parent?.(),
|
|
|
|
|
width: { default: null },
|
|
|
|
|
height: { default: null },
|
|
|
|
|
style: { default: null },
|
|
|
|
|
}
|
|
|
|
|
},
|
2026-02-25 14:43:32 +08:00
|
|
|
|
2026-02-12 20:20:17 +08:00
|
|
|
addNodeView() {
|
|
|
|
|
return VueNodeViewRenderer(ImageResizeComponent)
|
|
|
|
|
},
|
2026-02-25 14:43:32 +08:00
|
|
|
|
|
|
|
|
renderHTML({ HTMLAttributes }) {
|
|
|
|
|
return ['img', HTMLAttributes]
|
|
|
|
|
},
|
2026-02-12 20:20:17 +08:00
|
|
|
})
|
|
|
|
|
|