simplified-story-fixes

This commit is contained in:
Untone 2024-09-15 23:17:02 +03:00
parent ebed7f38c3
commit 53299fc183
4 changed files with 148 additions and 139 deletions

View File

@ -0,0 +1,99 @@
import { Meta, StoryObj } from 'storybook-solidjs'
import SimplifiedEditor from './SimplifiedEditor'
const meta: Meta<typeof SimplifiedEditor> = {
title: 'Components/SimplifiedEditor',
component: SimplifiedEditor,
argTypes: {
placeholder: {
control: 'text',
description: 'Placeholder text when the editor is empty',
defaultValue: 'Type something...'
},
initialContent: {
control: 'text',
description: 'Initial content for the editor',
defaultValue: ''
},
maxLength: {
control: 'number',
description: 'Character limit for the editor',
defaultValue: 400
},
quoteEnabled: {
control: 'boolean',
description: 'Whether the blockquote feature is enabled',
defaultValue: true
},
imageEnabled: {
control: 'boolean',
description: 'Whether the image feature is enabled',
defaultValue: true
},
submitButtonText: {
control: 'text',
description: 'Text for the submit button',
defaultValue: 'Submit'
},
onSubmit: {
action: 'submitted',
description: 'Callback when the form is submitted'
},
onCancel: {
action: 'cancelled',
description: 'Callback when the editor is cleared'
},
onChange: {
action: 'changed',
description: 'Callback when the content changes'
}
}
}
export default meta
type Story = StoryObj<typeof SimplifiedEditor>
export const Default: Story = {
args: {
placeholder: 'Type something...',
initialContent: '',
maxLength: 400,
quoteEnabled: true,
imageEnabled: true,
submitButtonText: 'Submit'
}
}
export const WithInitialContent: Story = {
args: {
placeholder: 'Type something...',
initialContent: 'This is some initial content',
maxLength: 400,
quoteEnabled: true,
imageEnabled: true,
submitButtonText: 'Submit'
}
}
export const WithCharacterLimit: Story = {
args: {
placeholder: 'You have a 50 character limit...',
initialContent: '',
maxLength: 50,
quoteEnabled: true,
imageEnabled: true,
submitButtonText: 'Submit'
}
}
export const WithCustomPlaceholder: Story = {
args: {
placeholder: 'Custom placeholder here...',
initialContent: '',
maxLength: 400,
quoteEnabled: true,
imageEnabled: true,
submitButtonText: 'Submit'
}
}

View File

@ -1,16 +1,11 @@
import { Blockquote } from '@tiptap/extension-blockquote' import { Blockquote } from '@tiptap/extension-blockquote'
import { Bold } from '@tiptap/extension-bold'
import { BubbleMenu } from '@tiptap/extension-bubble-menu' import { BubbleMenu } from '@tiptap/extension-bubble-menu'
import { CharacterCount } from '@tiptap/extension-character-count' import { CharacterCount } from '@tiptap/extension-character-count'
import { Document } from '@tiptap/extension-document'
import { Image } from '@tiptap/extension-image' import { Image } from '@tiptap/extension-image'
import { Italic } from '@tiptap/extension-italic'
import { Link } from '@tiptap/extension-link' import { Link } from '@tiptap/extension-link'
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 { clsx } from 'clsx' import { clsx } from 'clsx'
import { Show, createEffect, createMemo, createSignal, on, onCleanup, onMount } from 'solid-js' import { Show, createEffect, createMemo, createSignal, onCleanup, onMount } from 'solid-js'
import { Portal } from 'solid-js/web' import { Portal } from 'solid-js/web'
import { import {
createEditorTransaction, createEditorTransaction,
@ -20,7 +15,6 @@ import {
useEditorIsFocused useEditorIsFocused
} from 'solid-tiptap' } from 'solid-tiptap'
import { useEditorContext } from '~/context/editor'
import { useLocalize } from '~/context/localize' import { useLocalize } from '~/context/localize'
import { UploadedFile } from '~/types/upload' import { UploadedFile } from '~/types/upload'
import { Button } from '../_shared/Button' import { Button } from '../_shared/Button'
@ -36,8 +30,10 @@ import { Figure } from './extensions/Figure'
import { Editor } from '@tiptap/core' import { Editor } from '@tiptap/core'
import { useUI } from '~/context/ui' import { useUI } from '~/context/ui'
import { base } from '~/lib/editorOptions'
import { Modal } from '../_shared/Modal/Modal' import { Modal } from '../_shared/Modal/Modal'
import styles from './SimplifiedEditor.module.scss' import styles from './SimplifiedEditor.module.scss'
import { renderUploadedImage } from './renderUploadedImage'
type Props = { type Props = {
placeholder: string placeholder: string
@ -65,6 +61,7 @@ type Props = {
} }
const DEFAULT_MAX_LENGTH = 400 const DEFAULT_MAX_LENGTH = 400
const ImageFigure = Figure.extend({ name: 'capturedImage', content: 'figcaption image' })
const SimplifiedEditor = (props: Props) => { const SimplifiedEditor = (props: Props) => {
const { t } = useLocalize() const { t } = useLocalize()
@ -73,135 +70,55 @@ const SimplifiedEditor = (props: Props) => {
const [shouldShowLinkBubbleMenu, setShouldShowLinkBubbleMenu] = createSignal(false) const [shouldShowLinkBubbleMenu, setShouldShowLinkBubbleMenu] = createSignal(false)
const isCancelButtonVisible = createMemo(() => props.isCancelButtonVisible !== false) const isCancelButtonVisible = createMemo(() => props.isCancelButtonVisible !== false)
const [editorElement, setEditorElement] = createSignal<HTMLDivElement>() const [editorElement, setEditorElement] = createSignal<HTMLDivElement>()
const { editor, setEditor } = useEditorContext() const editor = createTiptapEditor(() => ({
element: editorElement()!,
const maxLength = props.maxLength ?? DEFAULT_MAX_LENGTH extensions: [
let wrapperEditorElRef: HTMLElement | undefined ...base,
let textBubbleMenuRef: HTMLDivElement | undefined Placeholder.configure({ emptyNodeClass: styles.emptyNode, placeholder: props.placeholder }),
let linkBubbleMenuRef: HTMLDivElement | undefined CharacterCount.configure({ limit: props.noLimits ? undefined : props.maxLength }),
Link.extend({ inclusive: false }).configure({ autolink: true, openOnClick: false }),
const ImageFigure = Figure.extend({ Blockquote.configure({ HTMLAttributes: { class: styles.blockQuote } }),
name: 'capturedImage', BubbleMenu.configure({
content: 'figcaption image' pluginKey: 'textBubbleMenu',
}) element: textBubbleMenuRef(),
shouldShow: ({ view, state }) => Boolean(props.onlyBubbleControls && view.hasFocus() && !state.selection.empty)
createEffect( }),
on( BubbleMenu.configure({
() => editorElement(), pluginKey: 'linkBubbleMenu',
(ee: HTMLDivElement | undefined) => { element: linkBubbleMenuRef(),
if (ee && textBubbleMenuRef && linkBubbleMenuRef) { shouldShow: ({ state }) => !state.selection.empty && shouldShowLinkBubbleMenu(),
const freshEditor = createTiptapEditor<HTMLElement>(() => ({ tippyOptions: { placement: 'bottom' }
element: ee, }),
editorProps: { ImageFigure,
attributes: { Image,
class: styles.simplifiedEditorField Figcaption
} ],
}, editorProps: {
extensions: [ attributes: {
Document, class: styles.simplifiedEditorField
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)
} }
) },
content: props.initialContent || ''
}))
const html = useEditorHTML(() => editor()) const [textBubbleMenuRef, setTextBubbleMenuRef] = createSignal<HTMLDivElement | undefined>()
const [linkBubbleMenuRef, setLinkBubbleMenuRef] = createSignal<HTMLDivElement | undefined>()
const isEmpty = useEditorIsEmpty(editor)
const isFocused = useEditorIsFocused(editor)
const isActive = (name: string) => createEditorTransaction(editor, (ed) => ed?.isActive(name))
const html = useEditorHTML(editor)
const isBold = isActive('bold') const isBold = isActive('bold')
const isItalic = isActive('italic') const isItalic = isActive('italic')
const isLink = isActive('link') const isLink = isActive('link')
const isBlockquote = isActive('blockquote') const isBlockquote = isActive('blockquote')
const renderImage = (image: UploadedFile) => { const renderImage = (image: UploadedFile) => {
editor() renderUploadedImage(editor() as Editor, image)
?.chain()
.focus()
.insertContent({
type: 'figure',
attrs: { 'data-type': 'image' },
content: [
{
type: 'image',
attrs: { src: image.url }
},
{
type: 'figcaption',
content: [{ type: 'text', text: image.originalFilename }]
}
]
})
.run()
hideModal() hideModal()
} }
const handleClear = () => { const handleClear = () => {
if (props.onCancel) { props.onCancel?.()
props.onCancel()
}
editor()?.commands.clearContent(true) editor()?.commands.clearContent(true)
} }
@ -275,7 +192,6 @@ const SimplifiedEditor = (props: Props) => {
return ( return (
<ShowOnlyOnClient> <ShowOnlyOnClient>
<div <div
ref={(el) => (wrapperEditorElRef = el)}
class={clsx(styles.SimplifiedEditor, { class={clsx(styles.SimplifiedEditor, {
[styles.smallHeight]: props.smallHeight, [styles.smallHeight]: props.smallHeight,
[styles.minimal]: props.variant === 'minimal', [styles.minimal]: props.variant === 'minimal',
@ -285,7 +201,7 @@ const SimplifiedEditor = (props: Props) => {
})} })}
> >
<Show when={props.maxLength && editor()}> <Show when={props.maxLength && editor()}>
<div class={styles.limit}>{maxLength - counter()}</div> <div class={styles.limit}>{(props.maxLength || DEFAULT_MAX_LENGTH) - counter()}</div>
</Show> </Show>
<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>
@ -392,12 +308,12 @@ const SimplifiedEditor = (props: Props) => {
shouldShow={true} shouldShow={true}
isCommonMarkup={true} isCommonMarkup={true}
editor={editor() as Editor} editor={editor() as Editor}
ref={(el) => (textBubbleMenuRef = el)} ref={setTextBubbleMenuRef}
/> />
</Show> </Show>
<LinkBubbleMenuModule <LinkBubbleMenuModule
editor={editor() as Editor} editor={editor() as Editor}
ref={(el) => (linkBubbleMenuRef = el)} ref={setLinkBubbleMenuRef}
onClose={handleHideLinkBubble} onClose={handleHideLinkBubble}
/> />
</div> </div>

View File

@ -10,10 +10,6 @@
} }
.notificationsCounter { .notificationsCounter {
@include media-breakpoint-up(md) {
left: 1.8rem;
}
align-items: center; align-items: center;
background-color: #E84500; background-color: #E84500;
border-radius: 0.8rem; border-radius: 0.8rem;
@ -29,4 +25,8 @@
position: absolute; position: absolute;
text-align: center; text-align: center;
top: -0.5rem; top: -0.5rem;
@include media-breakpoint-up(md) {
left: 1.8rem;
}
} }

View File

@ -1,5 +1,4 @@
import { useMatch, useNavigate } from '@solidjs/router' import { useMatch, useNavigate } from '@solidjs/router'
import { Editor } from '@tiptap/core'
import type { JSX } from 'solid-js' import type { JSX } from 'solid-js'
import { Accessor, createContext, createMemo, createSignal, useContext } from 'solid-js' import { Accessor, createContext, createMemo, createSignal, useContext } from 'solid-js'
import { SetStoreFunction, createStore } from 'solid-js/store' import { SetStoreFunction, createStore } from 'solid-js/store'
@ -39,7 +38,6 @@ type EditorContextType = {
wordCounter: Accessor<WordCounter> wordCounter: Accessor<WordCounter>
form: ShoutForm form: ShoutForm
formErrors: Record<keyof ShoutForm, string> formErrors: Record<keyof ShoutForm, string>
editor: Accessor<Editor | undefined>
saveShout: (form: ShoutForm) => Promise<void> saveShout: (form: ShoutForm) => Promise<void>
saveDraft: (form: ShoutForm) => Promise<void> saveDraft: (form: ShoutForm) => Promise<void>
saveDraftToLocalStorage: (form: ShoutForm) => void saveDraftToLocalStorage: (form: ShoutForm) => void
@ -51,10 +49,9 @@ type EditorContextType = {
countWords: (value: WordCounter) => void countWords: (value: WordCounter) => void
setForm: SetStoreFunction<ShoutForm> setForm: SetStoreFunction<ShoutForm>
setFormErrors: SetStoreFunction<Record<keyof ShoutForm, string>> setFormErrors: SetStoreFunction<Record<keyof ShoutForm, string>>
setEditor: (editor: Editor) => void
} }
const EditorContext = createContext<EditorContextType>({ editor: () => new Editor() } as EditorContextType) const EditorContext = createContext<EditorContextType>({} as EditorContextType)
export function useEditorContext() { export function useEditorContext() {
return useContext(EditorContext) return useContext(EditorContext)
@ -90,7 +87,6 @@ export const EditorProvider = (props: { children: JSX.Element }) => {
const { addFeed } = useFeed() const { addFeed } = useFeed()
const snackbar = useSnackbar() const snackbar = useSnackbar()
const [isEditorPanelVisible, setIsEditorPanelVisible] = createSignal<boolean>(false) const [isEditorPanelVisible, setIsEditorPanelVisible] = createSignal<boolean>(false)
const [editor, setEditor] = createSignal<Editor>()
const [form, setForm] = createStore<ShoutForm>({ const [form, setForm] = createStore<ShoutForm>({
body: '', body: '',
slug: '', slug: '',
@ -283,14 +279,12 @@ export const EditorProvider = (props: { children: JSX.Element }) => {
countWords, countWords,
setForm, setForm,
setFormErrors, setFormErrors,
setEditor
} }
const value: EditorContextType = { const value: EditorContextType = {
...actions, ...actions,
form, form,
formErrors, formErrors,
editor,
isEditorPanelVisible, isEditorPanelVisible,
wordCounter wordCounter
} }