Merge branch 'drafts' into 'dev'
separate menu for images in editor See merge request discoursio/discoursio-webapp!60
This commit is contained in:
commit
2e3e32aabf
|
@ -35,7 +35,8 @@ import { useSession } from '../../context/session'
|
||||||
import uniqolor from 'uniqolor'
|
import uniqolor from 'uniqolor'
|
||||||
import { HocuspocusProvider } from '@hocuspocus/provider'
|
import { HocuspocusProvider } from '@hocuspocus/provider'
|
||||||
import { Embed } from './extensions/embed'
|
import { Embed } from './extensions/embed'
|
||||||
import { EditorBubbleMenu } from './EditorBubbleMenu'
|
import { TextBubbleMenu } from './TextBubbleMenu'
|
||||||
|
import { ImageBubbleMenu } from './ImageBubbleMenu'
|
||||||
import { EditorFloatingMenu } from './EditorFloatingMenu'
|
import { EditorFloatingMenu } from './EditorFloatingMenu'
|
||||||
import { useEditorContext } from '../../context/editor'
|
import { useEditorContext } from '../../context/editor'
|
||||||
|
|
||||||
|
@ -74,7 +75,13 @@ export const Editor = (props: EditorProps) => {
|
||||||
current: null
|
current: null
|
||||||
}
|
}
|
||||||
|
|
||||||
const bubbleMenuRef: {
|
const textBubbleMenuRef: {
|
||||||
|
current: HTMLDivElement
|
||||||
|
} = {
|
||||||
|
current: null
|
||||||
|
}
|
||||||
|
|
||||||
|
const imageBubbleMenuRef: {
|
||||||
current: HTMLDivElement
|
current: HTMLDivElement
|
||||||
} = {
|
} = {
|
||||||
current: null
|
current: null
|
||||||
|
@ -135,7 +142,19 @@ export const Editor = (props: EditorProps) => {
|
||||||
TrailingNode,
|
TrailingNode,
|
||||||
CharacterCount,
|
CharacterCount,
|
||||||
BubbleMenu.configure({
|
BubbleMenu.configure({
|
||||||
element: bubbleMenuRef.current
|
pluginKey: 'textBubbleMenu',
|
||||||
|
element: textBubbleMenuRef.current,
|
||||||
|
shouldShow: ({ editor: e, view, state, oldState, from, to }) => {
|
||||||
|
console.log(view)
|
||||||
|
return e.isFocused && !e.isActive('image')
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
BubbleMenu.configure({
|
||||||
|
pluginKey: 'imageBubbleMenu',
|
||||||
|
element: imageBubbleMenuRef.current,
|
||||||
|
shouldShow: ({ editor: e, view, state, oldState, from, to }) => {
|
||||||
|
return e.isFocused && e.isActive('image')
|
||||||
|
}
|
||||||
}),
|
}),
|
||||||
FloatingMenu.configure({
|
FloatingMenu.configure({
|
||||||
tippyOptions: {
|
tippyOptions: {
|
||||||
|
@ -165,7 +184,8 @@ export const Editor = (props: EditorProps) => {
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<div ref={(el) => (editorElRef.current = el)} />
|
<div ref={(el) => (editorElRef.current = el)} />
|
||||||
<EditorBubbleMenu editor={editor()} ref={(el) => (bubbleMenuRef.current = el)} />
|
<TextBubbleMenu editor={editor()} ref={(el) => (textBubbleMenuRef.current = el)} />
|
||||||
|
<ImageBubbleMenu editor={editor()} ref={(el) => (imageBubbleMenuRef.current = el)} />
|
||||||
<EditorFloatingMenu editor={editor()} ref={(el) => (floatingMenuRef.current = el)} />
|
<EditorFloatingMenu editor={editor()} ref={(el) => (floatingMenuRef.current = el)} />
|
||||||
</>
|
</>
|
||||||
)
|
)
|
||||||
|
|
|
@ -1,258 +0,0 @@
|
||||||
import { Switch, Match, createSignal, Show } from 'solid-js'
|
|
||||||
import type { Editor } from '@tiptap/core'
|
|
||||||
import styles from './EditorBubbleMenu.module.scss'
|
|
||||||
import { Icon } from '../../_shared/Icon'
|
|
||||||
import { clsx } from 'clsx'
|
|
||||||
import { createEditorTransaction } from 'solid-tiptap'
|
|
||||||
import { useLocalize } from '../../../context/localize'
|
|
||||||
import { InlineForm } from '../InlineForm'
|
|
||||||
import validateImage from '../../../utils/validateUrl'
|
|
||||||
|
|
||||||
type BubbleMenuProps = {
|
|
||||||
editor: Editor
|
|
||||||
ref: (el: HTMLDivElement) => void
|
|
||||||
}
|
|
||||||
|
|
||||||
export const EditorBubbleMenu = (props: BubbleMenuProps) => {
|
|
||||||
const { t } = useLocalize()
|
|
||||||
const [textSizeBubbleOpen, setTextSizeBubbleOpen] = createSignal<boolean>(false)
|
|
||||||
const [listBubbleOpen, setListBubbleOpen] = createSignal<boolean>(false)
|
|
||||||
const [linkEditorOpen, setLinkEditorOpen] = createSignal<boolean>(false)
|
|
||||||
|
|
||||||
const isActive = (name: string, attributes?: unknown) =>
|
|
||||||
createEditorTransaction(
|
|
||||||
() => props.editor,
|
|
||||||
(editor) => {
|
|
||||||
return editor && editor.isActive(name, attributes)
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
const isBold = isActive('bold')
|
|
||||||
const isItalic = isActive('italic')
|
|
||||||
const isH1 = isActive('heading', { level: 1 })
|
|
||||||
const isH2 = isActive('heading', { level: 2 })
|
|
||||||
const isH3 = isActive('heading', { level: 3 })
|
|
||||||
const isBlockQuote = isActive('blockquote')
|
|
||||||
const isOrderedList = isActive('isOrderedList')
|
|
||||||
const isBulletList = isActive('isBulletList')
|
|
||||||
const isLink = isActive('link')
|
|
||||||
|
|
||||||
const toggleLinkForm = () => {
|
|
||||||
setLinkEditorOpen(true)
|
|
||||||
}
|
|
||||||
|
|
||||||
const toggleTextSizePopup = () => {
|
|
||||||
if (listBubbleOpen()) {
|
|
||||||
setListBubbleOpen(false)
|
|
||||||
}
|
|
||||||
|
|
||||||
setTextSizeBubbleOpen((prev) => !prev)
|
|
||||||
}
|
|
||||||
const toggleListPopup = () => {
|
|
||||||
if (textSizeBubbleOpen()) {
|
|
||||||
setTextSizeBubbleOpen(false)
|
|
||||||
}
|
|
||||||
|
|
||||||
setListBubbleOpen((prev) => !prev)
|
|
||||||
}
|
|
||||||
|
|
||||||
const handleLinkFormSubmit = (value: string) => {
|
|
||||||
props.editor.chain().focus().setLink({ href: value }).run()
|
|
||||||
}
|
|
||||||
|
|
||||||
const currentUrl = createEditorTransaction(
|
|
||||||
() => props.editor,
|
|
||||||
(editor) => {
|
|
||||||
return (editor && editor.getAttributes('link').href) || ''
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
const handleClearLinkForm = () => {
|
|
||||||
if (currentUrl()) {
|
|
||||||
props.editor.chain().focus().unsetLink().run()
|
|
||||||
}
|
|
||||||
setLinkEditorOpen(false)
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<div ref={props.ref} class={styles.bubbleMenu}>
|
|
||||||
<Switch>
|
|
||||||
<Match when={linkEditorOpen()}>
|
|
||||||
<InlineForm
|
|
||||||
placeholder={t('Enter URL address')}
|
|
||||||
initialValue={currentUrl() ?? ''}
|
|
||||||
onClear={handleClearLinkForm}
|
|
||||||
validate={(value) => (validateImage(value) ? '' : t('Invalid url format'))}
|
|
||||||
onSubmit={handleLinkFormSubmit}
|
|
||||||
onClose={() => setLinkEditorOpen(false)}
|
|
||||||
errorMessage={t('Error')}
|
|
||||||
/>
|
|
||||||
</Match>
|
|
||||||
<Match when={!linkEditorOpen()}>
|
|
||||||
<>
|
|
||||||
<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}>
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
class={clsx(styles.bubbleMenuButton, {
|
|
||||||
[styles.bubbleMenuButtonActive]: isH1()
|
|
||||||
})}
|
|
||||||
onClick={() => {
|
|
||||||
props.editor.chain().focus().toggleHeading({ level: 1 }).run()
|
|
||||||
toggleTextSizePopup()
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<Icon name="editor-h1" />
|
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
class={clsx(styles.bubbleMenuButton, {
|
|
||||||
[styles.bubbleMenuButtonActive]: isH2()
|
|
||||||
})}
|
|
||||||
onClick={() => {
|
|
||||||
props.editor.chain().focus().toggleHeading({ level: 2 }).run()
|
|
||||||
toggleTextSizePopup()
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<Icon name="editor-h2" />
|
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
class={clsx(styles.bubbleMenuButton, {
|
|
||||||
[styles.bubbleMenuButtonActive]: isH3()
|
|
||||||
})}
|
|
||||||
onClick={() => {
|
|
||||||
props.editor.chain().focus().toggleHeading({ level: 3 }).run()
|
|
||||||
toggleTextSizePopup()
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<Icon name="editor-h3" />
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
<header>{t('Quotes')}</header>
|
|
||||||
<div class={styles.actions}>
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
class={clsx(styles.bubbleMenuButton, {
|
|
||||||
[styles.bubbleMenuButtonActive]: isBlockQuote()
|
|
||||||
})}
|
|
||||||
onClick={() => {
|
|
||||||
props.editor.chain().focus().toggleBlockquote().run()
|
|
||||||
toggleTextSizePopup()
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<Icon name="editor-blockquote" />
|
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
class={clsx(styles.bubbleMenuButton, {
|
|
||||||
[styles.bubbleMenuButtonActive]: isBlockQuote()
|
|
||||||
})}
|
|
||||||
onClick={() => {
|
|
||||||
props.editor.chain().focus().toggleBlockquote().run()
|
|
||||||
toggleTextSizePopup()
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<Icon name="editor-quote" />
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</Show>
|
|
||||||
</div>
|
|
||||||
<div class={styles.delimiter} />
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
class={clsx(styles.bubbleMenuButton, {
|
|
||||||
[styles.bubbleMenuButtonActive]: isBold()
|
|
||||||
})}
|
|
||||||
onClick={() => props.editor.chain().focus().toggleBold().run()}
|
|
||||||
>
|
|
||||||
<Icon name="editor-bold" />
|
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
class={clsx(styles.bubbleMenuButton, {
|
|
||||||
[styles.bubbleMenuButtonActive]: isItalic()
|
|
||||||
})}
|
|
||||||
onClick={() => props.editor.chain().focus().toggleItalic().run()}
|
|
||||||
>
|
|
||||||
<Icon name="editor-italic" />
|
|
||||||
</button>
|
|
||||||
<div class={styles.delimiter} />
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
onClick={toggleLinkForm}
|
|
||||||
class={clsx(styles.bubbleMenuButton, {
|
|
||||||
[styles.bubbleMenuButtonActive]: isLink()
|
|
||||||
})}
|
|
||||||
>
|
|
||||||
<Icon name="editor-link" />
|
|
||||||
</button>
|
|
||||||
<button type="button" class={styles.bubbleMenuButton}>
|
|
||||||
<Icon name="editor-footnote" />
|
|
||||||
</button>
|
|
||||||
<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}>
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
class={clsx(styles.bubbleMenuButton, {
|
|
||||||
[styles.bubbleMenuButtonActive]: isBulletList()
|
|
||||||
})}
|
|
||||||
onClick={() => {
|
|
||||||
props.editor.chain().focus().toggleBulletList().run()
|
|
||||||
toggleListPopup()
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<Icon name="editor-ul" />
|
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
class={clsx(styles.bubbleMenuButton, {
|
|
||||||
[styles.bubbleMenuButtonActive]: isOrderedList()
|
|
||||||
})}
|
|
||||||
onClick={() => {
|
|
||||||
props.editor.chain().focus().toggleOrderedList().run()
|
|
||||||
toggleListPopup()
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<Icon name="editor-ol" />
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</Show>
|
|
||||||
</div>
|
|
||||||
</>
|
|
||||||
</Match>
|
|
||||||
</Switch>
|
|
||||||
</div>
|
|
||||||
</>
|
|
||||||
)
|
|
||||||
}
|
|
|
@ -1 +0,0 @@
|
||||||
export { EditorBubbleMenu } from './EditorBubbleMenu'
|
|
15
src/components/Editor/ImageBubbleMenu/ImageBubbleMenu.tsx
Normal file
15
src/components/Editor/ImageBubbleMenu/ImageBubbleMenu.tsx
Normal file
|
@ -0,0 +1,15 @@
|
||||||
|
import type { Editor } from '@tiptap/core'
|
||||||
|
import styles from './ImageBubbleMenu.module.scss'
|
||||||
|
|
||||||
|
type BubbleMenuProps = {
|
||||||
|
editor: Editor
|
||||||
|
ref: (el: HTMLDivElement) => void
|
||||||
|
}
|
||||||
|
|
||||||
|
export const ImageBubbleMenu = (props: BubbleMenuProps) => {
|
||||||
|
return (
|
||||||
|
<div ref={props.ref} class={styles.bubbleMenu}>
|
||||||
|
test
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
1
src/components/Editor/ImageBubbleMenu/index.ts
Normal file
1
src/components/Editor/ImageBubbleMenu/index.ts
Normal file
|
@ -0,0 +1 @@
|
||||||
|
export { ImageBubbleMenu } from './ImageBubbleMenu'
|
|
@ -0,0 +1,85 @@
|
||||||
|
.bubbleMenu {
|
||||||
|
background: #fff;
|
||||||
|
box-shadow: 0 4px 10px rgba(#000, 0.25);
|
||||||
|
|
||||||
|
.bubbleMenuButton {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
flex-wrap: nowrap;
|
||||||
|
opacity: 0.5;
|
||||||
|
padding: 1rem;
|
||||||
|
|
||||||
|
.triangle {
|
||||||
|
margin-left: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.colorWheel {
|
||||||
|
display: inline-block;
|
||||||
|
width: 20px;
|
||||||
|
height: 20px;
|
||||||
|
border-radius: 50%;
|
||||||
|
background: #f6e3a1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.bubbleMenuButtonActive {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.delimiter {
|
||||||
|
background: #999;
|
||||||
|
display: inline-block;
|
||||||
|
height: 1.4em;
|
||||||
|
margin: 0 0.2em;
|
||||||
|
vertical-align: text-bottom;
|
||||||
|
width: 1px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dropDownHolder {
|
||||||
|
position: relative;
|
||||||
|
cursor: pointer;
|
||||||
|
display: inline-flex;
|
||||||
|
flex-flow: row nowrap;
|
||||||
|
align-items: center;
|
||||||
|
|
||||||
|
.dropDown {
|
||||||
|
position: absolute;
|
||||||
|
padding: 6px;
|
||||||
|
top: calc(100% + 8px);
|
||||||
|
left: 50%;
|
||||||
|
transform: translateX(-50%);
|
||||||
|
box-shadow: 0 4px 10px rgb(0 0 0 / 25%);
|
||||||
|
background: #fff;
|
||||||
|
color: #898c94;
|
||||||
|
|
||||||
|
& > header {
|
||||||
|
font-size: 10px;
|
||||||
|
border-bottom: 1px solid #898c94;
|
||||||
|
}
|
||||||
|
|
||||||
|
.actions {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: flex-start;
|
||||||
|
gap: 12px;
|
||||||
|
flex-wrap: nowrap;
|
||||||
|
margin-bottom: 8px;
|
||||||
|
|
||||||
|
&:last-child {
|
||||||
|
margin-bottom: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bubbleMenuButton {
|
||||||
|
min-width: 40px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.dropDownEnter,
|
||||||
|
.dropDownExit {
|
||||||
|
height: 0;
|
||||||
|
color: transparent;
|
||||||
|
}
|
||||||
|
}
|
256
src/components/Editor/TextBubbleMenu/TextBubbleMenu.tsx
Normal file
256
src/components/Editor/TextBubbleMenu/TextBubbleMenu.tsx
Normal file
|
@ -0,0 +1,256 @@
|
||||||
|
import { Switch, Match, createSignal, Show } from 'solid-js'
|
||||||
|
import type { Editor } from '@tiptap/core'
|
||||||
|
import styles from './TextBubbleMenu.module.scss'
|
||||||
|
import { Icon } from '../../_shared/Icon'
|
||||||
|
import { clsx } from 'clsx'
|
||||||
|
import { createEditorTransaction } from 'solid-tiptap'
|
||||||
|
import { useLocalize } from '../../../context/localize'
|
||||||
|
import { InlineForm } from '../InlineForm'
|
||||||
|
import validateImage from '../../../utils/validateUrl'
|
||||||
|
|
||||||
|
type BubbleMenuProps = {
|
||||||
|
editor: Editor
|
||||||
|
ref: (el: HTMLDivElement) => void
|
||||||
|
}
|
||||||
|
|
||||||
|
export const TextBubbleMenu = (props: BubbleMenuProps) => {
|
||||||
|
const { t } = useLocalize()
|
||||||
|
const [textSizeBubbleOpen, setTextSizeBubbleOpen] = createSignal<boolean>(false)
|
||||||
|
const [listBubbleOpen, setListBubbleOpen] = createSignal<boolean>(false)
|
||||||
|
const [linkEditorOpen, setLinkEditorOpen] = createSignal<boolean>(false)
|
||||||
|
|
||||||
|
const isActive = (name: string, attributes?: unknown) =>
|
||||||
|
createEditorTransaction(
|
||||||
|
() => props.editor,
|
||||||
|
(editor) => {
|
||||||
|
return editor && editor.isActive(name, attributes)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
const isBold = isActive('bold')
|
||||||
|
const isItalic = isActive('italic')
|
||||||
|
const isH1 = isActive('heading', { level: 1 })
|
||||||
|
const isH2 = isActive('heading', { level: 2 })
|
||||||
|
const isH3 = isActive('heading', { level: 3 })
|
||||||
|
const isBlockQuote = isActive('blockquote')
|
||||||
|
const isOrderedList = isActive('isOrderedList')
|
||||||
|
const isBulletList = isActive('isBulletList')
|
||||||
|
const isLink = isActive('link')
|
||||||
|
|
||||||
|
const toggleLinkForm = () => {
|
||||||
|
setLinkEditorOpen(true)
|
||||||
|
}
|
||||||
|
|
||||||
|
const toggleTextSizePopup = () => {
|
||||||
|
if (listBubbleOpen()) {
|
||||||
|
setListBubbleOpen(false)
|
||||||
|
}
|
||||||
|
|
||||||
|
setTextSizeBubbleOpen((prev) => !prev)
|
||||||
|
}
|
||||||
|
const toggleListPopup = () => {
|
||||||
|
if (textSizeBubbleOpen()) {
|
||||||
|
setTextSizeBubbleOpen(false)
|
||||||
|
}
|
||||||
|
|
||||||
|
setListBubbleOpen((prev) => !prev)
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleLinkFormSubmit = (value: string) => {
|
||||||
|
props.editor.chain().focus().setLink({ href: value }).run()
|
||||||
|
}
|
||||||
|
|
||||||
|
const currentUrl = createEditorTransaction(
|
||||||
|
() => props.editor,
|
||||||
|
(editor) => {
|
||||||
|
return (editor && editor.getAttributes('link').href) || ''
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
const handleClearLinkForm = () => {
|
||||||
|
if (currentUrl()) {
|
||||||
|
props.editor.chain().focus().unsetLink().run()
|
||||||
|
}
|
||||||
|
setLinkEditorOpen(false)
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div ref={props.ref} class={styles.bubbleMenu}>
|
||||||
|
<Switch>
|
||||||
|
<Match when={linkEditorOpen()}>
|
||||||
|
<InlineForm
|
||||||
|
placeholder={t('Enter URL address')}
|
||||||
|
initialValue={currentUrl() ?? ''}
|
||||||
|
onClear={handleClearLinkForm}
|
||||||
|
validate={(value) => (validateImage(value) ? '' : t('Invalid url format'))}
|
||||||
|
onSubmit={handleLinkFormSubmit}
|
||||||
|
onClose={() => setLinkEditorOpen(false)}
|
||||||
|
errorMessage={t('Error')}
|
||||||
|
/>
|
||||||
|
</Match>
|
||||||
|
<Match when={!linkEditorOpen()}>
|
||||||
|
<>
|
||||||
|
<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}>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class={clsx(styles.bubbleMenuButton, {
|
||||||
|
[styles.bubbleMenuButtonActive]: isH1()
|
||||||
|
})}
|
||||||
|
onClick={() => {
|
||||||
|
props.editor.chain().focus().toggleHeading({ level: 1 }).run()
|
||||||
|
toggleTextSizePopup()
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Icon name="editor-h1" />
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class={clsx(styles.bubbleMenuButton, {
|
||||||
|
[styles.bubbleMenuButtonActive]: isH2()
|
||||||
|
})}
|
||||||
|
onClick={() => {
|
||||||
|
props.editor.chain().focus().toggleHeading({ level: 2 }).run()
|
||||||
|
toggleTextSizePopup()
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Icon name="editor-h2" />
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class={clsx(styles.bubbleMenuButton, {
|
||||||
|
[styles.bubbleMenuButtonActive]: isH3()
|
||||||
|
})}
|
||||||
|
onClick={() => {
|
||||||
|
props.editor.chain().focus().toggleHeading({ level: 3 }).run()
|
||||||
|
toggleTextSizePopup()
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Icon name="editor-h3" />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<header>{t('Quotes')}</header>
|
||||||
|
<div class={styles.actions}>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class={clsx(styles.bubbleMenuButton, {
|
||||||
|
[styles.bubbleMenuButtonActive]: isBlockQuote()
|
||||||
|
})}
|
||||||
|
onClick={() => {
|
||||||
|
props.editor.chain().focus().toggleBlockquote().run()
|
||||||
|
toggleTextSizePopup()
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Icon name="editor-blockquote" />
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class={clsx(styles.bubbleMenuButton, {
|
||||||
|
[styles.bubbleMenuButtonActive]: isBlockQuote()
|
||||||
|
})}
|
||||||
|
onClick={() => {
|
||||||
|
props.editor.chain().focus().toggleBlockquote().run()
|
||||||
|
toggleTextSizePopup()
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Icon name="editor-quote" />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Show>
|
||||||
|
</div>
|
||||||
|
<div class={styles.delimiter} />
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class={clsx(styles.bubbleMenuButton, {
|
||||||
|
[styles.bubbleMenuButtonActive]: isBold()
|
||||||
|
})}
|
||||||
|
onClick={() => props.editor.chain().focus().toggleBold().run()}
|
||||||
|
>
|
||||||
|
<Icon name="editor-bold" />
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class={clsx(styles.bubbleMenuButton, {
|
||||||
|
[styles.bubbleMenuButtonActive]: isItalic()
|
||||||
|
})}
|
||||||
|
onClick={() => props.editor.chain().focus().toggleItalic().run()}
|
||||||
|
>
|
||||||
|
<Icon name="editor-italic" />
|
||||||
|
</button>
|
||||||
|
<div class={styles.delimiter} />
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={toggleLinkForm}
|
||||||
|
class={clsx(styles.bubbleMenuButton, {
|
||||||
|
[styles.bubbleMenuButtonActive]: isLink()
|
||||||
|
})}
|
||||||
|
>
|
||||||
|
<Icon name="editor-link" />
|
||||||
|
</button>
|
||||||
|
<button type="button" class={styles.bubbleMenuButton}>
|
||||||
|
<Icon name="editor-footnote" />
|
||||||
|
</button>
|
||||||
|
<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}>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class={clsx(styles.bubbleMenuButton, {
|
||||||
|
[styles.bubbleMenuButtonActive]: isBulletList()
|
||||||
|
})}
|
||||||
|
onClick={() => {
|
||||||
|
props.editor.chain().focus().toggleBulletList().run()
|
||||||
|
toggleListPopup()
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Icon name="editor-ul" />
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class={clsx(styles.bubbleMenuButton, {
|
||||||
|
[styles.bubbleMenuButtonActive]: isOrderedList()
|
||||||
|
})}
|
||||||
|
onClick={() => {
|
||||||
|
props.editor.chain().focus().toggleOrderedList().run()
|
||||||
|
toggleListPopup()
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Icon name="editor-ol" />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Show>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
</Match>
|
||||||
|
</Switch>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
1
src/components/Editor/TextBubbleMenu/index.ts
Normal file
1
src/components/Editor/TextBubbleMenu/index.ts
Normal file
|
@ -0,0 +1 @@
|
||||||
|
export { TextBubbleMenu } from './TextBubbleMenu'
|
|
@ -50,6 +50,7 @@ export const HeaderAuth = (props: HeaderAuthProps) => {
|
||||||
const showNotifications = createMemo(() => isAuthenticated() && !isEditorPage())
|
const showNotifications = createMemo(() => isAuthenticated() && !isEditorPage())
|
||||||
const showSaveButton = createMemo(() => isAuthenticated() && isEditorPage())
|
const showSaveButton = createMemo(() => isAuthenticated() && isEditorPage())
|
||||||
const showCreatePostButton = createMemo(() => isAuthenticated() && !isEditorPage())
|
const showCreatePostButton = createMemo(() => isAuthenticated() && !isEditorPage())
|
||||||
|
const showAuthenticatedControls = createMemo(() => isAuthenticated() && isEditorPage())
|
||||||
|
|
||||||
const handleBurgerButtonClick = () => {
|
const handleBurgerButtonClick = () => {
|
||||||
toggleEditorPanel()
|
toggleEditorPanel()
|
||||||
|
@ -120,7 +121,7 @@ export const HeaderAuth = (props: HeaderAuthProps) => {
|
||||||
</Show>
|
</Show>
|
||||||
|
|
||||||
<Show
|
<Show
|
||||||
when={isAuthenticated() && page().route !== 'create'}
|
when={showAuthenticatedControls()}
|
||||||
fallback={
|
fallback={
|
||||||
<div class={clsx(styles.userControlItem, styles.userControlItemVerbose, 'loginbtn')}>
|
<div class={clsx(styles.userControlItem, styles.userControlItemVerbose, 'loginbtn')}>
|
||||||
<a href="?modal=auth&mode=login">
|
<a href="?modal=auth&mode=login">
|
||||||
|
|
|
@ -3,7 +3,6 @@ import { gql } from '@urql/core'
|
||||||
export default gql`
|
export default gql`
|
||||||
query LoadShoutQuery($slug: String!) {
|
query LoadShoutQuery($slug: String!) {
|
||||||
loadShout(slug: $slug) {
|
loadShout(slug: $slug) {
|
||||||
_id: slug
|
|
||||||
id
|
id
|
||||||
title
|
title
|
||||||
subtitle
|
subtitle
|
||||||
|
@ -15,12 +14,11 @@ export default gql`
|
||||||
# community
|
# community
|
||||||
mainTopic
|
mainTopic
|
||||||
topics {
|
topics {
|
||||||
# id
|
id
|
||||||
title
|
title
|
||||||
body
|
body
|
||||||
slug
|
slug
|
||||||
stat {
|
stat {
|
||||||
_id: shouts
|
|
||||||
shouts
|
shouts
|
||||||
authors
|
authors
|
||||||
followers
|
followers
|
||||||
|
@ -35,7 +33,6 @@ export default gql`
|
||||||
createdAt
|
createdAt
|
||||||
publishedAt
|
publishedAt
|
||||||
stat {
|
stat {
|
||||||
_id: viewed
|
|
||||||
viewed
|
viewed
|
||||||
reacted
|
reacted
|
||||||
rating
|
rating
|
||||||
|
|
Loading…
Reference in New Issue
Block a user