feat: improve editor
parent
b784264d96
commit
ccf6a17f72
|
@ -9,6 +9,7 @@ const {
|
||||||
draftKey,
|
draftKey,
|
||||||
initial = getDefaultDraft() as never /* Bug of vue-core */,
|
initial = getDefaultDraft() as never /* Bug of vue-core */,
|
||||||
expanded: _expanded = false,
|
expanded: _expanded = false,
|
||||||
|
placeholder,
|
||||||
} = defineProps<{
|
} = defineProps<{
|
||||||
draftKey: string
|
draftKey: string
|
||||||
initial?: () => Draft
|
initial?: () => Draft
|
||||||
|
@ -18,6 +19,7 @@ const {
|
||||||
expanded?: boolean
|
expanded?: boolean
|
||||||
}>()
|
}>()
|
||||||
|
|
||||||
|
const { t } = useI18n()
|
||||||
// eslint-disable-next-line prefer-const
|
// eslint-disable-next-line prefer-const
|
||||||
let { draft, isEmpty } = $(useDraft(draftKey, initial))
|
let { draft, isEmpty } = $(useDraft(draftKey, initial))
|
||||||
|
|
||||||
|
@ -30,7 +32,7 @@ const { editor } = useTiptap({
|
||||||
get: () => draft.params.status,
|
get: () => draft.params.status,
|
||||||
set: newVal => draft.params.status = newVal,
|
set: newVal => draft.params.status = newVal,
|
||||||
}),
|
}),
|
||||||
placeholder: computed(() => draft.placeholder),
|
placeholder: computed(() => placeholder || draft.params.inReplyToId ? t('placeholder.replying') : t('placeholder.default_1')),
|
||||||
autofocus: shouldExpanded,
|
autofocus: shouldExpanded,
|
||||||
onSubmit: publish,
|
onSubmit: publish,
|
||||||
onFocus() { isExpanded = true },
|
onFocus() { isExpanded = true },
|
||||||
|
|
|
@ -20,7 +20,7 @@ const selectedLanguage = computed({
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<NodeViewWrapper>
|
<NodeViewWrapper>
|
||||||
<div relative my2>
|
<div relative my2 class="code-block">
|
||||||
<select v-model="selectedLanguage" contenteditable="false" absolute top-1 right-1 rounded px2 op0 hover:op100 focus:op100 transition>
|
<select v-model="selectedLanguage" contenteditable="false" absolute top-1 right-1 rounded px2 op0 hover:op100 focus:op100 transition>
|
||||||
<option :value="null">
|
<option :value="null">
|
||||||
plain
|
plain
|
||||||
|
|
|
@ -125,12 +125,26 @@ function treeToText(input: Node): string {
|
||||||
pre = '\n'
|
pre = '\n'
|
||||||
|
|
||||||
if (input.nodeName === 'code') {
|
if (input.nodeName === 'code') {
|
||||||
|
if (input.parentNode?.nodeName === 'pre') {
|
||||||
const clz = input.attrs.find(attr => attr.name === 'class')
|
const clz = input.attrs.find(attr => attr.name === 'class')
|
||||||
const lang = clz?.value.replace('language-', '')
|
const lang = clz?.value.replace('language-', '')
|
||||||
|
|
||||||
pre = `\`\`\`${lang || ''}\n`
|
pre = `\`\`\`${lang || ''}\n`
|
||||||
post = '\n```'
|
post = '\n```'
|
||||||
}
|
}
|
||||||
|
else {
|
||||||
|
pre = '`'
|
||||||
|
post = '`'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else if (input.nodeName === 'b' || input.nodeName === 'strong') {
|
||||||
|
pre = '**'
|
||||||
|
post = '**'
|
||||||
|
}
|
||||||
|
else if (input.nodeName === 'i' || input.nodeName === 'em') {
|
||||||
|
pre = '*'
|
||||||
|
post = '*'
|
||||||
|
}
|
||||||
|
|
||||||
if ('childNodes' in input)
|
if ('childNodes' in input)
|
||||||
body = input.childNodes.map(n => treeToText(n)).join('')
|
body = input.childNodes.map(n => treeToText(n)).join('')
|
||||||
|
|
|
@ -8,7 +8,6 @@ export interface Draft {
|
||||||
status?: Exclude<CreateStatusParams['status'], null>
|
status?: Exclude<CreateStatusParams['status'], null>
|
||||||
}
|
}
|
||||||
attachments: Attachment[]
|
attachments: Attachment[]
|
||||||
placeholder: string
|
|
||||||
}
|
}
|
||||||
export type DraftMap = Record<string, Draft>
|
export type DraftMap = Record<string, Draft>
|
||||||
|
|
||||||
|
@ -24,14 +23,13 @@ export const currentUserDrafts = computed(() => {
|
||||||
})
|
})
|
||||||
|
|
||||||
export function getDefaultDraft(options: Partial<Draft['params'] & Omit<Draft, 'params'>> = {}): Draft {
|
export function getDefaultDraft(options: Partial<Draft['params'] & Omit<Draft, 'params'>> = {}): Draft {
|
||||||
const { t } = useI18n()
|
|
||||||
const {
|
const {
|
||||||
status = '',
|
status = '',
|
||||||
inReplyToId,
|
inReplyToId,
|
||||||
visibility = 'public',
|
visibility = 'public',
|
||||||
placeholder = t('placeholder.default_1'),
|
|
||||||
attachments = [],
|
attachments = [],
|
||||||
} = options
|
} = options
|
||||||
|
|
||||||
return {
|
return {
|
||||||
params: {
|
params: {
|
||||||
status,
|
status,
|
||||||
|
@ -39,7 +37,6 @@ export function getDefaultDraft(options: Partial<Draft['params'] & Omit<Draft, '
|
||||||
visibility,
|
visibility,
|
||||||
},
|
},
|
||||||
attachments,
|
attachments,
|
||||||
placeholder,
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -53,12 +50,10 @@ export function getDraftFromStatus(status: Status, text?: null | string): Draft
|
||||||
}
|
}
|
||||||
|
|
||||||
export function getReplyDraft(status: Status) {
|
export function getReplyDraft(status: Status) {
|
||||||
const { t } = useI18n()
|
|
||||||
return {
|
return {
|
||||||
key: `reply-${status.id}`,
|
key: `reply-${status.id}`,
|
||||||
draft: () => getDefaultDraft({
|
draft: () => getDefaultDraft({
|
||||||
inReplyToId: status!.id,
|
inReplyToId: status!.id,
|
||||||
placeholder: t('placeholder.reply_to_account', [status?.account ? getDisplayName(status.account) : t('placeholder.the_thread')]),
|
|
||||||
visibility: status.visibility,
|
visibility: status.visibility,
|
||||||
}),
|
}),
|
||||||
}
|
}
|
||||||
|
@ -76,7 +71,7 @@ export const isEmptyDraft = (draft: Draft | null | undefined) => {
|
||||||
|
|
||||||
export function useDraft(
|
export function useDraft(
|
||||||
draftKey: string,
|
draftKey: string,
|
||||||
initial: () => Draft = () => getDefaultDraft(),
|
initial: () => Draft = () => getDefaultDraft({}),
|
||||||
) {
|
) {
|
||||||
const draft = computed({
|
const draft = computed({
|
||||||
get() {
|
get() {
|
||||||
|
|
|
@ -98,6 +98,7 @@
|
||||||
"placeholder": {
|
"placeholder": {
|
||||||
"default_1": "What is on your mind?",
|
"default_1": "What is on your mind?",
|
||||||
"reply_to_account": "Reply to {0}",
|
"reply_to_account": "Reply to {0}",
|
||||||
|
"replying": "Replying",
|
||||||
"the_thread": "the thread"
|
"the_thread": "the thread"
|
||||||
},
|
},
|
||||||
"state": {
|
"state": {
|
||||||
|
|
|
@ -67,9 +67,15 @@ body {
|
||||||
p {
|
p {
|
||||||
--at-apply: my-2;
|
--at-apply: my-2;
|
||||||
}
|
}
|
||||||
|
code {
|
||||||
|
--at-apply: bg-code text-code px1 py0.5 rounded text-sm;
|
||||||
|
}
|
||||||
|
pre code {
|
||||||
|
--at-apply: text-base bg-transparent px0 py0 rounded-none;
|
||||||
|
}
|
||||||
|
|
||||||
.code-block {
|
.code-block {
|
||||||
--at-apply: bg-code text-0.9rem p3 mt-2 rounded overflow-auto leading-1.6em;
|
--at-apply: font-mono bg-code text-base p3 mt-2 rounded overflow-auto leading-1.6em;
|
||||||
|
|
||||||
.shiki {
|
.shiki {
|
||||||
background: transparent !important;
|
background: transparent !important;
|
||||||
|
@ -78,15 +84,7 @@ body {
|
||||||
}
|
}
|
||||||
|
|
||||||
.content-editor {
|
.content-editor {
|
||||||
--at-apply: 'outline-none flex-1';
|
--at-apply: outline-none flex-1;
|
||||||
|
|
||||||
pre {
|
|
||||||
--at-apply: font-mono bg-code rounded px3 py2;
|
|
||||||
|
|
||||||
code {
|
|
||||||
--at-apply: bg-transparent text-0.8rem p0;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.content-editor.content-rich {
|
.content-editor.content-rich {
|
||||||
|
|
|
@ -2,13 +2,17 @@
|
||||||
--c-primary: #EA9E44;
|
--c-primary: #EA9E44;
|
||||||
--c-primary-active: #C16929;
|
--c-primary-active: #C16929;
|
||||||
--c-border: #88888820;
|
--c-border: #88888820;
|
||||||
|
|
||||||
--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-bg-selection: #8885;
|
||||||
|
|
||||||
--c-text-base: #232323;
|
--c-text-base: #232323;
|
||||||
|
--c-text-code: #7ca72f;
|
||||||
--c-text-secondary: #686868;
|
--c-text-secondary: #686868;
|
||||||
--c-text-secondary-light: #919191;
|
--c-text-secondary-light: #919191;
|
||||||
|
|
||||||
--c-bg-btn-disabled: #a1a1a1;
|
--c-bg-btn-disabled: #a1a1a1;
|
||||||
--c-text-btn-disabled: #fff;
|
--c-text-btn-disabled: #fff;
|
||||||
--c-text-btn: #232323;
|
--c-text-btn: #232323;
|
||||||
|
@ -18,9 +22,12 @@
|
||||||
--c-bg-base: #111;
|
--c-bg-base: #111;
|
||||||
--c-bg-active: #191919;
|
--c-bg-active: #191919;
|
||||||
--c-bg-code: #ffffff06;
|
--c-bg-code: #ffffff06;
|
||||||
|
|
||||||
--c-text-base: #fff;
|
--c-text-base: #fff;
|
||||||
|
--c-text-code: #90be3d;
|
||||||
--c-text-secondary: #888;
|
--c-text-secondary: #888;
|
||||||
--c-text-secondary-light: #686868;
|
--c-text-secondary-light: #686868;
|
||||||
|
|
||||||
--c-bg-btn-disabled: #2a2a2a;
|
--c-bg-btn-disabled: #2a2a2a;
|
||||||
--c-text-btn-disabled: #919191;
|
--c-text-btn-disabled: #919191;
|
||||||
}
|
}
|
||||||
|
|
|
@ -22,7 +22,7 @@ exports[`content-rich > custom emoji 1`] = `
|
||||||
"Daniel Roe
|
"Daniel Roe
|
||||||
<img
|
<img
|
||||||
src=\\"https://media.mas.to/masto-public/cache/custom_emojis/images/000/288/667/original/c96ba3cb0e0e1eac.png\\"
|
src=\\"https://media.mas.to/masto-public/cache/custom_emojis/images/000/288/667/original/c96ba3cb0e0e1eac.png\\"
|
||||||
alt=\\"nuxt\\"
|
alt=\\":nuxt:\\"
|
||||||
class=\\"custom-emoji\\"
|
class=\\"custom-emoji\\"
|
||||||
/>
|
/>
|
||||||
"
|
"
|
|
@ -0,0 +1,24 @@
|
||||||
|
import { describe, expect, it } from 'vitest'
|
||||||
|
import { htmlToText } from '../composables/content'
|
||||||
|
|
||||||
|
describe('html-to-text', () => {
|
||||||
|
it('inline code', () => {
|
||||||
|
expect(htmlToText('<p>text <code>code</code> inline</p>'))
|
||||||
|
.toMatchInlineSnapshot('"text `code` inline"')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('code block', () => {
|
||||||
|
expect(htmlToText('<p>text </p><pre><code class="language-js">code</code></pre>'))
|
||||||
|
.toMatchInlineSnapshot(`
|
||||||
|
"text
|
||||||
|
\`\`\`js
|
||||||
|
code
|
||||||
|
\`\`\`"
|
||||||
|
`)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('bold & italic', () => {
|
||||||
|
expect(htmlToText('<p>text <b>bold</b> <em>italic</em></p>'))
|
||||||
|
.toMatchInlineSnapshot('"text **bold** *italic*"')
|
||||||
|
})
|
||||||
|
})
|
|
@ -29,3 +29,5 @@ export interface GroupedNotifications {
|
||||||
type: string
|
type: string
|
||||||
items: Notification[]
|
items: Notification[]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export type TranslateFn = ReturnType<typeof useI18n>['t']
|
||||||
|
|
|
@ -22,6 +22,7 @@ export default defineConfig({
|
||||||
|
|
||||||
// text colors
|
// text colors
|
||||||
'text-base': 'text-$c-text-base',
|
'text-base': 'text-$c-text-base',
|
||||||
|
'text-code': 'text-$c-text-code',
|
||||||
'text-inverted': 'text-$c-bg-base',
|
'text-inverted': 'text-$c-bg-base',
|
||||||
'text-secondary': 'text-$c-text-secondary',
|
'text-secondary': 'text-$c-text-secondary',
|
||||||
'text-secondary-light': 'text-$c-text-secondary-light',
|
'text-secondary-light': 'text-$c-text-secondary-light',
|
||||||
|
|
Loading…
Reference in New Issue