Merge pull request #3 from KUN1007/milkdown

feat: remove wangEditor, use milkdown
This commit is contained in:
kun 2023-09-01 17:22:07 +08:00 committed by GitHub
commit c50bbf30b5
11 changed files with 1820 additions and 150 deletions

View file

@ -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",

View file

@ -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"

File diff suppressed because it is too large Load diff

View 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>

View 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>

View 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>

View 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>

View 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)
},
},
})
})

View file

@ -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>
/* 编辑器的样式 */
.editorwrapper {
/* 编辑器的 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>

View file

@ -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>
<!-- 内容区的底部 -->

View file

@ -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">