parent
2ef8da6da5
commit
5a1699aa87
|
@ -34,4 +34,4 @@
|
|||
<polygon points="518.166,175.496 518.166,174.5 518.166,155.996 537.666,155.996 537.666,136.497 453.375,136.497 453.375,175.498 518.166,175.499 "/>
|
||||
<polygon points="576.666,175.496 537.83,175.496 537.83,175.5 537.836,175.5 537.355,321.503 555.168,321.503 555.168,281.752 576.8,281.752 577.149,175.496 "/>
|
||||
<polygon points="331.398,382.649 281,405.94 281,410.753 331.492,387.418 331.492,382.854 "/>
|
||||
</svg>
|
||||
</svg>
|
||||
|
|
Before Width: | Height: | Size: 3.0 KiB After Width: | Height: | Size: 3.0 KiB |
3
public/icons/editor-image-half-align-left.svg
Normal file
3
public/icons/editor-image-half-align-left.svg
Normal file
|
@ -0,0 +1,3 @@
|
|||
<svg width="22" height="22" viewBox="0 0 22 22" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M0 2.44444V0H22V2.44444H0ZM0 13H11V4H0V13ZM13 5H22V7H13V5ZM22 15V17H0V15H22ZM22 12H13V10H22V12ZM22 21.9999V19.5554H0V21.9999H22Z" fill="#ffffff"/>
|
||||
</svg>
|
After Width: | Height: | Size: 299 B |
3
public/icons/editor-image-half-align-right.svg
Normal file
3
public/icons/editor-image-half-align-right.svg
Normal file
|
@ -0,0 +1,3 @@
|
|||
<svg width="22" height="22" viewBox="0 0 22 22" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M22 2.44444V0H0V2.44444H22ZM22 13H11V4H22V13ZM9 5H0V7H9V5ZM0 15V17H22V15H0ZM0 12H9V10H0V12ZM0 21.9999V19.5554H22V21.9999H0Z" fill="#ffffff"/>
|
||||
</svg>
|
After Width: | Height: | Size: 294 B |
3
public/icons/editor-squib.svg
Normal file
3
public/icons/editor-squib.svg
Normal file
|
@ -0,0 +1,3 @@
|
|||
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M0 1.77779V0H16.0001V1.77779H0ZM10.9092 9.4546H5.09094V2.90911H10.9092V9.4546ZM12.3637 3.63638H16.0001V5.09094H12.3637V3.63638ZM0 10.9092H16.0001V12.3637H0V10.9092ZM3.63638 3.63638H0V5.09094H3.63638V3.63638ZM12.3637 8.72732H16.0001V7.27277H12.3637V8.72732ZM3.63638 8.72732H0V7.27277H3.63638V8.72732ZM0 16H16.0001V14.2222H0V16Z" fill="#898C94"/>
|
||||
</svg>
|
After Width: | Height: | Size: 497 B |
|
@ -295,5 +295,7 @@
|
|||
"number list": "number list",
|
||||
"delimiter": "delimiter",
|
||||
"cancel_low_caps": "cancel",
|
||||
"repeat": "repeat"
|
||||
"repeat": "repeat",
|
||||
"Add signature": "Add signature",
|
||||
"Substrate": "Substrate"
|
||||
}
|
||||
|
|
|
@ -300,6 +300,7 @@
|
|||
"sign up": "зарегистрироваться",
|
||||
"sign up or sign in": "зарегистрироваться или войти",
|
||||
"slug is used by another user": "Имя уже занято другим пользователем",
|
||||
"squib": "Подверстка",
|
||||
"terms of use": "правилами пользования сайтом",
|
||||
"topics": "темы",
|
||||
"user already exist": "пользователь уже существует",
|
||||
|
@ -316,5 +317,7 @@
|
|||
"number list": "нумер. список",
|
||||
"delimiter": "разделитель",
|
||||
"cancel_low_caps": "отменить",
|
||||
"repeat": "повторить"
|
||||
"repeat": "повторить",
|
||||
"Add signature": "Добавить подпись",
|
||||
"Substrate": "Подложка"
|
||||
}
|
||||
|
|
|
@ -372,15 +372,3 @@ img {
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
[data-float] {
|
||||
max-width: 50%;
|
||||
}
|
||||
|
||||
[data-float='left'] {
|
||||
float: left;
|
||||
}
|
||||
|
||||
[data-float='right'] {
|
||||
float: right;
|
||||
}
|
||||
|
|
40
src/components/Editor/BubbleMenu/BlockquoteBubbleMenu.tsx
Normal file
40
src/components/Editor/BubbleMenu/BlockquoteBubbleMenu.tsx
Normal file
|
@ -0,0 +1,40 @@
|
|||
import type { Editor } from '@tiptap/core'
|
||||
import styles from './FigureBubbleMenu.module.scss'
|
||||
import { clsx } from 'clsx'
|
||||
import { Icon } from '../../_shared/Icon'
|
||||
import { useLocalize } from '../../../context/localize'
|
||||
|
||||
type Props = {
|
||||
editor: Editor
|
||||
ref: (el: HTMLElement) => void
|
||||
}
|
||||
|
||||
export const BlockquoteBubbleMenu = (props: Props) => {
|
||||
return (
|
||||
<div ref={props.ref} class={styles.FigureBubbleMenu}>
|
||||
<button
|
||||
type="button"
|
||||
class={clsx(styles.bubbleMenuButton)}
|
||||
onClick={() => {
|
||||
props.editor.chain().focus().setBlockQuoteFloat('left').run()
|
||||
}}
|
||||
>
|
||||
<Icon name="editor-image-align-left" />
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
class={clsx(styles.bubbleMenuButton)}
|
||||
onClick={() => props.editor.chain().focus().setBlockQuoteFloat(null).run()}
|
||||
>
|
||||
<Icon name="editor-image-align-center" />
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
class={clsx(styles.bubbleMenuButton)}
|
||||
onClick={() => props.editor.chain().focus().setBlockQuoteFloat('right').run()}
|
||||
>
|
||||
<Icon name="editor-image-align-right" />
|
||||
</button>
|
||||
</div>
|
||||
)
|
||||
}
|
107
src/components/Editor/BubbleMenu/FigureBubbleMenu.module.scss
Normal file
107
src/components/Editor/BubbleMenu/FigureBubbleMenu.module.scss
Normal file
|
@ -0,0 +1,107 @@
|
|||
.FigureBubbleMenu {
|
||||
background: #000;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
|
||||
.bubbleMenuButton {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
flex-wrap: nowrap;
|
||||
opacity: 0.5;
|
||||
padding: 1rem;
|
||||
|
||||
.triangle {
|
||||
margin-left: 4px;
|
||||
}
|
||||
|
||||
img {
|
||||
display: block;
|
||||
}
|
||||
}
|
||||
|
||||
.bubbleMenuButtonActive {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.delimiter {
|
||||
background: #fff;
|
||||
opacity: 0.5;
|
||||
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: #000;
|
||||
color: #898c94;
|
||||
|
||||
& > header {
|
||||
font-size: 10px;
|
||||
border-bottom: 1px solid #898c94;
|
||||
}
|
||||
|
||||
.actions {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(3, 1fr);
|
||||
grid-gap: 10px;
|
||||
margin-bottom: 8px;
|
||||
|
||||
.color {
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
border-radius: 50%;
|
||||
border: 2px solid #3c3c3c;
|
||||
background: #ccc;
|
||||
|
||||
&.yellow {
|
||||
background: #f6e3a1;
|
||||
}
|
||||
&.white {
|
||||
background: #fff;
|
||||
box-shadow: inset 0 0 0 1px #000;
|
||||
border-color: #fff;
|
||||
}
|
||||
&.yellow {
|
||||
background: #f6e3a1;
|
||||
}
|
||||
&.pink {
|
||||
background: #f1b5bc;
|
||||
}
|
||||
&.green {
|
||||
background: #bfe9cb;
|
||||
box-shadow: inset 0 0 0 1px #000;
|
||||
border-color: #fff;
|
||||
}
|
||||
&.black {
|
||||
background: #000;
|
||||
}
|
||||
}
|
||||
|
||||
&:last-child {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.bubbleMenuButton {
|
||||
min-width: 40px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,43 +1,40 @@
|
|||
import type { Editor } from '@tiptap/core'
|
||||
import styles from './ImageBubbleMenu.module.scss'
|
||||
import styles from './FigureBubbleMenu.module.scss'
|
||||
import { clsx } from 'clsx'
|
||||
import { Icon } from '../../_shared/Icon'
|
||||
import { useLocalize } from '../../../context/localize'
|
||||
|
||||
type BubbleMenuProps = {
|
||||
type Props = {
|
||||
editor: Editor
|
||||
ref: (el: HTMLDivElement) => void
|
||||
ref: (el: HTMLElement) => void
|
||||
}
|
||||
|
||||
export const ImageBubbleMenu = (props: BubbleMenuProps) => {
|
||||
export const FigureBubbleMenu = (props: Props) => {
|
||||
const { t } = useLocalize()
|
||||
return (
|
||||
<div ref={props.ref} class={styles.ImageBubbleMenu}>
|
||||
<div ref={props.ref} class={styles.FigureBubbleMenu}>
|
||||
<button
|
||||
type="button"
|
||||
class={clsx(styles.bubbleMenuButton)}
|
||||
onClick={() => {
|
||||
props.editor.chain().focus().setFloat('left').run()
|
||||
}}
|
||||
onClick={() => props.editor.chain().focus().setImageFloat('left').run()}
|
||||
>
|
||||
<Icon name="editor-image-align-left" />
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
class={clsx(styles.bubbleMenuButton)}
|
||||
onClick={() => {
|
||||
props.editor.chain().focus().setFloat(null).run()
|
||||
}}
|
||||
onClick={() => props.editor.chain().focus().setImageFloat(null).run()}
|
||||
>
|
||||
<Icon name="editor-image-align-center" />
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
class={clsx(styles.bubbleMenuButton)}
|
||||
onClick={() => {
|
||||
props.editor.chain().focus().setFloat('right').run()
|
||||
}}
|
||||
onClick={() => props.editor.chain().focus().setImageFloat('right').run()}
|
||||
>
|
||||
<Icon name="editor-image-align-right" />
|
||||
</button>
|
||||
|
||||
<div class={styles.delimiter} />
|
||||
<button
|
||||
type="button"
|
||||
|
@ -46,7 +43,7 @@ export const ImageBubbleMenu = (props: BubbleMenuProps) => {
|
|||
props.editor.chain().focus().imageToFigure().run()
|
||||
}}
|
||||
>
|
||||
<span style={{ color: 'white' }}>Добавить подпись</span>
|
||||
<span style={{ color: 'white' }}>{t('Add signature')}</span>
|
||||
</button>
|
||||
<div class={styles.delimiter} />
|
||||
<button type="button" class={clsx(styles.bubbleMenuButton)}>
|
85
src/components/Editor/BubbleMenu/IncutBubbleMenu.tsx
Normal file
85
src/components/Editor/BubbleMenu/IncutBubbleMenu.tsx
Normal file
|
@ -0,0 +1,85 @@
|
|||
import { createSignal, Show, For } from 'solid-js'
|
||||
import type { Editor } from '@tiptap/core'
|
||||
import styles from './FigureBubbleMenu.module.scss'
|
||||
import { clsx } from 'clsx'
|
||||
import { Icon } from '../../_shared/Icon'
|
||||
import { useLocalize } from '../../../context/localize'
|
||||
|
||||
type Props = {
|
||||
editor: Editor
|
||||
ref: (el: HTMLElement) => void
|
||||
}
|
||||
|
||||
const backgrounds = [null, 'white', 'black', 'yellow', 'pink', 'green']
|
||||
|
||||
export const IncutBubbleMenu = (props: Props) => {
|
||||
const { t } = useLocalize()
|
||||
const [substratBubbleOpen, setSubstratBubbleOpen] = createSignal(false)
|
||||
return (
|
||||
<div ref={props.ref} class={styles.FigureBubbleMenu}>
|
||||
<button
|
||||
type="button"
|
||||
class={clsx(styles.bubbleMenuButton)}
|
||||
onClick={() => props.editor.chain().focus().setArticleFloat('left').run()}
|
||||
>
|
||||
<Icon name="editor-image-align-left" />
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
class={clsx(styles.bubbleMenuButton)}
|
||||
onClick={() => props.editor.chain().focus().setArticleFloat('half-left').run()}
|
||||
>
|
||||
<Icon name="editor-image-half-align-left" />
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
class={clsx(styles.bubbleMenuButton)}
|
||||
onClick={() => props.editor.chain().focus().setArticleFloat(null).run()}
|
||||
>
|
||||
<Icon name="editor-image-align-center" />
|
||||
</button>
|
||||
|
||||
<button
|
||||
type="button"
|
||||
class={clsx(styles.bubbleMenuButton)}
|
||||
onClick={() => props.editor.chain().focus().setArticleFloat('half-right').run()}
|
||||
>
|
||||
<Icon name="editor-image-half-align-right" />
|
||||
</button>
|
||||
|
||||
<button
|
||||
type="button"
|
||||
class={clsx(styles.bubbleMenuButton)}
|
||||
onClick={() => props.editor.chain().focus().setArticleFloat('right').run()}
|
||||
>
|
||||
<Icon name="editor-image-align-right" />
|
||||
</button>
|
||||
|
||||
<div class={styles.delimiter} />
|
||||
<div class={styles.dropDownHolder}>
|
||||
<button
|
||||
type="button"
|
||||
class={clsx(styles.bubbleMenuButton)}
|
||||
onClick={() => setSubstratBubbleOpen(!substratBubbleOpen())}
|
||||
>
|
||||
<span style={{ color: 'white' }}>{t('Substrate')}</span>
|
||||
<Icon name="down-triangle" class={styles.triangle} />
|
||||
</button>
|
||||
<Show when={!substratBubbleOpen()}>
|
||||
<div class={styles.dropDown}>
|
||||
<div class={styles.actions}>
|
||||
<For each={backgrounds}>
|
||||
{(bg) => (
|
||||
<div
|
||||
onClick={() => props.editor.chain().focus().setArticleBg(bg).run()}
|
||||
class={clsx(styles.color, styles[bg])}
|
||||
/>
|
||||
)}
|
||||
</For>
|
||||
</div>
|
||||
</div>
|
||||
</Show>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
3
src/components/Editor/BubbleMenu/index.ts
Normal file
3
src/components/Editor/BubbleMenu/index.ts
Normal file
|
@ -0,0 +1,3 @@
|
|||
export { FigureBubbleMenu } from './FigureBubbleMenu'
|
||||
export { BlockquoteBubbleMenu } from './BlockquoteBubbleMenu'
|
||||
export { IncutBubbleMenu } from './IncutBubbleMenu'
|
|
@ -1,7 +1,6 @@
|
|||
import { createEffect, createSignal } from 'solid-js'
|
||||
import { createTiptapEditor, useEditorHTML } from 'solid-tiptap'
|
||||
import { useLocalize } from '../../context/localize'
|
||||
import { Blockquote } from '@tiptap/extension-blockquote'
|
||||
import { Bold } from '@tiptap/extension-bold'
|
||||
import { BubbleMenu } from '@tiptap/extension-bubble-menu'
|
||||
import { Dropcursor } from '@tiptap/extension-dropcursor'
|
||||
|
@ -23,26 +22,27 @@ import { Link } from '@tiptap/extension-link'
|
|||
import { Document } from '@tiptap/extension-document'
|
||||
import { Text } from '@tiptap/extension-text'
|
||||
import { CustomImage } from './extensions/CustomImage'
|
||||
import { CustomBlockquote } from './extensions/CustomBlockquote'
|
||||
import { Figure } from './extensions/Figure'
|
||||
import { Paragraph } from '@tiptap/extension-paragraph'
|
||||
import Focus from '@tiptap/extension-focus'
|
||||
import * as Y from 'yjs'
|
||||
import { CollaborationCursor } from '@tiptap/extension-collaboration-cursor'
|
||||
import { Collaboration } from '@tiptap/extension-collaboration'
|
||||
|
||||
import { IndexeddbPersistence } from 'y-indexeddb'
|
||||
import { useSession } from '../../context/session'
|
||||
import uniqolor from 'uniqolor'
|
||||
import { HocuspocusProvider } from '@hocuspocus/provider'
|
||||
import { Embed } from './extensions/Embed'
|
||||
import { TextBubbleMenu } from './TextBubbleMenu'
|
||||
import { ImageBubbleMenu } from './ImageBubbleMenu'
|
||||
import { FigureBubbleMenu, BlockquoteBubbleMenu, IncutBubbleMenu } from './BubbleMenu'
|
||||
import { EditorFloatingMenu } from './EditorFloatingMenu'
|
||||
import { useEditorContext } from '../../context/editor'
|
||||
import { isTextSelection } from '@tiptap/core'
|
||||
import type { Doc } from 'yjs/dist/src/utils/Doc'
|
||||
import './Prosemirror.scss'
|
||||
import { TrailingNode } from './extensions/TrailingNode'
|
||||
import Article from './extensions/Article'
|
||||
|
||||
type EditorProps = {
|
||||
shoutId: number
|
||||
|
@ -58,6 +58,7 @@ export const Editor = (props: EditorProps) => {
|
|||
const { t } = useLocalize()
|
||||
const { user } = useSession()
|
||||
const [isCommonMarkup, setIsCommonMarkup] = createSignal(false)
|
||||
const [floatMenuRef, setFloatMenuRef] = createSignal<'blockquote' | 'image' | 'incut'>()
|
||||
|
||||
const docName = `shout-${props.shoutId}`
|
||||
|
||||
|
@ -89,8 +90,18 @@ export const Editor = (props: EditorProps) => {
|
|||
current: null
|
||||
}
|
||||
|
||||
const imageBubbleMenuRef: {
|
||||
current: HTMLDivElement
|
||||
const incutBubbleMenuRef: {
|
||||
current: HTMLElement
|
||||
} = {
|
||||
current: null
|
||||
}
|
||||
const figureBubbleMenuRef: {
|
||||
current: HTMLElement
|
||||
} = {
|
||||
current: null
|
||||
}
|
||||
const blockquoteBubbleMenuRef: {
|
||||
current: HTMLElement
|
||||
} = {
|
||||
current: null
|
||||
}
|
||||
|
@ -108,7 +119,7 @@ export const Editor = (props: EditorProps) => {
|
|||
Text,
|
||||
Paragraph,
|
||||
Dropcursor,
|
||||
Blockquote,
|
||||
CustomBlockquote,
|
||||
Bold,
|
||||
Italic,
|
||||
Strike,
|
||||
|
@ -163,16 +174,36 @@ export const Editor = (props: EditorProps) => {
|
|||
shouldShow: ({ editor: e, view, state, from, to }) => {
|
||||
const { doc, selection } = state
|
||||
const { empty } = selection
|
||||
|
||||
const isEmptyTextBlock = doc.textBetween(from, to).length === 0 && isTextSelection(selection)
|
||||
|
||||
setIsCommonMarkup(e.isActive('figure'))
|
||||
return view.hasFocus() && !empty && !isEmptyTextBlock && !e.isActive('image')
|
||||
return (
|
||||
view.hasFocus() &&
|
||||
!empty &&
|
||||
!isEmptyTextBlock &&
|
||||
!e.isActive('image') &&
|
||||
!e.isActive('blockquote') &&
|
||||
!e.isActive('article')
|
||||
)
|
||||
}
|
||||
}),
|
||||
BubbleMenu.configure({
|
||||
pluginKey: 'blockquoteBubbleMenu',
|
||||
element: blockquoteBubbleMenuRef.current,
|
||||
shouldShow: ({ editor: e, view }) => {
|
||||
return view.hasFocus() && e.isActive('blockquote')
|
||||
}
|
||||
}),
|
||||
BubbleMenu.configure({
|
||||
pluginKey: 'incutBubbleMenu',
|
||||
element: incutBubbleMenuRef.current,
|
||||
shouldShow: ({ editor: e, view }) => {
|
||||
return view.hasFocus() && e.isActive('article')
|
||||
}
|
||||
}),
|
||||
BubbleMenu.configure({
|
||||
pluginKey: 'imageBubbleMenu',
|
||||
element: imageBubbleMenuRef.current,
|
||||
element: figureBubbleMenuRef.current,
|
||||
shouldShow: ({ editor: e, view }) => {
|
||||
return view.hasFocus() && e.isActive('image')
|
||||
}
|
||||
|
@ -183,7 +214,8 @@ export const Editor = (props: EditorProps) => {
|
|||
},
|
||||
element: floatingMenuRef.current
|
||||
}),
|
||||
TrailingNode
|
||||
TrailingNode,
|
||||
Article
|
||||
]
|
||||
}))
|
||||
|
||||
|
@ -213,7 +245,24 @@ export const Editor = (props: EditorProps) => {
|
|||
editor={editor()}
|
||||
ref={(el) => (textBubbleMenuRef.current = el)}
|
||||
/>
|
||||
<ImageBubbleMenu editor={editor()} ref={(el) => (imageBubbleMenuRef.current = el)} />
|
||||
<BlockquoteBubbleMenu
|
||||
ref={(el) => {
|
||||
blockquoteBubbleMenuRef.current = el
|
||||
}}
|
||||
editor={editor()}
|
||||
/>
|
||||
<FigureBubbleMenu
|
||||
editor={editor()}
|
||||
ref={(el) => {
|
||||
figureBubbleMenuRef.current = el
|
||||
}}
|
||||
/>
|
||||
<IncutBubbleMenu
|
||||
editor={editor()}
|
||||
ref={(el) => {
|
||||
incutBubbleMenuRef.current = el
|
||||
}}
|
||||
/>
|
||||
<EditorFloatingMenu editor={editor()} ref={(el) => (floatingMenuRef.current = el)} />
|
||||
</>
|
||||
)
|
||||
|
|
|
@ -1,33 +0,0 @@
|
|||
.ImageBubbleMenu {
|
||||
background: #000;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
|
||||
.bubbleMenuButton {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
flex-wrap: nowrap;
|
||||
opacity: 0.5;
|
||||
padding: 1rem;
|
||||
|
||||
img {
|
||||
display: block;
|
||||
}
|
||||
}
|
||||
|
||||
.bubbleMenuButtonActive {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.delimiter {
|
||||
background: #fff;
|
||||
opacity: 0.5;
|
||||
display: inline-block;
|
||||
height: 1.4em;
|
||||
margin: 0 0.2em;
|
||||
vertical-align: text-bottom;
|
||||
width: 1px;
|
||||
}
|
||||
}
|
|
@ -1 +0,0 @@
|
|||
export { ImageBubbleMenu } from './ImageBubbleMenu'
|
|
@ -1,14 +1,6 @@
|
|||
.ProseMirror {
|
||||
outline: none;
|
||||
min-height: 300px;
|
||||
|
||||
blockquote {
|
||||
@include font-size(1.6rem);
|
||||
|
||||
border-left: 2px solid;
|
||||
margin: 1.5em 0;
|
||||
padding-left: 1.6em;
|
||||
}
|
||||
}
|
||||
|
||||
.ProseMirror p.is-editor-empty:first-child::before {
|
||||
|
@ -78,3 +70,110 @@ mark.highlight {
|
|||
box-decoration-break: clone;
|
||||
padding: 0.125em 0;
|
||||
}
|
||||
|
||||
// custom atibutes fro TipTap Nodes
|
||||
|
||||
[data-float] {
|
||||
max-width: 50%;
|
||||
}
|
||||
[data-float='left'] {
|
||||
max-width: 30%;
|
||||
float: left;
|
||||
margin: 1rem 1rem 0 0;
|
||||
}
|
||||
[data-float='right'] {
|
||||
max-width: 30%;
|
||||
float: right;
|
||||
margin: 1rem 0 1rem 1rem;
|
||||
}
|
||||
|
||||
[data-float='half-left'] {
|
||||
max-width: 50%;
|
||||
min-width: 30%;
|
||||
float: left;
|
||||
margin: 1rem 1rem 0;
|
||||
}
|
||||
[data-float='half-right'] {
|
||||
max-width: 50%;
|
||||
min-width: 30%;
|
||||
float: right;
|
||||
margin: 1rem 0 1rem;
|
||||
}
|
||||
|
||||
.ProseMirror blockquote {
|
||||
p:last-child {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
&[data-type='quote'] {
|
||||
@include font-size(1.6rem);
|
||||
|
||||
border: solid #000;
|
||||
border-width: 0 0 0 2px;
|
||||
margin: 1.6rem 0;
|
||||
padding: 1rem;
|
||||
|
||||
&[data-float='left'] {
|
||||
padding-left: 0;
|
||||
padding-right: 0;
|
||||
margin-right: 1.6rem;
|
||||
border-width: 0 2px 0 0;
|
||||
}
|
||||
|
||||
&[data-float='right'] {
|
||||
margin-left: 1.6rem;
|
||||
border-width: 0 0 0 2px;
|
||||
}
|
||||
}
|
||||
|
||||
&[data-type='punchline'] {
|
||||
padding: 1.6rem 0 0;
|
||||
border: solid #000;
|
||||
border-width: 2px 0;
|
||||
font-size: 3.2rem;
|
||||
font-weight: 700;
|
||||
margin: 0;
|
||||
|
||||
&[data-float='left'],
|
||||
&[data-float='right'] {
|
||||
font-size: 2.2rem;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.ProseMirror article[data-type='incut'] {
|
||||
background: #f1f2f3;
|
||||
padding: 1.6rem;
|
||||
font-size: 1.4rem;
|
||||
transition: background 0.3s ease-in-out;
|
||||
|
||||
&[data-float] img {
|
||||
float: none;
|
||||
max-width: unset;
|
||||
width: 100% !important;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
*:last-child {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
&[data-bg='black'] {
|
||||
background: #000;
|
||||
color: #fff;
|
||||
}
|
||||
&[data-bg='yellow'] {
|
||||
background: #f6e3a1;
|
||||
}
|
||||
&[data-bg='pink'] {
|
||||
background: #f1b5bc;
|
||||
}
|
||||
&[data-bg='green'] {
|
||||
background: #bfe9cb;
|
||||
box-shadow: 0 0 0 1px #000;
|
||||
}
|
||||
&[data-bg='white'] {
|
||||
background: #fff;
|
||||
box-shadow: 0 0 0 1px #000;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -54,7 +54,6 @@ export const TextBubbleMenu = (props: BubbleMenuProps) => {
|
|||
if (textSizeBubbleOpen()) {
|
||||
setTextSizeBubbleOpen(false)
|
||||
}
|
||||
|
||||
setListBubbleOpen((prev) => !prev)
|
||||
}
|
||||
|
||||
|
@ -153,7 +152,7 @@ export const TextBubbleMenu = (props: BubbleMenuProps) => {
|
|||
[styles.bubbleMenuButtonActive]: isBlockQuote()
|
||||
})}
|
||||
onClick={() => {
|
||||
props.editor.chain().focus().toggleBlockquote().run()
|
||||
props.editor.chain().focus().toggleBlockquote('quote').run()
|
||||
toggleTextSizePopup()
|
||||
}}
|
||||
>
|
||||
|
@ -165,13 +164,28 @@ export const TextBubbleMenu = (props: BubbleMenuProps) => {
|
|||
[styles.bubbleMenuButtonActive]: isBlockQuote()
|
||||
})}
|
||||
onClick={() => {
|
||||
props.editor.chain().focus().toggleBlockquote().run()
|
||||
props.editor.chain().focus().toggleBlockquote('punchline').run()
|
||||
toggleTextSizePopup()
|
||||
}}
|
||||
>
|
||||
<Icon name="editor-quote" />
|
||||
</button>
|
||||
</div>
|
||||
<header>{t('squib')}</header>
|
||||
<div class={styles.actions}>
|
||||
<button
|
||||
type="button"
|
||||
class={clsx(styles.bubbleMenuButton, {
|
||||
[styles.bubbleMenuButtonActive]: isBlockQuote()
|
||||
})}
|
||||
onClick={() => {
|
||||
props.editor.chain().focus().toggleArticle().run()
|
||||
toggleTextSizePopup()
|
||||
}}
|
||||
>
|
||||
<Icon name="editor-squib" />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</Show>
|
||||
</div>
|
||||
|
|
66
src/components/Editor/extensions/Article.ts
Normal file
66
src/components/Editor/extensions/Article.ts
Normal file
|
@ -0,0 +1,66 @@
|
|||
import { Node, mergeAttributes } from '@tiptap/core'
|
||||
|
||||
declare module '@tiptap/core' {
|
||||
interface Commands<ReturnType> {
|
||||
Article: {
|
||||
toggleArticle: () => ReturnType
|
||||
setArticleFloat: (float: null | 'left' | 'half-left' | 'right' | 'half-right') => ReturnType
|
||||
setArticleBg: (bg: null | string) => ReturnType
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export default Node.create({
|
||||
name: 'article',
|
||||
|
||||
defaultOptions: {
|
||||
HTMLAttributes: {
|
||||
'data-type': 'incut'
|
||||
}
|
||||
},
|
||||
group: 'block',
|
||||
content: 'block+',
|
||||
|
||||
parseHTML() {
|
||||
return [
|
||||
{
|
||||
tag: 'article'
|
||||
}
|
||||
]
|
||||
},
|
||||
|
||||
renderHTML({ HTMLAttributes }) {
|
||||
return ['article', mergeAttributes(this.options.HTMLAttributes, HTMLAttributes), 0]
|
||||
},
|
||||
|
||||
addAttributes() {
|
||||
return {
|
||||
'data-float': {
|
||||
default: null
|
||||
},
|
||||
'data-bg': {
|
||||
default: null
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
addCommands() {
|
||||
return {
|
||||
toggleArticle:
|
||||
() =>
|
||||
({ commands }) => {
|
||||
return commands.toggleWrap('article')
|
||||
},
|
||||
setArticleFloat:
|
||||
(value) =>
|
||||
({ commands }) => {
|
||||
return commands.updateAttributes(this.name, { 'data-float': value })
|
||||
},
|
||||
setArticleBg:
|
||||
(value) =>
|
||||
({ commands }) => {
|
||||
return commands.updateAttributes(this.name, { 'data-bg': value })
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
48
src/components/Editor/extensions/CustomBlockquote.ts
Normal file
48
src/components/Editor/extensions/CustomBlockquote.ts
Normal file
|
@ -0,0 +1,48 @@
|
|||
import { Blockquote } from '@tiptap/extension-blockquote'
|
||||
import { Command } from '@tiptap/core'
|
||||
|
||||
export type QuoteTypes = 'quote' | 'punchline'
|
||||
|
||||
declare module '@tiptap/core' {
|
||||
interface Commands<ReturnType> {
|
||||
CustomBlockquote: {
|
||||
toggleBlockquote: (type: QuoteTypes) => ReturnType
|
||||
setBlockQuoteFloat: (float: null | 'left' | 'right') => ReturnType
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export const CustomBlockquote = Blockquote.extend({
|
||||
name: 'blockquote',
|
||||
defaultOptions: {
|
||||
HTMLAttributes: {},
|
||||
group: 'block',
|
||||
content: 'block+'
|
||||
},
|
||||
addAttributes() {
|
||||
return {
|
||||
'data-float': {
|
||||
default: null
|
||||
},
|
||||
'data-type': {
|
||||
default: null
|
||||
}
|
||||
}
|
||||
},
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
// @ts-ignore
|
||||
addCommands() {
|
||||
return {
|
||||
toggleBlockquote:
|
||||
(type) =>
|
||||
({ commands }) => {
|
||||
return commands.toggleWrap(this.name, { 'data-type': type })
|
||||
},
|
||||
setBlockQuoteFloat:
|
||||
(value) =>
|
||||
({ commands }) => {
|
||||
return commands.updateAttributes(this.name, { 'data-float': value })
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
|
@ -7,7 +7,7 @@ declare module '@tiptap/core' {
|
|||
* Add an image
|
||||
*/
|
||||
setImage: (options: { src: string; alt?: string; title?: string }) => ReturnType
|
||||
setFloat: (float: null | 'left' | 'right') => ReturnType
|
||||
setImageFloat: (float: null | 'left' | 'right') => ReturnType
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -42,7 +42,7 @@ export const CustomImage = Image.extend({
|
|||
attrs: options
|
||||
})
|
||||
},
|
||||
setFloat:
|
||||
setImageFloat:
|
||||
(value) =>
|
||||
({ commands }) => {
|
||||
return commands.updateAttributes(this.name, { 'data-float': value })
|
||||
|
|
Loading…
Reference in New Issue
Block a user