Compare commits

...

3 commits

7 changed files with 335 additions and 8 deletions

View file

@ -14,15 +14,20 @@
"@egoist/tailwindcss-icons": "^1.0.7",
"@iconify/json": "^2.2.40",
"@prosemirror-adapter/vue": "^0.2.3",
"@tiptap/core": "2.0.0-beta.220",
"@tiptap/extension-bubble-menu": "2.0.0-beta.220",
"@tiptap/extension-character-count": "2.0.0-beta.220",
"@tiptap/extension-code-block": "^2.0.0",
"@tiptap/extension-highlight": "^2.0.0",
"@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/suggestion": "^2.0.0",
"@tiptap/vue-3": "2.0.0-beta.220",
"install": "^0.13.0",
"masto": "^5.10.0",
"tippy.js": "^6.3.7",
"vue": "^3.2.47"
},
"devDependencies": {
@ -36,6 +41,7 @@
"husky": "^8.0.3",
"lint-staged": "^13.2.0",
"postcss": "^8.4.21",
"sass": "^1.60.0",
"tailwindcss": "^3.2.7",
"typescript": "^5.0.2",
"vite": "^4.2.1",

View file

@ -10,12 +10,21 @@ dependencies:
'@prosemirror-adapter/vue':
specifier: ^0.2.3
version: 0.2.3(vue@3.2.47)
'@tiptap/core':
specifier: 2.0.0-beta.220
version: 2.0.0-beta.220(@tiptap/pm@2.0.0-beta.220)
'@tiptap/extension-bubble-menu':
specifier: 2.0.0-beta.220
version: 2.0.0-beta.220(@tiptap/core@2.0.0-beta.220)(@tiptap/pm@2.0.0-beta.220)
'@tiptap/extension-character-count':
specifier: 2.0.0-beta.220
version: 2.0.0-beta.220(@tiptap/core@2.0.0-beta.220)(@tiptap/pm@2.0.0-beta.220)
'@tiptap/extension-code-block':
specifier: ^2.0.0
version: 2.0.0(@tiptap/core@2.0.0-beta.220)(@tiptap/pm@2.0.0-beta.220)
'@tiptap/extension-highlight':
specifier: ^2.0.0
version: 2.0.0(@tiptap/core@2.0.0-beta.220)
'@tiptap/extension-link':
specifier: 2.0.0-beta.220
version: 2.0.0-beta.220(@tiptap/core@2.0.0-beta.220)(@tiptap/pm@2.0.0-beta.220)
@ -28,6 +37,9 @@ dependencies:
'@tiptap/starter-kit':
specifier: 2.0.0-beta.220
version: 2.0.0-beta.220(@tiptap/pm@2.0.0-beta.220)
'@tiptap/suggestion':
specifier: ^2.0.0
version: 2.0.0(@tiptap/core@2.0.0-beta.220)(@tiptap/pm@2.0.0-beta.220)
'@tiptap/vue-3':
specifier: 2.0.0-beta.220
version: 2.0.0-beta.220(@tiptap/core@2.0.0-beta.220)(@tiptap/pm@2.0.0-beta.220)(vue@3.2.47)
@ -37,6 +49,9 @@ dependencies:
masto:
specifier: ^5.10.0
version: 5.10.0
tippy.js:
specifier: ^6.3.7
version: 6.3.7
vue:
specifier: ^3.2.47
version: 3.2.47
@ -72,6 +87,9 @@ devDependencies:
postcss:
specifier: ^8.4.21
version: 8.4.21
sass:
specifier: ^1.60.0
version: 1.60.0
tailwindcss:
specifier: ^3.2.7
version: 3.2.7(postcss@8.4.21)
@ -80,7 +98,7 @@ devDependencies:
version: 5.0.2
vite:
specifier: ^4.2.1
version: 4.2.1
version: 4.2.1(sass@1.60.0)
vue-tsc:
specifier: ^1.2.0
version: 1.2.0(typescript@5.0.2)
@ -688,11 +706,11 @@ packages:
'@tiptap/pm': 2.0.0-beta.220(@tiptap/core@2.0.0-beta.220)
dev: false
/@tiptap/extension-code-block@2.0.0-beta.220(@tiptap/core@2.0.0-beta.220)(@tiptap/pm@2.0.0-beta.220):
resolution: {integrity: sha512-fgA7yTfHqhBtMJF7I9FPJ6UWuZPtxOQiN45Iv9LNmFIB6YRucdpmF+daZ27sElu0a+eICZyXwVn4w4iJphifuw==}
/@tiptap/extension-code-block@2.0.0(@tiptap/core@2.0.0-beta.220)(@tiptap/pm@2.0.0-beta.220):
resolution: {integrity: sha512-rkI2W8037A9BWtsYNhuzA4/IjJF1jafmGGXKh56xLW7hkW563u33jizvQ+f+g+5dofKWUd+0coMv0bDax7ANCg==}
peerDependencies:
'@tiptap/core': ^2.0.0-beta.209
'@tiptap/pm': ^2.0.0-beta.209
'@tiptap/core': ^2.0.0
'@tiptap/pm': ^2.0.0
dependencies:
'@tiptap/core': 2.0.0-beta.220(@tiptap/pm@2.0.0-beta.220)
'@tiptap/pm': 2.0.0-beta.220(@tiptap/core@2.0.0-beta.220)
@ -761,6 +779,14 @@ packages:
'@tiptap/core': 2.0.0-beta.220(@tiptap/pm@2.0.0-beta.220)
dev: false
/@tiptap/extension-highlight@2.0.0(@tiptap/core@2.0.0-beta.220):
resolution: {integrity: sha512-2EbfBMmWRQj06LaG2cwncmKNQJzxrquSubVVK7wmrVOTm7oCbgCiofYZ3Fv0vE9qNowKUnPsd1oza7m5x2FAeA==}
peerDependencies:
'@tiptap/core': ^2.0.0
dependencies:
'@tiptap/core': 2.0.0-beta.220(@tiptap/pm@2.0.0-beta.220)
dev: false
/@tiptap/extension-history@2.0.0-beta.220(@tiptap/core@2.0.0-beta.220)(@tiptap/pm@2.0.0-beta.220):
resolution: {integrity: sha512-qNL2a9UhnlmCs4y2iQYrfeMB8vEX3bHozBJanHu0PWNQJcj90R5xqorBp/bRcqZdi0kuQfxcTnGHtLUpN/U0TA==}
peerDependencies:
@ -882,7 +908,7 @@ packages:
'@tiptap/extension-bold': 2.0.0-beta.220(@tiptap/core@2.0.0-beta.220)
'@tiptap/extension-bullet-list': 2.0.0-beta.220(@tiptap/core@2.0.0-beta.220)
'@tiptap/extension-code': 2.0.0-beta.220(@tiptap/core@2.0.0-beta.220)
'@tiptap/extension-code-block': 2.0.0-beta.220(@tiptap/core@2.0.0-beta.220)(@tiptap/pm@2.0.0-beta.220)
'@tiptap/extension-code-block': 2.0.0(@tiptap/core@2.0.0-beta.220)(@tiptap/pm@2.0.0-beta.220)
'@tiptap/extension-document': 2.0.0-beta.220(@tiptap/core@2.0.0-beta.220)
'@tiptap/extension-dropcursor': 2.0.0-beta.220(@tiptap/core@2.0.0-beta.220)(@tiptap/pm@2.0.0-beta.220)
'@tiptap/extension-gapcursor': 2.0.0-beta.220(@tiptap/core@2.0.0-beta.220)(@tiptap/pm@2.0.0-beta.220)
@ -900,6 +926,16 @@ packages:
- '@tiptap/pm'
dev: false
/@tiptap/suggestion@2.0.0(@tiptap/core@2.0.0-beta.220)(@tiptap/pm@2.0.0-beta.220):
resolution: {integrity: sha512-U4POIQXKOJu/1a81W2A0COVx0ncAh9VxyMx3DkSZd/gPxjXbWIq1EKcb+TJOo+317fA9WVtWYKRCkFRMv5f24g==}
peerDependencies:
'@tiptap/core': ^2.0.0
'@tiptap/pm': ^2.0.0
dependencies:
'@tiptap/core': 2.0.0-beta.220(@tiptap/pm@2.0.0-beta.220)
'@tiptap/pm': 2.0.0-beta.220(@tiptap/core@2.0.0-beta.220)
dev: false
/@tiptap/vue-3@2.0.0-beta.220(@tiptap/core@2.0.0-beta.220)(@tiptap/pm@2.0.0-beta.220)(vue@3.2.47):
resolution: {integrity: sha512-rhSKUECLE6NOjTYZHheXAGpyIqruhxkU/9YfWNLWNFIHHW9wHO+t/B3XMJAWBwgkUvRRepU5JmBBIfYd8RgqTA==}
peerDependencies:
@ -1100,7 +1136,7 @@ packages:
vite: ^4.0.0
vue: ^3.2.25
dependencies:
vite: 4.2.1
vite: 4.2.1(sass@1.60.0)
vue: 3.2.47
dev: true
@ -2685,6 +2721,10 @@ packages:
engines: {node: '>= 4'}
dev: true
/immutable@4.3.0:
resolution: {integrity: sha512-0AOCmOip+xgJwEVTQj1EfiDDOkPmuyllDuTuEX+DDXUgapLAsBIfkg3sxCYyCEA8mQqZrrxPUGjcOQ2JS3WLkg==}
dev: true
/import-fresh@3.3.0:
resolution: {integrity: sha512-veYYhQa+D1QBKznvhUHxb8faxlrwUnxseDAbAp457E0wLNio2bOSKnjYDhMj+YiAq61xrMGhQk9iXVk5FzgQMw==}
engines: {node: '>=6'}
@ -3912,6 +3952,16 @@ packages:
regexp-tree: 0.1.24
dev: true
/sass@1.60.0:
resolution: {integrity: sha512-updbwW6fNb5gGm8qMXzVO7V4sWf7LMXnMly/JEyfbfERbVH46Fn6q02BX7/eHTdKpE7d+oTkMMQpFWNUMfFbgQ==}
engines: {node: '>=12.0.0'}
hasBin: true
dependencies:
chokidar: 3.5.3
immutable: 4.3.0
source-map-js: 1.0.2
dev: true
/semver@5.7.1:
resolution: {integrity: sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==}
hasBin: true
@ -4328,7 +4378,7 @@ packages:
spdx-expression-parse: 3.0.1
dev: true
/vite@4.2.1:
/vite@4.2.1(sass@1.60.0):
resolution: {integrity: sha512-7MKhqdy0ISo4wnvwtqZkjke6XN4taqQ2TBaTccLIpOKv7Vp2h4Y+NpmWCnGDeSvvn45KxvWgGyb0MkHvY1vgbg==}
engines: {node: ^14.18.0 || >=16.0.0}
hasBin: true
@ -4357,6 +4407,7 @@ packages:
postcss: 8.4.21
resolve: 1.22.1
rollup: 3.20.2
sass: 1.60.0
optionalDependencies:
fsevents: 2.3.2
dev: true

View file

@ -12,10 +12,15 @@ const inputUrl = ref<HTMLInputElement | null>(null)
const openLinkInput = () => {
url.value = editor.getAttributes('link').href || ''
showUrlInput.value = true
editor.chain().toggleHighlight().run()
nextTick(() => inputUrl.value?.focus())
}
const setLink = () => {
editor.chain().focus().unsetHighlight().run()
if (url.value === null) {
showUrlInput.value = false
return

View file

@ -0,0 +1,117 @@
<script>
export default {
props: {
items: {
type: Array,
required: true,
},
command: {
type: Function,
required: true,
},
},
data() {
return {
selectedIndex: 0,
}
},
watch: {
items() {
this.selectedIndex = 0
},
},
methods: {
onKeyDown({ event }) {
if (event.key === 'ArrowUp') {
this.upHandler()
return true
}
if (event.key === 'ArrowDown') {
this.downHandler()
return true
}
if (event.key === 'Enter') {
this.enterHandler()
return true
}
return false
},
upHandler() {
this.selectedIndex = ((this.selectedIndex + this.items.length) - 1) % this.items.length
},
downHandler() {
this.selectedIndex = (this.selectedIndex + 1) % this.items.length
},
enterHandler() {
this.selectItem(this.selectedIndex)
},
selectItem(index) {
const item = this.items[index]
if (item)
this.command(item)
},
},
}
</script>
<template>
<div class="items">
<template v-if="items.length">
<button
v-for="(item, index) in items"
:key="index"
class="item"
:class="{ 'is-selected': index === selectedIndex }"
@click="selectItem(index)"
>
{{ item.title }}
</button>
</template>
<div v-else class="item">
No result
</div>
</div>
</template>
<style lang="scss">
.items {
padding: 0.2rem;
position: relative;
border-radius: 0.5rem;
background: #FFF;
color: rgba(0, 0, 0, 0.8);
overflow: hidden;
font-size: 0.9rem;
box-shadow:
0 0 0 1px rgba(0, 0, 0, 0.05),
0px 10px 20px rgba(0, 0, 0, 0.1),
;
}
.item {
display: block;
margin: 0;
width: 100%;
text-align: left;
background: transparent;
border-radius: 0.4rem;
border: 1px solid transparent;
padding: 0.2rem 0.4rem;
&.is-selected {
border-color: #000;
}
}
</style>

View file

@ -3,7 +3,10 @@ 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 Highlight from '@tiptap/extension-highlight'
import suggestion from './suggestion'
import BubbleMenu from './BubbleMenu.vue'
import Commands from './commands'
const editor = useEditor({
content: `<p>
@ -18,6 +21,14 @@ const editor = useEditor({
Link.configure({
openOnClick: false,
}),
Commands.configure({
suggestion,
}),
Highlight.configure({
HTMLAttributes: {
class: 'bg-blue-100',
},
}),
],
editable: true,
autofocus: true,

View file

@ -0,0 +1,26 @@
import { Extension } from '@tiptap/core'
import Suggestion from '@tiptap/suggestion'
export default Extension.create({
name: 'commands',
addOptions() {
return {
suggestion: {
char: '/',
command: ({ editor, range, props }) => {
props.command({ editor, range })
},
},
}
},
addProseMirrorPlugins() {
return [
Suggestion({
editor: this.editor,
...this.options.suggestion,
}),
]
},
})

View file

@ -0,0 +1,111 @@
import { VueRenderer } from '@tiptap/vue-3'
import tippy from 'tippy.js'
import CommandsList from './CommandsList.vue'
export default {
items: ({ query }) => {
return [
{
title: 'H1',
command: ({ editor, range }) => {
editor
.chain()
.focus()
.deleteRange(range)
.setNode('heading', { level: 1 })
.run()
},
},
{
title: 'H2',
command: ({ editor, range }) => {
editor
.chain()
.focus()
.deleteRange(range)
.setNode('heading', { level: 2 })
.run()
},
},
{
title: 'code block',
command: ({ editor, range }) => {
editor
.chain()
.focus()
.deleteRange(range)
.setNode('pre')
.run()
},
},
{
title: 'italic',
command: ({ editor, range }) => {
editor
.chain()
.focus()
.deleteRange(range)
.setMark('italic')
.run()
},
},
].filter(item => item.title.toLowerCase().startsWith(query.toLowerCase())).slice(0, 10)
},
render: () => {
let component
let popup
return {
onStart: (props) => {
component = new VueRenderer(CommandsList, {
// using vue 2:
// parent: this,
// propsData: props,
props,
editor: props.editor,
})
if (!props.clientRect)
return
popup = tippy('body', {
getReferenceClientRect: props.clientRect,
appendTo: () => document.body,
content: component.element,
showOnCreate: true,
interactive: true,
trigger: 'manual',
placement: 'bottom-start',
})
},
onUpdate(props) {
component.updateProps(props)
if (!props.clientRect)
return
popup[0].setProps({
getReferenceClientRect: props.clientRect,
})
},
onKeyDown(props) {
if (props.event.key === 'Escape') {
popup[0].hide()
return true
}
return component.ref?.onKeyDown(props)
},
onExit() {
popup[0].destroy()
component.destroy()
},
}
},
}