editor-refactored-2
This commit is contained in:
parent
90cd3988a1
commit
30de1ddb3e
|
@ -7,9 +7,9 @@ import { Popover } from '~/components/_shared/Popover'
|
||||||
import { useLocalize } from '~/context/localize'
|
import { useLocalize } from '~/context/localize'
|
||||||
import { InsertLinkForm } from '../EditorToolbar/InsertLinkForm'
|
import { InsertLinkForm } from '../EditorToolbar/InsertLinkForm'
|
||||||
|
|
||||||
import styles from './TextBubbleMenu.module.scss'
|
import styles from '../TextBubbleMenu/TextBubbleMenu.module.scss'
|
||||||
|
|
||||||
const MiniEditor = lazy(() => import('../../Editor/MiniEditor/MiniEditor'))
|
const MiniEditor = lazy(() => import('../MiniEditor/MiniEditor'))
|
||||||
|
|
||||||
type BubbleMenuProps = {
|
type BubbleMenuProps = {
|
||||||
editor: Editor
|
editor: Editor
|
|
@ -18,10 +18,10 @@ import { base, custom, extended } from '~/lib/editorExtensions'
|
||||||
import { handleImageUpload } from '~/lib/handleImageUpload'
|
import { handleImageUpload } from '~/lib/handleImageUpload'
|
||||||
import { renderUploadedImage } from '../Upload/renderUploadedImage'
|
import { renderUploadedImage } from '../Upload/renderUploadedImage'
|
||||||
import { BlockquoteBubbleMenu, FigureBubbleMenu, IncutBubbleMenu } from './BubbleMenu'
|
import { BlockquoteBubbleMenu, FigureBubbleMenu, IncutBubbleMenu } from './BubbleMenu'
|
||||||
|
import { TextBubbleMenu } from './BubbleMenu/TextBubbleMenu'
|
||||||
import { EditorFloatingMenu } from './EditorFloatingMenu'
|
import { EditorFloatingMenu } from './EditorFloatingMenu'
|
||||||
import { TextBubbleMenu } from './TextBubbleMenu'
|
|
||||||
|
|
||||||
import './Prosemirror.scss'
|
import './Editor.module.scss'
|
||||||
|
|
||||||
export type EditorComponentProps = {
|
export type EditorComponentProps = {
|
||||||
shoutId: number
|
shoutId: number
|
||||||
|
|
|
@ -12,7 +12,7 @@ import { UploadedFile } from '~/types/upload'
|
||||||
import { InsertLinkForm } from './InsertLinkForm'
|
import { InsertLinkForm } from './InsertLinkForm'
|
||||||
import { ToolbarControl as Control } from './ToolbarControl'
|
import { ToolbarControl as Control } from './ToolbarControl'
|
||||||
|
|
||||||
import styles from '../SimplifiedEditor.module.scss'
|
import styles from '../MiniEditor/MiniEditor.module.scss'
|
||||||
|
|
||||||
interface EditorToolbarProps {
|
interface EditorToolbarProps {
|
||||||
editor: Accessor<Editor | undefined>
|
editor: Accessor<Editor | undefined>
|
||||||
|
|
|
@ -1,6 +1,5 @@
|
||||||
import { Editor } from '@tiptap/core'
|
import { Editor } from '@tiptap/core'
|
||||||
import { createEffect, createSignal, onCleanup } from 'solid-js'
|
import { createEffect, createSignal, onCleanup } from 'solid-js'
|
||||||
|
|
||||||
import { useLocalize } from '~/context/localize'
|
import { useLocalize } from '~/context/localize'
|
||||||
import { validateUrl } from '~/utils/validate'
|
import { validateUrl } from '~/utils/validate'
|
||||||
import { InlineForm } from '../../_shared/InlineForm'
|
import { InlineForm } from '../../_shared/InlineForm'
|
||||||
|
|
|
@ -1,133 +0,0 @@
|
||||||
import clsx from 'clsx'
|
|
||||||
import { Show } from 'solid-js'
|
|
||||||
import { createEditorTransaction, useEditorHTML, useEditorIsEmpty } from 'solid-tiptap'
|
|
||||||
import { useEditorContext } from '~/context/editor'
|
|
||||||
import { useLocalize } from '~/context/localize'
|
|
||||||
import { useUI } from '~/context/ui'
|
|
||||||
import { Button } from '../../_shared/Button'
|
|
||||||
import { Icon } from '../../_shared/Icon'
|
|
||||||
import { Loading } from '../../_shared/Loading'
|
|
||||||
import { Popover } from '../../_shared/Popover'
|
|
||||||
import { SimplifiedEditorProps } from '../SimplifiedEditor'
|
|
||||||
|
|
||||||
import styles from '../SimplifiedEditor.module.scss'
|
|
||||||
|
|
||||||
export const ToolbarControls = (
|
|
||||||
props: SimplifiedEditorProps & { setShouldShowLinkBubbleMenu: (x: boolean) => void }
|
|
||||||
) => {
|
|
||||||
const { t } = useLocalize()
|
|
||||||
const { showModal } = useUI()
|
|
||||||
const { editor } = useEditorContext()
|
|
||||||
const isActive = (name: string) => createEditorTransaction(editor, (ed) => ed?.isActive(name))
|
|
||||||
const isBold = isActive('bold')
|
|
||||||
const isItalic = isActive('italic')
|
|
||||||
const isLink = isActive('link')
|
|
||||||
const isBlockquote = isActive('blockquote')
|
|
||||||
const isEmpty = useEditorIsEmpty(editor)
|
|
||||||
const html = useEditorHTML(editor)
|
|
||||||
|
|
||||||
const handleClear = () => {
|
|
||||||
props.onCancel?.()
|
|
||||||
editor()?.commands.clearContent(true)
|
|
||||||
}
|
|
||||||
|
|
||||||
const handleShowLinkBubble = () => {
|
|
||||||
editor()?.chain().focus().run()
|
|
||||||
props.setShouldShowLinkBubbleMenu(true)
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Show when={!props.hideToolbar}>
|
|
||||||
{/* Only show controls if 'hideToolbar' is false */}
|
|
||||||
<div class={clsx(styles.controls, { [styles.alwaysVisible]: props.controlsAlwaysVisible })}>
|
|
||||||
<div class={styles.actions}>
|
|
||||||
{/* Bold button */}
|
|
||||||
<Popover content={t('Bold')}>
|
|
||||||
{(triggerRef: (el: HTMLElement) => void) => (
|
|
||||||
<button
|
|
||||||
ref={triggerRef}
|
|
||||||
type="button"
|
|
||||||
class={clsx(styles.actionButton, { [styles.active]: isBold() })}
|
|
||||||
onClick={() => editor()?.chain().focus().toggleBold().run()}
|
|
||||||
>
|
|
||||||
<Icon name="editor-bold" />
|
|
||||||
</button>
|
|
||||||
)}
|
|
||||||
</Popover>
|
|
||||||
{/* Italic button */}
|
|
||||||
<Popover content={t('Italic')}>
|
|
||||||
{(triggerRef) => (
|
|
||||||
<button
|
|
||||||
ref={triggerRef}
|
|
||||||
type="button"
|
|
||||||
class={clsx(styles.actionButton, { [styles.active]: isItalic() })}
|
|
||||||
onClick={() => editor()?.chain().focus().toggleItalic().run()}
|
|
||||||
>
|
|
||||||
<Icon name="editor-italic" />
|
|
||||||
</button>
|
|
||||||
)}
|
|
||||||
</Popover>
|
|
||||||
{/* Link button */}
|
|
||||||
<Popover content={t('Add url')}>
|
|
||||||
{(triggerRef) => (
|
|
||||||
<button
|
|
||||||
ref={triggerRef}
|
|
||||||
type="button"
|
|
||||||
onClick={handleShowLinkBubble}
|
|
||||||
class={clsx(styles.actionButton, { [styles.active]: isLink() })}
|
|
||||||
>
|
|
||||||
<Icon name="editor-link" />
|
|
||||||
</button>
|
|
||||||
)}
|
|
||||||
</Popover>
|
|
||||||
{/* Blockquote button (optional) */}
|
|
||||||
<Show when={props.quoteEnabled}>
|
|
||||||
<Popover content={t('Add blockquote')}>
|
|
||||||
{(triggerRef) => (
|
|
||||||
<button
|
|
||||||
ref={triggerRef}
|
|
||||||
type="button"
|
|
||||||
onClick={() => editor()?.chain().focus().toggleBlockquote().run()}
|
|
||||||
class={clsx(styles.actionButton, { [styles.active]: isBlockquote() })}
|
|
||||||
>
|
|
||||||
<Icon name="editor-quote" />
|
|
||||||
</button>
|
|
||||||
)}
|
|
||||||
</Popover>
|
|
||||||
</Show>
|
|
||||||
{/* Image button (optional) */}
|
|
||||||
<Show when={props.imageEnabled}>
|
|
||||||
<Popover content={t('Add image')}>
|
|
||||||
{(triggerRef) => (
|
|
||||||
<button
|
|
||||||
ref={triggerRef}
|
|
||||||
type="button"
|
|
||||||
onClick={() => showModal('simplifiedEditorUploadImage')}
|
|
||||||
class={clsx(styles.actionButton, { [styles.active]: isBlockquote() })}
|
|
||||||
>
|
|
||||||
<Icon name="editor-image-dd-full" />
|
|
||||||
</button>
|
|
||||||
)}
|
|
||||||
</Popover>
|
|
||||||
</Show>
|
|
||||||
</div>
|
|
||||||
{/* Cancel and submit buttons */}
|
|
||||||
<Show when={!props.onChange}>
|
|
||||||
<div class={styles.buttons}>
|
|
||||||
<Show when={props.isCancelButtonVisible}>
|
|
||||||
<Button value={t('Cancel')} variant="secondary" onClick={handleClear} />
|
|
||||||
</Show>
|
|
||||||
<Show when={!props.isPosting} fallback={<Loading />}>
|
|
||||||
<Button
|
|
||||||
value={props.submitButtonText ?? t('Send')}
|
|
||||||
variant="primary"
|
|
||||||
disabled={isEmpty()}
|
|
||||||
onClick={() => props.onSubmit?.(html() || '')}
|
|
||||||
/>
|
|
||||||
</Show>
|
|
||||||
</div>
|
|
||||||
</Show>
|
|
||||||
</div>
|
|
||||||
</Show>
|
|
||||||
)
|
|
||||||
}
|
|
|
@ -3,7 +3,7 @@ import clsx from 'clsx'
|
||||||
import { JSX } from 'solid-js'
|
import { JSX } from 'solid-js'
|
||||||
import { Popover } from '~/components/_shared/Popover'
|
import { Popover } from '~/components/_shared/Popover'
|
||||||
|
|
||||||
import styles from '../SimplifiedEditor.module.scss'
|
import styles from '../MiniEditor/MiniEditor.module.scss'
|
||||||
|
|
||||||
interface ControlProps {
|
interface ControlProps {
|
||||||
editor: Editor
|
editor: Editor
|
||||||
|
|
|
@ -1,4 +0,0 @@
|
||||||
.LinkBubbleMenu {
|
|
||||||
background: var(--editor-bubble-menu-background);
|
|
||||||
box-shadow: 0 4px 10px rgba(#000, 0.25);
|
|
||||||
}
|
|
|
@ -1,19 +0,0 @@
|
||||||
import type { Editor } from '@tiptap/core'
|
|
||||||
|
|
||||||
import { InsertLinkForm } from '../InsertLinkForm'
|
|
||||||
|
|
||||||
import styles from './LinkBubbleMenu.module.scss'
|
|
||||||
|
|
||||||
type Props = {
|
|
||||||
editor: Editor
|
|
||||||
ref: (el: HTMLDivElement) => void
|
|
||||||
onClose: () => void
|
|
||||||
}
|
|
||||||
|
|
||||||
export const LinkBubbleMenuModule = (props: Props) => {
|
|
||||||
return (
|
|
||||||
<div ref={props.ref} class={styles.LinkBubbleMenu}>
|
|
||||||
<InsertLinkForm editor={props.editor} onClose={props.onClose} />
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
|
@ -1 +0,0 @@
|
||||||
export { LinkBubbleMenuModule } from './LinkBubbleMenu.module'
|
|
|
@ -5,7 +5,7 @@ import { createTiptapEditor, useEditorHTML, useEditorIsFocused } from 'solid-tip
|
||||||
import { minimal } from '~/lib/editorExtensions'
|
import { minimal } from '~/lib/editorExtensions'
|
||||||
import { EditorToolbar } from '../EditorToolbar/EditorToolbar'
|
import { EditorToolbar } from '../EditorToolbar/EditorToolbar'
|
||||||
|
|
||||||
import styles from '../SimplifiedEditor.module.scss'
|
import styles from '../MiniEditor/MiniEditor.module.scss'
|
||||||
|
|
||||||
interface MicroEditorProps {
|
interface MicroEditorProps {
|
||||||
content?: string
|
content?: string
|
||||||
|
@ -36,7 +36,7 @@ export const MicroEditor = (props: MicroEditorProps): JSX.Element => {
|
||||||
createEffect(on(html, (c?: string) => c && props.onChange?.(c)))
|
createEffect(on(html, (c?: string) => c && props.onChange?.(c)))
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div class={clsx(styles.SimplifiedEditor, styles.bordered, { [styles.isFocused]: isFocused() })}>
|
<div class={clsx(styles.MiniEditor, styles.bordered, { [styles.isFocused]: isFocused() })}>
|
||||||
<div>
|
<div>
|
||||||
<EditorToolbar editor={editor} mode={'micro'} />
|
<EditorToolbar editor={editor} mode={'micro'} />
|
||||||
<div id="micro-editor" ref={setEditorElement} style={styles.minimal} />
|
<div id="micro-editor" ref={setEditorElement} style={styles.minimal} />
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
.SimplifiedEditor {
|
.MiniEditor {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
|
@ -3,12 +3,12 @@ import Placeholder from '@tiptap/extension-placeholder'
|
||||||
import clsx from 'clsx'
|
import clsx from 'clsx'
|
||||||
import { type JSX, Show, createEffect, createSignal, on } from 'solid-js'
|
import { type JSX, Show, createEffect, createSignal, on } from 'solid-js'
|
||||||
import { createEditorTransaction, createTiptapEditor, useEditorHTML } from 'solid-tiptap'
|
import { createEditorTransaction, createTiptapEditor, useEditorHTML } from 'solid-tiptap'
|
||||||
|
import { Button } from '~/components/_shared/Button'
|
||||||
|
import { useLocalize } from '~/context/localize'
|
||||||
import { base } from '~/lib/editorExtensions'
|
import { base } from '~/lib/editorExtensions'
|
||||||
import { EditorToolbar } from '../EditorToolbar/EditorToolbar'
|
import { EditorToolbar } from '../EditorToolbar/EditorToolbar'
|
||||||
|
|
||||||
import { Button } from '~/components/_shared/Button'
|
import styles from './MiniEditor.module.scss'
|
||||||
import { useLocalize } from '~/context/localize'
|
|
||||||
import styles from '../SimplifiedEditor.module.scss'
|
|
||||||
|
|
||||||
interface MiniEditorProps {
|
interface MiniEditorProps {
|
||||||
content?: string
|
content?: string
|
||||||
|
@ -58,7 +58,7 @@ export default function MiniEditor(props: MiniEditorProps): JSX.Element {
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div class={clsx(styles.SimplifiedEditor, styles.bordered, { [styles.isFocused]: isFocused() })}>
|
<div class={clsx(styles.MiniEditor, styles.bordered, { [styles.isFocused]: isFocused() })}>
|
||||||
<div>
|
<div>
|
||||||
<div id="mini-editor" ref={setEditorElement} />
|
<div id="mini-editor" ref={setEditorElement} />
|
||||||
|
|
||||||
|
|
|
@ -1,244 +0,0 @@
|
||||||
import { Editor, FocusPosition } from '@tiptap/core'
|
|
||||||
import { BubbleMenu } from '@tiptap/extension-bubble-menu'
|
|
||||||
import { CharacterCount } from '@tiptap/extension-character-count'
|
|
||||||
import { Placeholder } from '@tiptap/extension-placeholder'
|
|
||||||
import { clsx } from 'clsx'
|
|
||||||
import { Show, createEffect, createSignal, on, onCleanup, onMount } from 'solid-js'
|
|
||||||
import { Portal } from 'solid-js/web'
|
|
||||||
import { createEditorTransaction, useEditorHTML, useEditorIsEmpty, useEditorIsFocused } from 'solid-tiptap'
|
|
||||||
import { useEditorContext } from '~/context/editor'
|
|
||||||
import { useUI } from '~/context/ui'
|
|
||||||
import { base, custom } from '~/lib/editorExtensions'
|
|
||||||
import { useEscKeyDownHandler } from '~/lib/useEscKeyDownHandler'
|
|
||||||
import { UploadedFile } from '~/types/upload'
|
|
||||||
import { UploadModalContent } from '../Upload/UploadModalContent'
|
|
||||||
import { renderUploadedImage } from '../Upload/renderUploadedImage'
|
|
||||||
import { Modal } from '../_shared/Modal/Modal'
|
|
||||||
import { ShowOnlyOnClient } from '../_shared/ShowOnlyOnClient'
|
|
||||||
import { ToolbarControls } from './EditorToolbar/SimplifiedToolbar'
|
|
||||||
import { LinkBubbleMenuModule } from './LinkBubbleMenu'
|
|
||||||
import { TextBubbleMenu } from './TextBubbleMenu'
|
|
||||||
|
|
||||||
import styles from './Editor.module.scss'
|
|
||||||
|
|
||||||
export type SimplifiedEditorProps = {
|
|
||||||
placeholder: string
|
|
||||||
initialContent?: string
|
|
||||||
label?: string
|
|
||||||
onSubmit?: (text: string) => void
|
|
||||||
onCancel?: () => void
|
|
||||||
onChange?: (text: string) => void
|
|
||||||
variant?: 'minimal' | 'bordered'
|
|
||||||
maxLength?: number
|
|
||||||
noLimits?: boolean
|
|
||||||
maxHeight?: number
|
|
||||||
submitButtonText?: string
|
|
||||||
quoteEnabled?: boolean
|
|
||||||
imageEnabled?: boolean
|
|
||||||
setClear?: boolean
|
|
||||||
resetToInitial?: boolean
|
|
||||||
smallHeight?: boolean
|
|
||||||
submitByCtrlEnter?: boolean
|
|
||||||
hideToolbar?: boolean
|
|
||||||
controlsAlwaysVisible?: boolean
|
|
||||||
autoFocus?: boolean
|
|
||||||
isCancelButtonVisible?: boolean
|
|
||||||
isPosting?: boolean
|
|
||||||
}
|
|
||||||
|
|
||||||
const DEFAULT_MAX_LENGTH = 400
|
|
||||||
|
|
||||||
const SimplifiedEditor = (props: SimplifiedEditorProps) => {
|
|
||||||
// local signals
|
|
||||||
const [counter, setCounter] = createSignal<number>(0)
|
|
||||||
const [shouldShowLinkBubbleMenu, setShouldShowLinkBubbleMenu] = createSignal(false)
|
|
||||||
const [shouldShowTextBubbleMenu, setShouldShowTextBubbleMenu] = createSignal(false)
|
|
||||||
const [editorElement, setEditorElement] = createSignal<HTMLDivElement | undefined>()
|
|
||||||
const [textBubbleMenuRef, setTextBubbleMenuRef] = createSignal<HTMLDivElement | undefined>()
|
|
||||||
const [linkBubbleMenuRef, setLinkBubbleMenuRef] = createSignal<HTMLDivElement | undefined>()
|
|
||||||
|
|
||||||
// contexts
|
|
||||||
const { hideModal } = useUI()
|
|
||||||
const { editor, createEditor } = useEditorContext()
|
|
||||||
|
|
||||||
const initEditor = (element?: HTMLElement) => {
|
|
||||||
if (element instanceof HTMLElement && editor()?.options.element !== element) {
|
|
||||||
const opts = {
|
|
||||||
element,
|
|
||||||
extensions: [
|
|
||||||
// common extensions
|
|
||||||
...base,
|
|
||||||
...custom,
|
|
||||||
|
|
||||||
// setup from component props
|
|
||||||
Placeholder.configure({ emptyNodeClass: styles.emptyNode, placeholder: props.placeholder }),
|
|
||||||
CharacterCount.configure({ limit: props.noLimits ? undefined : props.maxLength }),
|
|
||||||
|
|
||||||
// bubble menu 1
|
|
||||||
BubbleMenu.configure({
|
|
||||||
pluginKey: 'bubble-menu',
|
|
||||||
element: textBubbleMenuRef(),
|
|
||||||
shouldShow: ({ view }) => view.hasFocus() && shouldShowTextBubbleMenu()
|
|
||||||
}),
|
|
||||||
|
|
||||||
// bubble menu 2
|
|
||||||
BubbleMenu.configure({
|
|
||||||
pluginKey: 'bubble-link-input',
|
|
||||||
element: linkBubbleMenuRef(),
|
|
||||||
shouldShow: ({ state }) => !state.selection.empty && shouldShowLinkBubbleMenu(),
|
|
||||||
tippyOptions: { placement: 'bottom' }
|
|
||||||
})
|
|
||||||
],
|
|
||||||
editorProps: {
|
|
||||||
attributes: { class: styles.simplifiedEditorField }
|
|
||||||
},
|
|
||||||
content: props.initialContent || '',
|
|
||||||
onCreate: () => console.info('[SimplifiedEditor] created'),
|
|
||||||
onContentError: console.error,
|
|
||||||
autofocus: (props.autoFocus && 'end') as FocusPosition | undefined,
|
|
||||||
editable: true,
|
|
||||||
enableCoreExtensions: true,
|
|
||||||
enableContentCheck: true,
|
|
||||||
injectNonce: undefined, // TODO: can be useful copyright/copyleft mark
|
|
||||||
parseOptions: undefined // see: https://prosemirror.net/docs/ref/#model.ParseOptions
|
|
||||||
}
|
|
||||||
|
|
||||||
createEditor(opts)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// editor observers
|
|
||||||
const isEmpty = useEditorIsEmpty(editor)
|
|
||||||
const isFocused = useEditorIsFocused(editor)
|
|
||||||
const selection = createEditorTransaction(editor, (ed) => ed?.state.selection)
|
|
||||||
const html = useEditorHTML(editor)
|
|
||||||
|
|
||||||
/// EFFECTS ///
|
|
||||||
|
|
||||||
// Mount event listeners for handling key events and clean up on component unmount
|
|
||||||
onMount(() => {
|
|
||||||
window.addEventListener('keydown', handleKeyDown)
|
|
||||||
onCleanup(() => {
|
|
||||||
window.removeEventListener('keydown', handleKeyDown)
|
|
||||||
editor()?.destroy()
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
// watch changes
|
|
||||||
createEffect(on(editorElement, initEditor, { defer: true })) // element -> editorOptions -> set editor
|
|
||||||
createEffect(
|
|
||||||
on(selection, (s?: Editor['state']['selection']) => s && setShouldShowTextBubbleMenu(!s?.empty))
|
|
||||||
)
|
|
||||||
createEffect(
|
|
||||||
on(
|
|
||||||
() => props.setClear,
|
|
||||||
(x?: boolean) => x && editor()?.commands.clearContent(true)
|
|
||||||
)
|
|
||||||
)
|
|
||||||
createEffect(
|
|
||||||
on(
|
|
||||||
() => props.resetToInitial,
|
|
||||||
(x?: boolean) => x && editor()?.commands.setContent(props.initialContent || '')
|
|
||||||
)
|
|
||||||
)
|
|
||||||
createEffect(on([html, () => props.onChange], ([c, handler]) => c && handler && handler(c))) // onChange
|
|
||||||
createEffect(on(html, (c?: string) => c && setCounter(editor()?.storage.characterCount.characters()))) //counter
|
|
||||||
|
|
||||||
/// HANDLERS ///
|
|
||||||
|
|
||||||
const handleImageRender = (image?: UploadedFile) => {
|
|
||||||
image && renderUploadedImage(editor() as Editor, image)
|
|
||||||
hideModal()
|
|
||||||
}
|
|
||||||
|
|
||||||
const handleKeyDown = (event: KeyboardEvent) => {
|
|
||||||
if (
|
|
||||||
isFocused() &&
|
|
||||||
!isEmpty() &&
|
|
||||||
event.code === 'Enter' &&
|
|
||||||
props.submitByCtrlEnter &&
|
|
||||||
(event.metaKey || event.ctrlKey)
|
|
||||||
) {
|
|
||||||
event.preventDefault()
|
|
||||||
props.onSubmit?.(html() || '')
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const handleHideLinkBubble = () => {
|
|
||||||
editor()?.commands.focus()
|
|
||||||
setShouldShowLinkBubbleMenu(false)
|
|
||||||
}
|
|
||||||
|
|
||||||
useEscKeyDownHandler(handleHideLinkBubble)
|
|
||||||
|
|
||||||
return (
|
|
||||||
<ShowOnlyOnClient>
|
|
||||||
<div
|
|
||||||
class={clsx(styles.SimplifiedEditor, {
|
|
||||||
[styles.smallHeight]: props.smallHeight,
|
|
||||||
[styles.minimal]: props.variant === 'minimal',
|
|
||||||
[styles.bordered]: props.variant === 'bordered',
|
|
||||||
[styles.isFocused]: isFocused() || !isEmpty(),
|
|
||||||
[styles.labelVisible]: props.label && counter() > 0
|
|
||||||
})}
|
|
||||||
>
|
|
||||||
{/* Display label when applicable */}
|
|
||||||
<Show when={props.label && counter() > 0}>
|
|
||||||
<div class={styles.label}>{props.label}</div>
|
|
||||||
</Show>
|
|
||||||
|
|
||||||
<Show
|
|
||||||
when={props.hideToolbar}
|
|
||||||
fallback={
|
|
||||||
<ToolbarControls {...props} setShouldShowLinkBubbleMenu={setShouldShowLinkBubbleMenu} />
|
|
||||||
}
|
|
||||||
>
|
|
||||||
<TextBubbleMenu
|
|
||||||
editor={editor() as Editor}
|
|
||||||
ref={setTextBubbleMenuRef}
|
|
||||||
shouldShow={shouldShowTextBubbleMenu()}
|
|
||||||
isCommonMarkup={true}
|
|
||||||
/>
|
|
||||||
|
|
||||||
{/* Link bubble menu */}
|
|
||||||
<Show when={shouldShowLinkBubbleMenu()}>
|
|
||||||
<LinkBubbleMenuModule
|
|
||||||
editor={editor() as Editor}
|
|
||||||
ref={setLinkBubbleMenuRef}
|
|
||||||
onClose={handleHideLinkBubble}
|
|
||||||
/>
|
|
||||||
</Show>
|
|
||||||
</Show>
|
|
||||||
|
|
||||||
{/* editor element */}
|
|
||||||
<div
|
|
||||||
style={
|
|
||||||
props.maxHeight
|
|
||||||
? {
|
|
||||||
overflow: 'auto',
|
|
||||||
'max-height': `${props.maxHeight}px`
|
|
||||||
}
|
|
||||||
: undefined
|
|
||||||
}
|
|
||||||
ref={setEditorElement}
|
|
||||||
/>
|
|
||||||
|
|
||||||
{/* Display character limit if maxLength is provided */}
|
|
||||||
<Show when={props.maxLength && editor()}>
|
|
||||||
<div class={styles.limit}>{(props.maxLength || DEFAULT_MAX_LENGTH) - counter()}</div>
|
|
||||||
</Show>
|
|
||||||
|
|
||||||
{/* Image upload modal (show/hide) */}
|
|
||||||
<Show when={props.imageEnabled}>
|
|
||||||
<Portal>
|
|
||||||
<Modal variant="narrow" name="simplifiedEditorUploadImage">
|
|
||||||
<UploadModalContent onClose={handleImageRender} />
|
|
||||||
</Modal>
|
|
||||||
</Portal>
|
|
||||||
</Show>
|
|
||||||
</div>
|
|
||||||
</ShowOnlyOnClient>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
export default SimplifiedEditor // Export component for lazy loading
|
|
|
@ -1 +0,0 @@
|
||||||
export { TextBubbleMenu } from './TextBubbleMenu'
|
|
|
@ -1,4 +0,0 @@
|
||||||
export { EditorComponent as Editor } from './Editor'
|
|
||||||
export { Panel } from './Panel'
|
|
||||||
export { TopicSelect } from '../TopicSelect'
|
|
||||||
export { UploadModalContent } from '../Upload/UploadModalContent'
|
|
|
@ -3,6 +3,7 @@ import deepEqual from 'fast-deep-equal'
|
||||||
import { Show, createEffect, createSignal, on, onCleanup, onMount } from 'solid-js'
|
import { Show, createEffect, createSignal, on, onCleanup, onMount } from 'solid-js'
|
||||||
import { createStore } from 'solid-js/store'
|
import { createStore } from 'solid-js/store'
|
||||||
import { debounce } from 'throttle-debounce'
|
import { debounce } from 'throttle-debounce'
|
||||||
|
import { Panel } from '~/components/Editor/Panel/Panel'
|
||||||
import { Icon } from '~/components/_shared/Icon'
|
import { Icon } from '~/components/_shared/Icon'
|
||||||
import { InviteMembers } from '~/components/_shared/InviteMembers'
|
import { InviteMembers } from '~/components/_shared/InviteMembers'
|
||||||
import { ShoutForm, useEditorContext } from '~/context/editor'
|
import { ShoutForm, useEditorContext } from '~/context/editor'
|
||||||
|
@ -12,7 +13,6 @@ import getMyShoutQuery from '~/graphql/query/core/article-my'
|
||||||
import type { Shout, Topic } from '~/graphql/schema/core.gen'
|
import type { Shout, Topic } from '~/graphql/schema/core.gen'
|
||||||
import { isDesktop } from '~/lib/mediaQuery'
|
import { isDesktop } from '~/lib/mediaQuery'
|
||||||
import { clone } from '~/utils/clone'
|
import { clone } from '~/utils/clone'
|
||||||
import { Panel } from '../../Editor'
|
|
||||||
import { AutoSaveNotice } from '../../Editor/AutoSaveNotice'
|
import { AutoSaveNotice } from '../../Editor/AutoSaveNotice'
|
||||||
import { Modal } from '../../_shared/Modal'
|
import { Modal } from '../../_shared/Modal'
|
||||||
import { TableOfContents } from '../../_shared/TableOfContents'
|
import { TableOfContents } from '../../_shared/TableOfContents'
|
||||||
|
|
|
@ -3,6 +3,8 @@ import deepEqual from 'fast-deep-equal'
|
||||||
import { Show, createEffect, createSignal, lazy, on, onCleanup, onMount } from 'solid-js'
|
import { Show, createEffect, createSignal, lazy, on, onCleanup, onMount } from 'solid-js'
|
||||||
import { createStore } from 'solid-js/store'
|
import { createStore } from 'solid-js/store'
|
||||||
import { debounce } from 'throttle-debounce'
|
import { debounce } from 'throttle-debounce'
|
||||||
|
import { EditorComponent } from '~/components/Editor/Editor'
|
||||||
|
import { Panel } from '~/components/Editor/Panel/Panel'
|
||||||
import { DropArea } from '~/components/_shared/DropArea'
|
import { DropArea } from '~/components/_shared/DropArea'
|
||||||
import { Icon } from '~/components/_shared/Icon'
|
import { Icon } from '~/components/_shared/Icon'
|
||||||
import { InviteMembers } from '~/components/_shared/InviteMembers'
|
import { InviteMembers } from '~/components/_shared/InviteMembers'
|
||||||
|
@ -20,7 +22,6 @@ import { isDesktop } from '~/lib/mediaQuery'
|
||||||
import { LayoutType } from '~/types/common'
|
import { LayoutType } from '~/types/common'
|
||||||
import { MediaItem } from '~/types/mediaitem'
|
import { MediaItem } from '~/types/mediaitem'
|
||||||
import { clone } from '~/utils/clone'
|
import { clone } from '~/utils/clone'
|
||||||
import { Editor as EditorComponent, Panel } from '../../Editor'
|
|
||||||
import { AutoSaveNotice } from '../../Editor/AutoSaveNotice'
|
import { AutoSaveNotice } from '../../Editor/AutoSaveNotice'
|
||||||
import { AudioUploader } from '../../Upload/AudioUploader'
|
import { AudioUploader } from '../../Upload/AudioUploader'
|
||||||
import { VideoUploader } from '../../Upload/VideoUploader'
|
import { VideoUploader } from '../../Upload/VideoUploader'
|
||||||
|
|
|
@ -12,9 +12,10 @@ import { useTopics } from '~/context/topics'
|
||||||
import { useSnackbar, useUI } from '~/context/ui'
|
import { useSnackbar, useUI } from '~/context/ui'
|
||||||
import { Topic } from '~/graphql/schema/core.gen'
|
import { Topic } from '~/graphql/schema/core.gen'
|
||||||
import { UploadedFile } from '~/types/upload'
|
import { UploadedFile } from '~/types/upload'
|
||||||
import { TopicSelect, UploadModalContent } from '../../Editor'
|
|
||||||
import { Modal } from '../../_shared/Modal'
|
import { Modal } from '../../_shared/Modal'
|
||||||
|
|
||||||
|
import { TopicSelect } from '~/components/TopicSelect/TopicSelect'
|
||||||
|
import { UploadModalContent } from '~/components/Upload/UploadModalContent/UploadModalContent'
|
||||||
import stylesBeside from '../../Feed/Beside.module.scss'
|
import stylesBeside from '../../Feed/Beside.module.scss'
|
||||||
import styles from './PublishSettings.module.scss'
|
import styles from './PublishSettings.module.scss'
|
||||||
|
|
||||||
|
|
Loading…
Reference in New Issue
Block a user