editor-fixed

This commit is contained in:
Untone 2024-07-31 02:20:54 +03:00
parent ca629e8c26
commit e875212ae7
2 changed files with 92 additions and 110 deletions

View File

@ -1,3 +1,4 @@
import { Editor } from '@tiptap/core'
import { Blockquote } from '@tiptap/extension-blockquote'
import { Bold } from '@tiptap/extension-bold'
import { BubbleMenu } from '@tiptap/extension-bubble-menu'
@ -10,7 +11,7 @@ import { Paragraph } from '@tiptap/extension-paragraph'
import { Placeholder } from '@tiptap/extension-placeholder'
import { Text } from '@tiptap/extension-text'
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 {
createEditorTransaction,
@ -19,9 +20,9 @@ import {
useEditorIsEmpty,
useEditorIsFocused
} from 'solid-tiptap'
import { useEditorContext } from '~/context/editor'
import { useLocalize } from '~/context/localize'
import { useUI } from '~/context/ui'
import { UploadedFile } from '~/types/upload'
import { Button } from '../_shared/Button'
import { Icon } from '../_shared/Icon'
@ -30,15 +31,12 @@ import { Modal } from '../_shared/Modal'
import { Popover } from '../_shared/Popover'
import { ShowOnlyOnClient } from '../_shared/ShowOnlyOnClient'
import { LinkBubbleMenuModule } from './LinkBubbleMenu'
import styles from './SimplifiedEditor.module.scss'
import { TextBubbleMenu } from './TextBubbleMenu'
import { UploadModalContent } from './UploadModalContent'
import { Figcaption } from './extensions/Figcaption'
import { Figure } from './extensions/Figure'
import { Editor } from '@tiptap/core'
import { useUI } from '~/context/ui'
import styles from './SimplifiedEditor.module.scss'
type Props = {
placeholder: string
initialContent?: string
@ -71,103 +69,27 @@ const SimplifiedEditor = (props: Props) => {
const { showModal, hideModal } = useUI()
const [counter, setCounter] = createSignal<number>(0)
const [shouldShowLinkBubbleMenu, setShouldShowLinkBubbleMenu] = createSignal(false)
const isCancelButtonVisible = createMemo(() => props.isCancelButtonVisible !== false)
const [editorElement, setEditorElement] = createSignal<HTMLDivElement>()
const { editor, setEditor } = useEditorContext()
const maxLength = props.maxLength ?? DEFAULT_MAX_LENGTH
let editorEl: HTMLDivElement | undefined
let wrapperEditorElRef: HTMLElement | undefined
let textBubbleMenuRef: HTMLDivElement | undefined
let linkBubbleMenuRef: HTMLDivElement | undefined
// Extend the Figure extension to include Figcaption
const ImageFigure = Figure.extend({
name: 'capturedImage',
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 isFocused = useEditorIsFocused(() => editor())
const isActive = (name: string) =>
createEditorTransaction(
() => editor(),
(ed) => {
return ed?.isActive(name)
}
(ed) => ed?.isActive(name)
)
const html = useEditorHTML(() => editor())
@ -205,16 +127,6 @@ const SimplifiedEditor = (props: Props) => {
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) => {
if (isEmpty() || !isFocused()) {
return
@ -243,19 +155,89 @@ const SimplifiedEditor = (props: Props) => {
window.removeEventListener('keydown', handleKeyDown)
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) {
createEffect(() => {
props.onChange?.(html() || '')
})
}
createReaction(
on(
editor,
(e) => {
e?.commands.clearContent(props.resetToInitial || props.setClear)
props.initialContent && e?.commands.setContent(props.initialContent)
},
{}
)
)
createEffect(() => {
if (html()) {
setCounter(editor()?.storage.characterCount.characters())
}
})
createEffect(
on(
html,
(content) => {
content && setCounter(editor()?.storage.characterCount.characters())
props.onChange?.(content || '')
},
{}
)
)
const maxHeightStyle = {
overflow: 'auto',
@ -290,7 +272,7 @@ const SimplifiedEditor = (props: Props) => {
<Show when={props.label && counter() > 0}>
<div class={styles.label}>{props.label}</div>
</Show>
<div style={props.maxHeight ? maxHeightStyle : undefined} ref={setEditorElement} />
<div style={props.maxHeight ? maxHeightStyle : undefined} ref={(el) => (editorEl = el)} />
<Show when={!props.onlyBubbleControls}>
<div class={clsx(styles.controls, { [styles.alwaysVisible]: props.controlsAlwaysVisible })}>
<div class={styles.actions}>
@ -361,7 +343,7 @@ const SimplifiedEditor = (props: Props) => {
</div>
<Show when={!props.onChange}>
<div class={styles.buttons}>
<Show when={isCancelButtonVisible()}>
<Show when={props.isCancelButtonVisible}>
<Button value={t('Cancel')} variant="secondary" onClick={handleClear} />
</Show>
<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

View File

@ -20,13 +20,13 @@ export const ProfilePopup = (props: ProfilePopupProps) => {
<Popup {...props} horizontalAnchor="right" popupCssClass={styles.profilePopup}>
<ul class="nodash">
<li>
<A class={styles.action} href='/profile'>
<A class={styles.action} href="/profile">
<Icon name="profile" class={styles.icon} />
{t('Profile')}
</A>
</li>
<li>
<A class={styles.action} href='/edit'>
<A class={styles.action} href="/edit">
<Icon name="pencil-outline" class={styles.icon} />
{t('Drafts')}
</A>