editor-wip
This commit is contained in:
parent
d6c6545726
commit
6ca29a351f
|
@ -134,9 +134,6 @@
|
||||||
},
|
},
|
||||||
"overrides": {
|
"overrides": {
|
||||||
"sass": "1.77.6",
|
"sass": "1.77.6",
|
||||||
"sass-embedded": {
|
|
||||||
"sass": "1.77.6"
|
|
||||||
},
|
|
||||||
"vite": "5.3.5",
|
"vite": "5.3.5",
|
||||||
"yjs": "13.6.18",
|
"yjs": "13.6.18",
|
||||||
"y-prosemirror": "1.2.12"
|
"y-prosemirror": "1.2.12"
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
import { Editor } from '@tiptap/core'
|
import { Editor, EditorOptions } from '@tiptap/core'
|
||||||
import { createSignal } from 'solid-js'
|
import { createSignal } from 'solid-js'
|
||||||
import { createStore } from 'solid-js/store'
|
import { createStore } from 'solid-js/store'
|
||||||
import { Meta, StoryObj } from 'storybook-solidjs'
|
import { Meta, StoryObj } from 'storybook-solidjs'
|
||||||
|
@ -8,7 +8,7 @@ import { SessionContext, SessionContextType } from '~/context/session'
|
||||||
import { SnackbarContext, SnackbarContextType } from '~/context/ui'
|
import { SnackbarContext, SnackbarContextType } from '~/context/ui'
|
||||||
import { EditorComponent, EditorComponentProps } from './Editor'
|
import { EditorComponent, EditorComponentProps } from './Editor'
|
||||||
|
|
||||||
// Mock any necessary data
|
// Mock data
|
||||||
const mockSession = {
|
const mockSession = {
|
||||||
session: () => ({
|
session: () => ({
|
||||||
user: {
|
user: {
|
||||||
|
@ -37,54 +37,38 @@ const [_form, setForm] = createStore<ShoutForm>({
|
||||||
})
|
})
|
||||||
const [_formErrors, setFormErrors] = createStore({} as Record<keyof ShoutForm, string>)
|
const [_formErrors, setFormErrors] = createStore({} as Record<keyof ShoutForm, string>)
|
||||||
const [editor, setEditor] = createSignal<Editor | undefined>()
|
const [editor, setEditor] = createSignal<Editor | undefined>()
|
||||||
|
|
||||||
const mockEditorContext: EditorContextType = {
|
const mockEditorContext: EditorContextType = {
|
||||||
countWords: () => 0,
|
countWords: () => 0,
|
||||||
isEditorPanelVisible: (): boolean => {
|
isEditorPanelVisible: () => false,
|
||||||
throw new Error('Function not implemented.')
|
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,
|
editor,
|
||||||
saveShout: (_form: ShoutForm): Promise<void> => {
|
saveShout: async (_form: ShoutForm) => {
|
||||||
throw new Error('Function not implemented.')
|
// Simulate save
|
||||||
},
|
},
|
||||||
saveDraft: (_form: ShoutForm): Promise<void> => {
|
saveDraft: async (_form: ShoutForm) => {
|
||||||
throw new Error('Function not implemented.')
|
// Simulate save draft
|
||||||
},
|
},
|
||||||
saveDraftToLocalStorage: (_form: ShoutForm): void => {
|
saveDraftToLocalStorage: (_form: ShoutForm) => {
|
||||||
throw new Error('Function not implemented.')
|
// Simulate save to local storage
|
||||||
},
|
},
|
||||||
getDraftFromLocalStorage: (_shoutId: number): ShoutForm => {
|
getDraftFromLocalStorage: (_shoutId: number): ShoutForm => _form,
|
||||||
throw new Error('Function not implemented.')
|
publishShout: async (_form: ShoutForm) => {
|
||||||
|
// Simulate publish
|
||||||
},
|
},
|
||||||
publishShout: (_form: ShoutForm): Promise<void> => {
|
publishShoutById: async (_shoutId: number) => {
|
||||||
throw new Error('Function not implemented.')
|
// Simulate publish by ID
|
||||||
},
|
},
|
||||||
publishShoutById: (_shoutId: number): Promise<void> => {
|
deleteShout: async (_shoutId: number): Promise<boolean> => true,
|
||||||
throw new Error('Function not implemented.')
|
toggleEditorPanel: () => {
|
||||||
},
|
// Simulate toggle
|
||||||
deleteShout: (_shoutId: number): Promise<boolean> => {
|
|
||||||
throw new Error('Function not implemented.')
|
|
||||||
},
|
|
||||||
toggleEditorPanel: (): void => {
|
|
||||||
throw new Error('Function not implemented.')
|
|
||||||
},
|
},
|
||||||
setForm,
|
setForm,
|
||||||
setFormErrors
|
setFormErrors
|
||||||
|
|
|
@ -1,34 +1,21 @@
|
||||||
import { HocuspocusProvider } from '@hocuspocus/provider'
|
import { HocuspocusProvider } from '@hocuspocus/provider'
|
||||||
import { Editor, EditorOptions, isTextSelection } from '@tiptap/core'
|
import { Editor, EditorOptions, isTextSelection } from '@tiptap/core'
|
||||||
import { Bold } from '@tiptap/extension-bold'
|
|
||||||
import { BubbleMenu } from '@tiptap/extension-bubble-menu'
|
import { BubbleMenu } from '@tiptap/extension-bubble-menu'
|
||||||
import { BulletList } from '@tiptap/extension-bullet-list'
|
|
||||||
import { CharacterCount } from '@tiptap/extension-character-count'
|
import { CharacterCount } from '@tiptap/extension-character-count'
|
||||||
import { Collaboration } from '@tiptap/extension-collaboration'
|
import { Collaboration } from '@tiptap/extension-collaboration'
|
||||||
import { CollaborationCursor } from '@tiptap/extension-collaboration-cursor'
|
import { CollaborationCursor } from '@tiptap/extension-collaboration-cursor'
|
||||||
import { Document } from '@tiptap/extension-document'
|
|
||||||
import { Dropcursor } from '@tiptap/extension-dropcursor'
|
import { Dropcursor } from '@tiptap/extension-dropcursor'
|
||||||
import { FloatingMenu } from '@tiptap/extension-floating-menu'
|
import { FloatingMenu } from '@tiptap/extension-floating-menu'
|
||||||
import Focus from '@tiptap/extension-focus'
|
import Focus from '@tiptap/extension-focus'
|
||||||
import { Gapcursor } from '@tiptap/extension-gapcursor'
|
import { Gapcursor } from '@tiptap/extension-gapcursor'
|
||||||
import { HardBreak } from '@tiptap/extension-hard-break'
|
import { HardBreak } from '@tiptap/extension-hard-break'
|
||||||
import { Heading } from '@tiptap/extension-heading'
|
|
||||||
import { Highlight } from '@tiptap/extension-highlight'
|
import { Highlight } from '@tiptap/extension-highlight'
|
||||||
import { HorizontalRule } from '@tiptap/extension-horizontal-rule'
|
import { HorizontalRule } from '@tiptap/extension-horizontal-rule'
|
||||||
import { Image } from '@tiptap/extension-image'
|
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 { 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 { Show, createEffect, createMemo, createSignal, on, onCleanup } from 'solid-js'
|
||||||
import { createTiptapEditor } from 'solid-tiptap'
|
|
||||||
import uniqolor from 'uniqolor'
|
import uniqolor from 'uniqolor'
|
||||||
import { Doc } from 'yjs'
|
import { Doc, Transaction } from 'yjs'
|
||||||
import { useEditorContext } from '~/context/editor'
|
import { useEditorContext } from '~/context/editor'
|
||||||
import { useLocalize } from '~/context/localize'
|
import { useLocalize } from '~/context/localize'
|
||||||
import { useSession } from '~/context/session'
|
import { useSession } from '~/context/session'
|
||||||
|
@ -50,6 +37,7 @@ import { TrailingNode } from './extensions/TrailingNode'
|
||||||
import { renderUploadedImage } from './renderUploadedImage'
|
import { renderUploadedImage } from './renderUploadedImage'
|
||||||
|
|
||||||
import './Prosemirror.scss'
|
import './Prosemirror.scss'
|
||||||
|
import { base } from '~/lib/editorOptions'
|
||||||
|
|
||||||
export type EditorComponentProps = {
|
export type EditorComponentProps = {
|
||||||
shoutId: number
|
shoutId: number
|
||||||
|
@ -79,8 +67,8 @@ export const EditorComponent = (props: EditorComponentProps) => {
|
||||||
const [isCommonMarkup, setIsCommonMarkup] = createSignal(false)
|
const [isCommonMarkup, setIsCommonMarkup] = createSignal(false)
|
||||||
const [shouldShowTextBubbleMenu, setShouldShowTextBubbleMenu] = createSignal(false)
|
const [shouldShowTextBubbleMenu, setShouldShowTextBubbleMenu] = createSignal(false)
|
||||||
const { showSnackbar } = useSnackbar()
|
const { showSnackbar } = useSnackbar()
|
||||||
const { setEditor, countWords } = useEditorContext()
|
const { createEditor, countWords, editor } = useEditorContext()
|
||||||
const [extensions, setExtensions] = createSignal<EditorOptions['extensions']>([])
|
const [editorOptions, setEditorOptions] = createSignal<Partial<EditorOptions>>({})
|
||||||
const [editorElRef, setEditorElRef] = createSignal<HTMLElement | undefined>()
|
const [editorElRef, setEditorElRef] = createSignal<HTMLElement | undefined>()
|
||||||
const [textBubbleMenuRef, setTextBubbleMenuRef] = createSignal<HTMLDivElement | undefined>()
|
const [textBubbleMenuRef, setTextBubbleMenuRef] = createSignal<HTMLDivElement | undefined>()
|
||||||
const [incutBubbleMenuRef, setIncutBubbleMenuRef] = createSignal<HTMLElement | undefined>()
|
const [incutBubbleMenuRef, setIncutBubbleMenuRef] = createSignal<HTMLElement | undefined>()
|
||||||
|
@ -115,152 +103,136 @@ export const EditorComponent = (props: EditorComponentProps) => {
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('[Paste Image Error]:', error)
|
console.error('[Paste Image Error]:', error)
|
||||||
}
|
}
|
||||||
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
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(
|
createEffect(
|
||||||
on(
|
on([editorOptions, editorElRef, author], ([opts, element, a]) => {
|
||||||
[extensions, editorElRef, author, () => `shout-${props.shoutId}`],
|
if (!opts && a && element) {
|
||||||
([eee, element, a, docName]) =>
|
const options = {
|
||||||
eee.length === 0 &&
|
element: editorElRef()!,
|
||||||
a &&
|
editorProps: {
|
||||||
element &&
|
attributes: { class: 'articleEditor' },
|
||||||
setExtensions([
|
transformPastedHTML: (c: string) => c.replaceAll(/<img.*?>/g, ''),
|
||||||
Document,
|
handlePaste: handleClipboardPaste
|
||||||
Text,
|
},
|
||||||
Paragraph,
|
extensions: [
|
||||||
Bold,
|
...base,
|
||||||
Italic,
|
|
||||||
Strike,
|
|
||||||
Heading.configure({ levels: [2, 3, 4] }),
|
|
||||||
BulletList,
|
|
||||||
OrderedList,
|
|
||||||
ListItem,
|
|
||||||
|
|
||||||
HorizontalRule.configure({ HTMLAttributes: { class: 'horizontalRule' } }),
|
HorizontalRule.configure({ HTMLAttributes: { class: 'horizontalRule' } }),
|
||||||
Dropcursor,
|
Dropcursor,
|
||||||
CustomBlockquote,
|
CustomBlockquote,
|
||||||
Span,
|
Span,
|
||||||
ToggleTextWrap,
|
ToggleTextWrap,
|
||||||
Underline,
|
Placeholder.configure({ placeholder: t('Add a link or click plus to embed media') }),
|
||||||
Link.extend({ inclusive: false }).configure({ autolink: true, openOnClick: false }),
|
Focus,
|
||||||
Collaboration.configure({ document: yDocs[docName] }),
|
Gapcursor,
|
||||||
CollaborationCursor.configure({
|
HardBreak,
|
||||||
provider: providers[docName],
|
Highlight.configure({ multicolor: true, HTMLAttributes: { class: 'highlight' } }),
|
||||||
user: { name: a.name, color: uniqolor(a.slug).color }
|
Image,
|
||||||
}),
|
Iframe,
|
||||||
Placeholder.configure({ placeholder: t('Add a link or click plus to embed media') }),
|
Figure,
|
||||||
Focus,
|
Figcaption,
|
||||||
Gapcursor,
|
Footnote,
|
||||||
HardBreak,
|
ToggleTextWrap,
|
||||||
Highlight.configure({ multicolor: true, HTMLAttributes: { class: 'highlight' } }),
|
CharacterCount.configure(), // https://github.com/ueberdosis/tiptap/issues/2589#issuecomment-1093084689
|
||||||
Image,
|
TrailingNode,
|
||||||
Iframe,
|
ArticleNode,
|
||||||
Figure,
|
|
||||||
Figcaption,
|
|
||||||
Footnote,
|
|
||||||
ToggleTextWrap,
|
|
||||||
CharacterCount.configure(), // https://github.com/ueberdosis/tiptap/issues/2589#issuecomment-1093084689
|
|
||||||
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)
|
|
||||||
isEmptyTextBlock &&
|
|
||||||
e?.chain().focus().removeTextWrap({ class: 'highlight-fake-selection' }).run()
|
|
||||||
|
|
||||||
setIsCommonMarkup(e?.isActive('figcaption'))
|
// menus
|
||||||
const result =
|
|
||||||
(view.hasFocus() &&
|
BubbleMenu.configure({
|
||||||
!selection.empty &&
|
pluginKey: 'textBubbleMenu',
|
||||||
!isEmptyTextBlock &&
|
element: textBubbleMenuRef(),
|
||||||
!e.isActive('image') &&
|
shouldShow: ({ editor: e, view, state: { doc, selection }, from, to }) => {
|
||||||
!e.isActive('figure')) ||
|
const isEmptyTextBlock =
|
||||||
e.isActive('footnote') ||
|
doc.textBetween(from, to).length === 0 && isTextSelection(selection)
|
||||||
(e.isActive('figcaption') && !selection.empty)
|
isEmptyTextBlock &&
|
||||||
setShouldShowTextBubbleMenu(result)
|
e?.chain().focus().removeTextWrap({ class: 'highlight-fake-selection' }).run()
|
||||||
return result
|
|
||||||
},
|
setIsCommonMarkup(e?.isActive('figcaption'))
|
||||||
tippyOptions: {
|
const result =
|
||||||
onHide: () => editor()?.commands.focus() as false
|
(view.hasFocus() &&
|
||||||
|
!selection.empty &&
|
||||||
|
!isEmptyTextBlock &&
|
||||||
|
!e.isActive('image') &&
|
||||||
|
!e.isActive('figure')) ||
|
||||||
|
e.isActive('footnote') ||
|
||||||
|
(e.isActive('figcaption') && !selection.empty)
|
||||||
|
setShouldShowTextBubbleMenu(result)
|
||||||
|
return result
|
||||||
|
},
|
||||||
|
tippyOptions: {
|
||||||
|
onHide: () => editor()?.commands.focus() as false
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
BubbleMenu.configure({
|
||||||
|
pluginKey: 'blockquoteBubbleMenu',
|
||||||
|
element: blockquoteBubbleMenuRef(),
|
||||||
|
shouldShow: ({ editor: e, view, state }) =>
|
||||||
|
view.hasFocus() && !state.selection.empty && e.isActive('blockquote')
|
||||||
|
}),
|
||||||
|
BubbleMenu.configure({
|
||||||
|
pluginKey: 'figureBubbleMenu',
|
||||||
|
element: figureBubbleMenuRef(),
|
||||||
|
shouldShow: ({ editor: e, view, state }) =>
|
||||||
|
view.hasFocus() && !state.selection.empty && e.isActive('figure')
|
||||||
|
}),
|
||||||
|
BubbleMenu.configure({
|
||||||
|
pluginKey: 'incutBubbleMenu',
|
||||||
|
element: incutBubbleMenuRef(),
|
||||||
|
shouldShow: ({ editor: e, view, state }) =>
|
||||||
|
view.hasFocus() && !state.selection.empty && e.isActive('figcaption')
|
||||||
|
}),
|
||||||
|
FloatingMenu.configure({
|
||||||
|
element: floatingMenuRef(),
|
||||||
|
pluginKey: 'floatingMenu',
|
||||||
|
shouldShow: ({ editor: e, state: { selection } }) => {
|
||||||
|
const isRootDepth = selection.$anchor.depth === 1
|
||||||
|
if (!(isRootDepth && selection.empty)) return false
|
||||||
|
return !(e.isActive('codeBlock') || e.isActive('heading'))
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
// 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)
|
||||||
}
|
}
|
||||||
}),
|
},
|
||||||
BubbleMenu.configure({
|
content: props.initialContent || ''
|
||||||
pluginKey: 'blockquoteBubbleMenu',
|
}
|
||||||
element: blockquoteBubbleMenuRef(),
|
setEditorOptions(options as unknown as Partial<EditorOptions>)
|
||||||
shouldShow: ({ editor: e, view, state }) =>
|
createEditor(options as unknown as Partial<EditorOptions>)
|
||||||
view.hasFocus() && !state.selection.empty && e.isActive('blockquote')
|
}
|
||||||
}),
|
})
|
||||||
BubbleMenu.configure({
|
|
||||||
pluginKey: 'figureBubbleMenu',
|
|
||||||
element: figureBubbleMenuRef(),
|
|
||||||
shouldShow: ({ editor: e, view, state }) =>
|
|
||||||
view.hasFocus() && !state.selection.empty && e.isActive('figure')
|
|
||||||
}),
|
|
||||||
BubbleMenu.configure({
|
|
||||||
pluginKey: 'incutBubbleMenu',
|
|
||||||
element: incutBubbleMenuRef(),
|
|
||||||
shouldShow: ({ editor: e, view, state }) =>
|
|
||||||
view.hasFocus() && !state.selection.empty && e.isActive('figcaption')
|
|
||||||
}),
|
|
||||||
FloatingMenu.configure({
|
|
||||||
element: floatingMenuRef(),
|
|
||||||
pluginKey: 'floatingMenu',
|
|
||||||
shouldShow: ({ editor: e, state: { selection } }) => {
|
|
||||||
const isRootDepth = selection.$anchor.depth === 1
|
|
||||||
if (!(isRootDepth && selection.empty)) return false
|
|
||||||
return !(e.isActive('codeBlock') || e.isActive('heading'))
|
|
||||||
}
|
|
||||||
}),
|
|
||||||
TrailingNode,
|
|
||||||
ArticleNode
|
|
||||||
])
|
|
||||||
)
|
|
||||||
)
|
)
|
||||||
|
|
||||||
createEffect(
|
createEffect(
|
||||||
on(
|
on(
|
||||||
[
|
[
|
||||||
|
editor,
|
||||||
() => !props.disableCollaboration,
|
() => !props.disableCollaboration,
|
||||||
() => `shout-${props.shoutId}`,
|
() => `shout-${props.shoutId}`,
|
||||||
() => session()?.access_token || '',
|
() => session()?.access_token || '',
|
||||||
author
|
author
|
||||||
],
|
],
|
||||||
([collab, docName, token, profile]) => {
|
([e, collab, docName, token, profile]) => {
|
||||||
|
if (!e) return
|
||||||
|
|
||||||
if (!yDocs[docName]) {
|
if (!yDocs[docName]) {
|
||||||
yDocs[docName] = new Doc()
|
yDocs[docName] = new Doc()
|
||||||
}
|
}
|
||||||
|
@ -275,17 +247,17 @@ export const EditorComponent = (props: EditorComponentProps) => {
|
||||||
}
|
}
|
||||||
|
|
||||||
collab &&
|
collab &&
|
||||||
setExtensions((old: EditorOptions['extensions']) => [
|
createEditor({
|
||||||
...old,
|
...editorOptions(),
|
||||||
Collaboration.configure({ document: yDocs[docName] }),
|
extensions: [
|
||||||
CollaborationCursor.configure({
|
...(editor()?.options.extensions || []),
|
||||||
provider: providers[docName],
|
Collaboration.configure({ document: yDocs[docName] }),
|
||||||
user: {
|
CollaborationCursor.configure({
|
||||||
name: profile.name,
|
provider: providers[docName],
|
||||||
color: uniqolor(profile.slug).color
|
user: { name: profile.name, color: uniqolor(profile.slug).color }
|
||||||
}
|
})
|
||||||
})
|
]
|
||||||
])
|
})
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
Loading…
Reference in New Issue
Block a user