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

View File

@ -63,15 +63,6 @@ export const MicroEditor = (props: MicroEditorProps): JSX.Element => {
const [showLinkInput, setShowLinkInput] = createSignal(false) const [showLinkInput, setShowLinkInput] = createSignal(false)
const [showSimpleMenu, setShowSimpleMenu] = createSignal(false) const [showSimpleMenu, setShowSimpleMenu] = createSignal(false)
const [toolbarElement, setToolbarElement] = createSignal<HTMLElement>() 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(() => ({ const editor = createTiptapEditor(() => ({
element: editorElement()!, element: editorElement()!,
@ -87,12 +78,32 @@ export const MicroEditor = (props: MicroEditorProps): JSX.Element => {
content: props.content || '' 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 isEmpty = useEditorIsEmpty(editor)
const isFocused = useEditorIsFocused(editor) const isFocused = useEditorIsFocused(editor)
const isTextSelection = createEditorTransaction(editor, (instance) => !instance?.state.selection.empty)
const html = useEditorHTML(editor) const html = useEditorHTML(editor)
createEffect(on([selection, showLinkInput], ([s, l]) => !l && setShowSimpleMenu(!s?.empty)))
createEffect(on([isTextSelection, showLinkInput],([selected, linkEditing]) => !linkEditing && setShowSimpleMenu(selected)))
createEffect(on(html, (c?: string) => c && props.onChange?.(c))) createEffect(on(html, (c?: string) => c && props.onChange?.(c)))
createEffect(on(showLinkInput, (x?: boolean) => x && editor()?.chain().focus().run())) createEffect(on(showLinkInput, (x?: boolean) => x && editor()?.chain().focus().run()))
createReaction(on(toolbarElement, (t?: HTMLElement) => t?.addEventListener('mousedown', prevent))) createReaction(on(toolbarElement, (t?: HTMLElement) => t?.addEventListener('mousedown', prevent)))
@ -117,19 +128,6 @@ export const MicroEditor = (props: MicroEditorProps): JSX.Element => {
ref={setToolbarElement} ref={setToolbarElement}
> >
<div class={styles.controls}> <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}> <div class={styles.actions}>
<Control <Control
key="bold" key="bold"
@ -157,6 +155,14 @@ export const MicroEditor = (props: MicroEditorProps): JSX.Element => {
<Icon name="editor-link" /> <Icon name="editor-link" />
</Control> </Control>
</div> </div>
<Show when={showLinkInput()}>
<InsertLinkForm
editor={instance}
onClose={() => {
setShowLinkInput(false)
recoverSelection()
}}
/>
</Show> </Show>
</div> </div>
</div> </div>
@ -164,7 +170,7 @@ export const MicroEditor = (props: MicroEditorProps): JSX.Element => {
)} )}
</Show> </Show>
<div id="micro-editor" ref={setEditorElement} style={styles.minimal} /> <div id="micro-editor" ref={setEditorElement} style={styles.minimal} onFocusOut={storeSelection} />
</div> </div>
</div> </div>
) )

View File

@ -2,13 +2,10 @@ import type { Editor } from '@tiptap/core'
import CharacterCount from '@tiptap/extension-character-count' import CharacterCount from '@tiptap/extension-character-count'
import Placeholder from '@tiptap/extension-placeholder' import Placeholder from '@tiptap/extension-placeholder'
import clsx from 'clsx' 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 { import {
createEditorTransaction,
createTiptapEditor, createTiptapEditor,
useEditorHTML, useEditorHTML,
useEditorIsEmpty,
useEditorIsFocused
} from 'solid-tiptap' } from 'solid-tiptap'
import { Toolbar } from 'terracotta' import { Toolbar } from 'terracotta'
import { Icon } from '~/components/_shared/Icon/Icon' 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 [editorElement, setEditorElement] = createSignal<HTMLDivElement>()
const [counter, setCounter] = createSignal(0) const [counter, setCounter] = createSignal(0)
const [showLinkInput, setShowLinkInput] = createSignal(false) const [showLinkInput, setShowLinkInput] = createSignal(false)
const [showSimpleMenu, setShowSimpleMenu] = createSignal(false)
const { t } = useLocalize() const { t } = useLocalize()
const { showModal } = useUI() const { showModal } = useUI()
@ -82,12 +78,9 @@ export default function MiniEditor(props: MiniEditorProps): JSX.Element {
content: props.content || '' content: props.content || ''
})) }))
const isEmpty = useEditorIsEmpty(editor)
const isFocused = useEditorIsFocused(editor)
const isTextSelection = createEditorTransaction(editor, (instance) => !instance?.state.selection.empty)
const html = useEditorHTML(editor) const html = useEditorHTML(editor)
createEffect(on(html, (c?: string) => c && props.onChange?.(c)))
createEffect(() => setShowSimpleMenu(isTextSelection())) createEffect(on(showLinkInput, (x?: boolean) => x && editor()?.chain().focus().run()))
createEffect(() => { createEffect(() => {
const textLength = editor()?.getText().length || 0 const textLength = editor()?.getText().length || 0
@ -112,13 +105,12 @@ export default function MiniEditor(props: MiniEditorProps): JSX.Element {
}) })
return ( return (
<div <div
class={clsx(styles.SimplifiedEditor, styles.bordered, { class={clsx(styles.SimplifiedEditor, styles.bordered, styles.isFocused)}
[styles.isFocused]: isEmpty() || isFocused()
})}
> >
<div> <div>
<Show when={showSimpleMenu() || showLinkInput()}> <div id="mini-editor" ref={setEditorElement} />
<Toolbar style={{ 'background-color': 'white' }} ref={setToolbarElement} horizontal>
<Toolbar style={{ 'background-color': 'white', display: 'inline-flex' }} ref={setToolbarElement} horizontal>
<Show when={editor()} keyed> <Show when={editor()} keyed>
{(instance) => ( {(instance) => (
<div class={styles.controls}> <div class={styles.controls}>
@ -174,15 +166,13 @@ export default function MiniEditor(props: MiniEditorProps): JSX.Element {
)} )}
</Show> </Show>
</Toolbar> </Toolbar>
</Show>
<div id="mini-editor" ref={setEditorElement} />
<Show when={counter() > 0}> <Show when={counter() > 0}>
<small class={styles.limit}> <small class={styles.limit}>
{counter()} / {props.limit || '∞'} {counter()} / {props.limit || '∞'}
</small> </small>
</Show> </Show>
</div> </div>
</div> </div>
) )