输入链接创建视频 DOM 元素可以这么写
https://github.com/KUN1007/kun-touchgal-next
https://github.com/KUN1007/kun-touchgal-next/tree/main/components/kun/milkdown/plugins/components/video
下面是一个插入 plyr 播放器播放视频链接的插件,这个是编辑器的插入效果
回渲到 DOM 上需要在 remark 时标记这个视频元素,然后在 DOM 挂载时将这个标记的元素替换为 plyr 播放器,这样就可以插入了
'use client'
import { $command, $inputRule, $node, $remark } from '@milkdown/utils'
import { Node } from '@milkdown/prose/model'
import { InputRule } from '@milkdown/prose/inputrules'
import { createRoot } from 'react-dom/client'
import dynamic from 'next/dynamic'
import directive from 'remark-directive'
export const kunVideoRemarkDirective = $remark('kun-video', () => directive)
const KunPlyr = dynamic(() => import('./Plyr').then((mod) => mod.KunPlyr), {
ssr: false
})
export const videoNode = $node('kun-video', () => ({
content: 'block+',
group: 'block',
selectable: true,
draggable: true,
atom: true,
isolating: true,
defining: true,
marks: '',
attrs: {
src: { default: '' }
},
parseDOM: [
{
tag: 'div[data-video-player]',
getAttrs: (dom) => ({
src: dom.getAttribute('data-src')
})
}
],
toDOM: (node: Node) => {
const container = document.createElement('div')
container.setAttribute('data-video-player', '')
container.setAttribute('data-src', node.attrs.src)
container.setAttribute('contenteditable', 'false')
container.className = 'w-full my-4 overflow-hidden shadow-lg rounded-xl'
const root = createRoot(container)
root.render(<KunPlyr src={node.attrs.src} />)
return container
},
parseMarkdown: {
match: (node) => node.name === 'kun-video',
runner: (state, node, type) => {
state.addNode(type, { src: (node.attributes as { src: string }).src })
}
},
toMarkdown: {
match: (node) => node.type.name === 'kun-video',
runner: (state, node) => {
state.addNode('leafDirective', undefined, undefined, {
name: 'kun-video',
attributes: node.attrs
})
}
}
}))
interface InsertKunVideoCommandPayload {
src: string
}
export const insertKunVideoCommand = $command(
'InsertKunVideo',
(ctx) =>
(payload: InsertKunVideoCommandPayload = { src: '' }) =>
(state, dispatch) => {
if (!dispatch) {
return true
}
const { src = '' } = payload
const node = videoNode.type(ctx).create({ src })
if (!node) {
return true
}
dispatch(state.tr.replaceSelectionWith(node).scrollIntoView())
return true
}
)
export const videoInputRule = $inputRule(
(ctx) =>
new InputRule(
/{{kun-video="(?<src>[^"]+)?"?\}}/,
(state, match, start, end) => {
const [matched, src = ''] = match
const { tr } = state
if (matched) {
return tr.replaceWith(
start - 1,
end,
videoNode.type(ctx).create({ src })
)
}
return null
}
)
)
论坛的这个是一个 TOC,实现代码在
https://github.com/KUN1007/kun-galgame-nuxt3/blob/master/components/topic/detail/TableOfContent.vue
https://github.com/KUN1007/kun-galgame-nuxt3/blob/master/composables/topic/useTopicTOC.ts
这个代码的要点是生成了一个 heading list 以及使用 IntersectionObserver 监视这个 list,所以当页面滚动时对应的 TOC item 会变为高亮
这里使用了一个 composable ,如果你使用 React,这类似于 hooks
import { ref } from 'vue'
interface TOCItem {
id: string
text: string
level: number
type: 'heading' | 'reply'
}
export const useTopicTOC = () => {
const headings = ref<TOCItem[]>([])
const activeId = ref('')
let observer: IntersectionObserver | null = null
const refreshTOC = () => {
if (observer) {
observer.disconnect()
}
const elements = Array.from(
document.querySelectorAll(
'.kun-master h1, .kun-master h2, .kun-master h3, .kun-reply'
)
)
headings.value = elements.map((element) => {
if (element.matches('.kun-master h1, .kun-master h2, .kun-master h3')) {
return {
id: element.id,
text: element.textContent || '',
level: Number(element.tagName.charAt(1)),
type: 'heading' as const
}
} else {
const [floor, content] = element.id.split('.', 2)
return {
id: element.id,
text: content ? `${floor}. ${content}` : floor,
level: 2,
type: 'reply' as const
}
}
})
observer = new IntersectionObserver(
(entries) => {
const visibleEntry = entries.find((entry) => entry.isIntersecting)
if (visibleEntry) activeId.value = visibleEntry.target.id
},
{ rootMargin: '0px 0px -80% 0px' }
)
elements.forEach((element) => observer?.observe(element))
}
onMounted(() => {
refreshTOC()
})
onBeforeUnmount(() => {
if (observer) {
observer.disconnect()
}
})
return {
headings,
activeId,
refreshTOC
}
}
点击滚动的逻辑在这里,这里是纯 DOM 操作,点击后会滚动到相应的位置并且添加高亮
https://github.com/KUN1007/kun-galgame-nuxt3/blob/master/components/topic/_helper.ts
export const scrollPage = throttle((rid: number) => {
let timeout: NodeJS.Timeout | null = null
const element = document.querySelector(`[id^="${rid}"]`) as HTMLElement
if (element) {
element.scrollIntoView({ behavior: 'smooth', block: 'center' })
element.classList.add(
'outline-2',
'outline-offset-2',
'outline-primary',
'rounded-lg'
)
if (timeout !== null) {
clearTimeout(timeout)
}
timeout = setTimeout(() => {
element.classList.remove(
'outline-2',
'outline-offset-2',
'outline-primary',
'rounded-lg'
)
}, 3000)
} else {
useMessage(10215, 'info')
}
}, 1000)
export const scrollToTOCElement = (id: string) => {
let timeout: NodeJS.Timeout | null = null
const element = document.getElementById(id)
if (element) {
element.scrollIntoView({ behavior: 'smooth', block: 'center' })
element.classList.add(
'outline-2',
'outline-offset-2',
'outline-primary',
'rounded-lg'
)
if (timeout !== null) {
clearTimeout(timeout)
}
timeout = setTimeout(() => {
element.classList.remove(
'outline-2',
'outline-offset-2',
'outline-primary',
'rounded-lg'
)
}, 3000)
}
}
如果还有任何不懂的问题欢迎继续提问,也可以加入开发群组 https://t.me/KUNForum 或者闲聊群组 https://t.me/kungalgame 以讨论任何关于全栈开发以及 CS 相关的技术问题