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

View File

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

View File

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