Merge pull request #3 from KUN1007/milkdown
feat: remove wangEditor, use milkdown
This commit is contained in:
commit
c50bbf30b5
7
.vscode/settings.json
vendored
7
.vscode/settings.json
vendored
|
@ -6,13 +6,16 @@
|
|||
"azkhx",
|
||||
"bangumi",
|
||||
"Bilibili",
|
||||
"commonmark",
|
||||
"dompurify",
|
||||
"fontawesome",
|
||||
"galgame",
|
||||
"Galgame",
|
||||
"gsap",
|
||||
"Hardbreak",
|
||||
"iconify",
|
||||
"INTLIFY",
|
||||
"Keymap",
|
||||
"kungal",
|
||||
"kungalgame",
|
||||
"kungalgamer",
|
||||
|
@ -20,13 +23,17 @@
|
|||
"Licence",
|
||||
"loli",
|
||||
"majesticons",
|
||||
"Milkdown",
|
||||
"mockjs",
|
||||
"moemoe",
|
||||
"moemoepoint",
|
||||
"non-moe",
|
||||
"nord",
|
||||
"persistedstate",
|
||||
"Pinia",
|
||||
"Prosemirror",
|
||||
"rdquo",
|
||||
"Shiki",
|
||||
"shinnku",
|
||||
"signin",
|
||||
"sina",
|
||||
|
|
22
package.json
22
package.json
|
@ -18,6 +18,27 @@
|
|||
"preview": "vite preview"
|
||||
},
|
||||
"dependencies": {
|
||||
"@milkdown/core": "^7.3.0",
|
||||
"@milkdown/ctx": "^7.3.0",
|
||||
"@milkdown/plugin-clipboard": "^7.3.0",
|
||||
"@milkdown/plugin-cursor": "^7.3.0",
|
||||
"@milkdown/plugin-emoji": "^7.3.0",
|
||||
"@milkdown/plugin-history": "^7.3.0",
|
||||
"@milkdown/plugin-indent": "^7.3.0",
|
||||
"@milkdown/plugin-listener": "^7.3.0",
|
||||
"@milkdown/plugin-math": "^7.3.0",
|
||||
"@milkdown/plugin-prism": "^7.3.0",
|
||||
"@milkdown/plugin-slash": "^7.3.0",
|
||||
"@milkdown/plugin-tooltip": "^7.3.0",
|
||||
"@milkdown/plugin-trailing": "^7.3.0",
|
||||
"@milkdown/plugin-upload": "^7.3.0",
|
||||
"@milkdown/preset-commonmark": "^7.3.0",
|
||||
"@milkdown/preset-gfm": "^7.3.0",
|
||||
"@milkdown/prose": "^7.3.0",
|
||||
"@milkdown/transformer": "^7.3.0",
|
||||
"@milkdown/utils": "^7.3.0",
|
||||
"@milkdown/vue": "^7.3.0",
|
||||
"@prosemirror-adapter/vue": "^0.2.6",
|
||||
"@wangeditor/editor": "^5.1.23",
|
||||
"@wangeditor/editor-for-vue": "^5.1.12",
|
||||
"animate.css": "^4.1.1",
|
||||
|
@ -25,6 +46,7 @@
|
|||
"dompurify": "^3.0.5",
|
||||
"pinia": "^2.1.6",
|
||||
"pinia-plugin-persistedstate": "^3.2.0",
|
||||
"shiki": "^0.14.4",
|
||||
"vue": "^3.3.4",
|
||||
"vue-i18n": "^9.2.2",
|
||||
"vue-router": "^4.2.4"
|
||||
|
|
1478
pnpm-lock.yaml
1478
pnpm-lock.yaml
File diff suppressed because it is too large
Load diff
94
src/components/Milkdown/MilkdownEditor.vue
Normal file
94
src/components/Milkdown/MilkdownEditor.vue
Normal file
|
@ -0,0 +1,94 @@
|
|||
<script setup lang="ts">
|
||||
import { defaultValueCtx, Editor, rootCtx } from '@milkdown/core'
|
||||
import { Milkdown, useEditor } from '@milkdown/vue'
|
||||
import { commonmark } from '@milkdown/preset-commonmark'
|
||||
import MilkdownMenu from './MilkdownMenu.vue'
|
||||
|
||||
/**
|
||||
* 编辑器插件
|
||||
*/
|
||||
|
||||
// import { slash } from '@milkdown/plugin-slash'
|
||||
// Ctrl + Z
|
||||
import { history, historyKeymap } from '@milkdown/plugin-history'
|
||||
// 数学公式插件
|
||||
import { math } from '@milkdown/plugin-math'
|
||||
// 表情
|
||||
import { emoji } from '@milkdown/plugin-emoji'
|
||||
// 代码语法高亮插件
|
||||
import { prism } from '@milkdown/plugin-prism'
|
||||
import { tooltipFactory } from '@milkdown/plugin-tooltip'
|
||||
import { indent } from '@milkdown/plugin-indent'
|
||||
import { trailing } from '@milkdown/plugin-trailing'
|
||||
import { upload } from '@milkdown/plugin-upload'
|
||||
import { cursor } from '@milkdown/plugin-cursor'
|
||||
import { clipboard } from '@milkdown/plugin-clipboard'
|
||||
import { listener, listenerCtx } from '@milkdown/plugin-listener'
|
||||
|
||||
// prosemirror
|
||||
import { usePluginViewFactory } from '@prosemirror-adapter/vue'
|
||||
const pluginViewFactory = usePluginViewFactory()
|
||||
|
||||
// Tooltip
|
||||
import Tooltip from './MilkdownTooltip.vue'
|
||||
const tooltip = tooltipFactory('Text')
|
||||
|
||||
// code highlight
|
||||
import { milkShiki } from './shiki'
|
||||
|
||||
const markdown = `啊这可海星`
|
||||
|
||||
useEditor((root) =>
|
||||
Editor.make()
|
||||
.config((ctx) => {
|
||||
const listener = ctx.get(listenerCtx)
|
||||
|
||||
// listener.markdownUpdated((ctx, markdown, prevMarkdown) => {
|
||||
// if (markdown !== prevMarkdown) {
|
||||
// YourMarkdownUpdater(markdown)
|
||||
// }
|
||||
// })
|
||||
|
||||
// Ctrl + z
|
||||
ctx.set(historyKeymap.key, {
|
||||
// Remap to one shortcut.
|
||||
Undo: 'Mod-z',
|
||||
// Remap to multiple shortcuts.
|
||||
Redo: ['Mod-y', 'Shift-Mod-z'],
|
||||
})
|
||||
ctx.set(rootCtx, root)
|
||||
ctx.set(defaultValueCtx, markdown)
|
||||
ctx.set(tooltip.key, {
|
||||
view: pluginViewFactory({
|
||||
component: Tooltip,
|
||||
}),
|
||||
})
|
||||
})
|
||||
.use(listener)
|
||||
.use(milkShiki)
|
||||
.use(commonmark)
|
||||
.use(history)
|
||||
.use(math)
|
||||
.use(emoji)
|
||||
.use(prism)
|
||||
.use(tooltip)
|
||||
)
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="editor">
|
||||
<MilkdownMenu />
|
||||
<div class="text-area">
|
||||
<Milkdown />
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.text-area {
|
||||
min-height: 200px;
|
||||
margin-top: 10px;
|
||||
white-space: pre-wrap;
|
||||
border: 1px solid var(--kungalgame-red-4);
|
||||
}
|
||||
</style>
|
64
src/components/Milkdown/MilkdownMenu.vue
Normal file
64
src/components/Milkdown/MilkdownMenu.vue
Normal file
|
@ -0,0 +1,64 @@
|
|||
<script setup lang="ts">
|
||||
import {
|
||||
turnIntoTextCommand,
|
||||
wrapInBlockquoteCommand,
|
||||
wrapInHeadingCommand,
|
||||
downgradeHeadingCommand,
|
||||
createCodeBlockCommand,
|
||||
insertHardbreakCommand,
|
||||
insertHrCommand,
|
||||
insertImageCommand,
|
||||
updateImageCommand,
|
||||
wrapInOrderedListCommand,
|
||||
wrapInBulletListCommand,
|
||||
sinkListItemCommand,
|
||||
splitListItemCommand,
|
||||
liftListItemCommand,
|
||||
liftFirstListItemCommand,
|
||||
toggleEmphasisCommand,
|
||||
toggleInlineCodeCommand,
|
||||
toggleStrongCommand,
|
||||
toggleLinkCommand,
|
||||
updateLinkCommand,
|
||||
} from '@milkdown/preset-commonmark'
|
||||
// 编辑器 Ctrl + z 插件
|
||||
import { redoCommand, undoCommand } from '@milkdown/plugin-history'
|
||||
|
||||
import {
|
||||
insertTableCommand,
|
||||
toggleStrikethroughCommand,
|
||||
} from '@milkdown/preset-gfm'
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="menu">
|
||||
<div icon="undo" @click="undoCommand">undo</div>
|
||||
<div icon="redo" @click="redoCommand">redo</div>
|
||||
<div icon="format_bold" @click="toggleStrongCommand">format_bold</div>
|
||||
<div icon="format_italic" @click="toggleEmphasisCommand">format_italic</div>
|
||||
<div icon="format_strikethrough" @click="toggleStrikethroughCommand">
|
||||
format_strikethrough
|
||||
</div>
|
||||
<div icon="table" @click="insertTableCommand">table</div>
|
||||
<div icon="format_list_bulleted" @click="wrapInBulletListCommand">
|
||||
format_list_bulleted
|
||||
</div>
|
||||
<div icon="format_list_numbered" @click="wrapInOrderedListCommand">
|
||||
format_list_numbered
|
||||
</div>
|
||||
<div icon="format_quote" @click="wrapInBlockquoteCommand">format_quote</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.menu {
|
||||
border: 1px solid var(--kungalgame-blue-4);
|
||||
div {
|
||||
margin: 10px;
|
||||
&:hover {
|
||||
cursor: pointer;
|
||||
color: var(--kungalgame-blue-4);
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
13
src/components/Milkdown/MilkdownProvider.vue
Normal file
13
src/components/Milkdown/MilkdownProvider.vue
Normal file
|
@ -0,0 +1,13 @@
|
|||
<script setup lang="ts">
|
||||
import { MilkdownProvider } from '@milkdown/vue'
|
||||
import MilkdownEditor from './MilkdownEditor.vue'
|
||||
import { ProsemirrorAdapterProvider } from '@prosemirror-adapter/vue'
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<MilkdownProvider>
|
||||
<ProsemirrorAdapterProvider>
|
||||
<MilkdownEditor />
|
||||
</ProsemirrorAdapterProvider>
|
||||
</MilkdownProvider>
|
||||
</template>
|
52
src/components/Milkdown/MilkdownTooltip.vue
Normal file
52
src/components/Milkdown/MilkdownTooltip.vue
Normal file
|
@ -0,0 +1,52 @@
|
|||
<script setup lang="ts">
|
||||
import { TooltipProvider } from '@milkdown/plugin-tooltip'
|
||||
import { toggleStrongCommand } from '@milkdown/preset-commonmark'
|
||||
import { callCommand } from '@milkdown/utils'
|
||||
import { useInstance } from '@milkdown/vue'
|
||||
import { usePluginViewContext } from '@prosemirror-adapter/vue'
|
||||
import { onMounted, onUnmounted, ref, VNodeRef, watch } from 'vue'
|
||||
|
||||
const { view, prevState } = usePluginViewContext()
|
||||
const [loading, get] = useInstance()
|
||||
|
||||
const divRef = ref<VNodeRef>()
|
||||
|
||||
let tooltipProvider: TooltipProvider
|
||||
|
||||
onMounted(() => {
|
||||
tooltipProvider = new TooltipProvider({
|
||||
content: divRef.value as any,
|
||||
})
|
||||
|
||||
tooltipProvider.update(view.value, prevState.value)
|
||||
})
|
||||
|
||||
watch([view, prevState], () => {
|
||||
tooltipProvider?.update(view.value, prevState.value)
|
||||
})
|
||||
|
||||
onUnmounted(() => {
|
||||
tooltipProvider.destroy()
|
||||
})
|
||||
|
||||
const toggleBold = (e: Event) => {
|
||||
if (loading.value) return
|
||||
|
||||
e.preventDefault()
|
||||
|
||||
get()!.action(callCommand(toggleStrongCommand.key))
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div ref="divRef">
|
||||
<button class="tooltip" @mousedown="toggleBold">Bold</button>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.tooltip {
|
||||
padding: 7px;
|
||||
border: 1px solid var(--kungalgame-blue-4);
|
||||
}
|
||||
</style>
|
88
src/components/Milkdown/shiki.ts
Normal file
88
src/components/Milkdown/shiki.ts
Normal file
|
@ -0,0 +1,88 @@
|
|||
import { getHighlighter, Highlighter } from 'shiki'
|
||||
import { $proseAsync } from '@milkdown/utils'
|
||||
import { Node } from '@milkdown/prose/model'
|
||||
import { Plugin, PluginKey } from '@milkdown/prose/state'
|
||||
import { Decoration, DecorationSet } from '@milkdown/prose/view'
|
||||
import { findChildren } from '@milkdown/prose'
|
||||
import { codeBlockSchema } from '@milkdown/preset-commonmark'
|
||||
|
||||
function getDecorations(doc: Node, highlighter: Highlighter) {
|
||||
const decorations: Decoration[] = []
|
||||
|
||||
const children = findChildren((node) => node.type === codeBlockSchema.type())(
|
||||
doc
|
||||
)
|
||||
|
||||
children.forEach(async (block) => {
|
||||
let from = block.pos + 1
|
||||
const { language } = block.node.attrs
|
||||
if (!language) return
|
||||
const nodes = highlighter
|
||||
.codeToThemedTokens(block.node.textContent, language)
|
||||
.map((token) =>
|
||||
token.map(({ content, color }) => ({
|
||||
content,
|
||||
color,
|
||||
}))
|
||||
)
|
||||
nodes.forEach((block) => {
|
||||
block.forEach((node) => {
|
||||
const to = from + node.content.length
|
||||
const decoration = Decoration.inline(from, to, {
|
||||
style: `color: ${node.color}`,
|
||||
})
|
||||
decorations.push(decoration)
|
||||
from = to
|
||||
})
|
||||
from += 1
|
||||
})
|
||||
})
|
||||
|
||||
return DecorationSet.create(doc, decorations)
|
||||
}
|
||||
|
||||
export const milkShiki = $proseAsync(async () => {
|
||||
const highlighter = await getHighlighter({
|
||||
theme: 'nord',
|
||||
langs: ['javascript', 'tsx', 'markdown'],
|
||||
})
|
||||
const key = new PluginKey('shiki')
|
||||
|
||||
return new Plugin({
|
||||
key,
|
||||
state: {
|
||||
init: (_, { doc }) => getDecorations(doc, highlighter),
|
||||
apply: (tr, value, oldState, newState) => {
|
||||
const codeBlockType = codeBlockSchema.type()
|
||||
const isNodeName =
|
||||
newState.selection.$head.parent.type === codeBlockType
|
||||
const isPreviousNodeName =
|
||||
oldState.selection.$head.parent.type === codeBlockType
|
||||
const oldNode = findChildren((node) => node.type === codeBlockType)(
|
||||
oldState.doc
|
||||
)
|
||||
const newNode = findChildren((node) => node.type === codeBlockType)(
|
||||
newState.doc
|
||||
)
|
||||
|
||||
const codeBlockChanged =
|
||||
tr.docChanged &&
|
||||
(isNodeName ||
|
||||
isPreviousNodeName ||
|
||||
oldNode.length !== newNode.length ||
|
||||
oldNode[0]?.node.attrs.language !== newNode[0]?.node.attrs.language)
|
||||
|
||||
if (codeBlockChanged) {
|
||||
return getDecorations(tr.doc, highlighter)
|
||||
}
|
||||
|
||||
return value.map(tr.mapping, tr.doc)
|
||||
},
|
||||
},
|
||||
props: {
|
||||
decorations(state) {
|
||||
return key.getState(state)
|
||||
},
|
||||
},
|
||||
})
|
||||
})
|
|
@ -1,139 +0,0 @@
|
|||
<!--
|
||||
编辑器实例共用组件
|
||||
-->
|
||||
<script setup lang="ts">
|
||||
import '@wangeditor/editor/dist/css/style.css'
|
||||
import '@/styles/editor/editor.scss'
|
||||
import { IDomEditor } from '@wangeditor/editor'
|
||||
import { onBeforeMount, onBeforeUnmount, ref, shallowRef } from 'vue'
|
||||
import { Editor, Toolbar } from '@wangeditor/editor-for-vue'
|
||||
// 导入编辑帖子的 store
|
||||
import { useKUNGalgameEditStore } from '@/store/modules/edit'
|
||||
import { storeToRefs } from 'pinia'
|
||||
// 导入过滤 xss 的工具
|
||||
import DOMPurify from 'dompurify'
|
||||
// 导入防抖函数
|
||||
import { debounce } from '@/utils/debounce'
|
||||
|
||||
const topicData = storeToRefs(useKUNGalgameEditStore())
|
||||
|
||||
// 定义父组件传参
|
||||
const props = defineProps(['height', 'isShowToolbar', 'isShowAdvance'])
|
||||
|
||||
// 自定义编辑区域高度
|
||||
const editorHeight = `height: ${props.height}px`
|
||||
|
||||
// 创建编辑器,必须用 shallowRef
|
||||
const editorRef = shallowRef<IDomEditor | undefined>(undefined)
|
||||
|
||||
// 编辑器内的内容
|
||||
const valueHtml = ref('')
|
||||
// 编辑器文字计数
|
||||
const textCount = ref(0)
|
||||
|
||||
// 编辑器相关配置
|
||||
const editorConfig = {
|
||||
placeholder: 'Moe Moe Moe!',
|
||||
readOnly: false,
|
||||
MENU_CONF: {
|
||||
uploadImage: {
|
||||
server: 'http://127.0.0.1:10008/upload/img',
|
||||
|
||||
timeout: 5 * 1000, // 5s
|
||||
|
||||
fieldName: 'custom-fileName',
|
||||
meta: { token: 'xxx', a: 100 },
|
||||
metaWithUrl: true, // join params to url
|
||||
headers: { Accept: 'text/x-json' },
|
||||
|
||||
maxFileSize: 10 * 1024 * 1024, // 10M
|
||||
|
||||
base64LimitSize: 5 * 1024, // insert base64 format, if file's size less than 5kb
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
const handleCreated = (editor: IDomEditor) => {}
|
||||
|
||||
// 挂载之前载入数据,如果不保存,则不载入
|
||||
onBeforeMount(() => {
|
||||
if (topicData.isSave.value) {
|
||||
valueHtml.value = topicData.content.value
|
||||
}
|
||||
})
|
||||
|
||||
// 组件销毁时,也及时销毁编辑器
|
||||
onBeforeUnmount(() => {
|
||||
const editor = editorRef.value
|
||||
if (editor == null) return
|
||||
editor.destroy()
|
||||
})
|
||||
|
||||
// 编辑器文本改变时自动保存数据
|
||||
const handleChange = (editor: IDomEditor) => {
|
||||
editorRef.value = editor
|
||||
// 创建一个防抖处理函数
|
||||
const debouncedUpdateContent = debounce(() => {
|
||||
// 过滤 xss
|
||||
topicData.content.value = DOMPurify.sanitize(editor.getHtml())
|
||||
}, 1007)
|
||||
|
||||
// 调用防抖处理函数,会在延迟时间内只执行一次更新操作
|
||||
debouncedUpdateContent()
|
||||
// 计算用户输入了多少个字符
|
||||
textCount.value = editor.getText().trim().length
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<!-- 编辑器 -->
|
||||
<div class="editor—wrapper">
|
||||
<!-- 这里不能用 v-if,否则加载不出来 toolBar -->
|
||||
<Toolbar
|
||||
class="toolbar-container"
|
||||
:editor="editorRef"
|
||||
:mode="$props.isShowAdvance ? 'default' : 'simple'"
|
||||
v-show="props.isShowToolbar"
|
||||
/>
|
||||
<Editor
|
||||
:style="editorHeight"
|
||||
v-model="valueHtml"
|
||||
:defaultConfig="editorConfig"
|
||||
@onCreated="handleCreated"
|
||||
@onChange="handleChange"
|
||||
/>
|
||||
<span class="count">{{ textCount + ` ${$tm('edit.word')}` }}</span>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
/* 编辑器的样式 */
|
||||
.editor—wrapper {
|
||||
/* 编辑器的 border */
|
||||
border: 1px solid var(--kungalgame-blue-4);
|
||||
box-sizing: border-box;
|
||||
/* 编辑器的宽度 */
|
||||
width: 100%;
|
||||
margin: 0 auto;
|
||||
z-index: 1008; /* 按需定义 */
|
||||
}
|
||||
.toolbar-container {
|
||||
border-bottom: 1px solid var(--kungalgame-blue-4);
|
||||
}
|
||||
|
||||
.count {
|
||||
padding: 3px 7px;
|
||||
width: 100%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: end;
|
||||
color: var(--kungalgame-font-color-0);
|
||||
background-color: var(--kungalgame-white);
|
||||
}
|
||||
|
||||
@media (max-width: 700px) {
|
||||
.toolbar-container {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
</style>
|
|
@ -1,6 +1,6 @@
|
|||
<script setup lang="ts">
|
||||
import { onBeforeMount, ref } from 'vue'
|
||||
import WangEditor from '@/components/WangEditor.vue'
|
||||
import Milkdown from '@/components/Milkdown/MilkdownProvider.vue'
|
||||
import Tags from './components/Tags.vue'
|
||||
import Footer from './components/Footer.vue'
|
||||
import KUNGalgameFooter from '@/components/KUNGalgameFooter.vue'
|
||||
|
@ -68,12 +68,7 @@ const handelInput = () => {
|
|||
</div>
|
||||
</div>
|
||||
<!-- 编辑器 -->
|
||||
<WangEditor
|
||||
class="editor"
|
||||
:height="400"
|
||||
:isShowToolbar="true"
|
||||
:isShowAdvance="true"
|
||||
/>
|
||||
<Milkdown />
|
||||
</div>
|
||||
|
||||
<!-- 内容区的底部 -->
|
||||
|
|
|
@ -9,7 +9,7 @@ import 'animate.css'
|
|||
import { defineAsyncComponent } from 'vue'
|
||||
|
||||
// 导入编辑器
|
||||
import WangEditor from '@/components/WangEditor.vue'
|
||||
import Milkdown from '@/components/Milkdown/MilkdownProvider.vue'
|
||||
|
||||
// 异步导入话题标签
|
||||
const Tags = defineAsyncComponent(
|
||||
|
@ -52,7 +52,7 @@ const handelClosePanel = () => {
|
|||
</div>
|
||||
<!-- 回复的编辑器 -->
|
||||
<div class="content">
|
||||
<WangEditor :height="300" :isShowToolbar="isShowAdvance" />
|
||||
<Milkdown />
|
||||
</div>
|
||||
<!-- 回复的页脚 -->
|
||||
<div class="footer">
|
||||
|
|
Loading…
Reference in a new issue