editor-refactoring

This commit is contained in:
Untone 2024-09-27 19:31:54 +03:00
parent b393810f7a
commit 595e2b8a4b
39 changed files with 382 additions and 484 deletions

View File

@ -9,7 +9,7 @@ import { SharePopup, getShareUrl } from '../SharePopup'
import styles from './AudioPlayer.module.scss'
const SimplifiedEditor = lazy(() => import('../../Editor/SimplifiedEditor'))
const MicroEditor = lazy(() => import('../../Editor/MicroEditor/MicroEditor'))
const GrowingTextarea = lazy(() => import('~/components/_shared/GrowingTextarea/GrowingTextarea'))
type Props = {
@ -171,10 +171,9 @@ export const PlayerPlaylist = (props: Props) => {
}
>
<div class={styles.descriptionBlock}>
<SimplifiedEditor
initialContent={mi.body}
<MicroEditor
content={mi.body}
placeholder={`${t('Description')}...`}
smallHeight={true}
onChange={(value) => handleMediaItemFieldChange('body', value)}
/>
<GrowingTextarea

View File

@ -21,7 +21,7 @@ import { CommentDate } from '../CommentDate'
import { CommentRatingControl } from '../CommentRatingControl'
import styles from './Comment.module.scss'
const SimplifiedEditor = lazy(() => import('../../Editor/SimplifiedEditor'))
const MiniEditor = lazy(() => import('../../Editor/MiniEditor/MiniEditor'))
type Props = {
comment: Reaction
@ -41,7 +41,6 @@ export const Comment = (props: Props) => {
const [isReplyVisible, setIsReplyVisible] = createSignal(false)
const [loading, setLoading] = createSignal(false)
const [editMode, setEditMode] = createSignal(false)
const [clearEditor, setClearEditor] = createSignal(false)
const [editedBody, setEditedBody] = createSignal<string>()
const { session, client } = useSession()
const author = createMemo<Author>(() => session()?.user?.app_data?.profile as Author)
@ -104,13 +103,11 @@ export const Comment = (props: Props) => {
shout: props.comment.shout.id
}
} as MutationCreate_ReactionArgs)
setClearEditor(true)
setIsReplyVisible(false)
setLoading(false)
} catch (error) {
console.error('[handleCreate reaction]:', error)
}
setClearEditor(false)
}
const toggleEditMode = () => {
@ -189,16 +186,11 @@ export const Comment = (props: Props) => {
<div class={styles.commentBody}>
<Show when={editMode()} fallback={<div innerHTML={body()} />}>
<Suspense fallback={<p>{t('Loading')}</p>}>
<SimplifiedEditor
initialContent={editedBody() || props.comment.body || ''}
submitButtonText={t('Save')}
quoteEnabled={true}
imageEnabled={true}
<MiniEditor
content={editedBody() || props.comment.body || ''}
placeholder={t('Write a comment...')}
onSubmit={(value) => handleUpdate(value)}
submitByCtrlEnter={true}
onCancel={() => setEditMode(false)}
setClear={clearEditor()}
/>
</Suspense>
</Show>
@ -258,12 +250,9 @@ export const Comment = (props: Props) => {
<Show when={isReplyVisible() && props.clickedReplyId === props.comment.id}>
<Suspense fallback={<p>{t('Loading')}</p>}>
<SimplifiedEditor
quoteEnabled={true}
imageEnabled={true}
<MiniEditor
placeholder={t('Write a comment...')}
onSubmit={(value) => handleCreate(value)}
submitByCtrlEnter={true}
/>
</Suspense>
</Show>

View File

@ -9,11 +9,12 @@ import { Author, Reaction, ReactionKind, ReactionSort } from '~/graphql/schema/c
import { SortFunction } from '~/types/common'
import { byCreated, byStat } from '~/utils/sort'
import { Button } from '../_shared/Button'
import { Loading } from '../_shared/Loading'
import { ShowIfAuthenticated } from '../_shared/ShowIfAuthenticated'
import styles from './Article.module.scss'
import { Comment } from './Comment'
const SimplifiedEditor = lazy(() => import('../Editor/SimplifiedEditor'))
const MiniEditor = lazy(() => import('../Editor/MiniEditor/MiniEditor'))
type Props = {
articleAuthors: Author[]
@ -27,7 +28,6 @@ export const CommentsTree = (props: Props) => {
const [commentsOrder, setCommentsOrder] = createSignal<ReactionSort>(ReactionSort.Newest)
const [onlyNew, setOnlyNew] = createSignal(false)
const [newReactions, setNewReactions] = createSignal<Reaction[]>([])
const [clearEditor, setClearEditor] = createSignal(false)
const [clickedReplyId, setClickedReplyId] = createSignal<number>()
const { reactionEntities, createShoutReaction, loadReactionsBy } = useReactions()
@ -70,6 +70,7 @@ export const CommentsTree = (props: Props) => {
setCookie()
}
})
const [posting, setPosting] = createSignal(false)
const handleSubmitComment = async (value: string) => {
setPosting(true)
@ -81,12 +82,10 @@ export const CommentsTree = (props: Props) => {
shout: props.shoutId
}
})
setClearEditor(true)
await loadReactionsBy({ by: { shout: props.shoutSlug } })
} catch (error) {
console.error('[handleCreate reaction]:', error)
}
setClearEditor(false)
setPosting(false)
}
@ -155,16 +154,13 @@ export const CommentsTree = (props: Props) => {
</div>
}
>
<SimplifiedEditor
quoteEnabled={true}
imageEnabled={true}
autoFocus={false}
submitByCtrlEnter={true}
<MiniEditor
placeholder={t('Write a comment...')}
onSubmit={(value) => handleSubmitComment(value)}
setClear={clearEditor()}
isPosting={posting()}
/>
<Show when={posting()}>
<Loading />
</Show>
</ShowIfAuthenticated>
</>
)

View File

@ -1,14 +1,13 @@
import type { Editor } from '@tiptap/core'
import { renderUploadedImage } from '~/components/Editor/renderUploadedImage'
import { renderUploadedImage } from '~/components/Upload/renderUploadedImage'
import { Icon } from '~/components/_shared/Icon'
import { Popover } from '~/components/_shared/Popover'
import { useLocalize } from '~/context/localize'
import { UploadedFile } from '~/types/upload'
import { Modal } from '../../_shared/Modal'
import { UploadModalContent } from '../UploadModalContent'
import { useUI } from '~/context/ui'
import { UploadedFile } from '~/types/upload'
import { UploadModalContent } from '../../Upload/UploadModalContent'
import { Modal } from '../../_shared/Modal'
import styles from './BubbleMenu.module.scss'
type Props = {

View File

@ -1,105 +1,28 @@
import { Editor, EditorOptions } from '@tiptap/core'
import { createSignal } from 'solid-js'
import { createStore } from 'solid-js/store'
import { Meta, StoryObj } from 'storybook-solidjs'
import { EditorContext, EditorContextType, ShoutForm } from '~/context/editor'
import { LocalizeContext, LocalizeContextType } from '~/context/localize'
import { SessionContext, SessionContextType } from '~/context/session'
import { SnackbarContext, SnackbarContextType } from '~/context/ui'
import { EditorComponent, EditorComponentProps } from './Editor'
// Mock data
const mockSession = {
session: () => ({
user: {
app_data: {
profile: {
name: 'Test User',
slug: 'test-user'
}
}
},
access_token: 'mock-access-token'
})
}
const mockLocalize = {
t: (key: string) => key,
lang: () => 'en'
}
const [_form, setForm] = createStore<ShoutForm>({
body: '',
slug: '',
shoutId: 0,
title: '',
selectedTopics: []
})
const [_formErrors, setFormErrors] = createStore({} as Record<keyof ShoutForm, string>)
const [editor, setEditor] = createSignal<Editor | undefined>()
const mockEditorContext: EditorContextType = {
countWords: () => 0,
isEditorPanelVisible: () => false,
wordCounter: () => ({ characters: 0, words: 0 }),
form: _form,
formErrors: _formErrors,
createEditor: (opts?: Partial<EditorOptions>) => {
const newEditor = new Editor(opts)
setEditor(newEditor)
return newEditor
},
editor,
saveShout: async (_form: ShoutForm) => {
// Simulate save
},
saveDraft: async (_form: ShoutForm) => {
// Simulate save draft
},
saveDraftToLocalStorage: (_form: ShoutForm) => {
// Simulate save to local storage
},
getDraftFromLocalStorage: (_shoutId: number): ShoutForm => _form,
publishShout: async (_form: ShoutForm) => {
// Simulate publish
},
publishShoutById: async (_shoutId: number) => {
// Simulate publish by ID
},
deleteShout: async (_shoutId: number): Promise<boolean> => true,
toggleEditorPanel: () => {
// Simulate toggle
},
setForm,
setFormErrors
}
const mockSnackbarContext = {
showSnackbar: console.log
}
import { EditorComponent } from './Editor'
const meta: Meta<typeof EditorComponent> = {
title: 'Components/Editor',
component: EditorComponent,
argTypes: {
shoutId: {
control: 'number',
description: 'Unique identifier for the shout (document)',
defaultValue: 1
},
initialContent: {
content: {
control: 'text',
description: 'Initial content for the editor',
defaultValue: ''
},
onChange: {
action: 'contentChanged',
description: 'Callback when the content changes'
limit: {
control: 'number',
description: 'Character limit for the editor',
defaultValue: 500
},
disableCollaboration: {
control: 'boolean',
description: 'Disable collaboration features for Storybook',
defaultValue: true
placeholder: {
control: 'text',
description: 'Placeholder text when the editor is empty',
defaultValue: 'Start typing here...'
},
onChange: {
action: 'changed',
description: 'Callback when the content changes'
}
}
}
@ -109,38 +32,33 @@ export default meta
type Story = StoryObj<typeof EditorComponent>
export const Default: Story = {
render: (props: EditorComponentProps) => {
const [_content, setContent] = createSignal(props.initialContent || '')
return (
<SessionContext.Provider value={mockSession as SessionContextType}>
<LocalizeContext.Provider value={mockLocalize as LocalizeContextType}>
<SnackbarContext.Provider value={mockSnackbarContext as SnackbarContextType}>
<EditorContext.Provider value={mockEditorContext as EditorContextType}>
<EditorComponent
{...props}
onChange={(text: string) => {
props.onChange(text)
setContent(text)
}}
/>
</EditorContext.Provider>
</SnackbarContext.Provider>
</LocalizeContext.Provider>
</SessionContext.Provider>
)
},
args: {
shoutId: 1,
initialContent: '',
disableCollaboration: true
content: '',
limit: 500,
placeholder: 'Start typing here...'
}
}
export const WithInitialContent: Story = {
...Default,
args: {
...Default.args,
initialContent: '<p>This is some initial content in the editor.</p>'
content: 'This is some initial content',
limit: 500,
placeholder: 'Start typing here...'
}
}
export const WithCharacterLimit: Story = {
args: {
content: '',
limit: 50,
placeholder: 'You have a 50 character limit...'
}
}
export const WithCustomPlaceholder: Story = {
args: {
content: '',
limit: 500,
placeholder: 'Custom placeholder here...'
}
}

View File

@ -16,10 +16,10 @@ import { useSnackbar } from '~/context/ui'
import { Author } from '~/graphql/schema/core.gen'
import { base, custom, extended } from '~/lib/editorExtensions'
import { handleImageUpload } from '~/lib/handleImageUpload'
import { renderUploadedImage } from '../Upload/renderUploadedImage'
import { BlockquoteBubbleMenu, FigureBubbleMenu, IncutBubbleMenu } from './BubbleMenu'
import { EditorFloatingMenu } from './EditorFloatingMenu'
import { TextBubbleMenu } from './TextBubbleMenu'
import { renderUploadedImage } from './renderUploadedImage'
import './Prosemirror.scss'

View File

@ -1,15 +1,14 @@
import type { Editor } from '@tiptap/core'
import { Show, createEffect, createSignal } from 'solid-js'
import { renderUploadedImage } from '~/components/Editor/renderUploadedImage'
import { renderUploadedImage } from '~/components/Upload/renderUploadedImage'
import { Icon } from '~/components/_shared/Icon'
import { useLocalize } from '~/context/localize'
import { useUI } from '~/context/ui'
import { useOutsideClickHandler } from '~/lib/useOutsideClickHandler'
import { UploadedFile } from '~/types/upload'
import { UploadModalContent } from '../../Upload/UploadModalContent'
import { InlineForm } from '../../_shared/InlineForm'
import { Modal } from '../../_shared/Modal'
import { InlineForm } from '../InlineForm'
import { UploadModalContent } from '../UploadModalContent'
import { Menu } from './Menu'
import type { MenuItem } from './Menu/Menu'

View File

@ -0,0 +1,113 @@
import { Editor } from '@tiptap/core'
import { Show, createEffect, createSignal, on } from 'solid-js'
import { createEditorTransaction } from 'solid-tiptap'
import { Icon } from '~/components/_shared/Icon/Icon'
import { useLocalize } from '~/context/localize'
import { InsertLinkForm } from '../InsertLinkForm/InsertLinkForm'
import { ToolbarControl as Control } from './ToolbarControl'
import styles from '../SimplifiedEditor.module.scss'
export interface MicroToolbarProps {
showing?: boolean
editor?: Editor
}
export const MicroToolbar = (props: MicroToolbarProps) => {
const { t } = useLocalize()
// show / hide for menu
const [showSimpleMenu, setShowSimpleMenu] = createSignal(!props.showing)
const selection = createEditorTransaction(
() => props.editor,
(instance) => instance?.state.selection
)
// show / hide for link input
const [showLinkInput, setShowLinkInput] = createSignal(false)
// change visibility on selection if not in link input mode
createEffect(on([selection, showLinkInput], ([s, l]) => !l && setShowSimpleMenu(!s?.empty)))
// focus on link input when it shows up
createEffect(on(showLinkInput, (x?: boolean) => x && props.editor?.chain().focus().run()))
const [storedSelection, setStoredSelection] = createSignal<Editor['state']['selection']>()
const recoverSelection = () => {
if (!storedSelection()?.empty) {
createEditorTransaction(
() => props.editor,
(instance?: Editor) => {
const r = selection()
if (instance && r) {
instance.state.selection.from === r.from
instance.state.selection.to === r.to
}
}
)
}
}
const storeSelection = () => {
const selection = props.editor?.state.selection
if (!selection?.empty) {
setStoredSelection(selection)
}
}
const toggleShowLink = () => {
if (showLinkInput()) {
props.editor?.chain().focus().run()
recoverSelection()
} else {
storeSelection()
}
setShowLinkInput(!showLinkInput())
}
return (
<Show when={props.editor} keyed>
{(instance) => (
<Show when={!showSimpleMenu()}>
<div
style={{
display: 'inline-flex',
background: 'var(--editor-bubble-menu-background)',
border: '1px solid black'
}}
>
<div class={styles.controls}>
<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={toggleShowLink}
title={t('Add url')}
isActive={showLinkInput}
>
<Icon name="editor-link" />
</Control>
</div>
<Show when={showLinkInput()}>
<InsertLinkForm editor={instance} onClose={toggleShowLink} />
</Show>
</div>
</div>
</Show>
)}
</Show>
)
}

View File

@ -0,0 +1,117 @@
import { Editor } from '@tiptap/core'
import { Show, createEffect, createSignal, on } from 'solid-js'
import { Icon } from '~/components/_shared/Icon/Icon'
import { useLocalize } from '~/context/localize'
import { useUI } from '~/context/ui'
import { InsertLinkForm } from '../InsertLinkForm/InsertLinkForm'
import { ToolbarControl as Control } from './ToolbarControl'
import { createEditorTransaction } from 'solid-tiptap'
import styles from '../SimplifiedEditor.module.scss'
interface MiniToolbarProps {
editor?: Editor
}
export const MiniToolbar = (props: MiniToolbarProps) => {
const { t } = useLocalize()
const { showModal } = useUI()
// show / hide for link input
const [showLinkInput, setShowLinkInput] = createSignal(false)
// focus on link input when it shows up
createEffect(on(showLinkInput, (x?: boolean) => x && props.editor?.chain().focus().run()))
const selection = createEditorTransaction(
() => props.editor,
(instance) => instance?.state.selection
)
const [storedSelection, setStoredSelection] = createSignal<Editor['state']['selection']>()
const recoverSelection = () => {
if (!storedSelection()?.empty) {
createEditorTransaction(
() => props.editor,
(instance?: Editor) => {
const r = selection()
if (instance && r) {
instance.state.selection.from === r.from
instance.state.selection.to === r.to
}
}
)
}
}
const storeSelection = () => {
const selection = props.editor?.state.selection
if (!selection?.empty) {
setStoredSelection(selection)
}
}
const toggleShowLink = () => {
if (showLinkInput()) {
props.editor?.chain().focus().run()
recoverSelection()
} else {
storeSelection()
}
setShowLinkInput(!showLinkInput())
}
return (
<div style={{ 'background-color': 'white', display: 'inline-flex' }}>
<Show when={props.editor} keyed>
{(instance) => (
<div class={styles.controls}>
<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={toggleShowLink}
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 when={showLinkInput()}>
<InsertLinkForm editor={instance} onClose={toggleShowLink} />
</Show>
</div>
)}
</Show>
</div>
)
}

View File

@ -4,13 +4,13 @@ import { createEditorTransaction, useEditorHTML, useEditorIsEmpty } from 'solid-
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 { 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'
import styles from '../SimplifiedEditor.module.scss'
export const ToolbarControls = (
props: SimplifiedEditorProps & { setShouldShowLinkBubbleMenu: (x: boolean) => void }

View File

@ -0,0 +1,40 @@
import { Editor } from '@tiptap/core'
import clsx from 'clsx'
import { JSX } from 'solid-js'
import { Popover } from '~/components/_shared/Popover'
import styles from '../SimplifiedEditor.module.scss'
interface ControlProps {
editor: Editor
title: string
key: string
onChange: () => void
isActive?: (editor: Editor) => boolean
children: JSX.Element
}
export const ToolbarControl = (props: ControlProps): JSX.Element => {
const handleClick = (ev?: MouseEvent) => {
ev?.preventDefault()
ev?.stopPropagation()
props.onChange?.()
}
return (
<Popover content={props.title}>
{(triggerRef: (el: HTMLElement) => void) => (
<button
ref={triggerRef}
type="button"
class={clsx(styles.actionButton, { [styles.active]: props.editor.isActive(props.key) })}
onClick={handleClick}
>
{props.children}
</button>
)}
</Popover>
)
}
export default ToolbarControl

View File

@ -3,7 +3,7 @@ import { createEffect, createSignal, onCleanup } from 'solid-js'
import { useLocalize } from '~/context/localize'
import { validateUrl } from '~/utils/validate'
import { InlineForm } from '../InlineForm'
import { InlineForm } from '../../_shared/InlineForm'
type Props = {
editor: Editor

View File

@ -1,68 +1,21 @@
import type { Editor } from '@tiptap/core'
import Placeholder from '@tiptap/extension-placeholder'
import clsx from 'clsx'
import { type JSX, Show, createEffect, createReaction, createSignal, on, onCleanup } from 'solid-js'
import {
createEditorTransaction,
createTiptapEditor,
useEditorHTML,
useEditorIsEmpty,
useEditorIsFocused
} from 'solid-tiptap'
import { Icon } from '~/components/_shared/Icon/Icon'
import { Popover } from '~/components/_shared/Popover/Popover'
import { useLocalize } from '~/context/localize'
import { type JSX, createEffect, createSignal, on } from 'solid-js'
import { createTiptapEditor, useEditorHTML, useEditorIsEmpty, useEditorIsFocused } from 'solid-tiptap'
import { minimal } from '~/lib/editorExtensions'
import { InsertLinkForm } from '../InsertLinkForm/InsertLinkForm'
import { MicroToolbar } from '../EditorToolbar/MicroToolbar'
import styles from '../SimplifiedEditor.module.scss'
interface ControlProps {
editor: Editor
title: string
key: string
onChange: () => void
isActive?: (editor: Editor) => boolean
children: JSX.Element
}
function Control(props: ControlProps): JSX.Element {
const handleClick = (ev?: MouseEvent) => {
ev?.preventDefault()
ev?.stopPropagation()
props.onChange?.()
}
return (
<Popover content={props.title}>
{(triggerRef: (el: HTMLElement) => void) => (
<button
ref={triggerRef}
type="button"
class={clsx(styles.actionButton, { [styles.active]: props.editor.isActive(props.key) })}
onClick={handleClick}
>
{props.children}
</button>
)}
</Popover>
)
}
interface MicroEditorProps {
content?: string
onChange?: (content: string) => void
onSubmit?: (content: string) => void
placeholder?: string
}
const prevent = (e: Event) => e.preventDefault()
export const MicroEditor = (props: MicroEditorProps): JSX.Element => {
const { t } = useLocalize()
const [editorElement, setEditorElement] = createSignal<HTMLDivElement>()
const [showLinkInput, setShowLinkInput] = createSignal(false)
const [showSimpleMenu, setShowSimpleMenu] = createSignal(false)
const [toolbarElement, setToolbarElement] = createSignal<HTMLElement>()
const editor = createTiptapEditor(() => ({
element: editorElement()!,
@ -78,36 +31,10 @@ 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 html = useEditorHTML(editor)
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)))
onCleanup(() => toolbarElement()?.removeEventListener('mousedown', prevent))
return (
<div
@ -116,61 +43,9 @@ export const MicroEditor = (props: MicroEditorProps): JSX.Element => {
})}
>
<div>
<Show when={editor()} keyed>
{(instance) => (
<Show when={showSimpleMenu()}>
<div
style={{
display: 'inline-flex',
background: 'var(--editor-bubble-menu-background)',
border: '1px solid black'
}}
ref={setToolbarElement}
>
<div class={styles.controls}>
<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>
</Show>
)}
</Show>
<MicroToolbar />
<div id="micro-editor" ref={setEditorElement} style={styles.minimal} onFocusOut={storeSelection} />
<div id="micro-editor" ref={setEditorElement} style={styles.minimal} />
</div>
</div>
)

View File

@ -1,54 +1,18 @@
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, on, onCleanup } from 'solid-js'
import { createTiptapEditor, useEditorHTML } from 'solid-tiptap'
import { Toolbar } from 'terracotta'
import { Icon } from '~/components/_shared/Icon/Icon'
import { Popover } from '~/components/_shared/Popover/Popover'
import { useLocalize } from '~/context/localize'
import { useUI } from '~/context/ui'
import { type JSX, Show, createEffect, createSignal, on } from 'solid-js'
import { createEditorTransaction, createTiptapEditor, useEditorHTML } from 'solid-tiptap'
import { base } from '~/lib/editorExtensions'
import { InsertLinkForm } from '../InsertLinkForm/InsertLinkForm'
import { MiniToolbar } from '../EditorToolbar/MiniToolbar'
import styles from '../SimplifiedEditor.module.scss'
interface ControlProps {
editor: Editor
title: string
key: string
onChange: () => void
isActive?: (editor: Editor) => boolean
children: JSX.Element
}
function Control(props: ControlProps): JSX.Element {
const handleClick = (ev?: MouseEvent) => {
ev?.preventDefault()
ev?.stopPropagation()
props.onChange?.()
}
return (
<Popover content={props.title}>
{(triggerRef: (el: HTMLElement) => void) => (
<button
ref={triggerRef}
type="button"
class={clsx(styles.actionButton, { [styles.active]: props.editor.isActive(props.key) })}
onClick={handleClick}
>
{props.children}
</button>
)}
</Popover>
)
}
interface MiniEditorProps {
content?: string
onChange?: (content: string) => void
onSubmit?: (content: string) => void
onCancel?: () => void
limit?: number
placeholder?: string
}
@ -56,9 +20,6 @@ interface MiniEditorProps {
export default function MiniEditor(props: MiniEditorProps): JSX.Element {
const [editorElement, setEditorElement] = createSignal<HTMLDivElement>()
const [counter, setCounter] = createSignal(0)
const [showLinkInput, setShowLinkInput] = createSignal(false)
const { t } = useLocalize()
const { showModal } = useUI()
const editor = createTiptapEditor(() => ({
element: editorElement()!,
@ -77,7 +38,6 @@ export default function MiniEditor(props: MiniEditorProps): JSX.Element {
const html = useEditorHTML(editor)
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
@ -86,85 +46,14 @@ export default function MiniEditor(props: MiniEditorProps): JSX.Element {
content && props.onChange?.(content)
})
const handleLinkClick = () => {
setShowLinkInput(!showLinkInput())
editor()?.chain().focus().run()
}
const isFocused = createEditorTransaction(editor, (instance) => instance?.isFocused)
// Prevent focus loss when clicking inside the toolbar
const handleMouseDownOnToolbar = (event: MouseEvent) => {
event.preventDefault() // Prevent the default focus shift
}
const [toolbarElement, setToolbarElement] = createSignal<HTMLElement>()
// Attach the event handler to the toolbar
onCleanup(() => {
toolbarElement()?.removeEventListener('mousedown', handleMouseDownOnToolbar)
})
return (
<div class={clsx(styles.SimplifiedEditor, styles.bordered, styles.isFocused)}>
<div class={clsx(styles.SimplifiedEditor, styles.bordered, { [styles.isFocused]: isFocused() })}>
<div>
<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>
<MiniToolbar />
<Show when={counter() > 0}>
<small class={styles.limit}>

View File

@ -11,15 +11,15 @@ 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'
import { ToolbarControls } from './EditorToolbar/SimplifiedToolbar'
import { LinkBubbleMenuModule } from './LinkBubbleMenu'
import { TextBubbleMenu } from './TextBubbleMenu'
import { UploadModalContent } from './UploadModalContent'
import { renderUploadedImage } from './renderUploadedImage'
import styles from './SimplifiedEditor.module.scss'
import styles from './Editor.module.scss'
export type SimplifiedEditorProps = {
placeholder: string

View File

@ -11,7 +11,7 @@ import { InsertLinkForm } from '../InsertLinkForm'
import styles from './TextBubbleMenu.module.scss'
const SimplifiedEditor = lazy(() => import('../../Editor/SimplifiedEditor'))
const MiniEditor = lazy(() => import('../../Editor/MiniEditor/MiniEditor'))
type BubbleMenuProps = {
editor: Editor
@ -146,18 +146,13 @@ export const TextBubbleMenu = (props: BubbleMenuProps) => {
<InsertLinkForm editor={props.editor} onClose={handleCloseLinkForm} />
</Match>
<Match when={footnoteEditorOpen()}>
<SimplifiedEditor
maxHeight={180}
controlsAlwaysVisible={true}
imageEnabled={true}
<MiniEditor
placeholder={t('Enter footnote text')}
onSubmit={(value) => handleAddFootnote(value)}
variant={'bordered'}
initialContent={footNote()}
onSubmit={(value: string) => handleAddFootnote(value)}
content={footNote()}
onCancel={() => {
setFootnoteEditorOpen(false)
}}
submitButtonText={t('Send')}
/>
</Match>
<Match when={!(linkEditorOpen() && footnoteEditorOpen())}>

View File

@ -1,4 +1,4 @@
export { EditorComponent as Editor } from './Editor'
export { Panel } from './Panel'
export { TopicSelect } from './TopicSelect'
export { UploadModalContent } from './UploadModalContent'
export { TopicSelect } from '../TopicSelect'
export { UploadModalContent } from '../Upload/UploadModalContent'

View File

@ -10,7 +10,7 @@ import { useSession } from '~/context/session'
import { useUI } from '~/context/ui'
import { handleImageUpload } from '~/lib/handleImageUpload'
import { UploadedFile } from '~/types/upload'
import { InlineForm } from '../InlineForm'
import { InlineForm } from '../../_shared/InlineForm'
import styles from './UploadModalContent.module.scss'

View File

@ -21,14 +21,14 @@ import { LayoutType } from '~/types/common'
import { MediaItem } from '~/types/mediaitem'
import { clone } from '~/utils/clone'
import { Editor as EditorComponent, Panel } from '../../Editor'
import { AudioUploader } from '../../Editor/AudioUploader'
import { AutoSaveNotice } from '../../Editor/AutoSaveNotice'
import { VideoUploader } from '../../Editor/VideoUploader'
import { AudioUploader } from '../../Upload/AudioUploader'
import { VideoUploader } from '../../Upload/VideoUploader'
import { Modal } from '../../_shared/Modal'
import { TableOfContents } from '../../_shared/TableOfContents'
import styles from './EditView.module.scss'
const SimplifiedEditor = lazy(() => import('../../Editor/SimplifiedEditor'))
const MicroEditor = lazy(() => import('../../Editor/MicroEditor/MicroEditor'))
const GrowingTextarea = lazy(() => import('~/components/_shared/GrowingTextarea/GrowingTextarea'))
type Props = {
@ -358,13 +358,10 @@ export const EditView = (props: Props) => {
/>
</Show>
<Show when={isLeadVisible()}>
<SimplifiedEditor
variant="minimal"
hideToolbar={true}
smallHeight={true}
<MicroEditor
placeholder={t('A short introduction to keep the reader interested')}
initialContent={form.lead}
onChange={(value) => handleInputChange('lead', value)}
content={form.lead}
onChange={(value: string) => handleInputChange('lead', value)}
/>
</Show>
</Show>

View File

@ -1,6 +1,6 @@
import { useNavigate } from '@solidjs/router'
import { clsx } from 'clsx'
import { For, Show, createEffect, createMemo, createSignal, on, onMount } from 'solid-js'
import { For, Show, createEffect, createMemo, createSignal, lazy, on, onMount } from 'solid-js'
import QuotedMessage from '~/components/Inbox/QuotedMessage'
import { Icon } from '~/components/_shared/Icon'
import { InviteMembers } from '~/components/_shared/InviteMembers'
@ -17,7 +17,6 @@ import type {
} from '~/graphql/schema/chat.gen'
import type { Author } from '~/graphql/schema/core.gen'
import { getShortDate } from '~/utils/date'
import SimplifiedEditor from '../../Editor/SimplifiedEditor'
import DialogCard from '../../Inbox/DialogCard'
import DialogHeader from '../../Inbox/DialogHeader'
import { Message } from '../../Inbox/Message'
@ -26,6 +25,8 @@ import Search from '../../Inbox/Search'
import { Modal } from '../../_shared/Modal'
import styles from './Inbox.module.scss'
const MiniEditor = lazy(() => import('../../Editor/MiniEditor/MiniEditor'))
const userSearch = (array: Author[], keyword: string) => {
return array.filter((value) => new RegExp(keyword.trim(), 'gi').test(value.name || ''))
}
@ -38,7 +39,6 @@ export const InboxView = (props: { authors: Author[]; chat?: Chat }) => {
const [sortByPerToPer, setSortByPerToPer] = createSignal(false)
const [currentDialog, setCurrentDialog] = createSignal<Chat>()
const [messageToReply, setMessageToReply] = createSignal<MessageType | null>(null)
const [isClear, setClear] = createSignal(false)
const [isScrollToNewVisible, setIsScrollToNewVisible] = createSignal(false)
const { session } = useSession()
const authorId = createMemo<number>(() => session()?.user?.app_data?.profile?.id || 0)
@ -77,11 +77,9 @@ export const InboxView = (props: { authors: Author[]; chat?: Chat }) => {
reply_to: messageToReply()?.id,
chat_id: currentDialog()?.id || ''
} as MutationCreate_MessageArgs)
setClear(true)
setMessageToReply(null)
if (messagesContainerRef)
(messagesContainerRef as HTMLDivElement).scrollTop = messagesContainerRef?.scrollHeight || 0
setClear(false)
}
createEffect(
@ -291,15 +289,7 @@ export const InboxView = (props: { authors: Author[]; chat?: Chat }) => {
/>
</Show>
<div class={styles.wrapper}>
<SimplifiedEditor
smallHeight={true}
imageEnabled={true}
isCancelButtonVisible={false}
placeholder={t('New message')}
setClear={isClear()}
onSubmit={(message) => handleSubmit(message)}
submitByCtrlEnter={true}
/>
<MiniEditor placeholder={t('New message')} onSubmit={handleSubmit} />
</div>
</div>
</Show>

View File

@ -14,7 +14,6 @@ import {
onMount
} from 'solid-js'
import { createStore } from 'solid-js/store'
import SimplifiedEditor from '~/components/Editor/SimplifiedEditor'
import { useLocalize } from '~/context/localize'
import { useProfile } from '~/context/profile'
import { useSession } from '~/context/session'
@ -35,7 +34,7 @@ import { SocialNetworkInput } from '../../_shared/SocialNetworkInput'
import styles from './Settings.module.scss'
import { profileSocialLinks } from './profileSocialLinks'
// const SimplifiedEditor = lazy(() => import('~/components/Editor/SimplifiedEditor'))
const MicroEditor = lazy(() => import('../../Editor/MicroEditor/MicroEditor'))
const GrowingTextarea = lazy(() => import('~/components/_shared/GrowingTextarea/GrowingTextarea'))
function filterNulls(arr: InputMaybe<string>[]): string[] {
@ -340,18 +339,7 @@ export const ProfileSettings = () => {
/>
<h4>{t('About')}</h4>
<SimplifiedEditor
resetToInitial={true}
noLimits={true}
variant="bordered"
hideToolbar={true}
smallHeight={true}
label={t('About')}
initialContent={about() || ''}
autoFocus={false}
onChange={setAbout}
placeholder={t('About')}
/>
<MicroEditor content={about() || ''} onChange={setAbout} placeholder={t('About')} />
<div class={clsx(styles.multipleControls, 'pretty-form__item')}>
<div class={styles.multipleControlsHeader}>
<h4>{t('Social networks')}</h4>

View File

@ -18,7 +18,7 @@ import { Modal } from '../../_shared/Modal'
import stylesBeside from '../../Feed/Beside.module.scss'
import styles from './PublishSettings.module.scss'
const SimplifiedEditor = lazy(() => import('../../Editor/SimplifiedEditor'))
const MicroEditor = lazy(() => import('../../Editor/MicroEditor/MicroEditor'))
const GrowingTextarea = lazy(() => import('~/components/_shared/GrowingTextarea/GrowingTextarea'))
const DESCRIPTION_MAX_LENGTH = 400
@ -224,16 +224,10 @@ export const PublishSettings = (props: Props) => {
allowEnterKey={false}
maxLength={100}
/>
<SimplifiedEditor
variant="bordered"
hideToolbar={true}
smallHeight={true}
<MicroEditor
placeholder={t('Write a short introduction')}
label={t('Description')}
initialContent={composeDescription()}
// biome-ignore lint/suspicious/noExplicitAny: <explanation>
onChange={(value: any) => setForm('description', value)}
maxLength={DESCRIPTION_MAX_LENGTH}
content={composeDescription()}
onChange={(value?: string) => value && setForm('description', value)}
/>
</div>

View File

@ -3,7 +3,6 @@ import { clsx } from 'clsx'
import { For, Show, createEffect, createSignal, lazy, on, onMount } from 'solid-js'
import SwiperCore from 'swiper'
import { Manipulation, Navigation, Pagination } from 'swiper/modules'
import { useLocalize } from '~/context/localize'
import { useSnackbar } from '~/context/ui'
import { composeMediaItems } from '~/lib/composeMediaItems'
@ -23,7 +22,7 @@ import { MediaItem } from '~/types/mediaitem'
import { UploadedFile } from '~/types/upload'
import styles from './Swiper.module.scss'
const SimplifiedEditor = lazy(() => import('../../Editor/SimplifiedEditor'))
const MicroEditor = lazy(() => import('../../Editor/MicroEditor/MicroEditor'))
type Props = {
images: MediaItem[]
@ -316,9 +315,8 @@ export const EditorSwiper = (props: Props) => {
value={props.images[slideIndex()]?.source}
onChange={(event) => handleSlideDescriptionChange(slideIndex(), 'source', event.target.value)}
/>
<SimplifiedEditor
initialContent={props.images[slideIndex()]?.body}
smallHeight={true}
<MicroEditor
content={props.images[slideIndex()]?.body}
placeholder={t('Enter image description')}
onChange={(value) => setSlideBody(value)}
/>

View File

@ -9,3 +9,5 @@ mount(() => <StartClient />, document.getElementById('app') || document.body)
// navigator.serviceWorker.register(`/sw.js`);
// });
// }
export default {}