feat: basic integration with TipTap (#87)

This commit is contained in:
Anthony Fu 2022-11-25 21:21:02 +08:00 committed by GitHub
parent 019a36c9bb
commit c2810fd5eb
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
14 changed files with 722 additions and 31 deletions

93
composables/tiptap.ts Normal file
View file

@ -0,0 +1,93 @@
import { Extension, useEditor } from '@tiptap/vue-3'
import Placeholder from '@tiptap/extension-placeholder'
import Document from '@tiptap/extension-document'
import Paragraph from '@tiptap/extension-paragraph'
import Text from '@tiptap/extension-text'
import Mention from '@tiptap/extension-mention'
import CodeBlock from '@tiptap/extension-code-block'
import CharacterCount from '@tiptap/extension-character-count'
import { Plugin } from 'prosemirror-state'
import type { Ref } from 'vue'
import { HashSuggestion, MentionSuggestion } from './tiptap/suggestion'
import { POST_CHARS_LIMIT } from '~/constants'
export interface UseTiptapOptions {
content: Ref<string | undefined>
placeholder: string
onSubimit: () => void
onFocus: () => void
onPaste: (event: ClipboardEvent) => void
autofocus: boolean
}
export function useTiptap(options: UseTiptapOptions) {
const {
autofocus,
content,
placeholder,
} = options
const editor = useEditor({
content: content.value,
extensions: [
Document,
Paragraph,
Text,
Mention.configure({
suggestion: MentionSuggestion,
}),
Mention.configure({
suggestion: HashSuggestion,
}),
Placeholder.configure({
placeholder,
}),
CharacterCount.configure({
limit: POST_CHARS_LIMIT,
}),
CodeBlock,
Extension.create({
name: 'api',
addKeyboardShortcuts() {
return {
'Mod-Enter': () => {
options.onSubimit()
return true
},
}
},
onFocus() {
options.onFocus()
},
addProseMirrorPlugins() {
return [
new Plugin({
props: {
handleDOMEvents: {
paste(view, event) {
options.onPaste(event)
},
},
},
}),
]
},
}),
],
onUpdate({ editor }) {
content.value = editor.getHTML()
},
editorProps: {
attributes: {
class: 'content-editor content-rich',
},
},
autofocus,
editable: true,
})
return {
editor,
}
}

View file

@ -0,0 +1,83 @@
import type { GetReferenceClientRect, Instance } from 'tippy.js'
import tippy from 'tippy.js'
import { VueRenderer } from '@tiptap/vue-3'
import type { SuggestionOptions } from '@tiptap/suggestion'
import { PluginKey } from 'prosemirror-state'
import TiptapMentionList from '~/components/tiptap/TiptapMentionList.vue'
export const MentionSuggestion: Partial<SuggestionOptions> = {
pluginKey: new PluginKey('mention'),
char: '@',
items({ query }) {
// TODO: query
return [
'TODO MENTION QUERY', 'Lea Thompson', 'Cyndi Lauper', 'Tom Cruise', 'Madonna', 'Jerry Hall', 'Joan Collins', 'Winona Ryder', 'Christina Applegate', 'Alyssa Milano', 'Molly Ringwald', 'Ally Sheedy', 'Debbie Harry', 'Olivia Newton-John', 'Elton John', 'Michael J. Fox', 'Axl Rose', 'Emilio Estevez', 'Ralph Macchio', 'Rob Lowe', 'Jennifer Grey', 'Mickey Rourke', 'John Cusack', 'Matthew Broderick', 'Justine Bateman', 'Lisa Bonet',
].filter(item => item.toLowerCase().startsWith(query.toLowerCase())).slice(0, 5)
},
render: createSuggestionRenderer(),
}
export const HashSuggestion: Partial<SuggestionOptions> = {
pluginKey: new PluginKey('hashtag'),
char: '#',
items({ query }) {
// TODO: query
return [
'TODO HASH QUERY',
].filter(item => item.toLowerCase().startsWith(query.toLowerCase())).slice(0, 5)
},
render: createSuggestionRenderer(),
}
function createSuggestionRenderer(): SuggestionOptions['render'] {
return () => {
let component: VueRenderer
let popup: Instance
return {
onStart: (props) => {
component = new VueRenderer(TiptapMentionList, {
props,
editor: props.editor,
})
if (!props.clientRect)
return
popup = tippy(document.body, {
getReferenceClientRect: props.clientRect as GetReferenceClientRect,
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?.setProps({
getReferenceClientRect: props.clientRect as GetReferenceClientRect,
})
},
onKeyDown(props) {
if (props.event.key === 'Escape') {
popup?.hide()
return true
}
return component?.ref?.onKeyDown(props.event)
},
onExit() {
popup?.destroy()
component?.destroy()
},
}
}
}