not-module-prosemirror-styles
All checks were successful
deploy / testbuild (push) Successful in 2m11s
deploy / Update templates on Mailgun (push) Has been skipped

This commit is contained in:
Untone 2024-10-09 13:40:20 +03:00
parent 194e40aa86
commit 1a755f4c69
6 changed files with 491 additions and 449 deletions

View File

@ -1,6 +1,6 @@
import { HocuspocusProvider } from '@hocuspocus/provider' import { HocuspocusProvider } from '@hocuspocus/provider'
import { UploadFile } from '@solid-primitives/upload' import { UploadFile } from '@solid-primitives/upload'
import { Editor, EditorOptions, isTextSelection } from '@tiptap/core' import { Editor, EditorOptions } from '@tiptap/core'
import { BubbleMenu } from '@tiptap/extension-bubble-menu' import { BubbleMenu } from '@tiptap/extension-bubble-menu'
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'
@ -8,6 +8,7 @@ import { CollaborationCursor } from '@tiptap/extension-collaboration-cursor'
import { FloatingMenu } from '@tiptap/extension-floating-menu' import { FloatingMenu } from '@tiptap/extension-floating-menu'
import { Placeholder } from '@tiptap/extension-placeholder' import { Placeholder } from '@tiptap/extension-placeholder'
import { Show, createEffect, createMemo, createSignal, on, onCleanup, onMount } from 'solid-js' import { Show, createEffect, createMemo, createSignal, on, onCleanup, onMount } from 'solid-js'
import { isServer } from 'solid-js/web'
import { createTiptapEditor } from 'solid-tiptap' import { createTiptapEditor } from 'solid-tiptap'
import uniqolor from 'uniqolor' import uniqolor from 'uniqolor'
import { Doc } from 'yjs' import { Doc } from 'yjs'
@ -19,15 +20,14 @@ import { Author } from '~/graphql/schema/core.gen'
import { base, custom, extended } from '~/lib/editorExtensions' import { base, custom, extended } from '~/lib/editorExtensions'
import { handleImageUpload } from '~/lib/handleImageUpload' import { handleImageUpload } from '~/lib/handleImageUpload'
import { allowedImageTypes, renderUploadedImage } from '../Upload/renderUploadedImage' import { allowedImageTypes, renderUploadedImage } from '../Upload/renderUploadedImage'
import { Panel } from './Panel/Panel'
import { BlockquoteBubbleMenu } from './Toolbar/BlockquoteBubbleMenu' import { BlockquoteBubbleMenu } from './Toolbar/BlockquoteBubbleMenu'
import { EditorFloatingMenu } from './Toolbar/EditorFloatingMenu' import { EditorFloatingMenu } from './Toolbar/EditorFloatingMenu'
import { FigureBubbleMenu } from './Toolbar/FigureBubbleMenu' import { FigureBubbleMenu } from './Toolbar/FigureBubbleMenu'
import { IncutBubbleMenu } from './Toolbar/IncutBubbleMenu' import { IncutBubbleMenu } from './Toolbar/IncutBubbleMenu'
import { TextBubbleMenu } from './Toolbar/TextBubbleMenu' import { TextBubbleMenu } from './Toolbar/TextBubbleMenu'
import './Editor.module.scss' import './Prosemirror.scss'
import { isServer } from 'solid-js/web'
import { Panel } from './Panel/Panel'
export type EditorComponentProps = { export type EditorComponentProps = {
shoutId: number shoutId: number
@ -43,8 +43,13 @@ export const EditorComponent = (props: EditorComponentProps) => {
const { t } = useLocalize() const { t } = useLocalize()
const { session, requireAuthentication } = useSession() const { session, requireAuthentication } = useSession()
const author = createMemo<Author>(() => session()?.user?.app_data?.profile as Author) const author = createMemo<Author>(() => session()?.user?.app_data?.profile as Author)
const [isCommonMarkup, setIsCommonMarkup] = createSignal(false) const [isCommonMarkup, _setIsCommonMarkup] = createSignal(false)
const [shouldShowTextBubbleMenu, setShouldShowTextBubbleMenu] = createSignal(false) const createMenuSignal = () => createSignal(false)
const [shouldShowTextBubbleMenu, _setShouldShowTextBubbleMenu] = createMenuSignal()
const [shouldShowBlockquoteBubbleMenu, _setShouldShowBlockquoteBubbleMenu] = createMenuSignal()
const [shouldShowFigureBubbleMenu, _setShouldShowFigureBubbleMenu] = createMenuSignal()
const [shouldShowIncutBubbleMenu, _setShouldShowIncutBubbleMenu] = createMenuSignal()
const [shouldShowFloatingMenu, _setShouldShowFloatingMenu] = createMenuSignal()
const { showSnackbar } = useSnackbar() const { showSnackbar } = useSnackbar()
const { countWords, setEditing } = useEditorContext() const { countWords, setEditing } = useEditorContext()
const [editorOptions, setEditorOptions] = createSignal<Partial<EditorOptions>>({}) const [editorOptions, setEditorOptions] = createSignal<Partial<EditorOptions>>({})
@ -71,11 +76,18 @@ export const EditorComponent = (props: EditorComponentProps) => {
} }
console.log('stage 2: create editor instance without menus', opts) console.log('stage 2: create editor instance without menus', opts)
const old = editor() || { options: {} } const old = editor() || { options: {} as EditorOptions }
const uniqueExtensions = [
...new Map(
[...(old?.options?.extensions || []), ...(opts?.extensions || [])].map((ext) => [ext.name, ext])
).values()
]
const fresh = createTiptapEditor(() => ({ const fresh = createTiptapEditor(() => ({
...old?.options, ...old?.options,
...opts, ...opts,
element: opts.element as HTMLElement element: opts.element as HTMLElement,
extensions: uniqueExtensions
})) }))
if (old instanceof Editor) old?.destroy() if (old instanceof Editor) old?.destroy()
setEditor(fresh() || null) setEditor(fresh() || null)
@ -147,7 +159,7 @@ export const EditorComponent = (props: EditorComponentProps) => {
}, },
content: props.initialContent ?? null content: props.initialContent ?? null
} }
console.log('Editor options created:', options) console.log(options)
setEditorOptions(() => options) setEditorOptions(() => options)
} }
@ -176,85 +188,39 @@ export const EditorComponent = (props: EditorComponentProps) => {
if (editor()) { if (editor()) {
initializeMenus() initializeMenus()
} }
// Инициализируем коллаборацию если необходимо
if (!props.disableCollaboration) {
initializeCollaboration()
}
}, 1200) }, 1200)
}, 'edit') }, 'edit')
}) })
const initializeMenus = () => { const initializeMenus = () => {
if (menusInitialized() || !editor()) return if (menusInitialized() || !editor()) return
if (blockquoteBubbleMenuRef() && figureBubbleMenuRef() && incutBubbleMenuRef() && floatingMenuRef()) {
console.log('stage 3: initialize menus when editor instance is ready') console.log('stage 3: initialize menus when editor instance is ready')
const menuConfigs = [
if ( { key: 'textBubbleMenu', ref: textBubbleMenuRef, shouldShow: shouldShowTextBubbleMenu },
textBubbleMenuRef() && {
blockquoteBubbleMenuRef() && key: 'blockquoteBubbleMenu',
figureBubbleMenuRef() && ref: blockquoteBubbleMenuRef,
incutBubbleMenuRef() && shouldShow: shouldShowBlockquoteBubbleMenu
floatingMenuRef()
) {
const 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)
isEmptyTextBlock &&
e?.chain().focus().removeTextWrap({ class: 'highlight-fake-selection' }).run()
setIsCommonMarkup(e?.isActive('figcaption'))
const result =
(view.hasFocus() &&
!selection.empty &&
!isEmptyTextBlock &&
!e.isActive('image') &&
!e.isActive('figure')) ||
e.isActive('footnote') ||
(e.isActive('figcaption') && !selection.empty)
setShouldShowTextBubbleMenu(result)
return result
}, },
tippyOptions: { { key: 'figureBubbleMenu', ref: figureBubbleMenuRef, shouldShow: shouldShowFigureBubbleMenu },
onHide: () => editor()?.commands.focus() as false { key: 'incutBubbleMenu', ref: incutBubbleMenuRef, shouldShow: shouldShowIncutBubbleMenu },
} { key: 'floatingMenu', ref: floatingMenuRef, shouldShow: shouldShowFloatingMenu, isFloating: true }
}),
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
const show =
isRootDepth && selection.empty && !(e?.isActive('codeBlock') || e?.isActive('heading'))
console.log('FloatingMenu shouldShow:', show)
return show
}
})
] ]
const extensions = [...(editorOptions().extensions || []), ...menus] const menus = menuConfigs.map((config) =>
setEditorOptions((prev) => ({ ...prev, extensions })) config.isFloating
console.log('Editor menus initialized:', extensions) ? FloatingMenu.configure({
pluginKey: config.key,
element: config.ref(),
shouldShow: config.shouldShow
})
: BubbleMenu.configure({
pluginKey: config.key,
element: config.ref(),
shouldShow: config.shouldShow
})
)
setEditorOptions((prev) => ({ ...prev, extensions: [...(prev.extensions || []), ...menus] }))
setMenusInitialized(true) setMenusInitialized(true)
} else { } else {
console.error('Some menu references are missing') console.error('Some menu references are missing')
@ -292,6 +258,13 @@ export const EditorComponent = (props: EditorComponentProps) => {
setEditorOptions((prev: Partial<EditorOptions>) => { setEditorOptions((prev: Partial<EditorOptions>) => {
const extensions = [...(prev.extensions || [])] const extensions = [...(prev.extensions || [])]
if (props.disableCollaboration) {
// Remove collaboration extensions if they exist
const filteredExtensions = extensions.filter(
(ext) => ext.name !== 'collaboration' && ext.name !== 'collaborationCursor'
)
return { ...prev, extensions: filteredExtensions }
}
extensions.push( extensions.push(
Collaboration.configure({ document: yDocs[docName] }), Collaboration.configure({ document: yDocs[docName] }),
CollaborationCursor.configure({ CollaborationCursor.configure({
@ -316,6 +289,17 @@ export const EditorComponent = (props: EditorComponentProps) => {
} }
} }
// Инициализируем коллаборацию если необходимо
createEffect(
on(
() => props.disableCollaboration,
() => {
initializeCollaboration()
},
{ defer: true }
)
)
onCleanup(() => { onCleanup(() => {
editorElRef()?.removeEventListener('focus', handleFocus) editorElRef()?.removeEventListener('focus', handleFocus)
editor()?.destroy() editor()?.destroy()

View File

@ -86,25 +86,25 @@ mark.highlight {
} }
[data-float='half-left'] { [data-float='half-left'] {
@include media-breakpoint-up(md) {
max-width: 50%;
min-width: 30%;
}
float: left; float: left;
margin: 1rem 1rem 0; margin: 1rem 1rem 0;
clear: left; clear: left;
}
[data-float='half-right'] {
@include media-breakpoint-up(md) { @include media-breakpoint-up(md) {
max-width: 50%; max-width: 50%;
min-width: 30%; min-width: 30%;
} }
}
[data-float='half-right'] {
float: right; float: right;
margin: 1rem 0; margin: 1rem 0;
clear: right; clear: right;
@include media-breakpoint-up(md) {
max-width: 50%;
min-width: 30%;
}
} }
} }
@ -136,6 +136,20 @@ mark.highlight {
} }
&[data-type='punchline'] { &[data-type='punchline'] {
border: solid #000;
border-width: 2px 0;
font-size: 3.2rem;
font-weight: 700;
line-height: 1.2;
margin: 1em 0;
padding: 2.4rem 0;
&[data-float='left'],
&[data-float='right'] {
font-size: 2.2rem;
line-height: 1.4;
}
@include media-breakpoint-up(sm) { @include media-breakpoint-up(sm) {
&[data-float='left'] { &[data-float='left'] {
margin-right: 1.5em; margin-right: 1.5em;
@ -147,24 +161,16 @@ mark.highlight {
clear: right; clear: right;
} }
} }
font-size:3.2rem;
border: solid #000;
border-width: 2px 0;
font-weight: 700;
line-height: 1.2;
margin: 1em 0;
padding: 2.4rem 0;
&[data-float='left'],
&[data-float='right'] {
font-size:2.2rem;
line-height: 1.4;
}
} }
} }
.ProseMirror article[data-type='incut'] { .ProseMirror article[data-type='incut'] {
background: #f1f2f3;
font-size: 1.4rem;
margin: 1em -1rem;
padding: 2em 2rem;
transition: background 0.3s ease-in-out;
@include media-breakpoint-up(sm) { @include media-breakpoint-up(sm) {
margin-left: -2rem; margin-left: -2rem;
margin-right: -2rem; margin-right: -2rem;
@ -181,12 +187,6 @@ mark.highlight {
margin-right: -3em; margin-right: -3em;
} }
font-size:1.4rem;
background: #f1f2f3;
margin: 1em -1rem;
padding: 2em 2rem;
transition: background 0.3s ease-in-out;
&[data-float] img { &[data-float] img {
float: none; float: none;
max-width: unset; max-width: unset;
@ -196,6 +196,9 @@ mark.highlight {
&[data-float='left'], &[data-float='left'],
&[data-float='half-left'] { &[data-float='half-left'] {
margin-left: -1rem;
clear: left;
@include media-breakpoint-up(sm) { @include media-breakpoint-up(sm) {
margin-left: -2rem; margin-left: -2rem;
margin-right: 2rem; margin-right: 2rem;
@ -208,13 +211,13 @@ mark.highlight {
@include media-breakpoint-up(xl) { @include media-breakpoint-up(xl) {
margin-left: -12.5%; margin-left: -12.5%;
} }
margin-left: -1rem;
clear: left;
} }
&[data-float='right'], &[data-float='right'],
&[data-float='half-right'] { &[data-float='half-right'] {
margin-right: -1rem;
clear: right;
@include media-breakpoint-up(sm) { @include media-breakpoint-up(sm) {
margin-left: 2rem; margin-left: 2rem;
margin-right: -2rem; margin-right: -2rem;
@ -227,9 +230,6 @@ mark.highlight {
@include media-breakpoint-up(xl) { @include media-breakpoint-up(xl) {
margin-right: -12.5%; margin-right: -12.5%;
} }
margin-right: -1rem;
clear: right;
} }
*:last-child { *:last-child {

View File

@ -1,6 +1,16 @@
import type { Editor } from '@tiptap/core' import type { Editor } from '@tiptap/core'
import { clsx } from 'clsx' import { clsx } from 'clsx'
import { Match, Show, Switch, createEffect, createSignal, lazy, onCleanup, onMount } from 'solid-js' import {
Match,
Show,
Switch,
createEffect,
createMemo,
createSignal,
lazy,
onCleanup,
onMount
} from 'solid-js'
import { createEditorTransaction } from 'solid-tiptap' import { createEditorTransaction } from 'solid-tiptap'
import { Icon } from '~/components/_shared/Icon' import { Icon } from '~/components/_shared/Icon'
import { Popover } from '~/components/_shared/Popover' import { Popover } from '~/components/_shared/Popover'
@ -21,157 +31,213 @@ type BubbleMenuProps = {
export const TextBubbleMenu = (props: BubbleMenuProps) => { export const TextBubbleMenu = (props: BubbleMenuProps) => {
const { t } = useLocalize() const { t } = useLocalize()
const isActive = (name: string, attributes?: Record<string, string | number>) => const isActive = createMemo(
createEditorTransaction( () => (name: string, attributes?: Record<string, string | number>) =>
() => { props.editor?.isActive(name, attributes)
console.log('isActive', name, attributes)
return props.editor
},
(editor) => editor?.isActive(name, attributes)
) )
const [textSizeBubbleOpen, setTextSizeBubbleOpen] = createSignal(false) const [menuState, setMenuState] = createSignal({
const [listBubbleOpen, setListBubbleOpen] = createSignal(false) textSizeBubbleOpen: false,
const [linkEditorOpen, setLinkEditorOpen] = createSignal(false) listBubbleOpen: false,
const [footnoteEditorOpen, setFootnoteEditorOpen] = createSignal(false) linkEditorOpen: false,
const [footNote, setFootNote] = createSignal<string>() footnoteEditorOpen: false,
footNote: undefined as string | undefined
})
createEffect(() => { createEffect(() => {
if (!props.shouldShow) { if (!props.shouldShow) {
setFootNote() setMenuState((prev) => ({
setFootnoteEditorOpen(false) ...prev,
setLinkEditorOpen(false) footNote: undefined,
setTextSizeBubbleOpen(false) footnoteEditorOpen: false,
setListBubbleOpen(false) linkEditorOpen: false,
textSizeBubbleOpen: false,
listBubbleOpen: false
}))
} }
}) })
const isBold = isActive('bold') const activeStates = createMemo(() => ({
const isItalic = isActive('italic') bold: isActive()('bold'),
const isH1 = isActive('heading', { level: 2 }) italic: isActive()('italic'),
const isH2 = isActive('heading', { level: 3 }) h1: isActive()('heading', { level: 2 }),
const isH3 = isActive('heading', { level: 4 }) h2: isActive()('heading', { level: 3 }),
const isQuote = isActive('blockquote', { 'data-type': 'quote' }) h3: isActive()('heading', { level: 4 }),
const isPunchLine = isActive('blockquote', { 'data-type': 'punchline' }) quote: isActive()('blockquote', { 'data-type': 'quote' }),
const isOrderedList = isActive('isOrderedList') punchLine: isActive()('blockquote', { 'data-type': 'punchline' }),
const isBulletList = isActive('isBulletList') orderedList: isActive()('orderedList'),
const isLink = isActive('link') bulletList: isActive()('bulletList'),
const isHighlight = isActive('highlight') link: isActive()('link'),
const isFootnote = isActive('footnote') highlight: isActive()('highlight'),
const isIncut = isActive('article') footnote: isActive()('footnote'),
incut: isActive()('article')
// underline: isActive()('underline'),
}))
const toggleTextSizePopup = () => { const togglePopup = (type: 'textSize' | 'list') => {
if (listBubbleOpen()) { setMenuState((prev) => ({
setListBubbleOpen(false) ...prev,
} textSizeBubbleOpen: type === 'textSize' ? !prev.textSizeBubbleOpen : false,
setTextSizeBubbleOpen((prev) => !prev) listBubbleOpen: type === 'list' ? !prev.listBubbleOpen : false
} }))
const toggleListPopup = () => {
if (textSizeBubbleOpen()) {
setTextSizeBubbleOpen(false)
}
setListBubbleOpen((prev) => !prev)
} }
const handleKeyDown = (event: KeyboardEvent) => { const handleKeyDown = (event: KeyboardEvent) => {
if (event.code === 'KeyK' && (event.metaKey || event.ctrlKey) && !props.editor.state.selection.empty) { if (event.code === 'KeyK' && (event.metaKey || event.ctrlKey) && !props.editor.state.selection.empty) {
event.preventDefault() event.preventDefault()
setLinkEditorOpen(true) setMenuState((prev) => ({ ...prev, linkEditorOpen: true }))
} }
} }
const updateCurrentFootnoteValue = createEditorTransaction( const updateCurrentFootnoteValue = createEditorTransaction(
() => props.editor, () => props.editor,
(ed) => { (ed) => {
if (!isFootnote()) { if (!activeStates().footnote) {
return return
} }
const value = ed.getAttributes('footnote').value const value = ed.getAttributes('footnote').value
setFootNote(value) setMenuState((prev) => ({ ...prev, footNote: value }))
} }
) )
const handleAddFootnote = (footnote: string) => { const handleAddFootnote = (footnote: string) => {
if (footNote()) { if (menuState().footNote) {
props.editor?.chain().focus().updateFootnote({ value: footnote }).run() props.editor?.chain().focus().updateFootnote({ value: footnote }).run()
} else { } else {
props.editor?.chain().focus().setFootnote({ value: footnote }).run() props.editor?.chain().focus().setFootnote({ value: footnote }).run()
} }
setFootNote() setMenuState((prev) => ({
setLinkEditorOpen(false) ...prev,
setFootnoteEditorOpen(false) footNote: undefined,
linkEditorOpen: false,
footnoteEditorOpen: false
}))
} }
const handleOpenFootnoteEditor = () => { const handleOpenFootnoteEditor = () => {
updateCurrentFootnoteValue() updateCurrentFootnoteValue()
setLinkEditorOpen(false) setMenuState((prev) => ({ ...prev, linkEditorOpen: false, footnoteEditorOpen: true }))
setFootnoteEditorOpen(true)
} }
const handleSetPunchline = () => { const handleSetPunchline = () => {
if (isPunchLine()) { if (activeStates().punchLine) {
props.editor?.chain().focus().toggleBlockquote('punchline').run() props.editor?.chain().focus().toggleBlockquote('punchline').run()
} }
props.editor?.chain().focus().toggleBlockquote('quote').run() props.editor?.chain().focus().toggleBlockquote('quote').run()
toggleTextSizePopup() togglePopup('textSize')
} }
const handleSetQuote = () => { const handleSetQuote = () => {
if (isQuote()) { if (activeStates().quote) {
props.editor?.chain().focus().toggleBlockquote('quote').run() props.editor?.chain().focus().toggleBlockquote('quote').run()
} }
props.editor?.chain().focus().toggleBlockquote('punchline').run() props.editor?.chain().focus().toggleBlockquote('punchline').run()
toggleTextSizePopup() togglePopup('textSize')
} }
onMount(() => { onMount(() => {
window.addEventListener('keydown', handleKeyDown) window.addEventListener('keydown', handleKeyDown)
onCleanup(() => { onCleanup(() => {
window.removeEventListener('keydown', handleKeyDown) window.removeEventListener('keydown', handleKeyDown)
setLinkEditorOpen(false) setMenuState((prev) => ({ ...prev, linkEditorOpen: false }))
}) })
}) })
const handleOpenLinkForm = () => { const handleOpenLinkForm = () => {
props.editor?.chain().focus().addTextWrap({ class: 'highlight-fake-selection' }).run() props.editor?.chain().focus().addTextWrap({ class: 'highlight-fake-selection' }).run()
setLinkEditorOpen(true) setMenuState((prev) => ({ ...prev, linkEditorOpen: true }))
} }
const handleCloseLinkForm = () => { const handleCloseLinkForm = () => {
setLinkEditorOpen(false) setMenuState((prev) => ({ ...prev, linkEditorOpen: false }))
props.editor?.chain().focus().removeTextWrap({ class: 'highlight-fake-selection' }).run() props.editor?.chain().focus().removeTextWrap({ class: 'highlight-fake-selection' }).run()
} }
const handleFormat = (type: 'Bold' | 'Italic' | 'Underline', _attributes?: Record<string, unknown>) => {
props.editor?.chain().focus()[`toggle${type}`]().run()
}
const ListBubbleMenu = (props: BubbleMenuProps) => {
return ( return (
<div ref={props.ref} class={clsx(styles.TextBubbleMenu, { [styles.growWidth]: footnoteEditorOpen() })}> <div class={styles.dropDown}>
<Switch> <header>{t('Lists')}</header>
<Match when={linkEditorOpen()}> <div class={styles.actions}>
<InsertLinkForm editor={props.editor} onClose={handleCloseLinkForm} /> <Popover content={t('Bullet list')}>
</Match> {(triggerRef: (el: HTMLElement) => void) => (
<Match when={footnoteEditorOpen()}> <button
<MiniEditor ref={triggerRef}
placeholder={t('Enter footnote text')} type="button"
onSubmit={(value: string) => handleAddFootnote(value)} class={clsx(styles.bubbleMenuButton, {
content={footNote()} [styles.bubbleMenuButtonActive]: activeStates().bulletList
onCancel={() => { })}
setFootnoteEditorOpen(false) onClick={() => {
props.editor?.chain().focus().toggleBulletList().run()
togglePopup('list')
}} }}
/> >
</Match> <Icon name="editor-ul" />
<Match when={!(linkEditorOpen() && footnoteEditorOpen())}> </button>
<> )}
<Show when={!props.isCommonMarkup}> </Popover>
<Popover content={t('Ordered list')}>
{(triggerRef: (el: HTMLElement) => void) => (
<button
ref={triggerRef}
type="button"
class={clsx(styles.bubbleMenuButton, {
[styles.bubbleMenuButtonActive]: activeStates().orderedList
})}
onClick={() => {
props.editor?.chain().focus().toggleOrderedList().run()
togglePopup('list')
}}
>
<Icon name="editor-ol" />
</button>
)}
</Popover>
</div>
</div>
)
}
const CommonMarkupBubbleMenu = (props: BubbleMenuProps) => {
return (
<> <>
<Popover content={t('Insert footnote')}>
{(triggerRef: (el: HTMLElement) => void) => (
<button
ref={triggerRef}
type="button"
class={clsx(styles.bubbleMenuButton, {
[styles.bubbleMenuButtonActive]: activeStates().footnote
})}
onClick={handleOpenFootnoteEditor}
>
<Icon name="editor-footnote" />
</button>
)}
</Popover>
<div class={styles.delimiter} />
<div class={styles.dropDownHolder}> <div class={styles.dropDownHolder}>
<button <button
type="button" type="button"
class={clsx(styles.bubbleMenuButton, { class={clsx(styles.bubbleMenuButton, {
[styles.bubbleMenuButtonActive]: textSizeBubbleOpen() [styles.bubbleMenuButtonActive]: menuState().listBubbleOpen
})} })}
onClick={toggleTextSizePopup} onClick={() => togglePopup('list')}
> >
<Icon name="editor-text-size" /> <Icon name="editor-ul" />
<Icon name="down-triangle" class={styles.triangle} /> <Icon name="down-triangle" class={styles.triangle} />
</button> </button>
<Show when={textSizeBubbleOpen()}> <Show when={menuState().listBubbleOpen}>
<ListBubbleMenu {...props} />
</Show>
</div>
</>
)
}
const TextSizeBubbleMenu = (props: BubbleMenuProps) => {
return (
<div class={styles.dropDown}> <div class={styles.dropDown}>
<header>{t('Headers')}</header> <header>{t('Headers')}</header>
<div class={styles.actions}> <div class={styles.actions}>
@ -181,11 +247,11 @@ export const TextBubbleMenu = (props: BubbleMenuProps) => {
ref={triggerRef} ref={triggerRef}
type="button" type="button"
class={clsx(styles.bubbleMenuButton, { class={clsx(styles.bubbleMenuButton, {
[styles.bubbleMenuButtonActive]: isH1() [styles.bubbleMenuButtonActive]: activeStates().h1
})} })}
onClick={() => { onClick={() => {
props.editor?.chain().focus().toggleHeading({ level: 2 }).run() props.editor?.chain().focus().toggleHeading({ level: 2 }).run()
toggleTextSizePopup() togglePopup('textSize')
}} }}
> >
<Icon name="editor-h1" /> <Icon name="editor-h1" />
@ -198,11 +264,11 @@ export const TextBubbleMenu = (props: BubbleMenuProps) => {
ref={triggerRef} ref={triggerRef}
type="button" type="button"
class={clsx(styles.bubbleMenuButton, { class={clsx(styles.bubbleMenuButton, {
[styles.bubbleMenuButtonActive]: isH2() [styles.bubbleMenuButtonActive]: activeStates().h2
})} })}
onClick={() => { onClick={() => {
props.editor?.chain().focus().toggleHeading({ level: 3 }).run() props.editor?.chain().focus().toggleHeading({ level: 3 }).run()
toggleTextSizePopup() togglePopup('textSize')
}} }}
> >
<Icon name="editor-h2" /> <Icon name="editor-h2" />
@ -215,11 +281,11 @@ export const TextBubbleMenu = (props: BubbleMenuProps) => {
ref={triggerRef} ref={triggerRef}
type="button" type="button"
class={clsx(styles.bubbleMenuButton, { class={clsx(styles.bubbleMenuButton, {
[styles.bubbleMenuButtonActive]: isH3() [styles.bubbleMenuButtonActive]: activeStates().h3
})} })}
onClick={() => { onClick={() => {
props.editor?.chain().focus().toggleHeading({ level: 4 }).run() props.editor?.chain().focus().toggleHeading({ level: 4 }).run()
toggleTextSizePopup() togglePopup('textSize')
}} }}
> >
<Icon name="editor-h3" /> <Icon name="editor-h3" />
@ -235,7 +301,7 @@ export const TextBubbleMenu = (props: BubbleMenuProps) => {
ref={triggerRef} ref={triggerRef}
type="button" type="button"
class={clsx(styles.bubbleMenuButton, { class={clsx(styles.bubbleMenuButton, {
[styles.bubbleMenuButtonActive]: isQuote() [styles.bubbleMenuButtonActive]: activeStates().quote
})} })}
onClick={handleSetPunchline} onClick={handleSetPunchline}
> >
@ -249,7 +315,7 @@ export const TextBubbleMenu = (props: BubbleMenuProps) => {
ref={triggerRef} ref={triggerRef}
type="button" type="button"
class={clsx(styles.bubbleMenuButton, { class={clsx(styles.bubbleMenuButton, {
[styles.bubbleMenuButtonActive]: isPunchLine() [styles.bubbleMenuButtonActive]: activeStates().punchLine
})} })}
onClick={handleSetQuote} onClick={handleSetQuote}
> >
@ -266,11 +332,11 @@ export const TextBubbleMenu = (props: BubbleMenuProps) => {
ref={triggerRef} ref={triggerRef}
type="button" type="button"
class={clsx(styles.bubbleMenuButton, { class={clsx(styles.bubbleMenuButton, {
[styles.bubbleMenuButtonActive]: isIncut() [styles.bubbleMenuButtonActive]: activeStates().incut
})} })}
onClick={() => { onClick={() => {
props.editor?.chain().focus().toggleArticle().run() props.editor?.chain().focus().toggleArticle().run()
toggleTextSizePopup() togglePopup('textSize')
}} }}
> >
<Icon name="editor-squib" /> <Icon name="editor-squib" />
@ -279,6 +345,27 @@ export const TextBubbleMenu = (props: BubbleMenuProps) => {
</Popover> </Popover>
</div> </div>
</div> </div>
)
}
const BaseTextBubbleMenu = (props: BubbleMenuProps) => {
return (
<>
<Show when={!props.isCommonMarkup}>
<>
<div class={styles.dropDownHolder}>
<button
type="button"
class={clsx(styles.bubbleMenuButton, {
[styles.bubbleMenuButtonActive]: menuState().textSizeBubbleOpen
})}
onClick={() => togglePopup('textSize')}
>
<Icon name="editor-text-size" />
<Icon name="down-triangle" class={styles.triangle} />
</button>
<Show when={menuState().textSizeBubbleOpen}>
<TextSizeBubbleMenu {...props} />
</Show> </Show>
</div> </div>
<div class={styles.delimiter} /> <div class={styles.delimiter} />
@ -290,9 +377,9 @@ export const TextBubbleMenu = (props: BubbleMenuProps) => {
ref={triggerRef} ref={triggerRef}
type="button" type="button"
class={clsx(styles.bubbleMenuButton, { class={clsx(styles.bubbleMenuButton, {
[styles.bubbleMenuButtonActive]: isBold() [styles.bubbleMenuButtonActive]: activeStates().bold
})} })}
onClick={() => props.editor?.chain().focus().toggleBold().run()} onClick={() => handleFormat('Bold')}
> >
<Icon name="editor-bold" /> <Icon name="editor-bold" />
</button> </button>
@ -304,15 +391,28 @@ export const TextBubbleMenu = (props: BubbleMenuProps) => {
ref={triggerRef} ref={triggerRef}
type="button" type="button"
class={clsx(styles.bubbleMenuButton, { class={clsx(styles.bubbleMenuButton, {
[styles.bubbleMenuButtonActive]: isItalic() [styles.bubbleMenuButtonActive]: activeStates().italic
})} })}
onClick={() => props.editor?.chain().focus().toggleItalic().run()} onClick={() => handleFormat('Italic')}
> >
<Icon name="editor-italic" /> <Icon name="editor-italic" />
</button> </button>
)} )}
</Popover> </Popover>
{/*<Popover content={t('Underline')}>
{(triggerRef: (el: HTMLElement) => void) => (
<button
ref={triggerRef}
type="button"
class={clsx(styles.bubbleMenuButton, {
[styles.bubbleMenuButtonActive]: activeStates().underline
})}
onClick={() => handleFormat('Underline')}
>
<Icon name="editor-underline" />
</button>
)}
</Popover> */}
<Show when={!props.isCommonMarkup}> <Show when={!props.isCommonMarkup}>
<Popover content={t('Highlight')}> <Popover content={t('Highlight')}>
{(triggerRef: (el: HTMLElement) => void) => ( {(triggerRef: (el: HTMLElement) => void) => (
@ -320,11 +420,9 @@ export const TextBubbleMenu = (props: BubbleMenuProps) => {
ref={triggerRef} ref={triggerRef}
type="button" type="button"
class={clsx(styles.bubbleMenuButton, { class={clsx(styles.bubbleMenuButton, {
[styles.bubbleMenuButtonActive]: isHighlight() [styles.bubbleMenuButtonActive]: activeStates().highlight
})} })}
onClick={() => onClick={() => props.editor?.chain().focus().toggleHighlight({ color: '#f6e3a1' }).run()}
props.editor?.chain().focus().toggleHighlight({ color: '#f6e3a1' }).run()
}
> >
<div class={styles.toggleHighlight} /> <div class={styles.toggleHighlight} />
</button> </button>
@ -339,7 +437,7 @@ export const TextBubbleMenu = (props: BubbleMenuProps) => {
type="button" type="button"
onClick={handleOpenLinkForm} onClick={handleOpenLinkForm}
class={clsx(styles.bubbleMenuButton, { class={clsx(styles.bubbleMenuButton, {
[styles.bubbleMenuButtonActive]: isLink() [styles.bubbleMenuButtonActive]: activeStates().link
})} })}
> >
<Icon name="editor-link" /> <Icon name="editor-link" />
@ -347,78 +445,31 @@ export const TextBubbleMenu = (props: BubbleMenuProps) => {
)} )}
</Popover> </Popover>
<Show when={!props.isCommonMarkup}> <Show when={!props.isCommonMarkup}>
<> <CommonMarkupBubbleMenu {...props} />
<Popover content={t('Insert footnote')}>
{(triggerRef: (el: HTMLElement) => void) => (
<button
ref={triggerRef}
type="button"
class={clsx(styles.bubbleMenuButton, {
[styles.bubbleMenuButtonActive]: isFootnote()
})}
onClick={handleOpenFootnoteEditor}
>
<Icon name="editor-footnote" />
</button>
)}
</Popover>
<div class={styles.delimiter} />
<div class={styles.dropDownHolder}>
<button
type="button"
class={clsx(styles.bubbleMenuButton, {
[styles.bubbleMenuButtonActive]: listBubbleOpen()
})}
onClick={toggleListPopup}
>
<Icon name="editor-ul" />
<Icon name="down-triangle" class={styles.triangle} />
</button>
<Show when={listBubbleOpen()}>
<div class={styles.dropDown}>
<header>{t('Lists')}</header>
<div class={styles.actions}>
<Popover content={t('Bullet list')}>
{(triggerRef: (el: HTMLElement) => void) => (
<button
ref={triggerRef}
type="button"
class={clsx(styles.bubbleMenuButton, {
[styles.bubbleMenuButtonActive]: isBulletList()
})}
onClick={() => {
props.editor?.chain().focus().toggleBulletList().run()
toggleListPopup()
}}
>
<Icon name="editor-ul" />
</button>
)}
</Popover>
<Popover content={t('Ordered list')}>
{(triggerRef: (el: HTMLElement) => void) => (
<button
ref={triggerRef}
type="button"
class={clsx(styles.bubbleMenuButton, {
[styles.bubbleMenuButtonActive]: isOrderedList()
})}
onClick={() => {
props.editor?.chain().focus().toggleOrderedList().run()
toggleListPopup()
}}
>
<Icon name="editor-ol" />
</button>
)}
</Popover>
</div>
</div>
</Show>
</div>
</>
</Show> </Show>
</> </>
)
}
return (
<div
ref={props.ref}
class={clsx(styles.TextBubbleMenu, { [styles.growWidth]: menuState().footnoteEditorOpen })}
>
<Switch>
<Match when={menuState().linkEditorOpen}>
<InsertLinkForm editor={props.editor} onClose={handleCloseLinkForm} />
</Match>
<Match when={menuState().footnoteEditorOpen}>
<MiniEditor
placeholder={t('Enter footnote text')}
onSubmit={handleAddFootnote}
content={menuState().footNote}
onCancel={() => setMenuState((prev) => ({ ...prev, footnoteEditorOpen: false }))}
/>
</Match>
<Match when={!(menuState().linkEditorOpen || menuState().footnoteEditorOpen)}>
<BaseTextBubbleMenu {...props} />
</Match> </Match>
</Switch> </Switch>
</div> </div>

View File

@ -95,7 +95,7 @@ export const EditSettingsView = (props: Props) => {
if (d) { if (d) {
const draftForm = Object.keys(d).length !== 0 ? d : { shoutId: props.shout.id } const draftForm = Object.keys(d).length !== 0 ? d : { shoutId: props.shout.id }
setForm(draftForm) setForm(draftForm)
console.debug('draft from localstorage: ', draftForm) console.debug('got draft from localstorage')
} }
}, },
{ defer: true } { defer: true }

View File

@ -62,7 +62,8 @@ export const EditView = (props: Props) => {
setFormErrors, setFormErrors,
saveDraft, saveDraft,
saveDraftToLocalStorage, saveDraftToLocalStorage,
getDraftFromLocalStorage getDraftFromLocalStorage,
isCollabMode
} = useEditorContext() } = useEditorContext()
const [subtitleInput, setSubtitleInput] = createSignal<HTMLTextAreaElement | undefined>() const [subtitleInput, setSubtitleInput] = createSignal<HTMLTextAreaElement | undefined>()
@ -453,6 +454,7 @@ export const EditView = (props: Props) => {
shoutId={form.shoutId} shoutId={form.shoutId}
initialContent={form.body} initialContent={form.body}
onChange={(body: string) => handleInputChange('body', body)} onChange={(body: string) => handleInputChange('body', body)}
disableCollaboration={!isCollabMode()}
/> />
</Show> </Show>
</div> </div>

View File

@ -50,6 +50,8 @@ export type EditorContextType = {
setFormErrors: SetStoreFunction<Record<keyof ShoutForm, string>> setFormErrors: SetStoreFunction<Record<keyof ShoutForm, string>>
editing: Accessor<Editor | undefined> editing: Accessor<Editor | undefined>
setEditing: SetStoreFunction<Editor | undefined> setEditing: SetStoreFunction<Editor | undefined>
isCollabMode: Accessor<boolean>
setIsCollabMode: SetStoreFunction<boolean>
} }
export const EditorContext = createContext<EditorContextType>({} as EditorContextType) export const EditorContext = createContext<EditorContextType>({} as EditorContextType)
@ -99,6 +101,7 @@ export const EditorProvider = (props: { children: JSX.Element }) => {
words: 0 words: 0
}) })
const toggleEditorPanel = () => setIsEditorPanelVisible((value) => !value) const toggleEditorPanel = () => setIsEditorPanelVisible((value) => !value)
const [isCollabMode, setIsCollabMode] = createSignal<boolean>(false)
const countWords = (value: WordCounter) => setWordCounter(value) const countWords = (value: WordCounter) => setWordCounter(value)
const validate = () => { const validate = () => {
if (!form.title) { if (!form.title) {
@ -281,7 +284,9 @@ export const EditorProvider = (props: { children: JSX.Element }) => {
countWords, countWords,
setForm, setForm,
setFormErrors, setFormErrors,
setEditing setEditing,
isCollabMode,
setIsCollabMode
} }
const value: EditorContextType = { const value: EditorContextType = {