editor toolbar debug wip
This commit is contained in:
parent
53944193f4
commit
5ef8de018e
|
@ -2,7 +2,7 @@ import { Meta, StoryObj } from 'storybook-solidjs'
|
||||||
import { EditorComponent } from './Editor'
|
import { EditorComponent } from './Editor'
|
||||||
|
|
||||||
const meta: Meta<typeof EditorComponent> = {
|
const meta: Meta<typeof EditorComponent> = {
|
||||||
title: 'Components/Editor',
|
title: 'Editor/Editor',
|
||||||
component: EditorComponent,
|
component: EditorComponent,
|
||||||
argTypes: {
|
argTypes: {
|
||||||
content: {
|
content: {
|
||||||
|
|
|
@ -2,7 +2,7 @@ import { Meta, StoryObj } from 'storybook-solidjs'
|
||||||
import { MicroEditor } from './MicroEditor'
|
import { MicroEditor } from './MicroEditor'
|
||||||
|
|
||||||
const meta: Meta<typeof MicroEditor> = {
|
const meta: Meta<typeof MicroEditor> = {
|
||||||
title: 'Components/MicroEditor',
|
title: 'Editor/MicroEditor',
|
||||||
component: MicroEditor,
|
component: MicroEditor,
|
||||||
argTypes: {
|
argTypes: {
|
||||||
content: {
|
content: {
|
||||||
|
|
|
@ -2,7 +2,7 @@ import { Meta, StoryObj } from 'storybook-solidjs'
|
||||||
import MiniEditor from './MiniEditor'
|
import MiniEditor from './MiniEditor'
|
||||||
|
|
||||||
const meta: Meta<typeof MiniEditor> = {
|
const meta: Meta<typeof MiniEditor> = {
|
||||||
title: 'Components/MiniEditor',
|
title: 'Editor/MiniEditor',
|
||||||
component: MiniEditor,
|
component: MiniEditor,
|
||||||
argTypes: {
|
argTypes: {
|
||||||
content: {
|
content: {
|
||||||
|
|
|
@ -0,0 +1,58 @@
|
||||||
|
import { Editor } from '@tiptap/core'
|
||||||
|
import StarterKit from '@tiptap/starter-kit'
|
||||||
|
import { createSignal } from 'solid-js'
|
||||||
|
import type { Meta, StoryObj } from 'storybook-solidjs'
|
||||||
|
import { BlockquoteBubbleMenu } from './BlockquoteBubbleMenu'
|
||||||
|
|
||||||
|
const meta: Meta<typeof BlockquoteBubbleMenu> = {
|
||||||
|
title: 'Editor/Toolbar/BlockquoteBubbleMenu',
|
||||||
|
component: BlockquoteBubbleMenu,
|
||||||
|
tags: ['autodocs']
|
||||||
|
}
|
||||||
|
|
||||||
|
export default meta
|
||||||
|
type Story = StoryObj<typeof BlockquoteBubbleMenu>
|
||||||
|
|
||||||
|
const createMockEditor = () => {
|
||||||
|
return new Editor({
|
||||||
|
extensions: [StarterKit],
|
||||||
|
content: '<blockquote>Это пример цитаты</blockquote>'
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
export const Default: Story = {
|
||||||
|
render: () => {
|
||||||
|
const [editor] = createSignal(createMockEditor())
|
||||||
|
return (
|
||||||
|
<div style={{ padding: '50px' }}>
|
||||||
|
<BlockquoteBubbleMenu editor={editor()} ref={(el) => console.log('Ref:', el)} />
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const LeftAligned: Story = {
|
||||||
|
render: () => {
|
||||||
|
const [editor] = createSignal(createMockEditor())
|
||||||
|
editor().commands.setBlockQuoteFloat('left')
|
||||||
|
return (
|
||||||
|
<div style={{ padding: '50px' }}>
|
||||||
|
<BlockquoteBubbleMenu editor={editor()} ref={(el) => console.log('Ref:', el)} />
|
||||||
|
<p>Цитата выровнена по левому краю</p>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const RightAligned: Story = {
|
||||||
|
render: () => {
|
||||||
|
const [editor] = createSignal(createMockEditor())
|
||||||
|
editor().commands.setBlockQuoteFloat('right')
|
||||||
|
return (
|
||||||
|
<div style={{ padding: '50px' }}>
|
||||||
|
<BlockquoteBubbleMenu editor={editor()} ref={(el) => console.log('Ref:', el)} />
|
||||||
|
<p>Цитата выровнена по правому краю</p>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
|
@ -4,7 +4,7 @@
|
||||||
vertical-align: middle;
|
vertical-align: middle;
|
||||||
padding-top: 5px;
|
padding-top: 5px;
|
||||||
|
|
||||||
button {
|
.actionButton {
|
||||||
opacity: 0.3;
|
opacity: 0.3;
|
||||||
transition: opacity 0.3s ease-in-out;
|
transition: opacity 0.3s ease-in-out;
|
||||||
|
|
||||||
|
@ -19,7 +19,23 @@
|
||||||
position: absolute;
|
position: absolute;
|
||||||
top: -4px;
|
top: -4px;
|
||||||
min-width: 64vw;
|
min-width: 64vw;
|
||||||
|
|
||||||
|
.innerMenu {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
|
||||||
|
.icon {
|
||||||
|
opacity: 0.5;
|
||||||
|
display: block;
|
||||||
|
transition: opacity 0.3s ease-in-out;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
:global(.tippy-box) {
|
:global(.tippy-box) {
|
||||||
|
|
56
src/components/Editor/Toolbar/EditorFloatingMenu.stories.tsx
Normal file
56
src/components/Editor/Toolbar/EditorFloatingMenu.stories.tsx
Normal file
|
@ -0,0 +1,56 @@
|
||||||
|
import { Editor } from '@tiptap/core'
|
||||||
|
import StarterKit from '@tiptap/starter-kit'
|
||||||
|
import { createSignal } from 'solid-js'
|
||||||
|
import type { Meta, StoryObj } from 'storybook-solidjs'
|
||||||
|
import { EditorFloatingMenu } from './EditorFloatingMenu'
|
||||||
|
|
||||||
|
const meta: Meta<typeof EditorFloatingMenu> = {
|
||||||
|
title: 'Editor/Toolbar/EditorFloatingMenu',
|
||||||
|
component: EditorFloatingMenu,
|
||||||
|
tags: ['autodocs']
|
||||||
|
}
|
||||||
|
|
||||||
|
export default meta
|
||||||
|
type Story = StoryObj<typeof EditorFloatingMenu>
|
||||||
|
|
||||||
|
const createMockEditor = () => {
|
||||||
|
return new Editor({
|
||||||
|
extensions: [StarterKit],
|
||||||
|
content: '<p>Нажмите на кнопку "+" для открытия меню</p>'
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
export const Default: Story = {
|
||||||
|
render: () => {
|
||||||
|
const [editor] = createSignal(createMockEditor())
|
||||||
|
return (
|
||||||
|
<div style={{ padding: '50px' }}>
|
||||||
|
<EditorFloatingMenu editor={editor()} ref={(el) => console.log('Ref:', el)} />
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const OpenMenu: Story = {
|
||||||
|
render: () => {
|
||||||
|
const [editor] = createSignal(createMockEditor())
|
||||||
|
return (
|
||||||
|
<div style={{ padding: '50px' }}>
|
||||||
|
<EditorFloatingMenu editor={editor()} ref={(el) => console.log('Ref:', el)} />
|
||||||
|
<p>Меню открыто. Выберите опцию для вставки.</p>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const WithImageUpload: Story = {
|
||||||
|
render: () => {
|
||||||
|
const [editor] = createSignal(createMockEditor())
|
||||||
|
return (
|
||||||
|
<div style={{ padding: '50px' }}>
|
||||||
|
<EditorFloatingMenu editor={editor()} ref={(el) => console.log('Ref:', el)} />
|
||||||
|
<p>Нажмите на кнопку "Изображение" для открытия модального окна загрузки.</p>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
|
@ -4,12 +4,12 @@ import { UploadModalContent } from '~/components/Upload/UploadModalContent/Uploa
|
||||||
import { renderUploadedImage } from '~/components/Upload/renderUploadedImage'
|
import { renderUploadedImage } from '~/components/Upload/renderUploadedImage'
|
||||||
import { InlineForm } from '~/components/_shared/InlineForm/InlineForm'
|
import { InlineForm } from '~/components/_shared/InlineForm/InlineForm'
|
||||||
import { Modal } from '~/components/_shared/Modal/Modal'
|
import { Modal } from '~/components/_shared/Modal/Modal'
|
||||||
|
import { Popover } from '~/components/_shared/Popover/Popover'
|
||||||
import { useUI } from '~/context/ui'
|
import { useUI } from '~/context/ui'
|
||||||
import { useOutsideClickHandler } from '~/lib/useOutsideClickHandler'
|
import { useOutsideClickHandler } from '~/lib/useOutsideClickHandler'
|
||||||
import { UploadedFile } from '~/types/upload'
|
import { UploadedFile } from '~/types/upload'
|
||||||
import { useLocalize } from '../../../context/localize'
|
import { useLocalize } from '../../../context/localize'
|
||||||
import { Icon } from '../../_shared/Icon'
|
import { Icon } from '../../_shared/Icon'
|
||||||
import { Menu, type MenuItem } from './Menu/Menu'
|
|
||||||
|
|
||||||
import styles from './EditorFloatingMenu.module.scss'
|
import styles from './EditorFloatingMenu.module.scss'
|
||||||
|
|
||||||
|
@ -45,7 +45,7 @@ const validateEmbed = (value: string): boolean => {
|
||||||
export const EditorFloatingMenu = (props: FloatingMenuProps) => {
|
export const EditorFloatingMenu = (props: FloatingMenuProps) => {
|
||||||
const { t } = useLocalize()
|
const { t } = useLocalize()
|
||||||
const { showModal } = useUI()
|
const { showModal } = useUI()
|
||||||
const [selectedMenuItem, setSelectedMenuItem] = createSignal<MenuItem | undefined>()
|
const [selectedMenuItem, setSelectedMenuItem] = createSignal()
|
||||||
const [menuOpen, setMenuOpen] = createSignal<boolean>(false)
|
const [menuOpen, setMenuOpen] = createSignal<boolean>(false)
|
||||||
const [menuRef, setMenuRef] = createSignal<HTMLDivElement | undefined>()
|
const [menuRef, setMenuRef] = createSignal<HTMLDivElement | undefined>()
|
||||||
const [plusButtonRef, setPlusButtonRef] = createSignal<HTMLButtonElement | undefined>()
|
const [plusButtonRef, setPlusButtonRef] = createSignal<HTMLButtonElement | undefined>()
|
||||||
|
@ -96,6 +96,32 @@ export const EditorFloatingMenu = (props: FloatingMenuProps) => {
|
||||||
renderUploadedImage(props.editor, image)
|
renderUploadedImage(props.editor, image)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const PlusButtonMenu = () => (
|
||||||
|
<div class={styles.innerMenu}>
|
||||||
|
<Popover content={t('Add image')}>
|
||||||
|
{(triggerRef: (el: HTMLElement) => void) => (
|
||||||
|
<button ref={triggerRef} type="button" onClick={() => setSelectedMenuItem('image')}>
|
||||||
|
<Icon class={styles.icon} name="editor-image" />
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
</Popover>
|
||||||
|
<Popover content={t('Add an embed widget')}>
|
||||||
|
{(triggerRef: (el: HTMLElement) => void) => (
|
||||||
|
<button ref={triggerRef} type="button" onClick={() => setSelectedMenuItem('embed')}>
|
||||||
|
<Icon class={styles.icon} name="editor-embed" />
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
</Popover>
|
||||||
|
<Popover content={t('Add rule')}>
|
||||||
|
{(triggerRef: (el: HTMLElement) => void) => (
|
||||||
|
<button ref={triggerRef} type="button" onClick={() => setSelectedMenuItem('horizontal-rule')}>
|
||||||
|
<Icon class={styles.icon} name="editor-horizontal-rule" />
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
</Popover>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<div ref={props.ref} class={styles.editorFloatingMenu}>
|
<div ref={props.ref} class={styles.editorFloatingMenu}>
|
||||||
|
@ -105,11 +131,7 @@ export const EditorFloatingMenu = (props: FloatingMenuProps) => {
|
||||||
<Show when={menuOpen()}>
|
<Show when={menuOpen()}>
|
||||||
<div class={styles.menuHolder} ref={setMenuRef}>
|
<div class={styles.menuHolder} ref={setMenuRef}>
|
||||||
<Show when={!selectedMenuItem()}>
|
<Show when={!selectedMenuItem()}>
|
||||||
<Menu
|
<PlusButtonMenu />
|
||||||
selectedItem={(value: string) => {
|
|
||||||
setSelectedMenuItem(value as MenuItem)
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</Show>
|
</Show>
|
||||||
<Show when={selectedMenuItem() === 'embed'}>
|
<Show when={selectedMenuItem() === 'embed'}>
|
||||||
<InlineForm
|
<InlineForm
|
||||||
|
|
58
src/components/Editor/Toolbar/FigureBubbleMenu.stories.tsx
Normal file
58
src/components/Editor/Toolbar/FigureBubbleMenu.stories.tsx
Normal file
|
@ -0,0 +1,58 @@
|
||||||
|
import { Editor } from '@tiptap/core'
|
||||||
|
import Image from '@tiptap/extension-image'
|
||||||
|
import StarterKit from '@tiptap/starter-kit'
|
||||||
|
import { createSignal } from 'solid-js'
|
||||||
|
import type { Meta, StoryObj } from 'storybook-solidjs'
|
||||||
|
import { FigureBubbleMenu } from './FigureBubbleMenu'
|
||||||
|
|
||||||
|
const meta: Meta<typeof FigureBubbleMenu> = {
|
||||||
|
title: 'Editor/Toolbar/FigureBubbleMenu',
|
||||||
|
component: FigureBubbleMenu,
|
||||||
|
tags: ['autodocs']
|
||||||
|
}
|
||||||
|
|
||||||
|
export default meta
|
||||||
|
type Story = StoryObj<typeof FigureBubbleMenu>
|
||||||
|
|
||||||
|
const createMockEditor = () => {
|
||||||
|
return new Editor({
|
||||||
|
extensions: [StarterKit, Image],
|
||||||
|
content:
|
||||||
|
'<figure><img src="https://example.com/image.jpg" alt="Пример изображения" /><figcaption>Подпись к изображению</figcaption></figure>'
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
export const Default: Story = {
|
||||||
|
render: () => {
|
||||||
|
const [editor] = createSignal(createMockEditor())
|
||||||
|
return (
|
||||||
|
<div style={{ padding: '20px' }}>
|
||||||
|
<FigureBubbleMenu editor={editor()} ref={(el) => console.log('Ref:', el)} />
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const WithAlignment: Story = {
|
||||||
|
render: () => {
|
||||||
|
const [editor] = createSignal(createMockEditor())
|
||||||
|
return (
|
||||||
|
<div style={{ padding: '20px' }}>
|
||||||
|
<FigureBubbleMenu editor={editor()} ref={(el) => console.log('Ref:', el)} />
|
||||||
|
<p>Используйте кнопки выравнивания для изменения позиции изображения</p>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const WithCaption: Story = {
|
||||||
|
render: () => {
|
||||||
|
const [editor] = createSignal(createMockEditor())
|
||||||
|
return (
|
||||||
|
<div style={{ padding: '20px' }}>
|
||||||
|
<FigureBubbleMenu editor={editor()} ref={(el) => console.log('Ref:', el)} />
|
||||||
|
<p>Нажмите на кнопку "Добавить подпись" для добавления подписи к изображению</p>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
59
src/components/Editor/Toolbar/FullBubbleMenu.stores.tsx
Normal file
59
src/components/Editor/Toolbar/FullBubbleMenu.stores.tsx
Normal file
|
@ -0,0 +1,59 @@
|
||||||
|
import { Editor } from '@tiptap/core'
|
||||||
|
import StarterKit from '@tiptap/starter-kit'
|
||||||
|
import { createSignal } from 'solid-js'
|
||||||
|
import type { Meta, StoryObj } from 'storybook-solidjs'
|
||||||
|
import { FullBubbleMenu } from './FullBubbleMenu'
|
||||||
|
|
||||||
|
const meta: Meta<typeof FullBubbleMenu> = {
|
||||||
|
title: 'Editor/Toolbar/FullBubbleMenu',
|
||||||
|
component: FullBubbleMenu,
|
||||||
|
tags: ['autodocs']
|
||||||
|
}
|
||||||
|
|
||||||
|
export default meta
|
||||||
|
type Story = StoryObj<typeof FullBubbleMenu>
|
||||||
|
|
||||||
|
const createMockEditor = () => {
|
||||||
|
return new Editor({
|
||||||
|
extensions: [StarterKit],
|
||||||
|
content: '<p>Пример текста для редактирования</p>'
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
export const Default: Story = {
|
||||||
|
render: () => {
|
||||||
|
const [editor] = createSignal(createMockEditor())
|
||||||
|
const [shouldShow, setShouldShow] = createSignal(true)
|
||||||
|
return (
|
||||||
|
<div style={{ padding: '20px' }}>
|
||||||
|
<FullBubbleMenu editor={() => editor()} shouldShow={() => shouldShow()} />
|
||||||
|
<button onClick={() => setShouldShow((x) => !x)}>
|
||||||
|
{shouldShow() ? 'Скрыть меню' : 'Показать меню'}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const CommonMarkup: Story = {
|
||||||
|
render: () => {
|
||||||
|
const [editor] = createSignal(createMockEditor())
|
||||||
|
return (
|
||||||
|
<div style={{ padding: '20px' }}>
|
||||||
|
<FullBubbleMenu editor={() => editor()} shouldShow={() => true} isCommonMarkup={true} />
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const WithFigcaption: Story = {
|
||||||
|
render: () => {
|
||||||
|
const [editor] = createSignal(createMockEditor())
|
||||||
|
editor().commands.setContent('<figcaption>Подпись к изображению</figcaption>')
|
||||||
|
return (
|
||||||
|
<div style={{ padding: '20px' }}>
|
||||||
|
<FullBubbleMenu editor={() => editor()} shouldShow={() => true} />
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
|
@ -13,7 +13,7 @@ import styles from './FullBubbleMenu.module.scss'
|
||||||
|
|
||||||
type FullBubbleMenuProps = {
|
type FullBubbleMenuProps = {
|
||||||
editor: () => Editor | undefined
|
editor: () => Editor | undefined
|
||||||
ref: (el: HTMLDivElement) => void
|
ref?: (el: HTMLDivElement) => void
|
||||||
shouldShow: Accessor<boolean>
|
shouldShow: Accessor<boolean>
|
||||||
isCommonMarkup?: boolean
|
isCommonMarkup?: boolean
|
||||||
}
|
}
|
||||||
|
|
56
src/components/Editor/Toolbar/IncutBubbleMenu.stories.tsx
Normal file
56
src/components/Editor/Toolbar/IncutBubbleMenu.stories.tsx
Normal file
|
@ -0,0 +1,56 @@
|
||||||
|
import { Editor } from '@tiptap/core'
|
||||||
|
import StarterKit from '@tiptap/starter-kit'
|
||||||
|
import { createSignal } from 'solid-js'
|
||||||
|
import type { Meta, StoryObj } from 'storybook-solidjs'
|
||||||
|
import { IncutBubbleMenu } from './IncutBubbleMenu'
|
||||||
|
|
||||||
|
const meta: Meta<typeof IncutBubbleMenu> = {
|
||||||
|
title: 'Editor/Toolbar/IncutBubbleMenu',
|
||||||
|
component: IncutBubbleMenu,
|
||||||
|
tags: ['autodocs']
|
||||||
|
}
|
||||||
|
|
||||||
|
export default meta
|
||||||
|
type Story = StoryObj<typeof IncutBubbleMenu>
|
||||||
|
|
||||||
|
const createMockEditor = () => {
|
||||||
|
return new Editor({
|
||||||
|
extensions: [StarterKit],
|
||||||
|
content: '<p>Пример текста для вставки</p>'
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
export const Default: Story = {
|
||||||
|
render: () => {
|
||||||
|
const [editor] = createSignal(createMockEditor())
|
||||||
|
return (
|
||||||
|
<div style={{ padding: '20px' }}>
|
||||||
|
<IncutBubbleMenu editor={editor()} ref={(el) => console.log('Ref:', el)} />
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const WithBackgroundSelection: Story = {
|
||||||
|
render: () => {
|
||||||
|
const [editor] = createSignal(createMockEditor())
|
||||||
|
return (
|
||||||
|
<div style={{ padding: '20px' }}>
|
||||||
|
<IncutBubbleMenu editor={editor()} ref={(el) => console.log('Ref:', el)} />
|
||||||
|
<p>Нажмите на кнопку "Substrate" для выбора фона</p>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const WithFloatOptions: Story = {
|
||||||
|
render: () => {
|
||||||
|
const [editor] = createSignal(createMockEditor())
|
||||||
|
return (
|
||||||
|
<div style={{ padding: '20px' }}>
|
||||||
|
<IncutBubbleMenu editor={editor()} ref={(el) => console.log('Ref:', el)} />
|
||||||
|
<p>Используйте кнопки выравнивания для изменения позиции вставки</p>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
51
src/components/Editor/Toolbar/InsertLinkForm.stories.tsx
Normal file
51
src/components/Editor/Toolbar/InsertLinkForm.stories.tsx
Normal file
|
@ -0,0 +1,51 @@
|
||||||
|
import { Editor } from '@tiptap/core'
|
||||||
|
import Link from '@tiptap/extension-link'
|
||||||
|
import StarterKit from '@tiptap/starter-kit'
|
||||||
|
import { createSignal } from 'solid-js'
|
||||||
|
import type { Meta, StoryObj } from 'storybook-solidjs'
|
||||||
|
import { InsertLinkForm } from './InsertLinkForm'
|
||||||
|
|
||||||
|
const meta: Meta<typeof InsertLinkForm> = {
|
||||||
|
title: 'Editor/Toolbar/InsertLinkForm',
|
||||||
|
component: InsertLinkForm,
|
||||||
|
tags: ['autodocs']
|
||||||
|
}
|
||||||
|
|
||||||
|
export default meta
|
||||||
|
type Story = StoryObj<typeof InsertLinkForm>
|
||||||
|
|
||||||
|
const createMockEditor = () => {
|
||||||
|
return new Editor({
|
||||||
|
extensions: [StarterKit, Link],
|
||||||
|
content: '<p>Текст с <a href="https://example.com">ссылкой</a></p>'
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
export const Default: Story = {
|
||||||
|
render: () => {
|
||||||
|
const [editor] = createSignal(createMockEditor())
|
||||||
|
return (
|
||||||
|
<InsertLinkForm
|
||||||
|
editor={editor()}
|
||||||
|
onClose={() => console.log('Форма закрыта')}
|
||||||
|
onSubmit={(value) => console.log('Отправлено:', value)}
|
||||||
|
onRemove={() => console.log('Ссылка удалена')}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const WithInitialLink: Story = {
|
||||||
|
render: () => {
|
||||||
|
const [editor] = createSignal(createMockEditor())
|
||||||
|
editor().commands.setTextSelection({ from: 10, to: 16 })
|
||||||
|
return (
|
||||||
|
<InsertLinkForm
|
||||||
|
editor={editor()}
|
||||||
|
onClose={() => console.log('Форма закрыта')}
|
||||||
|
onSubmit={(value) => console.log('Отправлено:', value)}
|
||||||
|
onRemove={() => console.log('Ссылка удалена')}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,14 +0,0 @@
|
||||||
.Menu {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: row;
|
|
||||||
|
|
||||||
.icon {
|
|
||||||
opacity: 0.5;
|
|
||||||
display: block;
|
|
||||||
transition: opacity 0.3s ease-in-out;
|
|
||||||
|
|
||||||
&:hover {
|
|
||||||
opacity: 1;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,44 +0,0 @@
|
||||||
import { useLocalize } from '~/context/localize'
|
|
||||||
import { Icon } from '../../../_shared/Icon'
|
|
||||||
import { Popover } from '../../../_shared/Popover'
|
|
||||||
|
|
||||||
import styles from './Menu.module.scss'
|
|
||||||
|
|
||||||
export type MenuItem = 'image' | 'embed' | 'horizontal-rule'
|
|
||||||
|
|
||||||
type Props = {
|
|
||||||
selectedItem: (value: string) => void
|
|
||||||
}
|
|
||||||
|
|
||||||
export const Menu = (props: Props) => {
|
|
||||||
const { t } = useLocalize()
|
|
||||||
const setSelectedMenuItem = (value: MenuItem) => {
|
|
||||||
props.selectedItem(value)
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div class={styles.Menu}>
|
|
||||||
<Popover content={t('Add image')}>
|
|
||||||
{(triggerRef: (el: HTMLElement) => void) => (
|
|
||||||
<button ref={triggerRef} type="button" onClick={() => setSelectedMenuItem('image')}>
|
|
||||||
<Icon class={styles.icon} name="editor-image" />
|
|
||||||
</button>
|
|
||||||
)}
|
|
||||||
</Popover>
|
|
||||||
<Popover content={t('Add an embed widget')}>
|
|
||||||
{(triggerRef: (el: HTMLElement) => void) => (
|
|
||||||
<button ref={triggerRef} type="button" onClick={() => setSelectedMenuItem('embed')}>
|
|
||||||
<Icon class={styles.icon} name="editor-embed" />
|
|
||||||
</button>
|
|
||||||
)}
|
|
||||||
</Popover>
|
|
||||||
<Popover content={t('Add rule')}>
|
|
||||||
{(triggerRef: (el: HTMLElement) => void) => (
|
|
||||||
<button ref={triggerRef} type="button" onClick={() => setSelectedMenuItem('horizontal-rule')}>
|
|
||||||
<Icon class={styles.icon} name="editor-horizontal-rule" />
|
|
||||||
</button>
|
|
||||||
)}
|
|
||||||
</Popover>
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
|
@ -1 +0,0 @@
|
||||||
export { Menu } from './Menu'
|
|
35
src/components/Editor/Toolbar/MicroBubbleMenu.stories.tsx
Normal file
35
src/components/Editor/Toolbar/MicroBubbleMenu.stories.tsx
Normal file
|
@ -0,0 +1,35 @@
|
||||||
|
import { Editor } from '@tiptap/core'
|
||||||
|
import StarterKit from '@tiptap/starter-kit'
|
||||||
|
import { createSignal } from 'solid-js'
|
||||||
|
import type { Meta, StoryObj } from 'storybook-solidjs'
|
||||||
|
import { MicroBubbleMenu } from './MicroBubbleMenu'
|
||||||
|
|
||||||
|
const meta: Meta<typeof MicroBubbleMenu> = {
|
||||||
|
title: 'Editor/Toolbar/MicroBubbleMenu',
|
||||||
|
component: MicroBubbleMenu,
|
||||||
|
tags: ['autodocs']
|
||||||
|
}
|
||||||
|
|
||||||
|
export default meta
|
||||||
|
type Story = StoryObj<typeof MicroBubbleMenu>
|
||||||
|
|
||||||
|
const createMockEditor = () => {
|
||||||
|
return new Editor({
|
||||||
|
extensions: [StarterKit],
|
||||||
|
content: '<p>Выделите этот текст, чтобы увидеть MicroBubbleMenu</p>'
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
export const Default: Story = {
|
||||||
|
render: () => {
|
||||||
|
const [editor] = createSignal(createMockEditor())
|
||||||
|
return <MicroBubbleMenu editor={editor} />
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const WithoutBorders: Story = {
|
||||||
|
render: () => {
|
||||||
|
const [editor] = createSignal(createMockEditor())
|
||||||
|
return <MicroBubbleMenu editor={editor} noBorders={true} />
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,97 +0,0 @@
|
||||||
.TextBubbleMenu {
|
|
||||||
background: var(--editor-bubble-menu-background);
|
|
||||||
box-shadow: 0 4px 10px rgba(var(--default-color), 0.25);
|
|
||||||
|
|
||||||
&.growWidth {
|
|
||||||
min-width: 460px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.bubbleMenuButton {
|
|
||||||
display: inline-flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
flex-wrap: nowrap;
|
|
||||||
opacity: 0.5;
|
|
||||||
padding: 1rem;
|
|
||||||
|
|
||||||
.triangle {
|
|
||||||
margin-left: 4px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.toggleHighlight {
|
|
||||||
display: inline-block;
|
|
||||||
width: 20px;
|
|
||||||
height: 20px;
|
|
||||||
border-radius: 50%;
|
|
||||||
background: #f6e3a1;
|
|
||||||
}
|
|
||||||
|
|
||||||
img {
|
|
||||||
filter: var(--icon-filter);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.bubbleMenuButtonActive {
|
|
||||||
opacity: 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
.delimiter {
|
|
||||||
background: var(--default-color);
|
|
||||||
display: inline-block;
|
|
||||||
height: 1.4em;
|
|
||||||
margin: 0 0.2em;
|
|
||||||
vertical-align: text-bottom;
|
|
||||||
width: 1px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.dropDownHolder {
|
|
||||||
position: relative;
|
|
||||||
cursor: pointer;
|
|
||||||
display: inline-flex;
|
|
||||||
flex-flow: row nowrap;
|
|
||||||
align-items: center;
|
|
||||||
|
|
||||||
.dropDown {
|
|
||||||
position: absolute;
|
|
||||||
padding: 6px;
|
|
||||||
top: calc(100% + 8px);
|
|
||||||
left: 50%;
|
|
||||||
transform: translateX(-50%);
|
|
||||||
box-shadow: 0 4px 10px rgb(0 0 0 / 25%);
|
|
||||||
background: var(--background-color);
|
|
||||||
color: var(--default-color);
|
|
||||||
|
|
||||||
& > header {
|
|
||||||
font-size: 10px;
|
|
||||||
border-bottom: 1px solid #898c94;
|
|
||||||
}
|
|
||||||
|
|
||||||
.actions {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: flex-start;
|
|
||||||
gap: 12px;
|
|
||||||
flex-wrap: nowrap;
|
|
||||||
margin-bottom: 8px;
|
|
||||||
|
|
||||||
&:last-child {
|
|
||||||
margin-bottom: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.bubbleMenuButton {
|
|
||||||
min-width: 40px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.dropDownEnter,
|
|
||||||
.dropDownExit {
|
|
||||||
height: 0;
|
|
||||||
color: transparent;
|
|
||||||
}
|
|
||||||
|
|
||||||
.noWrap {
|
|
||||||
white-space: nowrap;
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,404 +0,0 @@
|
||||||
import type { Editor } from '@tiptap/core'
|
|
||||||
import { clsx } from 'clsx'
|
|
||||||
import { Accessor, Match, Show, Switch, createEffect, createSignal, on, onCleanup, onMount } from 'solid-js'
|
|
||||||
import { createEditorTransaction } from 'solid-tiptap'
|
|
||||||
import { useLocalize } from '../../../context/localize'
|
|
||||||
import { Icon } from '../../_shared/Icon'
|
|
||||||
import { Popover } from '../../_shared/Popover'
|
|
||||||
import { MiniEditor } from '../MiniEditor'
|
|
||||||
import { InsertLinkForm } from './InsertLinkForm'
|
|
||||||
|
|
||||||
import styles from './TextBubbleMenu.module.scss'
|
|
||||||
|
|
||||||
type BubbleMenuProps = {
|
|
||||||
editor: Editor
|
|
||||||
isCommonMarkup: boolean
|
|
||||||
ref: (el: HTMLDivElement) => void
|
|
||||||
shouldShow: Accessor<boolean>
|
|
||||||
}
|
|
||||||
|
|
||||||
export const TextBubbleMenu = (props: BubbleMenuProps) => {
|
|
||||||
const { t } = useLocalize()
|
|
||||||
|
|
||||||
const isActive = (name: string, attributes?: Record<string, unknown>) =>
|
|
||||||
createEditorTransaction(
|
|
||||||
() => props.editor,
|
|
||||||
(e) => e?.isActive(name, attributes)
|
|
||||||
)
|
|
||||||
|
|
||||||
const [textSizeBubbleOpen, setTextSizeBubbleOpen] = createSignal(false)
|
|
||||||
const [listBubbleOpen, setListBubbleOpen] = createSignal(false)
|
|
||||||
const [linkEditorOpen, setLinkEditorOpen] = createSignal(false)
|
|
||||||
const [footnoteEditorOpen, setFootnoteEditorOpen] = createSignal(false)
|
|
||||||
const [footNote, setFootNote] = createSignal<string>()
|
|
||||||
|
|
||||||
createEffect(
|
|
||||||
on(props.shouldShow, (show?: boolean) => {
|
|
||||||
if (!show) {
|
|
||||||
setFootNote()
|
|
||||||
setFootnoteEditorOpen(false)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
)
|
|
||||||
|
|
||||||
const isBold = isActive('bold')
|
|
||||||
const isItalic = isActive('italic')
|
|
||||||
const isH1 = isActive('heading', { level: 2 })
|
|
||||||
const isH2 = isActive('heading', { level: 3 })
|
|
||||||
const isH3 = isActive('heading', { level: 4 })
|
|
||||||
const isQuote = isActive('blockquote', { 'data-type': 'quote' })
|
|
||||||
const isPunchLine = isActive('blockquote', { 'data-type': 'punchline' })
|
|
||||||
const isOrderedList = isActive('isOrderedList')
|
|
||||||
const isBulletList = isActive('isBulletList')
|
|
||||||
const isLink = isActive('link')
|
|
||||||
const isHighlight = isActive('highlight')
|
|
||||||
const isFootnote = isActive('footnote')
|
|
||||||
const isIncut = isActive('article')
|
|
||||||
|
|
||||||
const toggleTextSizePopup = () => {
|
|
||||||
if (listBubbleOpen()) {
|
|
||||||
setListBubbleOpen(false)
|
|
||||||
}
|
|
||||||
setTextSizeBubbleOpen((prev) => !prev)
|
|
||||||
}
|
|
||||||
const toggleListPopup = () => {
|
|
||||||
if (textSizeBubbleOpen()) {
|
|
||||||
setTextSizeBubbleOpen(false)
|
|
||||||
}
|
|
||||||
setListBubbleOpen((prev) => !prev)
|
|
||||||
}
|
|
||||||
const handleKeyDown = (event: KeyboardEvent) => {
|
|
||||||
if (event.code === 'KeyK' && (event.metaKey || event.ctrlKey) && !props.editor.state.selection.empty) {
|
|
||||||
event.preventDefault()
|
|
||||||
setLinkEditorOpen(true)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const updateCurrentFootnoteValue = createEditorTransaction(
|
|
||||||
() => props.editor,
|
|
||||||
(ed) => {
|
|
||||||
if (!isFootnote()) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
const value = ed.getAttributes('footnote').value
|
|
||||||
setFootNote(value)
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
const handleAddFootnote = (value: string) => {
|
|
||||||
if (footNote()) {
|
|
||||||
props.editor.chain().focus().updateFootnote({ value }).run()
|
|
||||||
} else {
|
|
||||||
props.editor.chain().focus().setFootnote({ value }).run()
|
|
||||||
}
|
|
||||||
setFootNote()
|
|
||||||
setFootnoteEditorOpen(false)
|
|
||||||
}
|
|
||||||
|
|
||||||
const handleOpenFootnoteEditor = () => {
|
|
||||||
updateCurrentFootnoteValue()
|
|
||||||
setFootnoteEditorOpen(true)
|
|
||||||
}
|
|
||||||
|
|
||||||
const handleSetPunchline = () => {
|
|
||||||
if (isPunchLine()) {
|
|
||||||
props.editor.chain().focus().toggleBlockquote('punchline').run()
|
|
||||||
}
|
|
||||||
props.editor.chain().focus().toggleBlockquote('quote').run()
|
|
||||||
toggleTextSizePopup()
|
|
||||||
}
|
|
||||||
const handleSetQuote = () => {
|
|
||||||
if (isQuote()) {
|
|
||||||
props.editor.chain().focus().toggleBlockquote('quote').run()
|
|
||||||
}
|
|
||||||
props.editor.chain().focus().toggleBlockquote('punchline').run()
|
|
||||||
toggleTextSizePopup()
|
|
||||||
}
|
|
||||||
|
|
||||||
onMount(() => {
|
|
||||||
window.addEventListener('keydown', handleKeyDown)
|
|
||||||
onCleanup(() => {
|
|
||||||
window.removeEventListener('keydown', handleKeyDown)
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div ref={props.ref} class={clsx(styles.TextBubbleMenu, { [styles.growWidth]: footnoteEditorOpen() })}>
|
|
||||||
<Switch>
|
|
||||||
<Match when={linkEditorOpen()}>
|
|
||||||
<InsertLinkForm editor={props.editor} onClose={() => setLinkEditorOpen(false)} />
|
|
||||||
</Match>
|
|
||||||
<Match when={footnoteEditorOpen()}>
|
|
||||||
<MiniEditor
|
|
||||||
placeholder={t('Enter footnote text')}
|
|
||||||
onSubmit={(value) => handleAddFootnote(value)}
|
|
||||||
content={footNote()}
|
|
||||||
onCancel={() => setFootnoteEditorOpen(false)}
|
|
||||||
/>
|
|
||||||
</Match>
|
|
||||||
<Match when={!(linkEditorOpen() && footnoteEditorOpen())}>
|
|
||||||
<>
|
|
||||||
<Show when={!props.isCommonMarkup}>
|
|
||||||
<>
|
|
||||||
<div class={styles.dropDownHolder}>
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
class={clsx(styles.bubbleMenuButton, {
|
|
||||||
[styles.bubbleMenuButtonActive]: textSizeBubbleOpen()
|
|
||||||
})}
|
|
||||||
onClick={toggleTextSizePopup}
|
|
||||||
>
|
|
||||||
<Icon name="editor-text-size" />
|
|
||||||
<Icon name="down-triangle" class={styles.triangle} />
|
|
||||||
</button>
|
|
||||||
<Show when={textSizeBubbleOpen()}>
|
|
||||||
<div class={styles.dropDown}>
|
|
||||||
<header>{t('Headers')}</header>
|
|
||||||
<div class={styles.actions}>
|
|
||||||
<Popover content={t('Header 1')}>
|
|
||||||
{(triggerRef: (el: HTMLButtonElement) => void) => (
|
|
||||||
<button
|
|
||||||
ref={triggerRef}
|
|
||||||
type="button"
|
|
||||||
class={clsx(styles.bubbleMenuButton, {
|
|
||||||
[styles.bubbleMenuButtonActive]: isH1()
|
|
||||||
})}
|
|
||||||
onClick={() => {
|
|
||||||
props.editor.chain().focus().toggleHeading({ level: 2 }).run()
|
|
||||||
toggleTextSizePopup()
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<Icon name="editor-h1" />
|
|
||||||
</button>
|
|
||||||
)}
|
|
||||||
</Popover>
|
|
||||||
<Popover content={t('Header 2')}>
|
|
||||||
{(triggerRef: (el: HTMLButtonElement) => void) => (
|
|
||||||
<button
|
|
||||||
ref={triggerRef}
|
|
||||||
type="button"
|
|
||||||
class={clsx(styles.bubbleMenuButton, {
|
|
||||||
[styles.bubbleMenuButtonActive]: isH2()
|
|
||||||
})}
|
|
||||||
onClick={() => {
|
|
||||||
props.editor.chain().focus().toggleHeading({ level: 3 }).run()
|
|
||||||
toggleTextSizePopup()
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<Icon name="editor-h2" />
|
|
||||||
</button>
|
|
||||||
)}
|
|
||||||
</Popover>
|
|
||||||
<Popover content={t('Header 3')}>
|
|
||||||
{(triggerRef: (el: HTMLButtonElement) => void) => (
|
|
||||||
<button
|
|
||||||
ref={triggerRef}
|
|
||||||
type="button"
|
|
||||||
class={clsx(styles.bubbleMenuButton, {
|
|
||||||
[styles.bubbleMenuButtonActive]: isH3()
|
|
||||||
})}
|
|
||||||
onClick={() => {
|
|
||||||
props.editor.chain().focus().toggleHeading({ level: 4 }).run()
|
|
||||||
toggleTextSizePopup()
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<Icon name="editor-h3" />
|
|
||||||
</button>
|
|
||||||
)}
|
|
||||||
</Popover>
|
|
||||||
</div>
|
|
||||||
<header>{t('Quotes')}</header>
|
|
||||||
<div class={styles.actions}>
|
|
||||||
<Popover content={t('Quote')}>
|
|
||||||
{(triggerRef: (el: HTMLButtonElement) => void) => (
|
|
||||||
<button
|
|
||||||
ref={triggerRef}
|
|
||||||
type="button"
|
|
||||||
class={clsx(styles.bubbleMenuButton, {
|
|
||||||
[styles.bubbleMenuButtonActive]: isQuote()
|
|
||||||
})}
|
|
||||||
onClick={handleSetPunchline}
|
|
||||||
>
|
|
||||||
<Icon name="editor-blockquote" />
|
|
||||||
</button>
|
|
||||||
)}
|
|
||||||
</Popover>
|
|
||||||
<Popover content={t('Punchline')}>
|
|
||||||
{(triggerRef: (el: HTMLButtonElement) => void) => (
|
|
||||||
<button
|
|
||||||
ref={triggerRef}
|
|
||||||
type="button"
|
|
||||||
class={clsx(styles.bubbleMenuButton, {
|
|
||||||
[styles.bubbleMenuButtonActive]: isPunchLine()
|
|
||||||
})}
|
|
||||||
onClick={handleSetQuote}
|
|
||||||
>
|
|
||||||
<Icon name="editor-quote" />
|
|
||||||
</button>
|
|
||||||
)}
|
|
||||||
</Popover>
|
|
||||||
</div>
|
|
||||||
<header>{t('squib')}</header>
|
|
||||||
<div class={styles.actions}>
|
|
||||||
<Popover content={t('Incut')}>
|
|
||||||
{(triggerRef: (el: HTMLButtonElement) => void) => (
|
|
||||||
<button
|
|
||||||
ref={triggerRef}
|
|
||||||
type="button"
|
|
||||||
class={clsx(styles.bubbleMenuButton, {
|
|
||||||
[styles.bubbleMenuButtonActive]: isIncut()
|
|
||||||
})}
|
|
||||||
onClick={() => {
|
|
||||||
props.editor.chain().focus().toggleArticle().run()
|
|
||||||
toggleTextSizePopup()
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<Icon name="editor-squib" />
|
|
||||||
</button>
|
|
||||||
)}
|
|
||||||
</Popover>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</Show>
|
|
||||||
</div>
|
|
||||||
<div class={styles.delimiter} />
|
|
||||||
</>
|
|
||||||
</Show>
|
|
||||||
<Popover content={t('Bold')}>
|
|
||||||
{(triggerRef: (el: HTMLButtonElement) => void) => (
|
|
||||||
<button
|
|
||||||
ref={triggerRef}
|
|
||||||
type="button"
|
|
||||||
class={clsx(styles.bubbleMenuButton, {
|
|
||||||
[styles.bubbleMenuButtonActive]: isBold()
|
|
||||||
})}
|
|
||||||
onClick={() => props.editor.chain().focus().toggleBold().run()}
|
|
||||||
>
|
|
||||||
<Icon name="editor-bold" />
|
|
||||||
</button>
|
|
||||||
)}
|
|
||||||
</Popover>
|
|
||||||
<Popover content={t('Italic')}>
|
|
||||||
{(triggerRef: (el: HTMLButtonElement) => void) => (
|
|
||||||
<button
|
|
||||||
ref={triggerRef}
|
|
||||||
type="button"
|
|
||||||
class={clsx(styles.bubbleMenuButton, {
|
|
||||||
[styles.bubbleMenuButtonActive]: isItalic()
|
|
||||||
})}
|
|
||||||
onClick={() => props.editor.chain().focus().toggleItalic().run()}
|
|
||||||
>
|
|
||||||
<Icon name="editor-italic" />
|
|
||||||
</button>
|
|
||||||
)}
|
|
||||||
</Popover>
|
|
||||||
|
|
||||||
<Show when={!props.isCommonMarkup}>
|
|
||||||
<Popover content={t('Highlight')}>
|
|
||||||
{(triggerRef: (el: HTMLButtonElement) => void) => (
|
|
||||||
<button
|
|
||||||
ref={triggerRef}
|
|
||||||
type="button"
|
|
||||||
class={clsx(styles.bubbleMenuButton, {
|
|
||||||
[styles.bubbleMenuButtonActive]: isHighlight()
|
|
||||||
})}
|
|
||||||
onClick={() => props.editor.chain().focus().toggleHighlight({ color: '#f6e3a1' }).run()}
|
|
||||||
>
|
|
||||||
<div class={styles.toggleHighlight} />
|
|
||||||
</button>
|
|
||||||
)}
|
|
||||||
</Popover>
|
|
||||||
<div class={styles.delimiter} />
|
|
||||||
</Show>
|
|
||||||
<Popover content={<div class={styles.noWrap}>{t('Add url')}</div>}>
|
|
||||||
{(triggerRef: (el: HTMLButtonElement) => void) => (
|
|
||||||
<button
|
|
||||||
ref={triggerRef}
|
|
||||||
type="button"
|
|
||||||
onClick={() => setLinkEditorOpen(true)}
|
|
||||||
class={clsx(styles.bubbleMenuButton, {
|
|
||||||
[styles.bubbleMenuButtonActive]: isLink()
|
|
||||||
})}
|
|
||||||
>
|
|
||||||
<Icon name="editor-link" />
|
|
||||||
</button>
|
|
||||||
)}
|
|
||||||
</Popover>
|
|
||||||
<Show when={!props.isCommonMarkup}>
|
|
||||||
<>
|
|
||||||
<Popover content={t('Insert footnote')}>
|
|
||||||
{(triggerRef: (el: HTMLButtonElement) => void) => (
|
|
||||||
<button
|
|
||||||
ref={triggerRef}
|
|
||||||
type="button"
|
|
||||||
class={clsx(styles.bubbleMenuButton, {
|
|
||||||
[styles.bubbleMenuButtonActive]: isFootnote()
|
|
||||||
})}
|
|
||||||
onClick={handleOpenFootnoteEditor}
|
|
||||||
>
|
|
||||||
<Icon name="editor-footnote" />
|
|
||||||
</button>
|
|
||||||
)}
|
|
||||||
</Popover>
|
|
||||||
<div class={styles.delimiter} />
|
|
||||||
<div class={styles.dropDownHolder}>
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
class={clsx(styles.bubbleMenuButton, {
|
|
||||||
[styles.bubbleMenuButtonActive]: listBubbleOpen()
|
|
||||||
})}
|
|
||||||
onClick={toggleListPopup}
|
|
||||||
>
|
|
||||||
<Icon name="editor-ul" />
|
|
||||||
<Icon name="down-triangle" class={styles.triangle} />
|
|
||||||
</button>
|
|
||||||
<Show when={listBubbleOpen()}>
|
|
||||||
<div class={styles.dropDown}>
|
|
||||||
<header>{t('Lists')}</header>
|
|
||||||
<div class={styles.actions}>
|
|
||||||
<Popover content={t('Bullet list')}>
|
|
||||||
{(triggerRef: (el: HTMLButtonElement) => void) => (
|
|
||||||
<button
|
|
||||||
ref={triggerRef}
|
|
||||||
type="button"
|
|
||||||
class={clsx(styles.bubbleMenuButton, {
|
|
||||||
[styles.bubbleMenuButtonActive]: isBulletList()
|
|
||||||
})}
|
|
||||||
onClick={() => {
|
|
||||||
props.editor.chain().focus().toggleBulletList().run()
|
|
||||||
toggleListPopup()
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<Icon name="editor-ul" />
|
|
||||||
</button>
|
|
||||||
)}
|
|
||||||
</Popover>
|
|
||||||
<Popover content={t('Ordered list')}>
|
|
||||||
{(triggerRef: (el: HTMLButtonElement) => void) => (
|
|
||||||
<button
|
|
||||||
ref={triggerRef}
|
|
||||||
type="button"
|
|
||||||
class={clsx(styles.bubbleMenuButton, {
|
|
||||||
[styles.bubbleMenuButtonActive]: isOrderedList()
|
|
||||||
})}
|
|
||||||
onClick={() => {
|
|
||||||
props.editor.chain().focus().toggleOrderedList().run()
|
|
||||||
toggleListPopup()
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<Icon name="editor-ol" />
|
|
||||||
</button>
|
|
||||||
)}
|
|
||||||
</Popover>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</Show>
|
|
||||||
</div>
|
|
||||||
</>
|
|
||||||
</Show>
|
|
||||||
</>
|
|
||||||
</Match>
|
|
||||||
</Switch>
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
66
src/components/Editor/Toolbar/ToolbarControl.stories.tsx
Normal file
66
src/components/Editor/Toolbar/ToolbarControl.stories.tsx
Normal file
|
@ -0,0 +1,66 @@
|
||||||
|
import { Editor } from '@tiptap/core'
|
||||||
|
import StarterKit from '@tiptap/starter-kit'
|
||||||
|
import { createSignal } from 'solid-js'
|
||||||
|
import type { Meta, StoryObj } from 'storybook-solidjs'
|
||||||
|
import { Icon } from '~/components/_shared/Icon/Icon'
|
||||||
|
import { ToolbarControl } from './ToolbarControl'
|
||||||
|
|
||||||
|
const meta: Meta<typeof ToolbarControl> = {
|
||||||
|
title: 'Editor/Toolbar/ToolbarControl',
|
||||||
|
component: ToolbarControl,
|
||||||
|
tags: ['autodocs']
|
||||||
|
}
|
||||||
|
|
||||||
|
export default meta
|
||||||
|
type Story = StoryObj<typeof ToolbarControl>
|
||||||
|
|
||||||
|
const createMockEditor = () => {
|
||||||
|
return new Editor({
|
||||||
|
extensions: [StarterKit],
|
||||||
|
content: '<p>Пример текста</p>'
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
export const Default: Story = {
|
||||||
|
render: () => {
|
||||||
|
const [editor] = createSignal(createMockEditor())
|
||||||
|
return (
|
||||||
|
<ToolbarControl
|
||||||
|
editor={editor()}
|
||||||
|
caption="Жирный"
|
||||||
|
key="bold"
|
||||||
|
onChange={() => console.log('Изменено')}
|
||||||
|
>
|
||||||
|
<Icon name="editor-bold" />
|
||||||
|
</ToolbarControl>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const Active: Story = {
|
||||||
|
render: () => {
|
||||||
|
const [editor] = createSignal(createMockEditor())
|
||||||
|
return (
|
||||||
|
<ToolbarControl
|
||||||
|
editor={editor()}
|
||||||
|
caption="Жирный"
|
||||||
|
key="bold"
|
||||||
|
onChange={() => console.log('Изменено')}
|
||||||
|
isActive={() => true}
|
||||||
|
>
|
||||||
|
<Icon name="editor-bold" />
|
||||||
|
</ToolbarControl>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const WithoutCaption: Story = {
|
||||||
|
render: () => {
|
||||||
|
const [editor] = createSignal(createMockEditor())
|
||||||
|
return (
|
||||||
|
<ToolbarControl editor={editor()} key="bold" onChange={() => console.log('Изменено')}>
|
||||||
|
<Icon name="editor-bold" />
|
||||||
|
</ToolbarControl>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
43
src/components/_shared/Popover/Popover.stoires.tsx
Normal file
43
src/components/_shared/Popover/Popover.stoires.tsx
Normal file
|
@ -0,0 +1,43 @@
|
||||||
|
import type { Meta, StoryObj } from 'storybook-solidjs'
|
||||||
|
import { Popover } from './Popover'
|
||||||
|
|
||||||
|
const meta: Meta<typeof Popover> = {
|
||||||
|
title: 'atoms/Popover',
|
||||||
|
component: Popover,
|
||||||
|
tags: ['autodocs']
|
||||||
|
}
|
||||||
|
|
||||||
|
export default meta
|
||||||
|
|
||||||
|
type Story = StoryObj<typeof Popover>
|
||||||
|
|
||||||
|
export const Default: Story = {
|
||||||
|
render: () => (
|
||||||
|
<Popover content="Это содержимое всплывающей подсказки">
|
||||||
|
{(setTooltipEl) => <button ref={setTooltipEl}>Наведите курсор</button>}
|
||||||
|
</Popover>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export const WithJSXContent: Story = {
|
||||||
|
render: () => (
|
||||||
|
<Popover
|
||||||
|
content={
|
||||||
|
<div>
|
||||||
|
<h3>Заголовок подсказки</h3>
|
||||||
|
<p>Это более сложное содержимое подсказки</p>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
>
|
||||||
|
{(setTooltipEl) => <button ref={setTooltipEl}>Наведите курсор для JSX контента</button>}
|
||||||
|
</Popover>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export const Disabled: Story = {
|
||||||
|
render: () => (
|
||||||
|
<Popover content="Эта подсказка не появится" disabled={true}>
|
||||||
|
{(setTooltipEl) => <button ref={setTooltipEl}>Подсказка отключена</button>}
|
||||||
|
</Popover>
|
||||||
|
)
|
||||||
|
}
|
Loading…
Reference in New Issue
Block a user