feat: basic integration with TipTap (#87)

zio/stable
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

View File

@ -14,6 +14,6 @@ const emojiObject = emojisArrayToObject(props.emojis || [])
export default () => h( export default () => h(
'div', 'div',
{ class: 'rich-content' }, { class: 'content-rich' },
contentToVNode(props.content, emojiObject), contentToVNode(props.content, emojiObject),
) )

View File

@ -13,6 +13,6 @@ import { isPreviewHelpOpen, isPublishDialogOpen, isSigninDialogOpen, isUserSwitc
<HelpPreview /> <HelpPreview />
</ModalDialog> </ModalDialog>
<ModalDialog v-model="isPublishDialogOpen"> <ModalDialog v-model="isPublishDialogOpen">
<PublishWidget draft-key="dialog" expanded min-w-180 p6 /> <PublishWidget draft-key="dialog" expanded min-w-180 />
</ModalDialog> </ModalDialog>
</template> </template>

View File

@ -2,6 +2,7 @@
import type { CreateStatusParams, StatusVisibility } from 'masto' import type { CreateStatusParams, StatusVisibility } from 'masto'
import { fileOpen } from 'browser-fs-access' import { fileOpen } from 'browser-fs-access'
import { useDropZone } from '@vueuse/core' import { useDropZone } from '@vueuse/core'
import { EditorContent } from '@tiptap/vue-3'
const { const {
draftKey, draftKey,
@ -15,10 +16,19 @@ const {
expanded?: boolean expanded?: boolean
}>() }>()
const expanded = $ref(_expanded) let isExpanded = $ref(_expanded)
let isSending = $ref(false) let isSending = $ref(false)
let { draft } = $(useDraft(draftKey, inReplyToId)) let { draft } = $(useDraft(draftKey, inReplyToId))
const { editor } = useTiptap({
content: toRef(draft.params, 'status'),
placeholder,
autofocus: isExpanded,
onSubimit: publish,
onFocus() { isExpanded = true },
onPaste: handlePaste,
})
const status = $computed(() => { const status = $computed(() => {
return { return {
...draft.params, ...draft.params,
@ -87,11 +97,16 @@ function chooseVisibility(visibility: StatusVisibility) {
} }
async function publish() { async function publish() {
if (process.dev) {
alert(JSON.stringify(draft.params, null, 2))
return
}
try { try {
isSending = true isSending = true
if (!draft.editingStatus) if (!draft.editingStatus)
await masto.statuses.create(status) await masto.statuses.create(status)
else await masto.statuses.update(draft.editingStatus.id, status) else
await masto.statuses.update(draft.editingStatus.id, status)
draft = getDefaultDraft({ inReplyToId }) draft = getDefaultDraft({ inReplyToId })
isPublishDialogOpen.value = false isPublishDialogOpen.value = false
@ -111,6 +126,7 @@ async function onDrop(files: File[] | null) {
const { isOverDropZone } = useDropZone(dropZoneRef, onDrop) const { isOverDropZone } = useDropZone(dropZoneRef, onDrop)
onUnmounted(() => { onUnmounted(() => {
// Remove draft if it's empty
if (!draft.attachments.length && !draft.params.status) { if (!draft.attachments.length && !draft.params.status) {
nextTick(() => { nextTick(() => {
delete currentUserDrafts.value[draftKey] delete currentUserDrafts.value[draftKey]
@ -138,7 +154,7 @@ onUnmounted(() => {
<div <div
ref="dropZoneRef" ref="dropZoneRef"
flex flex-col gap-3 flex-1 flex flex-col gap-3 flex-1
border="2 dashed transparent" p-1 border="2 dashed transparent"
:class="[isSending ? 'pointer-events-none' : '', isOverDropZone ? '!border-primary' : '']" :class="[isSending ? 'pointer-events-none' : '', isOverDropZone ? '!border-primary' : '']"
> >
<div v-if="draft.params.sensitive"> <div v-if="draft.params.sensitive">
@ -151,22 +167,17 @@ onUnmounted(() => {
> >
</div> </div>
<textarea <EditorContent
v-model="draft.params.status" :editor="editor"
:placeholder="placeholder" :class="isExpanded ? 'min-h-120px' : ''"
h-80px
:class="expanded ? '!h-200px' : ''"
p2 border-rounded w-full bg-transparent
transition="height"
outline-none border="~ base"
@paste="handlePaste"
@focus="expanded = true"
@keydown.esc="expanded = false"
@keydown.ctrl.enter="publish"
@keydown.meta.enter="publish"
/> />
<div flex="~ col gap-2" max-h-50vh overflow-auto> <div v-if="isUploading" flex gap-1 items-center text-sm p1 text-primary>
<div i-ri:loader-2-fill animate-spin />
Uploading...
</div>
<div v-if="draft.attachments.length" flex="~ col gap-2" overflow-auto>
<PublishAttachment <PublishAttachment
v-for="(att, idx) in draft.attachments" :key="att.id" v-for="(att, idx) in draft.attachments" :key="att.id"
:attachment="att" :attachment="att"
@ -174,18 +185,28 @@ onUnmounted(() => {
/> />
</div> </div>
<div v-if="isUploading" flex gap-2 justify-end items-center> <div
<div op50 i-ri:loader-2-fill animate-spin text-2xl /> v-if="isExpanded" flex="~ gap-2" m="l--1" pt-2
Uploading... border="t base"
</div> >
<div flex="~ gap-2">
<CommonTooltip placement="bottom" content="Add images, a video or an audio file"> <CommonTooltip placement="bottom" content="Add images, a video or an audio file">
<button btn-action-icon @click="pickAttachments"> <button btn-action-icon @click="pickAttachments">
<div i-ri:upload-line /> <div i-ri:image-add-line />
</button> </button>
</CommonTooltip> </CommonTooltip>
<template v-if="editor">
<CommonTooltip placement="bottom" content="Toggle code block">
<button
btn-action-icon
:class="editor.isActive('codeBlock') ? 'op100' : 'op50'"
@click="editor?.chain().focus().toggleCodeBlock().run()"
>
<div i-ri:code-s-slash-line />
</button>
</CommonTooltip>
</template>
<div flex-auto /> <div flex-auto />
<CommonTooltip placement="bottom" content="Add content warning"> <CommonTooltip placement="bottom" content="Add content warning">

View File

@ -0,0 +1,58 @@
<script setup lang="ts">
const { items, command } = defineProps<{
items: any[]
command: Function
}>()
let selectedIndex = $ref(0)
watch(items, () => {
selectedIndex = 0
})
function onKeyDown(event: KeyboardEvent) {
if (event.key === 'ArrowUp') {
selectedIndex = ((selectedIndex + items.length) - 1) % items.length
return true
}
else if (event.key === 'ArrowDown') {
selectedIndex = (selectedIndex + 1) % items.length
return true
}
else if (event.key === 'Enter') {
selectItem(selectedIndex)
return true
}
return false
}
function selectItem(index: number) {
const item = items[index]
if (item)
command({ id: item })
}
defineExpose({
onKeyDown,
})
</script>
<template>
<div relative bg-base text-base shadow border="~ base rounded" text-sm>
<template v-if="items.length">
<button
v-for="(item, index) in items"
:key="index"
:class="index === selectedIndex ? 'bg-active' : 'op50'"
block m0 w-full text-left px2 py1
@click="selectItem(index)"
>
{{ item }}asd
</button>
</template>
<div v-else block m0 w-full text-left px2 py1 italic op30>
No result
</div>
</div>
</template>

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

View File

@ -4,6 +4,8 @@ export const HOST_DOMAIN = process.dev
? 'http://localhost:3000' ? 'http://localhost:3000'
: 'https://elk.zone' : 'https://elk.zone'
export const POST_CHARS_LIMIT = 500
export const DEFAULT_SERVER = 'mas.to' export const DEFAULT_SERVER = 'mas.to'
export const STORAGE_KEY_DRAFTS = 'elk-drafts' export const STORAGE_KEY_DRAFTS = 'elk-drafts'

View File

@ -15,6 +15,7 @@ export default defineNuxtConfig({
'floating-vue/dist/style.css', 'floating-vue/dist/style.css',
'~/styles/vars.css', '~/styles/vars.css',
'~/styles/global.css', '~/styles/global.css',
'~/styles/tiptap.css',
'~/styles/dropdown.css', '~/styles/dropdown.css',
], ],
alias: { alias: {

View File

@ -23,6 +23,15 @@
"@iconify-json/ri": "^1.1.4", "@iconify-json/ri": "^1.1.4",
"@iconify-json/twemoji": "^1.1.6", "@iconify-json/twemoji": "^1.1.6",
"@pinia/nuxt": "^0.4.5", "@pinia/nuxt": "^0.4.5",
"@tiptap/extension-character-count": "2.0.0-beta.203",
"@tiptap/extension-code-block": "2.0.0-beta.203",
"@tiptap/extension-mention": "2.0.0-beta.203",
"@tiptap/extension-paragraph": "2.0.0-beta.203",
"@tiptap/extension-placeholder": "2.0.0-beta.203",
"@tiptap/extension-text": "2.0.0-beta.203",
"@tiptap/starter-kit": "2.0.0-beta.203",
"@tiptap/suggestion": "2.0.0-beta.203",
"@tiptap/vue-3": "2.0.0-beta.203",
"@types/fs-extra": "^9.0.13", "@types/fs-extra": "^9.0.13",
"@types/js-yaml": "^4.0.5", "@types/js-yaml": "^4.0.5",
"@types/prettier": "^2.7.1", "@types/prettier": "^2.7.1",
@ -51,6 +60,7 @@
"sanitize-html": "^2.7.3", "sanitize-html": "^2.7.3",
"shiki": "^0.11.1", "shiki": "^0.11.1",
"theme-vitesse": "^0.6.0", "theme-vitesse": "^0.6.0",
"tippy.js": "^6.3.7",
"typescript": "^4.9.3", "typescript": "^4.9.3",
"ufo": "^1.0.0", "ufo": "^1.0.0",
"unplugin-auto-import": "^0.11.5", "unplugin-auto-import": "^0.11.5",

View File

@ -8,6 +8,15 @@ specifiers:
'@iconify-json/ri': ^1.1.4 '@iconify-json/ri': ^1.1.4
'@iconify-json/twemoji': ^1.1.6 '@iconify-json/twemoji': ^1.1.6
'@pinia/nuxt': ^0.4.5 '@pinia/nuxt': ^0.4.5
'@tiptap/extension-character-count': 2.0.0-beta.203
'@tiptap/extension-code-block': 2.0.0-beta.203
'@tiptap/extension-mention': 2.0.0-beta.203
'@tiptap/extension-paragraph': 2.0.0-beta.203
'@tiptap/extension-placeholder': 2.0.0-beta.203
'@tiptap/extension-text': 2.0.0-beta.203
'@tiptap/starter-kit': 2.0.0-beta.203
'@tiptap/suggestion': 2.0.0-beta.203
'@tiptap/vue-3': 2.0.0-beta.203
'@types/fs-extra': ^9.0.13 '@types/fs-extra': ^9.0.13
'@types/js-yaml': ^4.0.5 '@types/js-yaml': ^4.0.5
'@types/prettier': ^2.7.1 '@types/prettier': ^2.7.1
@ -36,6 +45,7 @@ specifiers:
sanitize-html: ^2.7.3 sanitize-html: ^2.7.3
shiki: ^0.11.1 shiki: ^0.11.1
theme-vitesse: ^0.6.0 theme-vitesse: ^0.6.0
tippy.js: ^6.3.7
typescript: ^4.9.3 typescript: ^4.9.3
ufo: ^1.0.0 ufo: ^1.0.0
unplugin-auto-import: ^0.11.5 unplugin-auto-import: ^0.11.5
@ -49,6 +59,15 @@ devDependencies:
'@iconify-json/ri': 1.1.4 '@iconify-json/ri': 1.1.4
'@iconify-json/twemoji': 1.1.6 '@iconify-json/twemoji': 1.1.6
'@pinia/nuxt': 0.4.5_typescript@4.9.3 '@pinia/nuxt': 0.4.5_typescript@4.9.3
'@tiptap/extension-character-count': 2.0.0-beta.203
'@tiptap/extension-code-block': 2.0.0-beta.203
'@tiptap/extension-mention': 2.0.0-beta.203_66kmopqpbsmmkalw2shrvalvci
'@tiptap/extension-paragraph': 2.0.0-beta.203
'@tiptap/extension-placeholder': 2.0.0-beta.203
'@tiptap/extension-text': 2.0.0-beta.203
'@tiptap/starter-kit': 2.0.0-beta.203
'@tiptap/suggestion': 2.0.0-beta.203
'@tiptap/vue-3': 2.0.0-beta.203
'@types/fs-extra': 9.0.13 '@types/fs-extra': 9.0.13
'@types/js-yaml': 4.0.5 '@types/js-yaml': 4.0.5
'@types/prettier': 2.7.1 '@types/prettier': 2.7.1
@ -77,6 +96,7 @@ devDependencies:
sanitize-html: 2.7.3 sanitize-html: 2.7.3
shiki: 0.11.1 shiki: 0.11.1
theme-vitesse: 0.6.0 theme-vitesse: 0.6.0
tippy.js: 6.3.7
typescript: 4.9.3 typescript: 4.9.3
ufo: 1.0.0 ufo: 1.0.0
unplugin-auto-import: 0.11.5 unplugin-auto-import: 0.11.5
@ -926,6 +946,10 @@ packages:
resolution: {integrity: sha512-a5Sab1C4/icpTZVzZc5Ghpz88yQtGOyNqYXcZgOssB2uuAr+wF/MvN6bgtW32q7HHrvBki+BsZ0OuNv6EV3K9g==} resolution: {integrity: sha512-a5Sab1C4/icpTZVzZc5Ghpz88yQtGOyNqYXcZgOssB2uuAr+wF/MvN6bgtW32q7HHrvBki+BsZ0OuNv6EV3K9g==}
dev: true dev: true
/@popperjs/core/2.11.6:
resolution: {integrity: sha512-50/17A98tWUfQ176raKiOGXuYpLyyVMkxxG6oylzL3BPOlA6ADGdK7EYunSa4I064xerltq9TGXs8HmOk5E+vw==}
dev: true
/@rollup/plugin-alias/4.0.2_rollup@2.79.1: /@rollup/plugin-alias/4.0.2_rollup@2.79.1:
resolution: {integrity: sha512-1hv7dBOZZwo3SEupxn4UA2N0EDThqSSS+wI1St1TNTBtOZvUchyIClyHcnDcjjrReTPZ47Faedrhblv4n+T5UQ==} resolution: {integrity: sha512-1hv7dBOZZwo3SEupxn4UA2N0EDThqSSS+wI1St1TNTBtOZvUchyIClyHcnDcjjrReTPZ47Faedrhblv4n+T5UQ==}
engines: {node: '>=14.0.0'} engines: {node: '>=14.0.0'}
@ -1066,6 +1090,283 @@ packages:
rollup: 2.79.1 rollup: 2.79.1
dev: true dev: true
/@tiptap/core/2.0.0-beta.203:
resolution: {integrity: sha512-iRkFv4jjRtI7b18quyO3C8rilD8T24S3KYrZ3idRRw+ifO0dTeuDsRKjlcDT815zJRYdz99s5/lGq2ES0vC2gA==}
dependencies:
prosemirror-commands: 1.3.1
prosemirror-keymap: 1.2.0
prosemirror-model: 1.18.3
prosemirror-schema-list: 1.2.2
prosemirror-state: 1.4.2
prosemirror-transform: 1.7.0
prosemirror-view: 1.29.1
dev: true
/@tiptap/extension-blockquote/2.0.0-beta.203_ywil7uncaz323pl33xz5cqvwo4:
resolution: {integrity: sha512-e8jE9AZ5L21AO6exMxFWFgU0c0ygxWVcuYJRMiThNhj38NWCNoHGHDqXbOjh2kM9NoRnad9erFuilVgYRmXcTw==}
peerDependencies:
'@tiptap/core': ^2.0.0-beta.1
dependencies:
'@tiptap/core': 2.0.0-beta.203
dev: true
/@tiptap/extension-bold/2.0.0-beta.203_ywil7uncaz323pl33xz5cqvwo4:
resolution: {integrity: sha512-MuhBt7O44hJZ/5N42wVSN/9YB0iXkFgRkltbXPaWDqrXQjbZh9NRXMbQw7pOuW+3gn4NmUP5cd6pe9qHII1MtA==}
peerDependencies:
'@tiptap/core': ^2.0.0-beta.193
dependencies:
'@tiptap/core': 2.0.0-beta.203
dev: true
/@tiptap/extension-bubble-menu/2.0.0-beta.203:
resolution: {integrity: sha512-5EdYWCi6SyKVpY6xPqD5S0u7Jz8CG/FjgnFng0FBBJ2dCvxbeVdwTWL/WwN3KmIkY8T91ScQtbJb0bruC+GIUw==}
peerDependencies:
'@tiptap/core': ^2.0.0-beta.193
dependencies:
prosemirror-state: 1.4.2
prosemirror-view: 1.29.1
tippy.js: 6.3.7
dev: true
/@tiptap/extension-bullet-list/2.0.0-beta.203_ywil7uncaz323pl33xz5cqvwo4:
resolution: {integrity: sha512-u9uY4XL0y9cIwEsm8008fpMPGXr9IVxbbmRXGh19oRnBPS1C3vnxGmgThrXojYwEWkp+5NimoH/E6ljUbuNbBQ==}
peerDependencies:
'@tiptap/core': ^2.0.0-beta.193
dependencies:
'@tiptap/core': 2.0.0-beta.203
dev: true
/@tiptap/extension-character-count/2.0.0-beta.203:
resolution: {integrity: sha512-j/XzlqVXATzKqbStJyU2VlykIIjLraki8TK9vTTLagQB6lYTv/AJT6L/Y0Ei7yUSjKYOL6sL7/Rze8czqeykNw==}
peerDependencies:
'@tiptap/core': ^2.0.0-beta.193
dependencies:
prosemirror-model: 1.18.3
prosemirror-state: 1.4.2
dev: true
/@tiptap/extension-code-block/2.0.0-beta.203:
resolution: {integrity: sha512-wXN1POlJBA9NG0eTKRGoBQFX40+TQUvfYi3i1mDk47sNdtbITJIFR3WRkljqSWrFbKdLFKPADUaQ+k6f2KZm/w==}
peerDependencies:
'@tiptap/core': ^2.0.0-beta.193
dependencies:
prosemirror-state: 1.4.2
dev: true
/@tiptap/extension-code-block/2.0.0-beta.203_ywil7uncaz323pl33xz5cqvwo4:
resolution: {integrity: sha512-wXN1POlJBA9NG0eTKRGoBQFX40+TQUvfYi3i1mDk47sNdtbITJIFR3WRkljqSWrFbKdLFKPADUaQ+k6f2KZm/w==}
peerDependencies:
'@tiptap/core': ^2.0.0-beta.193
dependencies:
'@tiptap/core': 2.0.0-beta.203
prosemirror-state: 1.4.2
dev: true
/@tiptap/extension-code/2.0.0-beta.203_ywil7uncaz323pl33xz5cqvwo4:
resolution: {integrity: sha512-iYC26EI4V4aAh10dq6LuCbPgHHrNCURotn2jA90fOjFnCspIuAXdQsPPV6syCLIIUdjFT8t/dscijlhzMYzVOg==}
peerDependencies:
'@tiptap/core': ^2.0.0-beta.193
dependencies:
'@tiptap/core': 2.0.0-beta.203
dev: true
/@tiptap/extension-document/2.0.0-beta.203_ywil7uncaz323pl33xz5cqvwo4:
resolution: {integrity: sha512-H0HyFvnlMl0dHujGO+/+xjmOgu3aTslWVNLT2hOxBvfCAwW4hLmxQVaaMr+75so68rr1ndTPGUnPtXlQHvtzbA==}
peerDependencies:
'@tiptap/core': ^2.0.0-beta.193
dependencies:
'@tiptap/core': 2.0.0-beta.203
dev: true
/@tiptap/extension-dropcursor/2.0.0-beta.203_ywil7uncaz323pl33xz5cqvwo4:
resolution: {integrity: sha512-oR4WjZdNcxYeYuKDzugMgEZGH7c6uzkV6InewPpHSuKVcbzjF1HbS/EHgpBSRFvLRYJ+nrbJMzERLWn9ZS5njA==}
peerDependencies:
'@tiptap/core': ^2.0.0-beta.193
dependencies:
'@tiptap/core': 2.0.0-beta.203
prosemirror-dropcursor: 1.5.0
dev: true
/@tiptap/extension-floating-menu/2.0.0-beta.203:
resolution: {integrity: sha512-QiWXX9vmDTvP6jo/lwZpQ/7Sf2XCeD1RQQmWIC+cohfxdd606dMVvFhYt8OofjoWn+e4uxobEtfxkvA418zUkg==}
peerDependencies:
'@tiptap/core': ^2.0.0-beta.193
dependencies:
prosemirror-state: 1.4.2
prosemirror-view: 1.29.1
tippy.js: 6.3.7
dev: true
/@tiptap/extension-gapcursor/2.0.0-beta.203_ywil7uncaz323pl33xz5cqvwo4:
resolution: {integrity: sha512-HwY2dwAyIBOy+V2+Dbyw59POtAxz7UER9R470SGabi9/M7rBhZYrzL0gs+V9OB4AH1OO5hib02l8Dcnq+KKF7A==}
peerDependencies:
'@tiptap/core': ^2.0.0-beta.193
dependencies:
'@tiptap/core': 2.0.0-beta.203
prosemirror-gapcursor: 1.3.1
dev: true
/@tiptap/extension-hard-break/2.0.0-beta.203_ywil7uncaz323pl33xz5cqvwo4:
resolution: {integrity: sha512-FfMufoiIwzNTsTZkb6qaNpJbyh6dOYagnGzlDmjyvc6+wqdJWE4sxwVAcMO0j1tvCdkaozWeWSuJzYv4xZKMFA==}
peerDependencies:
'@tiptap/core': ^2.0.0-beta.193
dependencies:
'@tiptap/core': 2.0.0-beta.203
dev: true
/@tiptap/extension-heading/2.0.0-beta.203_ywil7uncaz323pl33xz5cqvwo4:
resolution: {integrity: sha512-pjBQNwWya+eGffesgn/Fz+7xC+RoiRVNo5xjahZVSP2MVZwbvcq/UV+fIut1nu9WJgPfAPkYnBSXmbPp0SRB0g==}
peerDependencies:
'@tiptap/core': ^2.0.0-beta.193
dependencies:
'@tiptap/core': 2.0.0-beta.203
dev: true
/@tiptap/extension-history/2.0.0-beta.203_ywil7uncaz323pl33xz5cqvwo4:
resolution: {integrity: sha512-3gaplasYTuHepP1gnP/p5qjN5Sz9QudXz3Vue+2j1XulTFDjoB83j4FSujnAPshw2hePXNxv1mdHeeJ219Odxw==}
peerDependencies:
'@tiptap/core': ^2.0.0-beta.193
dependencies:
'@tiptap/core': 2.0.0-beta.203
prosemirror-history: 1.3.0
dev: true
/@tiptap/extension-horizontal-rule/2.0.0-beta.203_ywil7uncaz323pl33xz5cqvwo4:
resolution: {integrity: sha512-mvERa4IfFBPVG1he1b+urtQD8mk9nGzZif9yPMfcpDIW8ewJv/MUH/mEASxZbb7cqXOMAWZqp1rVpH/MLBgtfw==}
peerDependencies:
'@tiptap/core': ^2.0.0-beta.193
dependencies:
'@tiptap/core': 2.0.0-beta.203
prosemirror-state: 1.4.2
dev: true
/@tiptap/extension-italic/2.0.0-beta.203_ywil7uncaz323pl33xz5cqvwo4:
resolution: {integrity: sha512-7X/Z6V2DnziNfHhIoCLHI+EKQoaz0nyjPvNvs7wfSno6LTYUz33bXBpPF7gNZPyBJK/F/znC2Mg2l6XzcI7c+g==}
peerDependencies:
'@tiptap/core': ^2.0.0-beta.193
dependencies:
'@tiptap/core': 2.0.0-beta.203
dev: true
/@tiptap/extension-list-item/2.0.0-beta.203_ywil7uncaz323pl33xz5cqvwo4:
resolution: {integrity: sha512-hfVxILSkLGKH4AVkj1imyHu1hAJjV6gWPDm7zQDi9MtQEoxU3fH+5nLwedsrveDZZHguwjHc/B+JyGcljzVeeg==}
peerDependencies:
'@tiptap/core': ^2.0.0-beta.193
dependencies:
'@tiptap/core': 2.0.0-beta.203
dev: true
/@tiptap/extension-mention/2.0.0-beta.203_66kmopqpbsmmkalw2shrvalvci:
resolution: {integrity: sha512-GR29PExMNE0mnQ4r30fgi+6QStrtxErRomnxKWlMMCdb+pHEgKrk1rbcZeLMc+efoainDMzN4HkFSAWmdKTSDw==}
peerDependencies:
'@tiptap/core': ^2.0.0-beta.193
'@tiptap/suggestion': ^2.0.0-beta.193
dependencies:
'@tiptap/suggestion': 2.0.0-beta.203
prosemirror-model: 1.18.3
prosemirror-state: 1.4.2
dev: true
/@tiptap/extension-ordered-list/2.0.0-beta.203_ywil7uncaz323pl33xz5cqvwo4:
resolution: {integrity: sha512-Dp+SzrJ4yrFheng8pAtow19/wviC2g4QcxT88ZVz4wjC6JTo0M6sOQg9slxvx+Q+VbqrmPdikbqTiE/Ef416ig==}
peerDependencies:
'@tiptap/core': ^2.0.0-beta.193
dependencies:
'@tiptap/core': 2.0.0-beta.203
dev: true
/@tiptap/extension-paragraph/2.0.0-beta.203:
resolution: {integrity: sha512-kqsW7KPl2orCEJiNjCRCY/p06TrTTiq2n2hxatFRbHwvpQC4Z71JgaRJ/28WCL61QVy9v2UwNmCT2NFxUvMLgg==}
peerDependencies:
'@tiptap/core': ^2.0.0-beta.193
dev: true
/@tiptap/extension-paragraph/2.0.0-beta.203_ywil7uncaz323pl33xz5cqvwo4:
resolution: {integrity: sha512-kqsW7KPl2orCEJiNjCRCY/p06TrTTiq2n2hxatFRbHwvpQC4Z71JgaRJ/28WCL61QVy9v2UwNmCT2NFxUvMLgg==}
peerDependencies:
'@tiptap/core': ^2.0.0-beta.193
dependencies:
'@tiptap/core': 2.0.0-beta.203
dev: true
/@tiptap/extension-placeholder/2.0.0-beta.203:
resolution: {integrity: sha512-MMwCzCZdapY6+LIubo/c4KmdXM1NKc2sBu8ahWF97h9pfs7UGxYDtWoAAUQlV4IzFiC5OhpYHwhStOaD3LhWjw==}
peerDependencies:
'@tiptap/core': ^2.0.0-beta.193
dependencies:
prosemirror-model: 1.18.3
prosemirror-state: 1.4.2
prosemirror-view: 1.29.1
dev: true
/@tiptap/extension-strike/2.0.0-beta.203_ywil7uncaz323pl33xz5cqvwo4:
resolution: {integrity: sha512-CxgaJybQs36AUn1PrXbiNbqliYkf4n7LM/NvqtkoXPLISvndqAEQGmx1hS0NdoqERoAIz2FTOBdoWrL0b60vFA==}
peerDependencies:
'@tiptap/core': ^2.0.0-beta.193
dependencies:
'@tiptap/core': 2.0.0-beta.203
dev: true
/@tiptap/extension-text/2.0.0-beta.203:
resolution: {integrity: sha512-hOAPb3C2nIFZNJaFCaWj72sgcXqxJNTazXcsiei9A/p0L4NAIVa0ySub7H3NxRvxY/hRLUniA6u3QTzMo7Xsug==}
peerDependencies:
'@tiptap/core': ^2.0.0-beta.193
dev: true
/@tiptap/extension-text/2.0.0-beta.203_ywil7uncaz323pl33xz5cqvwo4:
resolution: {integrity: sha512-hOAPb3C2nIFZNJaFCaWj72sgcXqxJNTazXcsiei9A/p0L4NAIVa0ySub7H3NxRvxY/hRLUniA6u3QTzMo7Xsug==}
peerDependencies:
'@tiptap/core': ^2.0.0-beta.193
dependencies:
'@tiptap/core': 2.0.0-beta.203
dev: true
/@tiptap/starter-kit/2.0.0-beta.203:
resolution: {integrity: sha512-tKnQW1MA+9MijptQuIUlJYIeulMLhKRFbcR++UM/K1oRw6nlOyyvFz07prehIPwsjV0RsZg0TYYiuNTWOaEOAg==}
dependencies:
'@tiptap/core': 2.0.0-beta.203
'@tiptap/extension-blockquote': 2.0.0-beta.203_ywil7uncaz323pl33xz5cqvwo4
'@tiptap/extension-bold': 2.0.0-beta.203_ywil7uncaz323pl33xz5cqvwo4
'@tiptap/extension-bullet-list': 2.0.0-beta.203_ywil7uncaz323pl33xz5cqvwo4
'@tiptap/extension-code': 2.0.0-beta.203_ywil7uncaz323pl33xz5cqvwo4
'@tiptap/extension-code-block': 2.0.0-beta.203_ywil7uncaz323pl33xz5cqvwo4
'@tiptap/extension-document': 2.0.0-beta.203_ywil7uncaz323pl33xz5cqvwo4
'@tiptap/extension-dropcursor': 2.0.0-beta.203_ywil7uncaz323pl33xz5cqvwo4
'@tiptap/extension-gapcursor': 2.0.0-beta.203_ywil7uncaz323pl33xz5cqvwo4
'@tiptap/extension-hard-break': 2.0.0-beta.203_ywil7uncaz323pl33xz5cqvwo4
'@tiptap/extension-heading': 2.0.0-beta.203_ywil7uncaz323pl33xz5cqvwo4
'@tiptap/extension-history': 2.0.0-beta.203_ywil7uncaz323pl33xz5cqvwo4
'@tiptap/extension-horizontal-rule': 2.0.0-beta.203_ywil7uncaz323pl33xz5cqvwo4
'@tiptap/extension-italic': 2.0.0-beta.203_ywil7uncaz323pl33xz5cqvwo4
'@tiptap/extension-list-item': 2.0.0-beta.203_ywil7uncaz323pl33xz5cqvwo4
'@tiptap/extension-ordered-list': 2.0.0-beta.203_ywil7uncaz323pl33xz5cqvwo4
'@tiptap/extension-paragraph': 2.0.0-beta.203_ywil7uncaz323pl33xz5cqvwo4
'@tiptap/extension-strike': 2.0.0-beta.203_ywil7uncaz323pl33xz5cqvwo4
'@tiptap/extension-text': 2.0.0-beta.203_ywil7uncaz323pl33xz5cqvwo4
dev: true
/@tiptap/suggestion/2.0.0-beta.203:
resolution: {integrity: sha512-Pqk8QgKB08Rinvpd0dQnWLr+SPwwlZF5NX/v3cGqZ18ZJvE3UahVJD+Suj6oTLsgMba5hsXbPAIdGMiy0Q9PUw==}
peerDependencies:
'@tiptap/core': ^2.0.0-beta.193
dependencies:
prosemirror-model: 1.18.3
prosemirror-state: 1.4.2
prosemirror-view: 1.29.1
dev: true
/@tiptap/vue-3/2.0.0-beta.203:
resolution: {integrity: sha512-JkRNyVJMnENZVYQRV6vvR6IO3UXq2sqwLbu3WeRKeTaqZtb1Tzt+80UA2vTELN+TB5PUGtaqs+MNrB94bdPGrA==}
peerDependencies:
'@tiptap/core': ^2.0.0-beta.193
vue: ^3.0.0
dependencies:
'@tiptap/extension-bubble-menu': 2.0.0-beta.203
'@tiptap/extension-floating-menu': 2.0.0-beta.203
prosemirror-state: 1.4.2
prosemirror-view: 1.29.1
dev: true
/@trysound/sax/0.2.0: /@trysound/sax/0.2.0:
resolution: {integrity: sha512-L7z9BgrNEcYyUYtF+HaEfiS5ebkh9jXqbszz7pC0hRBPaatV0XjSD3+eHrpqFemQfgwiFF0QPIarnIihIDn7OA==} resolution: {integrity: sha512-L7z9BgrNEcYyUYtF+HaEfiS5ebkh9jXqbszz7pC0hRBPaatV0XjSD3+eHrpqFemQfgwiFF0QPIarnIihIDn7OA==}
engines: {node: '>=10.13.0'} engines: {node: '>=10.13.0'}
@ -5424,6 +5725,10 @@ packages:
wcwidth: 1.0.1 wcwidth: 1.0.1
dev: true dev: true
/orderedmap/2.1.0:
resolution: {integrity: sha512-/pIFexOm6S70EPdznemIz3BQZoJ4VTFrhqzu0ACBqBgeLsLxq8e6Jim63ImIfwW/zAD1AlXpRMlOv3aghmo4dA==}
dev: true
/os-tmpdir/1.0.2: /os-tmpdir/1.0.2:
resolution: {integrity: sha512-D2FR03Vir7FIu45XBY20mTb+/ZSWB00sjU9jdQXt83gDrI4Ztz5Fs7/yy74g2N5SVQY4xY1qDr4rNddwYRVX0g==} resolution: {integrity: sha512-D2FR03Vir7FIu45XBY20mTb+/ZSWB00sjU9jdQXt83gDrI4Ztz5Fs7/yy74g2N5SVQY4xY1qDr4rNddwYRVX0g==}
engines: {node: '>=0.10.0'} engines: {node: '>=0.10.0'}
@ -5997,6 +6302,82 @@ packages:
resolution: {integrity: sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==} resolution: {integrity: sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==}
dev: true dev: true
/prosemirror-commands/1.3.1:
resolution: {integrity: sha512-XTporPgoECkOQACVw0JTe3RZGi+fls3/byqt+tXwGTkD7qLuB4KdVrJamDMJf4kfKga3uB8hZ+kUUyZ5oWpnfg==}
dependencies:
prosemirror-model: 1.18.3
prosemirror-state: 1.4.2
prosemirror-transform: 1.7.0
dev: true
/prosemirror-dropcursor/1.5.0:
resolution: {integrity: sha512-vy7i77ddKyXlu8kKBB3nlxLBnsWyKUmQIPB5x8RkYNh01QNp/qqGmdd5yZefJs0s3rtv5r7Izfu2qbtr+tYAMQ==}
dependencies:
prosemirror-state: 1.4.2
prosemirror-transform: 1.7.0
prosemirror-view: 1.29.1
dev: true
/prosemirror-gapcursor/1.3.1:
resolution: {integrity: sha512-GKTeE7ZoMsx5uVfc51/ouwMFPq0o8YrZ7Hx4jTF4EeGbXxBveUV8CGv46mSHuBBeXGmvu50guoV2kSnOeZZnUA==}
dependencies:
prosemirror-keymap: 1.2.0
prosemirror-model: 1.18.3
prosemirror-state: 1.4.2
prosemirror-view: 1.29.1
dev: true
/prosemirror-history/1.3.0:
resolution: {integrity: sha512-qo/9Wn4B/Bq89/YD+eNWFbAytu6dmIM85EhID+fz9Jcl9+DfGEo8TTSrRhP15+fFEoaPqpHSxlvSzSEbmlxlUA==}
dependencies:
prosemirror-state: 1.4.2
prosemirror-transform: 1.7.0
rope-sequence: 1.3.3
dev: true
/prosemirror-keymap/1.2.0:
resolution: {integrity: sha512-TdSfu+YyLDd54ufN/ZeD1VtBRYpgZnTPnnbY+4R08DDgs84KrIPEPbJL8t1Lm2dkljFx6xeBE26YWH3aIzkPKg==}
dependencies:
prosemirror-state: 1.4.2
w3c-keyname: 2.2.6
dev: true
/prosemirror-model/1.18.3:
resolution: {integrity: sha512-yUVejauEY3F1r7PDy4UJKEGeIU+KFc71JQl5sNvG66CLVdKXRjhWpBW6KMeduGsmGOsw85f6EGrs6QxIKOVILA==}
dependencies:
orderedmap: 2.1.0
dev: true
/prosemirror-schema-list/1.2.2:
resolution: {integrity: sha512-rd0pqSDp86p0MUMKG903g3I9VmElFkQpkZ2iOd3EOVg1vo5Cst51rAsoE+5IPy0LPXq64eGcCYlW1+JPNxOj2w==}
dependencies:
prosemirror-model: 1.18.3
prosemirror-state: 1.4.2
prosemirror-transform: 1.7.0
dev: true
/prosemirror-state/1.4.2:
resolution: {integrity: sha512-puuzLD2mz/oTdfgd8msFbe0A42j5eNudKAAPDB0+QJRw8cO1ygjLmhLrg9RvDpf87Dkd6D4t93qdef00KKNacQ==}
dependencies:
prosemirror-model: 1.18.3
prosemirror-transform: 1.7.0
prosemirror-view: 1.29.1
dev: true
/prosemirror-transform/1.7.0:
resolution: {integrity: sha512-O4T697Cqilw06Zvc3Wm+e237R6eZtJL/xGMliCi+Uo8VL6qHk6afz1qq0zNjT3eZMuYwnP8ZS0+YxX/tfcE9TQ==}
dependencies:
prosemirror-model: 1.18.3
dev: true
/prosemirror-view/1.29.1:
resolution: {integrity: sha512-OhujVZSDsh0l0PyHNdfaBj6DBkbhYaCfbaxmTeFrMKd/eWS+G6IC+OAbmR9IsLC8Se1HSbphMaXnsXjupHL3UQ==}
dependencies:
prosemirror-model: 1.18.3
prosemirror-state: 1.4.2
prosemirror-transform: 1.7.0
dev: true
/protocols/2.0.1: /protocols/2.0.1:
resolution: {integrity: sha512-/XJ368cyBJ7fzLMwLKv1e4vLxOju2MNAIokcr7meSaNcVbWz/CPcW22cP04mwxOErdA5mwjA8Q6w/cdAQxVn7Q==} resolution: {integrity: sha512-/XJ368cyBJ7fzLMwLKv1e4vLxOju2MNAIokcr7meSaNcVbWz/CPcW22cP04mwxOErdA5mwjA8Q6w/cdAQxVn7Q==}
dev: true dev: true
@ -6236,6 +6617,10 @@ packages:
fsevents: 2.3.2 fsevents: 2.3.2
dev: true dev: true
/rope-sequence/1.3.3:
resolution: {integrity: sha512-85aZYCxweiD5J8yTEbw+E6A27zSnLPNDL0WfPdw3YYodq7WjnTKo0q4dtyQ2gz23iPT8Q9CUyJtAaUNcTxRf5Q==}
dev: true
/run-async/2.4.1: /run-async/2.4.1:
resolution: {integrity: sha512-tvVnVv01b8c1RrA6Ep7JkStj85Guv/YrMcwqYQnwjsAS2cTmmPGBBjAjpCW7RrSodNSoE2/qg9O4bceNvUuDgQ==} resolution: {integrity: sha512-tvVnVv01b8c1RrA6Ep7JkStj85Guv/YrMcwqYQnwjsAS2cTmmPGBBjAjpCW7RrSodNSoE2/qg9O4bceNvUuDgQ==}
engines: {node: '>=0.12.0'} engines: {node: '>=0.12.0'}
@ -6732,6 +7117,12 @@ packages:
engines: {node: '>=14.0.0'} engines: {node: '>=14.0.0'}
dev: true dev: true
/tippy.js/6.3.7:
resolution: {integrity: sha512-E1d3oP2emgJ9dRQZdf3Kkn0qJgI6ZLpyS5z6ZkY1DF3kaQaBsGZsndEpHwx+eC+tYM41HaSNvNtLx8tU57FzTQ==}
dependencies:
'@popperjs/core': 2.11.6
dev: true
/tmp/0.0.33: /tmp/0.0.33:
resolution: {integrity: sha512-jRCJlojKnZ3addtTOjdIqoRuPEKBvNXcGYqzO6zWZX8KfKEpnGY5jfggJQ3EjKuu8D4bJRr0y+cYJFmYbImXGw==} resolution: {integrity: sha512-jRCJlojKnZ3addtTOjdIqoRuPEKBvNXcGYqzO6zWZX8KfKEpnGY5jfggJQ3EjKuu8D4bJRr0y+cYJFmYbImXGw==}
engines: {node: '>=0.6.0'} engines: {node: '>=0.6.0'}
@ -7448,6 +7839,10 @@ packages:
'@vue/shared': 3.2.45 '@vue/shared': 3.2.45
dev: true dev: true
/w3c-keyname/2.2.6:
resolution: {integrity: sha512-f+fciywl1SJEniZHD6H+kUO8gOnwIr7f4ijKA6+ZvJFjeGi1r4PDLl53Ayud9O/rk64RqgoQine0feoeOU0kXg==}
dev: true
/wcwidth/1.0.1: /wcwidth/1.0.1:
resolution: {integrity: sha512-XHPEwS0q6TaxcvG85+8EYkbiCux2XtWG2mkc47Ng2A77BQu9+DqIOJldST4HgPkuea7dvKSj5VgX3P1d4rW8Tg==} resolution: {integrity: sha512-XHPEwS0q6TaxcvG85+8EYkbiCux2XtWG2mkc47Ng2A77BQu9+DqIOJldST4HgPkuea7dvKSj5VgX3P1d4rW8Tg==}
dependencies: dependencies:

View File

@ -24,11 +24,23 @@
background: #8886; background: #8886;
} }
::-moz-selection {
background: var(--c-bg-selection);
}
::selection {
background: var(--c-bg-selection);
}
/* Force vertical scrollbar to be always visible to avoid layout shift while loading the content */ /* Force vertical scrollbar to be always visible to avoid layout shift while loading the content */
html { html {
overflow-y: scroll; overflow-y: scroll;
} }
.zen .zen-hide {
--at-apply: op0 hover:op100 transition duration-600;
}
.custom-emoji { .custom-emoji {
display: inline-block; display: inline-block;
max-height: 1.2em; max-height: 1.2em;
@ -36,7 +48,7 @@ html {
vertical-align: middle; vertical-align: middle;
} }
.rich-content { .content-rich {
a { a {
--at-apply: text-primary hover:underline hover:text-primary-active; --at-apply: text-primary hover:underline hover:text-primary-active;
.invisible { .invisible {
@ -46,7 +58,7 @@ html {
--at-apply: truncate overflow-hidden ws-nowrap; --at-apply: truncate overflow-hidden ws-nowrap;
} }
} }
b { b, strong {
--at-apply: font-bold; --at-apply: font-bold;
} }
p { p {
@ -62,6 +74,14 @@ html {
} }
} }
.zen .zen-hide { .content-editor {
--at-apply: op0 hover:op100 transition duration-600; --at-apply: outline-none;
pre {
--at-apply: font-mono bg-code rounded px3 py2;
code {
--at-apply: bg-transparent text-0.8rem p0;
}
}
} }

View File

@ -0,0 +1,7 @@
.ProseMirror p.is-editor-empty:first-child::before {
content: attr(data-placeholder);
float: left;
pointer-events: none;
height: 0;
opacity: 0.4;
}

View File

@ -5,6 +5,7 @@
--c-bg-base: #fff; --c-bg-base: #fff;
--c-bg-active: #f6f6f6; --c-bg-active: #f6f6f6;
--c-bg-code: #00000006; --c-bg-code: #00000006;
--c-bg-selection: #8885;
--c-text-base: #222; --c-text-base: #222;
--c-text-secondary: #888; --c-text-secondary: #888;
} }

View File

@ -4,7 +4,7 @@ import { renderToString } from 'vue/server-renderer'
import { format } from 'prettier' import { format } from 'prettier'
import { contentToVNode } from '~/composables/content' import { contentToVNode } from '~/composables/content'
describe('rich-content', () => { describe('content-rich', () => {
it('empty', async () => { it('empty', async () => {
const { formatted } = await render('') const { formatted } = await render('')
expect(formatted).toMatchSnapshot() expect(formatted).toMatchSnapshot()