feat: basic integration with TipTap (#87)
This commit is contained in:
parent
019a36c9bb
commit
c2810fd5eb
14 changed files with 722 additions and 31 deletions
93
composables/tiptap.ts
Normal file
93
composables/tiptap.ts
Normal 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,
|
||||
}
|
||||
}
|
83
composables/tiptap/suggestion.ts
Normal file
83
composables/tiptap/suggestion.ts
Normal 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()
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
Loading…
Add table
Add a link
Reference in a new issue