MiniEditor-fix

This commit is contained in:
Untone 2024-09-27 17:26:40 +03:00
parent 962140e755
commit 7aa01d6152
3 changed files with 121 additions and 127 deletions

View File

@ -8,7 +8,6 @@ import { InlineForm } from '../InlineForm'
type Props = {
editor: Editor
onClose: () => void
onFocus: (event: FocusEvent) => void
}
export const checkUrl = (url: string) => {
@ -62,7 +61,6 @@ export const InsertLinkForm = (props: Props) => {
validate={(value) => (validateUrl(value) ? '' : t('Invalid url format'))}
onSubmit={handleLinkFormSubmit}
onClose={props.onClose}
onFocus={props.onFocus}
/>
</div>
)

View File

@ -63,15 +63,6 @@ export const MicroEditor = (props: MicroEditorProps): JSX.Element => {
const [showLinkInput, setShowLinkInput] = createSignal(false)
const [showSimpleMenu, setShowSimpleMenu] = createSignal(false)
const [toolbarElement, setToolbarElement] = createSignal<HTMLElement>()
const [selectionRange, setSelectionRange] = createSignal<Range | null>(null)
const handleLinkInputFocus = (event: FocusEvent) => {
event.preventDefault()
const selection = window.getSelection()
if (selection?.rangeCount) {
setSelectionRange(selection.getRangeAt(0))
}
}
const editor = createTiptapEditor(() => ({
element: editorElement()!,
@ -87,12 +78,32 @@ export const MicroEditor = (props: MicroEditorProps): JSX.Element => {
content: props.content || ''
}))
const selection = createEditorTransaction(editor, (instance) => instance?.state.selection)
const [storedSelection, setStoredSelection] = createSignal<Editor['state']['selection']>()
const recoverSelection = () => {
if (!storedSelection()?.empty) {
// TODO set selection range from stored
createEditorTransaction(editor, (instance?: Editor) => {
const r = selection()
if (instance && r) {
instance.state.selection.from === r.from
instance.state.selection.to === r.to
}
})
}
}
const storeSelection = (event: Event) => {
event.preventDefault()
const selection = editor()?.state.selection
if (!selection?.empty) {
setStoredSelection(selection)
}
}
const isEmpty = useEditorIsEmpty(editor)
const isFocused = useEditorIsFocused(editor)
const isTextSelection = createEditorTransaction(editor, (instance) => !instance?.state.selection.empty)
const html = useEditorHTML(editor)
createEffect(on([isTextSelection, showLinkInput],([selected, linkEditing]) => !linkEditing && setShowSimpleMenu(selected)))
createEffect(on([selection, showLinkInput], ([s, l]) => !l && setShowSimpleMenu(!s?.empty)))
createEffect(on(html, (c?: string) => c && props.onChange?.(c)))
createEffect(on(showLinkInput, (x?: boolean) => x && editor()?.chain().focus().run()))
createReaction(on(toolbarElement, (t?: HTMLElement) => t?.addEventListener('mousedown', prevent)))
@ -117,46 +128,41 @@ export const MicroEditor = (props: MicroEditorProps): JSX.Element => {
ref={setToolbarElement}
>
<div class={styles.controls}>
<Show
when={!showLinkInput()}
fallback={<InsertLinkForm editor={instance}
onClose={() => {
setShowLinkInput(false)
if (selectionRange()) {
const selection = window.getSelection()
selection?.removeAllRanges()
selection?.addRange(selectionRange()!)
}
}}
onFocus={handleLinkInputFocus} />}
>
<div class={styles.actions}>
<Control
key="bold"
editor={instance}
onChange={() => instance.chain().focus().toggleBold().run()}
title={t('Bold')}
>
<Icon name="editor-bold" />
</Control>
<Control
key="italic"
editor={instance}
onChange={() => instance.chain().focus().toggleItalic().run()}
title={t('Italic')}
>
<Icon name="editor-italic" />
</Control>
<Control
key="link"
editor={instance}
onChange={() => setShowLinkInput(!showLinkInput())}
title={t('Add url')}
isActive={showLinkInput}
>
<Icon name="editor-link" />
</Control>
</div>
<div class={styles.actions}>
<Control
key="bold"
editor={instance}
onChange={() => instance.chain().focus().toggleBold().run()}
title={t('Bold')}
>
<Icon name="editor-bold" />
</Control>
<Control
key="italic"
editor={instance}
onChange={() => instance.chain().focus().toggleItalic().run()}
title={t('Italic')}
>
<Icon name="editor-italic" />
</Control>
<Control
key="link"
editor={instance}
onChange={() => setShowLinkInput(!showLinkInput())}
title={t('Add url')}
isActive={showLinkInput}
>
<Icon name="editor-link" />
</Control>
</div>
<Show when={showLinkInput()}>
<InsertLinkForm
editor={instance}
onClose={() => {
setShowLinkInput(false)
recoverSelection()
}}
/>
</Show>
</div>
</div>
@ -164,7 +170,7 @@ export const MicroEditor = (props: MicroEditorProps): JSX.Element => {
)}
</Show>
<div id="micro-editor" ref={setEditorElement} style={styles.minimal} />
<div id="micro-editor" ref={setEditorElement} style={styles.minimal} onFocusOut={storeSelection} />
</div>
</div>
)

View File

@ -2,13 +2,10 @@ import type { Editor } from '@tiptap/core'
import CharacterCount from '@tiptap/extension-character-count'
import Placeholder from '@tiptap/extension-placeholder'
import clsx from 'clsx'
import { type JSX, Show, createEffect, createSignal, onCleanup } from 'solid-js'
import { type JSX, Show, createEffect, createSignal, on, onCleanup } from 'solid-js'
import {
createEditorTransaction,
createTiptapEditor,
useEditorHTML,
useEditorIsEmpty,
useEditorIsFocused
} from 'solid-tiptap'
import { Toolbar } from 'terracotta'
import { Icon } from '~/components/_shared/Icon/Icon'
@ -63,7 +60,6 @@ export default function MiniEditor(props: MiniEditorProps): JSX.Element {
const [editorElement, setEditorElement] = createSignal<HTMLDivElement>()
const [counter, setCounter] = createSignal(0)
const [showLinkInput, setShowLinkInput] = createSignal(false)
const [showSimpleMenu, setShowSimpleMenu] = createSignal(false)
const { t } = useLocalize()
const { showModal } = useUI()
@ -82,12 +78,9 @@ export default function MiniEditor(props: MiniEditorProps): JSX.Element {
content: props.content || ''
}))
const isEmpty = useEditorIsEmpty(editor)
const isFocused = useEditorIsFocused(editor)
const isTextSelection = createEditorTransaction(editor, (instance) => !instance?.state.selection.empty)
const html = useEditorHTML(editor)
createEffect(() => setShowSimpleMenu(isTextSelection()))
createEffect(on(html, (c?: string) => c && props.onChange?.(c)))
createEffect(on(showLinkInput, (x?: boolean) => x && editor()?.chain().focus().run()))
createEffect(() => {
const textLength = editor()?.getText().length || 0
@ -112,77 +105,74 @@ export default function MiniEditor(props: MiniEditorProps): JSX.Element {
})
return (
<div
class={clsx(styles.SimplifiedEditor, styles.bordered, {
[styles.isFocused]: isEmpty() || isFocused()
})}
class={clsx(styles.SimplifiedEditor, styles.bordered, styles.isFocused)}
>
<div>
<Show when={showSimpleMenu() || showLinkInput()}>
<Toolbar style={{ 'background-color': 'white' }} ref={setToolbarElement} horizontal>
<Show when={editor()} keyed>
{(instance) => (
<div class={styles.controls}>
<Show
when={!showLinkInput()}
fallback={<InsertLinkForm editor={instance} onClose={() => setShowLinkInput(false)} />}
>
<div class={styles.actions}>
<Control
key="bold"
editor={instance}
onChange={() => instance.chain().focus().toggleBold().run()}
title={t('Bold')}
>
<Icon name="editor-bold" />
</Control>
<Control
key="italic"
editor={instance}
onChange={() => instance.chain().focus().toggleItalic().run()}
title={t('Italic')}
>
<Icon name="editor-italic" />
</Control>
<Control
key="link"
editor={instance}
onChange={handleLinkClick}
title={t('Add url')}
isActive={showLinkInput}
>
<Icon name="editor-link" />
</Control>
<Control
key="blockquote"
editor={instance}
onChange={() => instance.chain().focus().toggleBlockquote().run()}
title={t('Add blockquote')}
>
<Icon name="editor-quote" />
</Control>
<Control
key="image"
editor={instance}
onChange={() => showModal('simplifiedEditorUploadImage')}
title={t('Add image')}
>
<Icon name="editor-image-dd-full" />
</Control>
</div>
</Show>
</div>
)}
</Show>
</Toolbar>
</Show>
<div id="mini-editor" ref={setEditorElement} />
<Toolbar style={{ 'background-color': 'white', display: 'inline-flex' }} ref={setToolbarElement} horizontal>
<Show when={editor()} keyed>
{(instance) => (
<div class={styles.controls}>
<Show
when={!showLinkInput()}
fallback={<InsertLinkForm editor={instance} onClose={() => setShowLinkInput(false)} />}
>
<div class={styles.actions}>
<Control
key="bold"
editor={instance}
onChange={() => instance.chain().focus().toggleBold().run()}
title={t('Bold')}
>
<Icon name="editor-bold" />
</Control>
<Control
key="italic"
editor={instance}
onChange={() => instance.chain().focus().toggleItalic().run()}
title={t('Italic')}
>
<Icon name="editor-italic" />
</Control>
<Control
key="link"
editor={instance}
onChange={handleLinkClick}
title={t('Add url')}
isActive={showLinkInput}
>
<Icon name="editor-link" />
</Control>
<Control
key="blockquote"
editor={instance}
onChange={() => instance.chain().focus().toggleBlockquote().run()}
title={t('Add blockquote')}
>
<Icon name="editor-quote" />
</Control>
<Control
key="image"
editor={instance}
onChange={() => showModal('simplifiedEditorUploadImage')}
title={t('Add image')}
>
<Icon name="editor-image-dd-full" />
</Control>
</div>
</Show>
</div>
)}
</Show>
</Toolbar>
<Show when={counter() > 0}>
<small class={styles.limit}>
{counter()} / {props.limit || '∞'}
</small>
</Show>
</div>
</div>
)