editor-fixed
This commit is contained in:
parent
ca629e8c26
commit
e875212ae7
|
@ -1,3 +1,4 @@
|
||||||
|
import { Editor } from '@tiptap/core'
|
||||||
import { Blockquote } from '@tiptap/extension-blockquote'
|
import { Blockquote } from '@tiptap/extension-blockquote'
|
||||||
import { Bold } from '@tiptap/extension-bold'
|
import { Bold } from '@tiptap/extension-bold'
|
||||||
import { BubbleMenu } from '@tiptap/extension-bubble-menu'
|
import { BubbleMenu } from '@tiptap/extension-bubble-menu'
|
||||||
|
@ -10,7 +11,7 @@ import { Paragraph } from '@tiptap/extension-paragraph'
|
||||||
import { Placeholder } from '@tiptap/extension-placeholder'
|
import { Placeholder } from '@tiptap/extension-placeholder'
|
||||||
import { Text } from '@tiptap/extension-text'
|
import { Text } from '@tiptap/extension-text'
|
||||||
import { clsx } from 'clsx'
|
import { clsx } from 'clsx'
|
||||||
import { Show, createEffect, createMemo, createSignal, on, onCleanup, onMount } from 'solid-js'
|
import { Show, createEffect, createReaction, createSignal, on, onCleanup, onMount } from 'solid-js'
|
||||||
import { Portal } from 'solid-js/web'
|
import { Portal } from 'solid-js/web'
|
||||||
import {
|
import {
|
||||||
createEditorTransaction,
|
createEditorTransaction,
|
||||||
|
@ -19,9 +20,9 @@ import {
|
||||||
useEditorIsEmpty,
|
useEditorIsEmpty,
|
||||||
useEditorIsFocused
|
useEditorIsFocused
|
||||||
} from 'solid-tiptap'
|
} from 'solid-tiptap'
|
||||||
|
|
||||||
import { useEditorContext } from '~/context/editor'
|
import { useEditorContext } from '~/context/editor'
|
||||||
import { useLocalize } from '~/context/localize'
|
import { useLocalize } from '~/context/localize'
|
||||||
|
import { useUI } from '~/context/ui'
|
||||||
import { UploadedFile } from '~/types/upload'
|
import { UploadedFile } from '~/types/upload'
|
||||||
import { Button } from '../_shared/Button'
|
import { Button } from '../_shared/Button'
|
||||||
import { Icon } from '../_shared/Icon'
|
import { Icon } from '../_shared/Icon'
|
||||||
|
@ -30,15 +31,12 @@ import { Modal } from '../_shared/Modal'
|
||||||
import { Popover } from '../_shared/Popover'
|
import { Popover } from '../_shared/Popover'
|
||||||
import { ShowOnlyOnClient } from '../_shared/ShowOnlyOnClient'
|
import { ShowOnlyOnClient } from '../_shared/ShowOnlyOnClient'
|
||||||
import { LinkBubbleMenuModule } from './LinkBubbleMenu'
|
import { LinkBubbleMenuModule } from './LinkBubbleMenu'
|
||||||
|
import styles from './SimplifiedEditor.module.scss'
|
||||||
import { TextBubbleMenu } from './TextBubbleMenu'
|
import { TextBubbleMenu } from './TextBubbleMenu'
|
||||||
import { UploadModalContent } from './UploadModalContent'
|
import { UploadModalContent } from './UploadModalContent'
|
||||||
import { Figcaption } from './extensions/Figcaption'
|
import { Figcaption } from './extensions/Figcaption'
|
||||||
import { Figure } from './extensions/Figure'
|
import { Figure } from './extensions/Figure'
|
||||||
|
|
||||||
import { Editor } from '@tiptap/core'
|
|
||||||
import { useUI } from '~/context/ui'
|
|
||||||
import styles from './SimplifiedEditor.module.scss'
|
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
placeholder: string
|
placeholder: string
|
||||||
initialContent?: string
|
initialContent?: string
|
||||||
|
@ -71,103 +69,27 @@ const SimplifiedEditor = (props: Props) => {
|
||||||
const { showModal, hideModal } = useUI()
|
const { showModal, hideModal } = useUI()
|
||||||
const [counter, setCounter] = createSignal<number>(0)
|
const [counter, setCounter] = createSignal<number>(0)
|
||||||
const [shouldShowLinkBubbleMenu, setShouldShowLinkBubbleMenu] = createSignal(false)
|
const [shouldShowLinkBubbleMenu, setShouldShowLinkBubbleMenu] = createSignal(false)
|
||||||
const isCancelButtonVisible = createMemo(() => props.isCancelButtonVisible !== false)
|
|
||||||
const [editorElement, setEditorElement] = createSignal<HTMLDivElement>()
|
|
||||||
const { editor, setEditor } = useEditorContext()
|
const { editor, setEditor } = useEditorContext()
|
||||||
|
|
||||||
const maxLength = props.maxLength ?? DEFAULT_MAX_LENGTH
|
const maxLength = props.maxLength ?? DEFAULT_MAX_LENGTH
|
||||||
|
let editorEl: HTMLDivElement | undefined
|
||||||
let wrapperEditorElRef: HTMLElement | undefined
|
let wrapperEditorElRef: HTMLElement | undefined
|
||||||
let textBubbleMenuRef: HTMLDivElement | undefined
|
let textBubbleMenuRef: HTMLDivElement | undefined
|
||||||
let linkBubbleMenuRef: HTMLDivElement | undefined
|
let linkBubbleMenuRef: HTMLDivElement | undefined
|
||||||
|
|
||||||
|
// Extend the Figure extension to include Figcaption
|
||||||
const ImageFigure = Figure.extend({
|
const ImageFigure = Figure.extend({
|
||||||
name: 'capturedImage',
|
name: 'capturedImage',
|
||||||
content: 'figcaption image'
|
content: 'figcaption image'
|
||||||
})
|
})
|
||||||
|
|
||||||
createEffect(
|
|
||||||
on(
|
|
||||||
() => editorElement(),
|
|
||||||
(ee: HTMLDivElement | undefined) => {
|
|
||||||
if (ee && textBubbleMenuRef && linkBubbleMenuRef) {
|
|
||||||
const freshEditor = createTiptapEditor<HTMLElement>(() => ({
|
|
||||||
element: ee,
|
|
||||||
editorProps: {
|
|
||||||
attributes: {
|
|
||||||
class: styles.simplifiedEditorField
|
|
||||||
}
|
|
||||||
},
|
|
||||||
extensions: [
|
|
||||||
Document,
|
|
||||||
Text,
|
|
||||||
Paragraph,
|
|
||||||
Bold,
|
|
||||||
Italic,
|
|
||||||
Link.extend({
|
|
||||||
inclusive: false
|
|
||||||
}).configure({
|
|
||||||
autolink: true,
|
|
||||||
openOnClick: false
|
|
||||||
}),
|
|
||||||
CharacterCount.configure({
|
|
||||||
limit: props.noLimits ? null : maxLength
|
|
||||||
}),
|
|
||||||
Blockquote.configure({
|
|
||||||
HTMLAttributes: {
|
|
||||||
class: styles.blockQuote
|
|
||||||
}
|
|
||||||
}),
|
|
||||||
BubbleMenu.configure({
|
|
||||||
pluginKey: 'textBubbleMenu',
|
|
||||||
element: textBubbleMenuRef,
|
|
||||||
shouldShow: ({ view, state }) => {
|
|
||||||
if (!props.onlyBubbleControls) return false
|
|
||||||
const { selection } = state
|
|
||||||
const { empty } = selection
|
|
||||||
return view.hasFocus() && !empty
|
|
||||||
}
|
|
||||||
}),
|
|
||||||
BubbleMenu.configure({
|
|
||||||
pluginKey: 'linkBubbleMenu',
|
|
||||||
element: linkBubbleMenuRef,
|
|
||||||
shouldShow: ({ state }) => {
|
|
||||||
const { selection } = state
|
|
||||||
const { empty } = selection
|
|
||||||
return !empty && shouldShowLinkBubbleMenu()
|
|
||||||
},
|
|
||||||
tippyOptions: {
|
|
||||||
placement: 'bottom'
|
|
||||||
}
|
|
||||||
}),
|
|
||||||
ImageFigure,
|
|
||||||
Image,
|
|
||||||
Figcaption,
|
|
||||||
Placeholder.configure({
|
|
||||||
emptyNodeClass: styles.emptyNode,
|
|
||||||
placeholder: props.placeholder
|
|
||||||
})
|
|
||||||
],
|
|
||||||
autofocus: props.autoFocus,
|
|
||||||
content: props.initialContent || null
|
|
||||||
}))
|
|
||||||
const editorInstance = freshEditor()
|
|
||||||
if (!editorInstance) return
|
|
||||||
setEditor(editorInstance)
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{ defer: true }
|
|
||||||
)
|
|
||||||
)
|
|
||||||
|
|
||||||
const isEmpty = useEditorIsEmpty(() => editor())
|
const isEmpty = useEditorIsEmpty(() => editor())
|
||||||
const isFocused = useEditorIsFocused(() => editor())
|
const isFocused = useEditorIsFocused(() => editor())
|
||||||
|
|
||||||
const isActive = (name: string) =>
|
const isActive = (name: string) =>
|
||||||
createEditorTransaction(
|
createEditorTransaction(
|
||||||
() => editor(),
|
() => editor(),
|
||||||
(ed) => {
|
(ed) => ed?.isActive(name)
|
||||||
return ed?.isActive(name)
|
|
||||||
}
|
|
||||||
)
|
)
|
||||||
|
|
||||||
const html = useEditorHTML(() => editor())
|
const html = useEditorHTML(() => editor())
|
||||||
|
@ -205,16 +127,6 @@ const SimplifiedEditor = (props: Props) => {
|
||||||
editor()?.commands.clearContent(true)
|
editor()?.commands.clearContent(true)
|
||||||
}
|
}
|
||||||
|
|
||||||
createEffect(() => {
|
|
||||||
if (props.setClear) {
|
|
||||||
editor()?.commands.clearContent(true)
|
|
||||||
}
|
|
||||||
if (props.resetToInitial) {
|
|
||||||
editor()?.commands.clearContent(true)
|
|
||||||
if (props.initialContent) editor()?.commands.setContent(props.initialContent)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
const handleKeyDown = (event: KeyboardEvent) => {
|
const handleKeyDown = (event: KeyboardEvent) => {
|
||||||
if (isEmpty() || !isFocused()) {
|
if (isEmpty() || !isFocused()) {
|
||||||
return
|
return
|
||||||
|
@ -243,19 +155,89 @@ const SimplifiedEditor = (props: Props) => {
|
||||||
window.removeEventListener('keydown', handleKeyDown)
|
window.removeEventListener('keydown', handleKeyDown)
|
||||||
editor()?.destroy()
|
editor()?.destroy()
|
||||||
})
|
})
|
||||||
|
|
||||||
|
console.debug('[SimplifiedEditor] mounted')
|
||||||
|
const freshEditor = createTiptapEditor<HTMLElement>(() => ({
|
||||||
|
element: editorEl as HTMLDivElement,
|
||||||
|
editorProps: {
|
||||||
|
attributes: {
|
||||||
|
class: styles.simplifiedEditorField
|
||||||
|
}
|
||||||
|
},
|
||||||
|
extensions: [
|
||||||
|
Document,
|
||||||
|
Text,
|
||||||
|
Paragraph,
|
||||||
|
Bold,
|
||||||
|
Italic,
|
||||||
|
Link.extend({
|
||||||
|
inclusive: false
|
||||||
|
}).configure({
|
||||||
|
autolink: true,
|
||||||
|
openOnClick: false
|
||||||
|
}),
|
||||||
|
CharacterCount.configure({
|
||||||
|
limit: props.noLimits ? null : maxLength
|
||||||
|
}),
|
||||||
|
Blockquote.configure({
|
||||||
|
HTMLAttributes: {
|
||||||
|
class: styles.blockQuote
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
BubbleMenu.configure({
|
||||||
|
pluginKey: 'textBubbleMenu',
|
||||||
|
element: textBubbleMenuRef,
|
||||||
|
shouldShow: ({ view, state }) => {
|
||||||
|
if (!props.onlyBubbleControls) return false
|
||||||
|
const { selection } = state
|
||||||
|
return view.hasFocus() && !selection.empty
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
BubbleMenu.configure({
|
||||||
|
pluginKey: 'linkBubbleMenu',
|
||||||
|
element: linkBubbleMenuRef,
|
||||||
|
shouldShow: ({ state }) =>
|
||||||
|
state.selection && !state.selection.empty && shouldShowLinkBubbleMenu(),
|
||||||
|
tippyOptions: {
|
||||||
|
placement: 'bottom'
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
ImageFigure,
|
||||||
|
Image,
|
||||||
|
Figcaption,
|
||||||
|
Placeholder.configure({
|
||||||
|
emptyNodeClass: styles.emptyNode,
|
||||||
|
placeholder: props.placeholder
|
||||||
|
})
|
||||||
|
],
|
||||||
|
autofocus: props.autoFocus,
|
||||||
|
content: props.initialContent || null
|
||||||
|
}))
|
||||||
|
const ed = freshEditor()
|
||||||
|
ed && setEditor(ed)
|
||||||
})
|
})
|
||||||
|
|
||||||
if (props.onChange) {
|
createReaction(
|
||||||
createEffect(() => {
|
on(
|
||||||
props.onChange?.(html() || '')
|
editor,
|
||||||
})
|
(e) => {
|
||||||
}
|
e?.commands.clearContent(props.resetToInitial || props.setClear)
|
||||||
|
props.initialContent && e?.commands.setContent(props.initialContent)
|
||||||
|
},
|
||||||
|
{}
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
createEffect(() => {
|
createEffect(
|
||||||
if (html()) {
|
on(
|
||||||
setCounter(editor()?.storage.characterCount.characters())
|
html,
|
||||||
}
|
(content) => {
|
||||||
})
|
content && setCounter(editor()?.storage.characterCount.characters())
|
||||||
|
props.onChange?.(content || '')
|
||||||
|
},
|
||||||
|
{}
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
const maxHeightStyle = {
|
const maxHeightStyle = {
|
||||||
overflow: 'auto',
|
overflow: 'auto',
|
||||||
|
@ -290,7 +272,7 @@ const SimplifiedEditor = (props: Props) => {
|
||||||
<Show when={props.label && counter() > 0}>
|
<Show when={props.label && counter() > 0}>
|
||||||
<div class={styles.label}>{props.label}</div>
|
<div class={styles.label}>{props.label}</div>
|
||||||
</Show>
|
</Show>
|
||||||
<div style={props.maxHeight ? maxHeightStyle : undefined} ref={setEditorElement} />
|
<div style={props.maxHeight ? maxHeightStyle : undefined} ref={(el) => (editorEl = el)} />
|
||||||
<Show when={!props.onlyBubbleControls}>
|
<Show when={!props.onlyBubbleControls}>
|
||||||
<div class={clsx(styles.controls, { [styles.alwaysVisible]: props.controlsAlwaysVisible })}>
|
<div class={clsx(styles.controls, { [styles.alwaysVisible]: props.controlsAlwaysVisible })}>
|
||||||
<div class={styles.actions}>
|
<div class={styles.actions}>
|
||||||
|
@ -361,7 +343,7 @@ const SimplifiedEditor = (props: Props) => {
|
||||||
</div>
|
</div>
|
||||||
<Show when={!props.onChange}>
|
<Show when={!props.onChange}>
|
||||||
<div class={styles.buttons}>
|
<div class={styles.buttons}>
|
||||||
<Show when={isCancelButtonVisible()}>
|
<Show when={props.isCancelButtonVisible}>
|
||||||
<Button value={t('Cancel')} variant="secondary" onClick={handleClear} />
|
<Button value={t('Cancel')} variant="secondary" onClick={handleClear} />
|
||||||
</Show>
|
</Show>
|
||||||
<Show when={!props.isPosting} fallback={<Loading />}>
|
<Show when={!props.isPosting} fallback={<Loading />}>
|
||||||
|
@ -405,4 +387,4 @@ const SimplifiedEditor = (props: Props) => {
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
export default SimplifiedEditor // "export default" need to use for asynchronous (lazy) imports in the comments tree
|
export default SimplifiedEditor
|
||||||
|
|
|
@ -20,13 +20,13 @@ export const ProfilePopup = (props: ProfilePopupProps) => {
|
||||||
<Popup {...props} horizontalAnchor="right" popupCssClass={styles.profilePopup}>
|
<Popup {...props} horizontalAnchor="right" popupCssClass={styles.profilePopup}>
|
||||||
<ul class="nodash">
|
<ul class="nodash">
|
||||||
<li>
|
<li>
|
||||||
<A class={styles.action} href='/profile'>
|
<A class={styles.action} href="/profile">
|
||||||
<Icon name="profile" class={styles.icon} />
|
<Icon name="profile" class={styles.icon} />
|
||||||
{t('Profile')}
|
{t('Profile')}
|
||||||
</A>
|
</A>
|
||||||
</li>
|
</li>
|
||||||
<li>
|
<li>
|
||||||
<A class={styles.action} href='/edit'>
|
<A class={styles.action} href="/edit">
|
||||||
<Icon name="pencil-outline" class={styles.icon} />
|
<Icon name="pencil-outline" class={styles.icon} />
|
||||||
{t('Drafts')}
|
{t('Drafts')}
|
||||||
</A>
|
</A>
|
||||||
|
|
Loading…
Reference in New Issue
Block a user