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 { 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 { CharacterCount } from '@tiptap/extension-character-count'
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 { Placeholder } from '@tiptap/extension-placeholder'
import { Show, createEffect, createMemo, createSignal, on, onCleanup, onMount } from 'solid-js'
import { isServer } from 'solid-js/web'
import { createTiptapEditor } from 'solid-tiptap'
import uniqolor from 'uniqolor'
import { Doc } from 'yjs'
@ -19,15 +20,14 @@ import { Author } from '~/graphql/schema/core.gen'
import { base, custom, extended } from '~/lib/editorExtensions'
import { handleImageUpload } from '~/lib/handleImageUpload'
import { allowedImageTypes, renderUploadedImage } from '../Upload/renderUploadedImage'
import { Panel } from './Panel/Panel'
import { BlockquoteBubbleMenu } from './Toolbar/BlockquoteBubbleMenu'
import { EditorFloatingMenu } from './Toolbar/EditorFloatingMenu'
import { FigureBubbleMenu } from './Toolbar/FigureBubbleMenu'
import { IncutBubbleMenu } from './Toolbar/IncutBubbleMenu'
import { TextBubbleMenu } from './Toolbar/TextBubbleMenu'
import './Editor.module.scss'
import { isServer } from 'solid-js/web'
import { Panel } from './Panel/Panel'
import './Prosemirror.scss'
export type EditorComponentProps = {
shoutId: number
@ -43,8 +43,13 @@ export const EditorComponent = (props: EditorComponentProps) => {
const { t } = useLocalize()
const { session, requireAuthentication } = useSession()
const author = createMemo<Author>(() => session()?.user?.app_data?.profile as Author)
const [isCommonMarkup, setIsCommonMarkup] = createSignal(false)
const [shouldShowTextBubbleMenu, setShouldShowTextBubbleMenu] = createSignal(false)
const [isCommonMarkup, _setIsCommonMarkup] = 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 { countWords, setEditing } = useEditorContext()
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)
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(() => ({
...old?.options,
...opts,
element: opts.element as HTMLElement
element: opts.element as HTMLElement,
extensions: uniqueExtensions
}))
if (old instanceof Editor) old?.destroy()
setEditor(fresh() || null)
@ -147,7 +159,7 @@ export const EditorComponent = (props: EditorComponentProps) => {
},
content: props.initialContent ?? null
}
console.log('Editor options created:', options)
console.log(options)
setEditorOptions(() => options)
}
@ -176,85 +188,39 @@ export const EditorComponent = (props: EditorComponentProps) => {
if (editor()) {
initializeMenus()
}
// Инициализируем коллаборацию если необходимо
if (!props.disableCollaboration) {
initializeCollaboration()
}
}, 1200)
}, 'edit')
})
const initializeMenus = () => {
if (menusInitialized() || !editor()) return
console.log('stage 3: initialize menus when editor instance is ready')
if (
textBubbleMenuRef() &&
blockquoteBubbleMenuRef() &&
figureBubbleMenuRef() &&
incutBubbleMenuRef() &&
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: {
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
}
})
if (blockquoteBubbleMenuRef() && figureBubbleMenuRef() && incutBubbleMenuRef() && floatingMenuRef()) {
console.log('stage 3: initialize menus when editor instance is ready')
const menuConfigs = [
{ key: 'textBubbleMenu', ref: textBubbleMenuRef, shouldShow: shouldShowTextBubbleMenu },
{
key: 'blockquoteBubbleMenu',
ref: blockquoteBubbleMenuRef,
shouldShow: shouldShowBlockquoteBubbleMenu
},
{ key: 'figureBubbleMenu', ref: figureBubbleMenuRef, shouldShow: shouldShowFigureBubbleMenu },
{ key: 'incutBubbleMenu', ref: incutBubbleMenuRef, shouldShow: shouldShowIncutBubbleMenu },
{ key: 'floatingMenu', ref: floatingMenuRef, shouldShow: shouldShowFloatingMenu, isFloating: true }
]
const extensions = [...(editorOptions().extensions || []), ...menus]
setEditorOptions((prev) => ({ ...prev, extensions }))
console.log('Editor menus initialized:', extensions)
const menus = menuConfigs.map((config) =>
config.isFloating
? 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)
} else {
console.error('Some menu references are missing')
@ -292,6 +258,13 @@ export const EditorComponent = (props: EditorComponentProps) => {
setEditorOptions((prev: Partial<EditorOptions>) => {
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(
Collaboration.configure({ document: yDocs[docName] }),
CollaborationCursor.configure({
@ -316,6 +289,17 @@ export const EditorComponent = (props: EditorComponentProps) => {
}
}
// Инициализируем коллаборацию если необходимо
createEffect(
on(
() => props.disableCollaboration,
() => {
initializeCollaboration()
},
{ defer: true }
)
)
onCleanup(() => {
editorElRef()?.removeEventListener('focus', handleFocus)
editor()?.destroy()

View File

@ -86,25 +86,25 @@ mark.highlight {
}
[data-float='half-left'] {
@include media-breakpoint-up(md) {
max-width: 50%;
min-width: 30%;
}
float: left;
margin: 1rem 1rem 0;
clear: left;
}
[data-float='half-right'] {
@include media-breakpoint-up(md) {
max-width: 50%;
min-width: 30%;
}
}
[data-float='half-right'] {
float: right;
margin: 1rem 0;
clear: right;
@include media-breakpoint-up(md) {
max-width: 50%;
min-width: 30%;
}
}
}
@ -114,7 +114,7 @@ mark.highlight {
}
&[data-type='quote'] {
font-size:1.4rem;
font-size: 1.4rem;
border: solid #000;
border-width: 0 0 0 2px;
margin: 1.6rem 0;
@ -136,6 +136,20 @@ mark.highlight {
}
&[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) {
&[data-float='left'] {
margin-right: 1.5em;
@ -147,24 +161,16 @@ mark.highlight {
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'] {
background: #f1f2f3;
font-size: 1.4rem;
margin: 1em -1rem;
padding: 2em 2rem;
transition: background 0.3s ease-in-out;
@include media-breakpoint-up(sm) {
margin-left: -2rem;
margin-right: -2rem;
@ -181,12 +187,6 @@ mark.highlight {
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 {
float: none;
max-width: unset;
@ -196,6 +196,9 @@ mark.highlight {
&[data-float='left'],
&[data-float='half-left'] {
margin-left: -1rem;
clear: left;
@include media-breakpoint-up(sm) {
margin-left: -2rem;
margin-right: 2rem;
@ -208,13 +211,13 @@ mark.highlight {
@include media-breakpoint-up(xl) {
margin-left: -12.5%;
}
margin-left: -1rem;
clear: left;
}
&[data-float='right'],
&[data-float='half-right'] {
margin-right: -1rem;
clear: right;
@include media-breakpoint-up(sm) {
margin-left: 2rem;
margin-right: -2rem;
@ -227,9 +230,6 @@ mark.highlight {
@include media-breakpoint-up(xl) {
margin-right: -12.5%;
}
margin-right: -1rem;
clear: right;
}
*:last-child {
@ -311,4 +311,4 @@ footnote {
color: var(--selection-color);
border: solid var(--selection-background);
border-width: 0;
}
}

View File

@ -1,6 +1,16 @@
import type { Editor } from '@tiptap/core'
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 { Icon } from '~/components/_shared/Icon'
import { Popover } from '~/components/_shared/Popover'
@ -21,404 +31,445 @@ type BubbleMenuProps = {
export const TextBubbleMenu = (props: BubbleMenuProps) => {
const { t } = useLocalize()
const isActive = (name: string, attributes?: Record<string, string | number>) =>
createEditorTransaction(
() => {
console.log('isActive', name, attributes)
return props.editor
},
(editor) => editor?.isActive(name, attributes)
)
const isActive = createMemo(
() => (name: string, attributes?: Record<string, string | number>) =>
props.editor?.isActive(name, attributes)
)
const [textSizeBubbleOpen, setTextSizeBubbleOpen] = createSignal(false)
const [listBubbleOpen, setListBubbleOpen] = createSignal(false)
const [linkEditorOpen, setLinkEditorOpen] = createSignal(false)
const [footnoteEditorOpen, setFootnoteEditorOpen] = createSignal(false)
const [footNote, setFootNote] = createSignal<string>()
const [menuState, setMenuState] = createSignal({
textSizeBubbleOpen: false,
listBubbleOpen: false,
linkEditorOpen: false,
footnoteEditorOpen: false,
footNote: undefined as string | undefined
})
createEffect(() => {
if (!props.shouldShow) {
setFootNote()
setFootnoteEditorOpen(false)
setLinkEditorOpen(false)
setTextSizeBubbleOpen(false)
setListBubbleOpen(false)
setMenuState((prev) => ({
...prev,
footNote: undefined,
footnoteEditorOpen: false,
linkEditorOpen: false,
textSizeBubbleOpen: false,
listBubbleOpen: false
}))
}
})
const isBold = isActive('bold')
const isItalic = isActive('italic')
const isH1 = isActive('heading', { level: 2 })
const isH2 = isActive('heading', { level: 3 })
const isH3 = isActive('heading', { level: 4 })
const isQuote = isActive('blockquote', { 'data-type': 'quote' })
const isPunchLine = isActive('blockquote', { 'data-type': 'punchline' })
const isOrderedList = isActive('isOrderedList')
const isBulletList = isActive('isBulletList')
const isLink = isActive('link')
const isHighlight = isActive('highlight')
const isFootnote = isActive('footnote')
const isIncut = isActive('article')
const activeStates = createMemo(() => ({
bold: isActive()('bold'),
italic: isActive()('italic'),
h1: isActive()('heading', { level: 2 }),
h2: isActive()('heading', { level: 3 }),
h3: isActive()('heading', { level: 4 }),
quote: isActive()('blockquote', { 'data-type': 'quote' }),
punchLine: isActive()('blockquote', { 'data-type': 'punchline' }),
orderedList: isActive()('orderedList'),
bulletList: isActive()('bulletList'),
link: isActive()('link'),
highlight: isActive()('highlight'),
footnote: isActive()('footnote'),
incut: isActive()('article')
// underline: isActive()('underline'),
}))
const toggleTextSizePopup = () => {
if (listBubbleOpen()) {
setListBubbleOpen(false)
}
setTextSizeBubbleOpen((prev) => !prev)
}
const toggleListPopup = () => {
if (textSizeBubbleOpen()) {
setTextSizeBubbleOpen(false)
}
setListBubbleOpen((prev) => !prev)
const togglePopup = (type: 'textSize' | 'list') => {
setMenuState((prev) => ({
...prev,
textSizeBubbleOpen: type === 'textSize' ? !prev.textSizeBubbleOpen : false,
listBubbleOpen: type === 'list' ? !prev.listBubbleOpen : false
}))
}
const handleKeyDown = (event: KeyboardEvent) => {
if (event.code === 'KeyK' && (event.metaKey || event.ctrlKey) && !props.editor.state.selection.empty) {
event.preventDefault()
setLinkEditorOpen(true)
setMenuState((prev) => ({ ...prev, linkEditorOpen: true }))
}
}
const updateCurrentFootnoteValue = createEditorTransaction(
() => props.editor,
(ed) => {
if (!isFootnote()) {
if (!activeStates().footnote) {
return
}
const value = ed.getAttributes('footnote').value
setFootNote(value)
setMenuState((prev) => ({ ...prev, footNote: value }))
}
)
const handleAddFootnote = (footnote: string) => {
if (footNote()) {
if (menuState().footNote) {
props.editor?.chain().focus().updateFootnote({ value: footnote }).run()
} else {
props.editor?.chain().focus().setFootnote({ value: footnote }).run()
}
setFootNote()
setLinkEditorOpen(false)
setFootnoteEditorOpen(false)
setMenuState((prev) => ({
...prev,
footNote: undefined,
linkEditorOpen: false,
footnoteEditorOpen: false
}))
}
const handleOpenFootnoteEditor = () => {
updateCurrentFootnoteValue()
setLinkEditorOpen(false)
setFootnoteEditorOpen(true)
setMenuState((prev) => ({ ...prev, linkEditorOpen: false, footnoteEditorOpen: true }))
}
const handleSetPunchline = () => {
if (isPunchLine()) {
if (activeStates().punchLine) {
props.editor?.chain().focus().toggleBlockquote('punchline').run()
}
props.editor?.chain().focus().toggleBlockquote('quote').run()
toggleTextSizePopup()
togglePopup('textSize')
}
const handleSetQuote = () => {
if (isQuote()) {
if (activeStates().quote) {
props.editor?.chain().focus().toggleBlockquote('quote').run()
}
props.editor?.chain().focus().toggleBlockquote('punchline').run()
toggleTextSizePopup()
togglePopup('textSize')
}
onMount(() => {
window.addEventListener('keydown', handleKeyDown)
onCleanup(() => {
window.removeEventListener('keydown', handleKeyDown)
setLinkEditorOpen(false)
setMenuState((prev) => ({ ...prev, linkEditorOpen: false }))
})
})
const handleOpenLinkForm = () => {
props.editor?.chain().focus().addTextWrap({ class: 'highlight-fake-selection' }).run()
setLinkEditorOpen(true)
setMenuState((prev) => ({ ...prev, linkEditorOpen: true }))
}
const handleCloseLinkForm = () => {
setLinkEditorOpen(false)
setMenuState((prev) => ({ ...prev, linkEditorOpen: false }))
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 (
<div ref={props.ref} class={clsx(styles.TextBubbleMenu, { [styles.growWidth]: footnoteEditorOpen() })}>
<div
ref={props.ref}
class={clsx(styles.TextBubbleMenu, { [styles.growWidth]: menuState().footnoteEditorOpen })}
>
<Switch>
<Match when={linkEditorOpen()}>
<Match when={menuState().linkEditorOpen}>
<InsertLinkForm editor={props.editor} onClose={handleCloseLinkForm} />
</Match>
<Match when={footnoteEditorOpen()}>
<Match when={menuState().footnoteEditorOpen}>
<MiniEditor
placeholder={t('Enter footnote text')}
onSubmit={(value: string) => handleAddFootnote(value)}
content={footNote()}
onCancel={() => {
setFootnoteEditorOpen(false)
}}
onSubmit={handleAddFootnote}
content={menuState().footNote}
onCancel={() => setMenuState((prev) => ({ ...prev, footnoteEditorOpen: false }))}
/>
</Match>
<Match when={!(linkEditorOpen() && footnoteEditorOpen())}>
<>
<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 when={!(menuState().linkEditorOpen || menuState().footnoteEditorOpen)}>
<BaseTextBubbleMenu {...props} />
</Match>
</Switch>
</div>

View File

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

View File

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

View File

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