editor-wip

This commit is contained in:
Untone 2024-09-19 19:51:56 +03:00
parent d6c6545726
commit 6ca29a351f
3 changed files with 154 additions and 201 deletions

View File

@ -134,9 +134,6 @@
},
"overrides": {
"sass": "1.77.6",
"sass-embedded": {
"sass": "1.77.6"
},
"vite": "5.3.5",
"yjs": "13.6.18",
"y-prosemirror": "1.2.12"

View File

@ -1,4 +1,4 @@
import { Editor } from '@tiptap/core'
import { Editor, EditorOptions } from '@tiptap/core'
import { createSignal } from 'solid-js'
import { createStore } from 'solid-js/store'
import { Meta, StoryObj } from 'storybook-solidjs'
@ -8,7 +8,7 @@ import { SessionContext, SessionContextType } from '~/context/session'
import { SnackbarContext, SnackbarContextType } from '~/context/ui'
import { EditorComponent, EditorComponentProps } from './Editor'
// Mock any necessary data
// Mock data
const mockSession = {
session: () => ({
user: {
@ -37,54 +37,38 @@ const [_form, setForm] = createStore<ShoutForm>({
})
const [_formErrors, setFormErrors] = createStore({} as Record<keyof ShoutForm, string>)
const [editor, setEditor] = createSignal<Editor | undefined>()
const mockEditorContext: EditorContextType = {
countWords: () => 0,
isEditorPanelVisible: (): boolean => {
throw new Error('Function not implemented.')
isEditorPanelVisible: () => false,
wordCounter: () => ({ characters: 0, words: 0 }),
form: _form,
formErrors: _formErrors,
createEditor: (opts?: Partial<EditorOptions>) => {
const newEditor = new Editor(opts)
setEditor(newEditor)
return newEditor
},
wordCounter: (): { characters: number; words: number } => {
throw new Error('Function not implemented.')
},
form: {
layout: undefined,
shoutId: 0,
slug: '',
title: '',
subtitle: undefined,
lead: undefined,
description: undefined,
selectedTopics: [],
mainTopic: undefined,
body: '',
coverImageUrl: undefined,
media: undefined
},
formErrors: {} as Record<keyof ShoutForm, string>,
setEditor,
editor,
saveShout: (_form: ShoutForm): Promise<void> => {
throw new Error('Function not implemented.')
saveShout: async (_form: ShoutForm) => {
// Simulate save
},
saveDraft: (_form: ShoutForm): Promise<void> => {
throw new Error('Function not implemented.')
saveDraft: async (_form: ShoutForm) => {
// Simulate save draft
},
saveDraftToLocalStorage: (_form: ShoutForm): void => {
throw new Error('Function not implemented.')
saveDraftToLocalStorage: (_form: ShoutForm) => {
// Simulate save to local storage
},
getDraftFromLocalStorage: (_shoutId: number): ShoutForm => {
throw new Error('Function not implemented.')
getDraftFromLocalStorage: (_shoutId: number): ShoutForm => _form,
publishShout: async (_form: ShoutForm) => {
// Simulate publish
},
publishShout: (_form: ShoutForm): Promise<void> => {
throw new Error('Function not implemented.')
publishShoutById: async (_shoutId: number) => {
// Simulate publish by ID
},
publishShoutById: (_shoutId: number): Promise<void> => {
throw new Error('Function not implemented.')
},
deleteShout: (_shoutId: number): Promise<boolean> => {
throw new Error('Function not implemented.')
},
toggleEditorPanel: (): void => {
throw new Error('Function not implemented.')
deleteShout: async (_shoutId: number): Promise<boolean> => true,
toggleEditorPanel: () => {
// Simulate toggle
},
setForm,
setFormErrors

View File

@ -1,34 +1,21 @@
import { HocuspocusProvider } from '@hocuspocus/provider'
import { Editor, EditorOptions, isTextSelection } from '@tiptap/core'
import { Bold } from '@tiptap/extension-bold'
import { BubbleMenu } from '@tiptap/extension-bubble-menu'
import { BulletList } from '@tiptap/extension-bullet-list'
import { CharacterCount } from '@tiptap/extension-character-count'
import { Collaboration } from '@tiptap/extension-collaboration'
import { CollaborationCursor } from '@tiptap/extension-collaboration-cursor'
import { Document } from '@tiptap/extension-document'
import { Dropcursor } from '@tiptap/extension-dropcursor'
import { FloatingMenu } from '@tiptap/extension-floating-menu'
import Focus from '@tiptap/extension-focus'
import { Gapcursor } from '@tiptap/extension-gapcursor'
import { HardBreak } from '@tiptap/extension-hard-break'
import { Heading } from '@tiptap/extension-heading'
import { Highlight } from '@tiptap/extension-highlight'
import { HorizontalRule } from '@tiptap/extension-horizontal-rule'
import { Image } from '@tiptap/extension-image'
import { Italic } from '@tiptap/extension-italic'
import { Link } from '@tiptap/extension-link'
import { ListItem } from '@tiptap/extension-list-item'
import { OrderedList } from '@tiptap/extension-ordered-list'
import { Paragraph } from '@tiptap/extension-paragraph'
import { Placeholder } from '@tiptap/extension-placeholder'
import { Strike } from '@tiptap/extension-strike'
import { Text } from '@tiptap/extension-text'
import { Underline } from '@tiptap/extension-underline'
import { Show, createEffect, createMemo, createSignal, on, onCleanup } from 'solid-js'
import { createTiptapEditor } from 'solid-tiptap'
import uniqolor from 'uniqolor'
import { Doc } from 'yjs'
import { Doc, Transaction } from 'yjs'
import { useEditorContext } from '~/context/editor'
import { useLocalize } from '~/context/localize'
import { useSession } from '~/context/session'
@ -50,6 +37,7 @@ import { TrailingNode } from './extensions/TrailingNode'
import { renderUploadedImage } from './renderUploadedImage'
import './Prosemirror.scss'
import { base } from '~/lib/editorOptions'
export type EditorComponentProps = {
shoutId: number
@ -79,8 +67,8 @@ export const EditorComponent = (props: EditorComponentProps) => {
const [isCommonMarkup, setIsCommonMarkup] = createSignal(false)
const [shouldShowTextBubbleMenu, setShouldShowTextBubbleMenu] = createSignal(false)
const { showSnackbar } = useSnackbar()
const { setEditor, countWords } = useEditorContext()
const [extensions, setExtensions] = createSignal<EditorOptions['extensions']>([])
const { createEditor, countWords, editor } = useEditorContext()
const [editorOptions, setEditorOptions] = createSignal<Partial<EditorOptions>>({})
const [editorElRef, setEditorElRef] = createSignal<HTMLElement | undefined>()
const [textBubbleMenuRef, setTextBubbleMenuRef] = createSignal<HTMLDivElement | undefined>()
const [incutBubbleMenuRef, setIncutBubbleMenuRef] = createSignal<HTMLElement | undefined>()
@ -115,74 +103,27 @@ export const EditorComponent = (props: EditorComponentProps) => {
} catch (error) {
console.error('[Paste Image Error]:', error)
}
}
const editor = createTiptapEditor(() => ({
element: editorElRef()!,
editorProps: {
attributes: {
class: 'articleEditor'
},
transformPastedHTML(html) {
return html.replaceAll(/<img.*?>/g, '')
},
handlePaste: () => {
handleClipboardPaste()
return false
}
},
extensions: extensions(),
onTransaction: ({ transaction, editor }) => {
if (transaction.docChanged) {
// Get the current HTML content from the editor
const html = editor.getHTML()
// Trigger the onChange callback with the updated HTML
html && props.onChange(html)
// Get the word count from the editor's storage (using CharacterCount)
const wordCount = editor.storage.characterCount.words()
// Update the word count
wordCount && countWords(wordCount)
}
},
content: props.initialContent || ''
}))
createEffect(() => editor() && setEditor(editor() as Editor))
createEffect(
on(
[extensions, editorElRef, author, () => `shout-${props.shoutId}`],
([eee, element, a, docName]) =>
eee.length === 0 &&
a &&
element &&
setExtensions([
Document,
Text,
Paragraph,
Bold,
Italic,
Strike,
Heading.configure({ levels: [2, 3, 4] }),
BulletList,
OrderedList,
ListItem,
on([editorOptions, editorElRef, author], ([opts, element, a]) => {
if (!opts && a && element) {
const options = {
element: editorElRef()!,
editorProps: {
attributes: { class: 'articleEditor' },
transformPastedHTML: (c: string) => c.replaceAll(/<img.*?>/g, ''),
handlePaste: handleClipboardPaste
},
extensions: [
...base,
HorizontalRule.configure({ HTMLAttributes: { class: 'horizontalRule' } }),
Dropcursor,
CustomBlockquote,
Span,
ToggleTextWrap,
Underline,
Link.extend({ inclusive: false }).configure({ autolink: true, openOnClick: false }),
Collaboration.configure({ document: yDocs[docName] }),
CollaborationCursor.configure({
provider: providers[docName],
user: { name: a.name, color: uniqolor(a.slug).color }
}),
Placeholder.configure({ placeholder: t('Add a link or click plus to embed media') }),
Focus,
Gapcursor,
@ -195,11 +136,17 @@ export const EditorComponent = (props: EditorComponentProps) => {
Footnote,
ToggleTextWrap,
CharacterCount.configure(), // https://github.com/ueberdosis/tiptap/issues/2589#issuecomment-1093084689
TrailingNode,
ArticleNode,
// menus
BubbleMenu.configure({
pluginKey: 'textBubbleMenu',
element: textBubbleMenuRef(),
shouldShow: ({ editor: e, view, state: { doc, selection }, from, to }) => {
const isEmptyTextBlock = doc.textBetween(from, to).length === 0 && isTextSelection(selection)
const isEmptyTextBlock =
doc.textBetween(from, to).length === 0 && isTextSelection(selection)
isEmptyTextBlock &&
e?.chain().focus().removeTextWrap({ class: 'highlight-fake-selection' }).run()
@ -245,22 +192,47 @@ export const EditorComponent = (props: EditorComponentProps) => {
if (!(isRootDepth && selection.empty)) return false
return !(e.isActive('codeBlock') || e.isActive('heading'))
}
}),
TrailingNode,
ArticleNode
])
)
})
// dynamic
// Collaboration.configure({ document: yDocs[docName] }),
// CollaborationCursor.configure({ provider: providers[docName], user: { name: a.name, color: uniqolor(a.slug).color } }),
],
onTransaction({ transaction, editor }: { transaction: Transaction; editor: Editor }) {
if (transaction.changed) {
// Get the current HTML content from the editor
const html = editor.getHTML()
// Trigger the onChange callback with the updated HTML
html && props.onChange(html)
// Get the word count from the editor's storage (using CharacterCount)
const wordCount = editor.storage.characterCount.words()
// Update the word count
wordCount && countWords(wordCount)
}
},
content: props.initialContent || ''
}
setEditorOptions(options as unknown as Partial<EditorOptions>)
createEditor(options as unknown as Partial<EditorOptions>)
}
})
)
createEffect(
on(
[
editor,
() => !props.disableCollaboration,
() => `shout-${props.shoutId}`,
() => session()?.access_token || '',
author
],
([collab, docName, token, profile]) => {
([e, collab, docName, token, profile]) => {
if (!e) return
if (!yDocs[docName]) {
yDocs[docName] = new Doc()
}
@ -275,17 +247,17 @@ export const EditorComponent = (props: EditorComponentProps) => {
}
collab &&
setExtensions((old: EditorOptions['extensions']) => [
...old,
createEditor({
...editorOptions(),
extensions: [
...(editor()?.options.extensions || []),
Collaboration.configure({ document: yDocs[docName] }),
CollaborationCursor.configure({
provider: providers[docName],
user: {
name: profile.name,
color: uniqolor(profile.slug).color
}
user: { name: profile.name, color: uniqolor(profile.slug).color }
})
]
})
])
}
)
)