Compare commits

..

10 commits

Author SHA1 Message Date
c13b7b5b76
feat: add basis styling to editor 2023-03-29 18:25:11 +08:00
c96ddd6ce6
refactor: remove placeholder varible 2023-03-29 18:13:40 +08:00
98837926fe
feat: button status change with action 2023-03-29 18:09:51 +08:00
8adf00715b
chore: add lint-stage 2023-03-29 17:41:56 +08:00
deb6fa9702
chore: add husky 2023-03-29 17:17:56 +08:00
3ae0fc76a7
chore: add husky 2023-03-29 17:17:48 +08:00
e34e051f41
Add Url input menu 2023-03-29 16:50:53 +08:00
d2f26c1f64
Refactor BubbleMenu 2023-03-28 22:32:47 +08:00
4cf8033568
Refactor the style of editor 2023-03-28 22:20:44 +08:00
7cc16ebf40
Add Tooltip editor 2023-03-28 17:55:12 +08:00
13 changed files with 1660 additions and 2049 deletions

3
.husky/pre-commit Executable file
View file

@ -0,0 +1,3 @@
#!/usr/bin/env sh
. "$(dirname -- "$0")/_/husky.sh"
npx --no-install lint-staged

View file

@ -1,49 +1,47 @@
{ {
"name": "seigwai", "name": "seigwai",
"private": true,
"version": "0.0.0",
"type": "module", "type": "module",
"version": "0.0.0",
"private": true,
"scripts": { "scripts": {
"dev": "vite", "dev": "vite",
"build": "vue-tsc && vite build", "build": "vue-tsc && vite build",
"preview": "vite preview" "preview": "vite preview",
"lint": "eslint .",
"prepare": "husky install"
}, },
"dependencies": { "dependencies": {
"@egoist/tailwindcss-icons": "^1.0.7", "@egoist/tailwindcss-icons": "^1.0.7",
"@iconify/json": "^2.2.40", "@iconify/json": "^2.2.40",
"@milkdown/core": "^7.1.0",
"@milkdown/ctx": "^7.1.0",
"@milkdown/plugin-block": "^7.1.0",
"@milkdown/plugin-clipboard": "^7.1.0",
"@milkdown/plugin-cursor": "^7.1.0",
"@milkdown/plugin-emoji": "^7.1.0",
"@milkdown/plugin-history": "^7.1.0",
"@milkdown/plugin-indent": "^7.1.0",
"@milkdown/plugin-math": "^7.1.0",
"@milkdown/plugin-slash": "^7.1.0",
"@milkdown/plugin-tooltip": "^7.1.0",
"@milkdown/preset-commonmark": "^7.1.0",
"@milkdown/preset-gfm": "^7.1.0",
"@milkdown/prose": "^7.1.0",
"@milkdown/theme-nord": "^7.1.0",
"@milkdown/transformer": "^7.1.0",
"@milkdown/utils": "^7.1.0",
"@milkdown/vue": "^7.1.0",
"@prosemirror-adapter/vue": "^0.2.3", "@prosemirror-adapter/vue": "^0.2.3",
"@tiptap/extension-bubble-menu": "2.0.0-beta.220",
"@tiptap/extension-character-count": "2.0.0-beta.220",
"@tiptap/extension-link": "2.0.0-beta.220",
"@tiptap/extension-typography": "2.0.0-beta.220",
"@tiptap/pm": "2.0.0-beta.220",
"@tiptap/starter-kit": "2.0.0-beta.220",
"@tiptap/vue-3": "2.0.0-beta.220",
"install": "^0.13.0",
"masto": "^5.10.0", "masto": "^5.10.0",
"vue": "^3.2.47" "vue": "^3.2.47"
}, },
"devDependencies": { "devDependencies": {
"@antfu/eslint-config": "^0.37.0", "@antfu/eslint-config": "^0.37.0",
"@iconify-json/tabler": "^1.1.68",
"@tailwindcss/typography": "^0.5.9", "@tailwindcss/typography": "^0.5.9",
"@vitejs/plugin-vue": "^4.1.0", "@vitejs/plugin-vue": "^4.1.0",
"autoprefixer": "^10.4.14", "autoprefixer": "^10.4.14",
"daisyui": "^2.51.5", "daisyui": "^2.51.5",
"eslint": "^8.36.0", "eslint": "^8.36.0",
"husky": "^8.0.3",
"lint-staged": "^13.2.0",
"postcss": "^8.4.21", "postcss": "^8.4.21",
"tailwindcss": "^3.2.7", "tailwindcss": "^3.2.7",
"typescript": "^5.0.2", "typescript": "^5.0.2",
"vite": "^4.2.1", "vite": "^4.2.1",
"vue-tsc": "^1.2.0" "vue-tsc": "^1.2.0"
},
"lint-staged": {
"*": "eslint --fix"
} }
} }

File diff suppressed because it is too large Load diff

View file

@ -1,7 +1,7 @@
<script setup lang="ts"> <script setup lang="ts">
import EditorWrapper from './components/Milkdown/EditroWrapper.vue' import Editor from './components/Tiptap/Editor.vue'
</script> </script>
<template> <template>
<EditorWrapper /> <Editor />
</template> </template>

View file

@ -1,60 +0,0 @@
<script setup lang="ts">
import { BlockProvider } from '@milkdown/plugin-block'
import { useInstance } from '@milkdown/vue'
import { usePluginViewContext } from '@prosemirror-adapter/vue'
import { onUnmounted, ref, watch } from 'vue'
import type { VNodeRef } from 'vue'
const { view } = usePluginViewContext()
const [loading, get] = useInstance()
const divRef = ref<VNodeRef>()
let tooltipProvider: BlockProvider | undefined
watch([loading], () => {
const editor = get()
// eslint-disable-next-line antfu/if-newline
if (loading.value || !editor || tooltipProvider) return
editor.action((ctx) => {
tooltipProvider = new BlockProvider({
ctx,
content: divRef.value as any,
})
tooltipProvider.update(view.value)
})
})
watch([view], () => {
tooltipProvider?.update(view.value)
})
onUnmounted(() => {
tooltipProvider?.destroy()
tooltipProvider = undefined
})
</script>
<template>
<div
ref="divRef"
className="w-6 bg-slate-200 rounded hover:bg-slate-300 cursor-grab"
>
<svg
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
strokeWidth="{1.5}"
stroke="currentColor"
className="w-6 h-6"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
d="M12 6.75a.75.75 0 110-1.5.75.75 0 010 1.5zM12 12.75a.75.75 0 110-1.5.75.75 0 010 1.5zM12 18.75a.75.75 0 110-1.5.75.75 0 010 1.5z"
/>
</svg>
</div>
</template>

View file

@ -1,67 +0,0 @@
<script setup lang="ts">
import { Editor, defaultValueCtx, rootCtx } from '@milkdown/core'
import { commonmark } from '@milkdown/preset-commonmark'
import { nord } from '@milkdown/theme-nord'
import { history } from '@milkdown/plugin-history'
import { math } from '@milkdown/plugin-math'
import { Milkdown, useEditor } from '@milkdown/vue'
import { tooltipFactory } from '@milkdown/plugin-tooltip'
import { usePluginViewFactory } from '@prosemirror-adapter/vue'
import { gfm } from '@milkdown/preset-gfm'
import { clipboard } from '@milkdown/plugin-clipboard'
import { emoji } from '@milkdown/plugin-emoji'
import { block } from '@milkdown/plugin-block'
import { cursor } from '@milkdown/plugin-cursor'
import Tooltip from './Tooltip.vue'
import Slash from './Slash.vue'
import Block from './Block.vue'
const tooltip = tooltipFactory('Text')
const slash = tooltipFactory('Text')
const markdown = `# Milkdown Vue Commonmark
> You're scared of a world where you're needed.
This is a demo for using Milkdown with **Vue**.`
const pluginViewFactory = usePluginViewFactory()
useEditor((root) => {
return Editor.make()
.config(nord)
.config((ctx) => {
ctx.set(rootCtx, root)
ctx.set(defaultValueCtx, markdown)
ctx.set(tooltip.key, {
view: pluginViewFactory({
component: Tooltip,
}),
})
ctx.set(slash.key, {
view: pluginViewFactory({
component: Slash,
}),
})
ctx.set(block.key, {
view: pluginViewFactory({
component: Block,
}),
})
})
.use(commonmark)
.use(tooltip)
.use(slash)
.use(history)
.use(math)
.use(gfm)
.use(clipboard)
.use(emoji)
.use(block)
.use(cursor)
})
</script>
<template>
<Milkdown />
</template>

View file

@ -1,13 +0,0 @@
<script setup lang="ts">
import { MilkdownProvider } from '@milkdown/vue'
import { ProsemirrorAdapterProvider } from '@prosemirror-adapter/vue'
import Editor from './Editor.vue'
</script>
<template>
<MilkdownProvider>
<ProsemirrorAdapterProvider>
<Editor />
</ProsemirrorAdapterProvider>
</MilkdownProvider>
</template>

View file

@ -1,106 +0,0 @@
<script setup lang="ts">
import { onMounted, onUnmounted, ref, watch } from 'vue'
import { useInstance } from '@milkdown/vue'
import { usePluginViewContext } from '@prosemirror-adapter/vue'
import { editorViewCtx } from '@milkdown/core'
import { SlashProvider } from '@milkdown/plugin-slash'
import { callCommand } from '@milkdown/utils'
import type { VNodeRef } from 'vue'
import type { CmdKey } from '@milkdown/core'
import {
createCodeBlockCommand,
insertHrCommand,
wrapInBlockquoteCommand,
wrapInBulletListCommand,
wrapInHeadingCommand,
wrapInOrderedListCommand,
} from '@milkdown/preset-commonmark'
const [loading, get] = useInstance()
const call = <T>(command: CmdKey<T>, payload?: T) => {
return get()!.action((ctx) => {
const view = ctx.get(editorViewCtx)
const { dispatch, state } = view
const { tr, selection } = state
const { from } = selection
dispatch(tr.deleteRange(from - 1, from))
return callCommand(command, payload)(ctx)
})
}
let tooltipProvider: SlashProvider
const { view, prevState } = usePluginViewContext()
const divRef = ref<VNodeRef>()
onMounted(() => {
tooltipProvider = new SlashProvider({
content: divRef.value as any,
})
tooltipProvider.update(view.value, prevState.value)
})
watch([view, prevState], () => {
tooltipProvider?.update(view.value, prevState.value)
})
onUnmounted(() => {
tooltipProvider.destroy()
})
</script>
<template>
<div v-if="loading" ref="divRef">
<button
class="text-gray-600 bg-slate-200 px-2 py-1 rounded-lg hover:bg-slate-300 border hover:text-gray-900"
@click.prevent="call(createCodeBlockCommand.key)"
>
Code Block
</button>
<button
class="text-gray-600 bg-slate-200 px-2 py-1 rounded-lg hover:bg-slate-300 border hover:text-gray-900"
@click.prevent="call(wrapInHeadingCommand.key, 1)"
>
Heading1
</button>
<button
class="text-gray-600 bg-slate-200 px-2 py-1 rounded-lg hover:bg-slate-300 border hover:text-gray-900"
@click.prevent="call(wrapInHeadingCommand.key, 2)"
>
Heading2
</button>
<button
class="text-gray-600 bg-slate-200 px-2 py-1 rounded-lg hover:bg-slate-300 border hover:text-gray-900"
@click.prevent="call(wrapInHeadingCommand.key, 3)"
>
Heading3
</button>
<button
class="text-gray-600 bg-slate-200 px-2 py-1 rounded-lg hover:bg-slate-300 border hover:text-gray-900"
@click.prevent="call(wrapInBulletListCommand.key)"
>
BulletList
</button>
<button
class="text-gray-600 bg-slate-200 px-2 py-1 rounded-lg hover:bg-slate-300 border hover:text-gray-900"
@click.prevent="call(wrapInOrderedListCommand.key)"
>
OrderList
</button>
<button
class="text-gray-600 bg-slate-200 px-2 py-1 rounded-lg hover:bg-slate-300 border hover:text-gray-900"
@click.prevent="call(wrapInBlockquoteCommand.key)"
>
Quote
</button>
<button
class="text-gray-600 bg-slate-200 px-2 py-1 rounded-lg hover:bg-slate-300 border hover:text-gray-900"
@click.prevent="call(insertHrCommand.key)"
>
Hr
</button>
</div>
</template>

View file

@ -1,87 +0,0 @@
<script setup lang="ts">
import { useInstance } from '@milkdown/vue'
import { TooltipProvider } from '@milkdown/plugin-tooltip'
import { usePluginViewContext } from '@prosemirror-adapter/vue'
import { onMounted, onUnmounted, ref, watch } from 'vue'
import type { CmdKey } from '@milkdown/core'
import type { VNodeRef } from 'vue'
import { callCommand } from '@milkdown/utils'
import {
toggleEmphasisCommand,
toggleInlineCodeCommand,
toggleStrongCommand,
wrapInBlockquoteCommand,
} from '@milkdown/preset-commonmark'
import { toggleStrikethroughCommand } from '@milkdown/preset-gfm'
import LinkWidge from './LinkWidge.vue'
const [loading, get] = useInstance()
const call = <T>(command: CmdKey<T>, payload?: T) => {
return get()?.action(callCommand(command, payload))
}
const { view, prevState } = usePluginViewContext()
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()
})
</script>
<template>
<div v-if="loading" ref="divRef">
<div class="flex text-gray-700 bg-slate-50 border rounded-md grass">
<button
class="btn btn-sm btn-ghost"
@click.prevent="call(toggleStrongCommand.key)"
>
B
</button>
<button
class="btn btn-sm btn-ghost i-mingcute-code-line"
@click.prevent="call(toggleInlineCodeCommand.key)"
></button>
<button
class="btn btn-sm btn-ghost"
@click.prevent="call(toggleEmphasisCommand.key)"
>
Italic
</button>
<button
class="btn btn-sm btn-ghost"
@click.prevent="call(wrapInBlockquoteCommand.key)"
>
Quote
</button>
<button
class="btn btn-sm btn-ghost"
@click.prevent="call(toggleStrikethroughCommand.key)"
>
StrikeThrough
</button>
<button
class="btn btn-sm btn-ghost"
@click.prevent="call(toggleStrikethroughCommand.key)"
>
Link
</button>
</div>
</div>
</template>

View file

@ -0,0 +1,125 @@
<script setup lang="ts">
import type { Editor } from '@tiptap/vue-3'
import { BubbleMenu } from '@tiptap/vue-3'
import { nextTick, ref } from 'vue'
const { editor } = defineProps<{ editor: Editor }>()
const url = ref('')
const showUrlInput = ref(false)
const inputUrl = ref<HTMLInputElement | null>(null)
const openLinkInput = () => {
showUrlInput.value = true
nextTick(() =>
inputUrl.value?.focus(),
)
}
const setLink = () => {
const previousUrl = editor.getAttributes('link').href
if (previousUrl)
inputUrl.value = previousUrl
// cancelled
if (url.value === null)
return
if (url.value === '') {
editor.chain().focus().extendMarkRange('link').unsetAllMarks().run()
return
}
editor
.chain()
.focus()
.extendMarkRange('link')
.setLink({ href: url.value })
.run()
}
</script>
<template>
<BubbleMenu
:editor="editor"
:tippy-options="{ duration: 50 }"
class="flex text-gray-700 bg-white grass rounded-md p-[2px] shadow-xl border-slate-100 border"
>
<div v-show="!showUrlInput">
<button
class="menu-btn"
:class="{ 'btn-active': editor.isActive('bold') }"
@click="editor.chain().focus().toggleBold().run()"
>
<span
:class="[
editor.isActive('bold') ? 'i-tabler-bold-off' : 'i-tabler-bold',
]"
/>
</button>
<button
class="menu-btn"
:class="{ 'btn-active': editor.isActive('italic') }"
@click="editor.chain().focus().toggleItalic().run()"
>
<span class="i-tabler-italic" />
</button>
<button
class="menu-btn"
:class="{ 'btn-active': editor.isActive('strike') }"
@click="editor.chain().focus().toggleStrike().run()"
>
<span class="i-tabler-strikethrough" />
</button>
<button
class="menu-btn"
:class="{ 'btn-active': editor.isActive('code') }"
@click="editor.chain().focus().toggleCode().run()"
>
<span
:class="[
editor.isActive('code') ? 'i-tabler-code-off' : 'i-tabler-code',
]"
/>
</button>
<button
class="menu-btn"
:class="{ 'btn-active': editor.isActive('blockquote') }"
@click="editor.chain().focus().toggleBlockquote().run()"
>
<span
:class="[
editor.isActive('blockquote') ? 'i-tabler-quote-off' : 'i-tabler-quote',
]"
/>
</button>
<button
:class="{ 'btn-active': editor.isActive('link') }"
class="menu-btn"
@click="openLinkInput"
>
<span
:class="[
editor.isActive('link') ? 'i-tabler-unlink' : 'i-tabler-link',
]"
/>
</button>
</div>
<div
v-show="showUrlInput"
class="input-group input-group-sm border-slate-300 border-1"
>
<input
ref="inputUrl"
v-model.trim="url"
class="input input-sm focus:outline-none"
placeholder="Add Link to text"
@blur="showUrlInput = false"
>
<button class="btn btn-sm btn-square" @click="setLink">
<span class="i-tabler-link p-2" />
</button>
</div>
</BubbleMenu>
</template>

View file

@ -0,0 +1,35 @@
<script setup lang="ts">
import { EditorContent, useEditor } from '@tiptap/vue-3'
import StarterKit from '@tiptap/starter-kit'
import Typography from '@tiptap/extension-typography'
import Link from '@tiptap/extension-link'
import BubbleMenu from './BubbleMenu.vue'
const editor = useEditor({
content: `<p>
Wow, this editor has support for links to the whole <a href="https://en.wikipedia.org/wiki/World_Wide_Web">world wide web</a>. We tested a lot of URLs and I think you can add *every URL* you want. Isnt that cool? Lets try <a href="https://statamic.com/">another one!</a> Yep, seems to work.
</p>
<p>
By default every link will get a <code>rel="noopener noreferrer nofollow"</code> attribute. Its configurable though.
</p>`,
extensions: [
StarterKit,
Typography,
Link.configure({
openOnClick: false,
}),
],
editable: true,
autofocus: true,
editorProps: {
attributes: {
class: 'prose',
},
},
})
</script>
<template>
<BubbleMenu v-if="editor" :editor="editor" />
<EditorContent :editor="editor" />
</template>

View file

@ -9,3 +9,31 @@
.editor { .editor {
@apply mx-auto; @apply mx-auto;
} }
.btn-ghost {
@apply hover:bg-slate-200;
}
.btn-active{
@apply bg-slate-200;
}
.menu-btn {
@apply btn btn-ghost btn-sm rounded-md p-2;
}
.menu-btn > span {
@apply w-4;
}
.menu-btn.btn-active {
@apply bg-slate-200;
}
.input-group{
@apply h-4;
}
a {
color: #68CEF8;
}

View file

@ -10,7 +10,7 @@ module.exports = {
require('@tailwindcss/typography'), require('@tailwindcss/typography'),
require('daisyui'), require('daisyui'),
iconsPlugin({ iconsPlugin({
collections: getIconCollections(['mdi', 'lucide']), collections: getIconCollections(['tabler']),
}), }),
], ],
} }