not-module-prosemirror-styles
This commit is contained in:
parent
194e40aa86
commit
1a755f4c69
|
@ -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()
|
||||||
|
|
|
@ -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 {
|
|
@ -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>
|
||||||
|
|
|
@ -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 }
|
||||||
|
|
|
@ -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>
|
||||||
|
|
|
@ -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 = {
|
||||||
|
|
Loading…
Reference in New Issue
Block a user