bad update 2

This commit is contained in:
Untone 2024-09-15 21:47:21 +03:00
parent 344f716d1d
commit 2fad5b8db9
10 changed files with 404 additions and 55 deletions

View File

@ -0,0 +1,64 @@
import { Meta, StoryObj } from 'storybook-solidjs'
import MiniEditor from './MiniEditor'
const meta: Meta<typeof MiniEditor> = {
title: 'Components/MiniEditor',
component: MiniEditor,
argTypes: {
content: {
control: 'text',
description: 'Initial content for the editor',
defaultValue: ''
},
limit: {
control: 'number',
description: 'Character limit for the editor',
defaultValue: 500
},
placeholder: {
control: 'text',
description: 'Placeholder text when the editor is empty',
defaultValue: 'Start typing here...'
},
onChange: {
action: 'changed',
description: 'Callback when the content changes'
}
}
}
export default meta
type Story = StoryObj<typeof MiniEditor>
export const Default: Story = {
args: {
content: '',
limit: 500,
placeholder: 'Start typing here...'
}
}
export const WithInitialContent: Story = {
args: {
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

@ -0,0 +1,191 @@
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, onCleanup } from 'solid-js'
import {
createEditorTransaction,
createTiptapEditor,
useEditorHTML,
useEditorIsEmpty,
useEditorIsFocused
} from 'solid-tiptap'
import { Toolbar } from 'terracotta'
import { useLocalize } from '~/context/localize'
import { useUI } from '~/context/ui'
import { base, custom } from '~/lib/editorOptions'
import { Icon } from '../_shared/Icon/Icon'
import { Popover } from '../_shared/Popover/Popover'
import { InsertLinkForm } from './InsertLinkForm/InsertLinkForm'
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
limit?: number
placeholder?: string
}
export default function MiniEditor(props: MiniEditorProps): JSX.Element {
const [editorElement, setEditorElement] = createSignal<HTMLDivElement>()
const [counter, setCounter] = createSignal(0)
const [showLinkInput, setShowLinkInput] = createSignal(false)
const [showSimpleMenu, setShowSimpleMenu] = createSignal(false)
const { t } = useLocalize()
const { showModal } = useUI()
const editor = createTiptapEditor(() => ({
element: editorElement()!,
extensions: [
...base,
...custom,
Placeholder.configure({ emptyNodeClass: styles.emptyNode, placeholder: props.placeholder }),
CharacterCount.configure({ limit: props.limit })
],
editorProps: {
attributes: {
class: styles.simplifiedEditorField
}
},
content: props.content || ''
}))
const isEmpty = useEditorIsEmpty(editor)
const isFocused = useEditorIsFocused(editor)
const isTextSelection = createEditorTransaction(editor, (instance) => !instance?.state.selection.empty)
const html = useEditorHTML(editor)
createEffect(() => setShowSimpleMenu(isTextSelection()))
createEffect(() => {
const textLength = editor()?.getText().length || 0
setCounter(textLength)
const content = html()
content && props.onChange?.(content)
})
const handleLinkClick = () => {
setShowLinkInput(!showLinkInput())
editor()?.chain().focus().run()
}
// 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]: isEmpty() || isFocused()
})}
>
<div>
<Show when={showSimpleMenu() || showLinkInput()}>
<Toolbar style={{ 'background-color': 'white' }} 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>
</Show>
<div id="mini-editor" ref={setEditorElement} />
<Show when={counter() > 0}>
<small class={styles.limit}>
{counter()} / {props.limit || '∞'}
</small>
</Show>
</div>
</div>
)
}

View File

@ -55,7 +55,7 @@ const desktopCoverImageWidths: Record<string, number> = {
M: 600,
L: 800
}
const titleSeparator = /{!|\?|:|;}\s/
const getTitleAndSubtitle = (
article: Shout
): {
@ -69,7 +69,7 @@ const getTitleAndSubtitle = (
let titleParts = article.title?.split('. ') || []
if (titleParts?.length === 1) {
titleParts = article.title?.split(/{!|\?|:|;}\s/) || []
titleParts = article.title?.split(titleSeparator) || []
}
if (titleParts && titleParts.length > 1) {
@ -88,7 +88,7 @@ const getMainTopicTitle = (article: Shout, lng: string) => {
const mainTopicSlug = article.main_topic || ''
const mainTopic = (article.topics || []).find((tpc: Maybe<Topic>) => tpc?.slug === mainTopicSlug)
const mainTopicTitle =
mainTopicSlug && lng === 'en' ? mainTopicSlug.replace(/-/, ' ') : mainTopic?.title || ''
mainTopicSlug && lng === 'en' ? mainTopicSlug.replaceAll('-', ' ') : mainTopic?.title || ''
return [mainTopicTitle, mainTopicSlug]
}

View File

@ -33,7 +33,6 @@ export const FeedArticlePopup = (props: Props) => {
<li>
<button
class={styles.action}
role="button"
onClick={() => {
props.onShareClick()
setHidePopup(true)
@ -47,7 +46,6 @@ export const FeedArticlePopup = (props: Props) => {
<li>
<button
class={styles.action}
role="button"
onClick={() => {
alert('Help to edit')
setHidePopup(true)
@ -61,7 +59,6 @@ export const FeedArticlePopup = (props: Props) => {
<li>
<button
class={styles.action}
role="button"
onClick={() => {
props.onInviteClick()
setHidePopup(false)
@ -73,7 +70,7 @@ export const FeedArticlePopup = (props: Props) => {
</li>
<Show when={!props.canEdit}>
<li>
<button class={clsx(styles.action, styles.soon)} role="button">
<button class={clsx(styles.action, styles.soon)}>
<Icon name="bell-white" class={styles.icon} />
<div class={styles.title}>{t('Subscribe to comments')}</div>
<SoonChip />
@ -81,7 +78,7 @@ export const FeedArticlePopup = (props: Props) => {
</li>
</Show>
<li>
<button class={clsx(styles.action, styles.soon)} role="button">
<button class={clsx(styles.action, styles.soon)}>
<Icon name="bookmark" class={styles.icon} />
<div class={styles.title}>{t('Add to bookmarks')}</div>
<SoonChip />
@ -91,7 +88,7 @@ export const FeedArticlePopup = (props: Props) => {
{/* <li>*/}
{/* <button*/}
{/* class={styles.action}*/}
{/* role="button"*/}
{/* */}
{/* onClick={() => {*/}
{/* alert('Complain')*/}
{/* }}*/}
@ -103,7 +100,7 @@ export const FeedArticlePopup = (props: Props) => {
{/*<li>*/}
{/* <button*/}
{/* class={styles.action}*/}
{/* role="button"*/}
{/* */}
{/* onClick={() => {*/}
{/* alert('Get notifications')*/}
{/* }}*/}

View File

@ -35,7 +35,7 @@ export const FullTopic = (props: Props) => {
/* FIXME: use title translation*/
setTitle((_) => tpc?.title || '')
return `#${capitalize(
lang() === 'en' ? tpc.slug.replace(/-/, ' ') : tpc.title || tpc.slug.replace(/-/, ' '),
lang() === 'en' ? tpc.slug.replaceAll('-', ' ') : tpc.title || tpc.slug.replaceAll('-', ' '),
true
)}`
},
@ -44,7 +44,7 @@ export const FullTopic = (props: Props) => {
)
createEffect(() => {
if (follows?.topics?.length !== 0) {
if (follows?.topics?.length ?? true) {
const items = follows.topics || []
setFollowed(items.some((x: Topic) => x?.slug === props.topic?.slug))
}

View File

@ -56,7 +56,7 @@ export const AllAuthors = (props: Props) => {
// store by first char
const byLetterFiltered = createMemo<{ [letter: string]: Author[] }>(() => {
if (!(filteredAuthors()?.length > 0)) return {}
if (!filteredAuthors()) return {}
console.debug('[components.AllAuthors] update byLetterFiltered', filteredAuthors()?.length)
return (
filteredAuthors()?.reduce(

View File

@ -250,7 +250,7 @@ export const ProfileSecurityView = (_props: any) => {
class={clsx(
styles.socialButton,
styles.socialButtonApple,
'button' + ' button--light'
'button button--light'
)}
type="button"
>

View File

@ -14,6 +14,7 @@ import {
onMount
} from 'solid-js'
import { createStore } from 'solid-js/store'
import MiniEditor from '~/components/Editor/MiniEditor/MiniEditor'
import { useLocalize } from '~/context/localize'
import { useProfile } from '~/context/profile'
import { useSession } from '~/context/session'
@ -34,7 +35,7 @@ import { SocialNetworkInput } from '../../_shared/SocialNetworkInput'
import styles from './Settings.module.scss'
import { profileSocialLinks } from './profileSocialLinks'
const SimplifiedEditor = lazy(() => import('~/components/Editor/SimplifiedEditor'))
// const SimplifiedEditor = lazy(() => import('~/components/Editor/SimplifiedEditor'))
const GrowingTextarea = lazy(() => import('~/components/_shared/GrowingTextarea/GrowingTextarea'))
function filterNulls(arr: InputMaybe<string>[]): string[] {
@ -56,11 +57,11 @@ export const ProfileSettings = () => {
const [slugError, setSlugError] = createSignal<string>()
const [nameError, setNameError] = createSignal<string>()
const { form, submit, updateFormField, setForm } = useProfile()
const [about, setAbout] = createSignal(form.about)
const { showSnackbar } = useSnackbar()
const { loadSession, session } = useSession()
const [prevForm, setPrevForm] = createStore<ProfileInput>()
const { showConfirm } = useUI()
const [clearAbout, setClearAbout] = createSignal(false)
const { showModal, hideModal } = useUI()
const [loading, setLoading] = createSignal(true)
@ -111,6 +112,7 @@ export const ProfileSettings = () => {
try {
await submit(form)
setPrevForm(clone(form))
setAbout(form.about)
showSnackbar({ body: t('Profile successfully saved') })
} catch (error) {
if (error?.toString().search('duplicate_slug')) {
@ -132,11 +134,7 @@ export const ProfileSettings = () => {
confirmButtonVariant: 'primary',
declineButtonVariant: 'secondary'
})
if (isConfirmed) {
setClearAbout(true)
setForm(clone(prevForm))
setClearAbout(false)
}
isConfirmed && setForm(clone(prevForm))
}
const handleCropAvatar = () => {
@ -254,7 +252,7 @@ export const ProfileSettings = () => {
</Popover>
{/* @@TODO inspect popover below. onClick causes page refreshing */}
{/* <Popover content={t('Upload userpic')}>
<Popover content={t('Upload userpic')}>
{(triggerRef: (el: HTMLElement) => void) => (
<button
ref={triggerRef}
@ -264,7 +262,7 @@ export const ProfileSettings = () => {
<Icon name="user-image-black" />
</button>
)}
</Popover> */}
</Popover>
</div>
</Match>
<Match when={!form.pic}>
@ -284,6 +282,7 @@ export const ProfileSettings = () => {
)}
</p>
<div class="pretty-form__item">
<label for="nameOfUser">
<input
type="text"
name="nameOfUser"
@ -295,7 +294,8 @@ export const ProfileSettings = () => {
value={form.name || ''}
ref={(el) => (nameInputRef = el)}
/>
<label for="nameOfUser">{t('Name')}</label>
{t('Name')}
</label>
<Show when={nameError()}>
<div
style={{ position: 'absolute', 'margin-top': '-4px' }}
@ -340,17 +340,10 @@ export const ProfileSettings = () => {
/>
<h4>{t('About')}</h4>
<SimplifiedEditor
resetToInitial={clearAbout()}
noLimits={true}
variant="bordered"
onlyBubbleControls={true}
smallHeight={true}
placeholder={t('About')}
label={t('About')}
initialContent={form.about || ''}
autoFocus={false}
<MiniEditor
content={about() || ''}
onChange={(value) => updateFormField('about', value)}
placeholder={t('About')}
/>
<div class={clsx(styles.multipleControls, 'pretty-form__item')}>
<div class={styles.multipleControlsHeader}>

View File

@ -151,16 +151,6 @@ export const FollowingProvider = (props: { children: JSX.Element }) => {
setFollows((prevFollows: AuthorFollowsResult) => {
const updatedFollows = { ...prevFollows }
switch (what) {
case 'AUTHOR': {
if (value) {
if (!updatedFollows.authors?.some((author) => author.slug === slug)) {
updatedFollows.authors = [...(updatedFollows.authors || []), { slug } as Author]
}
} else {
updatedFollows.authors = updatedFollows.authors?.filter((author) => author.slug !== slug) || []
}
break
}
case 'TOPIC': {
if (value) {
if (!updatedFollows.topics?.some((topic) => topic.slug === slug)) {
@ -182,6 +172,17 @@ export const FollowingProvider = (props: { children: JSX.Element }) => {
}
break
}
// case 'AUTHOR':
default: {
if (value) {
if (!updatedFollows.authors?.some((author) => author.slug === slug)) {
updatedFollows.authors = [...(updatedFollows.authors || []), { slug } as Author]
}
} else {
updatedFollows.authors = updatedFollows.authors?.filter((author) => author.slug !== slug) || []
}
break
}
}
return updatedFollows
})

103
src/lib/editorOptions.ts Normal file
View File

@ -0,0 +1,103 @@
import { EditorOptions } from '@tiptap/core'
import Highlight from '@tiptap/extension-highlight'
import Image from '@tiptap/extension-image'
import Link from '@tiptap/extension-link'
import Underline from '@tiptap/extension-underline'
import StarterKit from '@tiptap/starter-kit'
import { CustomBlockquote } from '~/components/Editor/extensions/CustomBlockquote'
import { Figcaption } from '~/components/Editor/extensions/Figcaption'
import { Figure } from '~/components/Editor/extensions/Figure'
import { Footnote } from '~/components/Editor/extensions/Footnote'
import { Iframe } from '~/components/Editor/extensions/Iframe'
import { Span } from '~/components/Editor/extensions/Span'
import { ToggleTextWrap } from '~/components/Editor/extensions/ToggleTextWrap'
import { TrailingNode } from '~/components/Editor/extensions/TrailingNode'
// Extend the Figure extension to include Figcaption
const ImageFigure = Figure.extend({
name: 'capturedImage',
content: 'figcaption image'
})
export const base: EditorOptions['extensions'] = [
StarterKit.configure({
heading: {
levels: [2, 3, 4]
},
horizontalRule: {
HTMLAttributes: {
class: 'horizontalRule'
}
},
blockquote: undefined
}),
Underline, // не входит в StarterKit
Link.configure({
autolink: true,
openOnClick: false
}),
Image,
Highlight.configure({
multicolor: true,
HTMLAttributes: {
class: 'highlight'
}
})
]
export const custom: EditorOptions['extensions'] = [
ImageFigure,
Figure,
Figcaption,
Footnote,
CustomBlockquote,
Iframe,
Span,
ToggleTextWrap,
TrailingNode
// Добавьте другие кастомные расширения здесь
]
export const collab: EditorOptions['extensions'] = []
/*
content: '',
autofocus: false,
editable: false,
element: undefined,
injectCSS: false,
injectNonce: undefined,
editorProps: {} as EditorProps,
parseOptions: {} as EditorOptions['parseOptions'],
enableInputRules: false,
enablePasteRules: false,
enableCoreExtensions: false,
enableContentCheck: false,
onBeforeCreate: (_props: EditorEvents['beforeCreate']): void => {
throw new Error('Function not implemented.')
},
onCreate: (_props: EditorEvents['create']): void => {
throw new Error('Function not implemented.')
},
onContentError: (_props: EditorEvents['contentError']): void => {
throw new Error('Function not implemented.')
},
onUpdate: (_props: EditorEvents['update']): void => {
throw new Error('Function not implemented.')
},
onSelectionUpdate: (_props: EditorEvents['selectionUpdate']): void => {
throw new Error('Function not implemented.')
},
onTransaction: (_props: EditorEvents['transaction']): void => {
throw new Error('Function not implemented.')
},
onFocus: (_props: EditorEvents['focus']): void => {
throw new Error('Function not implemented.')
},
onBlur: (_props: EditorEvents['blur']): void => {
throw new Error('Function not implemented.')
},
onDestroy: (_props: EditorEvents['destroy']): void => {
throw new Error('Function not implemented.')
}
}
*/