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() },
) { { key: 'figureBubbleMenu', ref: figureBubbleMenuRef, shouldShow: shouldShowFigureBubbleMenu },
const menus = [ { key: 'incutBubbleMenu', ref: incutBubbleMenuRef, shouldShow: shouldShowIncutBubbleMenu },
BubbleMenu.configure({ { key: 'floatingMenu', ref: floatingMenuRef, shouldShow: shouldShowFloatingMenu, isFloating: true }
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: {
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
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%;
}
} }
} }
@ -114,7 +114,7 @@ mark.highlight {
} }
&[data-type='quote'] { &[data-type='quote'] {
font-size:1.4rem; font-size: 1.4rem;
border: solid #000; border: solid #000;
border-width: 0 0 0 2px; border-width: 0 0 0 2px;
margin: 1.6rem 0; margin: 1.6rem 0;
@ -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,404 +31,445 @@ 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 (
<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]: activeStates().bulletList
})}
onClick={() => {
props.editor?.chain().focus().toggleBulletList().run()
togglePopup('list')
}}
>
<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]: 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}>
<button
type="button"
class={clsx(styles.bubbleMenuButton, {
[styles.bubbleMenuButtonActive]: menuState().listBubbleOpen
})}
onClick={() => togglePopup('list')}
>
<Icon name="editor-ul" />
<Icon name="down-triangle" class={styles.triangle} />
</button>
<Show when={menuState().listBubbleOpen}>
<ListBubbleMenu {...props} />
</Show>
</div>
</>
)
}
const TextSizeBubbleMenu = (props: BubbleMenuProps) => {
return (
<div class={styles.dropDown}>
<header>{t('Headers')}</header>
<div class={styles.actions}>
<Popover content={t('Header 1')}>
{(triggerRef: (el: HTMLElement) => void) => (
<button
ref={triggerRef}
type="button"
class={clsx(styles.bubbleMenuButton, {
[styles.bubbleMenuButtonActive]: activeStates().h1
})}
onClick={() => {
props.editor?.chain().focus().toggleHeading({ level: 2 }).run()
togglePopup('textSize')
}}
>
<Icon name="editor-h1" />
</button>
)}
</Popover>
<Popover content={t('Header 2')}>
{(triggerRef: (el: HTMLElement) => void) => (
<button
ref={triggerRef}
type="button"
class={clsx(styles.bubbleMenuButton, {
[styles.bubbleMenuButtonActive]: activeStates().h2
})}
onClick={() => {
props.editor?.chain().focus().toggleHeading({ level: 3 }).run()
togglePopup('textSize')
}}
>
<Icon name="editor-h2" />
</button>
)}
</Popover>
<Popover content={t('Header 3')}>
{(triggerRef: (el: HTMLElement) => void) => (
<button
ref={triggerRef}
type="button"
class={clsx(styles.bubbleMenuButton, {
[styles.bubbleMenuButtonActive]: activeStates().h3
})}
onClick={() => {
props.editor?.chain().focus().toggleHeading({ level: 4 }).run()
togglePopup('textSize')
}}
>
<Icon name="editor-h3" />
</button>
)}
</Popover>
</div>
<header>{t('Quotes')}</header>
<div class={styles.actions}>
<Popover content={t('Quote')}>
{(triggerRef: (el: HTMLElement) => void) => (
<button
ref={triggerRef}
type="button"
class={clsx(styles.bubbleMenuButton, {
[styles.bubbleMenuButtonActive]: activeStates().quote
})}
onClick={handleSetPunchline}
>
<Icon name="editor-blockquote" />
</button>
)}
</Popover>
<Popover content={t('Punchline')}>
{(triggerRef: (el: HTMLElement) => void) => (
<button
ref={triggerRef}
type="button"
class={clsx(styles.bubbleMenuButton, {
[styles.bubbleMenuButtonActive]: activeStates().punchLine
})}
onClick={handleSetQuote}
>
<Icon name="editor-quote" />
</button>
)}
</Popover>
</div>
<header>{t('squib')}</header>
<div class={styles.actions}>
<Popover content={t('Incut')}>
{(triggerRef: (el: HTMLElement) => void) => (
<button
ref={triggerRef}
type="button"
class={clsx(styles.bubbleMenuButton, {
[styles.bubbleMenuButtonActive]: activeStates().incut
})}
onClick={() => {
props.editor?.chain().focus().toggleArticle().run()
togglePopup('textSize')
}}
>
<Icon name="editor-squib" />
</button>
)}
</Popover>
</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>
</div>
<div class={styles.delimiter} />
</>
</Show>
<Popover content={t('Bold')}>
{(triggerRef: (el: HTMLElement) => void) => (
<button
ref={triggerRef}
type="button"
class={clsx(styles.bubbleMenuButton, {
[styles.bubbleMenuButtonActive]: activeStates().bold
})}
onClick={() => handleFormat('Bold')}
>
<Icon name="editor-bold" />
</button>
)}
</Popover>
<Popover content={t('Italic')}>
{(triggerRef: (el: HTMLElement) => void) => (
<button
ref={triggerRef}
type="button"
class={clsx(styles.bubbleMenuButton, {
[styles.bubbleMenuButtonActive]: activeStates().italic
})}
onClick={() => handleFormat('Italic')}
>
<Icon name="editor-italic" />
</button>
)}
</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}>
<Popover content={t('Highlight')}>
{(triggerRef: (el: HTMLElement) => void) => (
<button
ref={triggerRef}
type="button"
class={clsx(styles.bubbleMenuButton, {
[styles.bubbleMenuButtonActive]: activeStates().highlight
})}
onClick={() => props.editor?.chain().focus().toggleHighlight({ color: '#f6e3a1' }).run()}
>
<div class={styles.toggleHighlight} />
</button>
)}
</Popover>
<div class={styles.delimiter} />
</Show>
<Popover content={<div class={styles.noWrap}>{t('Add url')}</div>}>
{(triggerRef: (el: HTMLElement) => void) => (
<button
ref={triggerRef}
type="button"
onClick={handleOpenLinkForm}
class={clsx(styles.bubbleMenuButton, {
[styles.bubbleMenuButtonActive]: activeStates().link
})}
>
<Icon name="editor-link" />
</button>
)}
</Popover>
<Show when={!props.isCommonMarkup}>
<CommonMarkupBubbleMenu {...props} />
</Show>
</>
)
}
return ( return (
<div ref={props.ref} class={clsx(styles.TextBubbleMenu, { [styles.growWidth]: footnoteEditorOpen() })}> <div
ref={props.ref}
class={clsx(styles.TextBubbleMenu, { [styles.growWidth]: menuState().footnoteEditorOpen })}
>
<Switch> <Switch>
<Match when={linkEditorOpen()}> <Match when={menuState().linkEditorOpen}>
<InsertLinkForm editor={props.editor} onClose={handleCloseLinkForm} /> <InsertLinkForm editor={props.editor} onClose={handleCloseLinkForm} />
</Match> </Match>
<Match when={footnoteEditorOpen()}> <Match when={menuState().footnoteEditorOpen}>
<MiniEditor <MiniEditor
placeholder={t('Enter footnote text')} placeholder={t('Enter footnote text')}
onSubmit={(value: string) => handleAddFootnote(value)} onSubmit={handleAddFootnote}
content={footNote()} content={menuState().footNote}
onCancel={() => { onCancel={() => setMenuState((prev) => ({ ...prev, footnoteEditorOpen: false }))}
setFootnoteEditorOpen(false)
}}
/> />
</Match> </Match>
<Match when={!(linkEditorOpen() && footnoteEditorOpen())}> <Match when={!(menuState().linkEditorOpen || menuState().footnoteEditorOpen)}>
<> <BaseTextBubbleMenu {...props} />
<Show when={!props.isCommonMarkup}>
<>
<div class={styles.dropDownHolder}>
<button
type="button"
class={clsx(styles.bubbleMenuButton, {
[styles.bubbleMenuButtonActive]: textSizeBubbleOpen()
})}
onClick={toggleTextSizePopup}
>
<Icon name="editor-text-size" />
<Icon name="down-triangle" class={styles.triangle} />
</button>
<Show when={textSizeBubbleOpen()}>
<div class={styles.dropDown}>
<header>{t('Headers')}</header>
<div class={styles.actions}>
<Popover content={t('Header 1')}>
{(triggerRef: (el: HTMLElement) => void) => (
<button
ref={triggerRef}
type="button"
class={clsx(styles.bubbleMenuButton, {
[styles.bubbleMenuButtonActive]: isH1()
})}
onClick={() => {
props.editor?.chain().focus().toggleHeading({ level: 2 }).run()
toggleTextSizePopup()
}}
>
<Icon name="editor-h1" />
</button>
)}
</Popover>
<Popover content={t('Header 2')}>
{(triggerRef: (el: HTMLElement) => void) => (
<button
ref={triggerRef}
type="button"
class={clsx(styles.bubbleMenuButton, {
[styles.bubbleMenuButtonActive]: isH2()
})}
onClick={() => {
props.editor?.chain().focus().toggleHeading({ level: 3 }).run()
toggleTextSizePopup()
}}
>
<Icon name="editor-h2" />
</button>
)}
</Popover>
<Popover content={t('Header 3')}>
{(triggerRef: (el: HTMLElement) => void) => (
<button
ref={triggerRef}
type="button"
class={clsx(styles.bubbleMenuButton, {
[styles.bubbleMenuButtonActive]: isH3()
})}
onClick={() => {
props.editor?.chain().focus().toggleHeading({ level: 4 }).run()
toggleTextSizePopup()
}}
>
<Icon name="editor-h3" />
</button>
)}
</Popover>
</div>
<header>{t('Quotes')}</header>
<div class={styles.actions}>
<Popover content={t('Quote')}>
{(triggerRef: (el: HTMLElement) => void) => (
<button
ref={triggerRef}
type="button"
class={clsx(styles.bubbleMenuButton, {
[styles.bubbleMenuButtonActive]: isQuote()
})}
onClick={handleSetPunchline}
>
<Icon name="editor-blockquote" />
</button>
)}
</Popover>
<Popover content={t('Punchline')}>
{(triggerRef: (el: HTMLElement) => void) => (
<button
ref={triggerRef}
type="button"
class={clsx(styles.bubbleMenuButton, {
[styles.bubbleMenuButtonActive]: isPunchLine()
})}
onClick={handleSetQuote}
>
<Icon name="editor-quote" />
</button>
)}
</Popover>
</div>
<header>{t('squib')}</header>
<div class={styles.actions}>
<Popover content={t('Incut')}>
{(triggerRef: (el: HTMLElement) => void) => (
<button
ref={triggerRef}
type="button"
class={clsx(styles.bubbleMenuButton, {
[styles.bubbleMenuButtonActive]: isIncut()
})}
onClick={() => {
props.editor?.chain().focus().toggleArticle().run()
toggleTextSizePopup()
}}
>
<Icon name="editor-squib" />
</button>
)}
</Popover>
</div>
</div>
</Show>
</div>
<div class={styles.delimiter} />
</>
</Show>
<Popover content={t('Bold')}>
{(triggerRef: (el: HTMLElement) => void) => (
<button
ref={triggerRef}
type="button"
class={clsx(styles.bubbleMenuButton, {
[styles.bubbleMenuButtonActive]: isBold()
})}
onClick={() => props.editor?.chain().focus().toggleBold().run()}
>
<Icon name="editor-bold" />
</button>
)}
</Popover>
<Popover content={t('Italic')}>
{(triggerRef: (el: HTMLElement) => void) => (
<button
ref={triggerRef}
type="button"
class={clsx(styles.bubbleMenuButton, {
[styles.bubbleMenuButtonActive]: isItalic()
})}
onClick={() => props.editor?.chain().focus().toggleItalic().run()}
>
<Icon name="editor-italic" />
</button>
)}
</Popover>
<Show when={!props.isCommonMarkup}>
<Popover content={t('Highlight')}>
{(triggerRef: (el: HTMLElement) => void) => (
<button
ref={triggerRef}
type="button"
class={clsx(styles.bubbleMenuButton, {
[styles.bubbleMenuButtonActive]: isHighlight()
})}
onClick={() =>
props.editor?.chain().focus().toggleHighlight({ color: '#f6e3a1' }).run()
}
>
<div class={styles.toggleHighlight} />
</button>
)}
</Popover>
<div class={styles.delimiter} />
</Show>
<Popover content={<div class={styles.noWrap}>{t('Add url')}</div>}>
{(triggerRef: (el: HTMLElement) => void) => (
<button
ref={triggerRef}
type="button"
onClick={handleOpenLinkForm}
class={clsx(styles.bubbleMenuButton, {
[styles.bubbleMenuButtonActive]: isLink()
})}
>
<Icon name="editor-link" />
</button>
)}
</Popover>
<Show when={!props.isCommonMarkup}>
<>
<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>
</>
</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 = {