simplified-story-fixes
This commit is contained in:
parent
ebed7f38c3
commit
53299fc183
99
src/components/Editor/SimplifiedEditor.stories.tsx
Normal file
99
src/components/Editor/SimplifiedEditor.stories.tsx
Normal 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'
|
||||||
|
}
|
||||||
|
}
|
|
@ -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>
|
||||||
|
|
|
@ -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;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in New Issue
Block a user