parent
2ef8da6da5
commit
5a1699aa87
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",
|
"number list": "number list",
|
||||||
"delimiter": "delimiter",
|
"delimiter": "delimiter",
|
||||||
"cancel_low_caps": "cancel",
|
"cancel_low_caps": "cancel",
|
||||||
"repeat": "repeat"
|
"repeat": "repeat",
|
||||||
|
"Add signature": "Add signature",
|
||||||
|
"Substrate": "Substrate"
|
||||||
}
|
}
|
||||||
|
|
|
@ -300,6 +300,7 @@
|
||||||
"sign up": "зарегистрироваться",
|
"sign up": "зарегистрироваться",
|
||||||
"sign up or sign in": "зарегистрироваться или войти",
|
"sign up or sign in": "зарегистрироваться или войти",
|
||||||
"slug is used by another user": "Имя уже занято другим пользователем",
|
"slug is used by another user": "Имя уже занято другим пользователем",
|
||||||
|
"squib": "Подверстка",
|
||||||
"terms of use": "правилами пользования сайтом",
|
"terms of use": "правилами пользования сайтом",
|
||||||
"topics": "темы",
|
"topics": "темы",
|
||||||
"user already exist": "пользователь уже существует",
|
"user already exist": "пользователь уже существует",
|
||||||
|
@ -316,5 +317,7 @@
|
||||||
"number list": "нумер. список",
|
"number list": "нумер. список",
|
||||||
"delimiter": "разделитель",
|
"delimiter": "разделитель",
|
||||||
"cancel_low_caps": "отменить",
|
"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 type { Editor } from '@tiptap/core'
|
||||||
import styles from './ImageBubbleMenu.module.scss'
|
import styles from './FigureBubbleMenu.module.scss'
|
||||||
import { clsx } from 'clsx'
|
import { clsx } from 'clsx'
|
||||||
import { Icon } from '../../_shared/Icon'
|
import { Icon } from '../../_shared/Icon'
|
||||||
|
import { useLocalize } from '../../../context/localize'
|
||||||
|
|
||||||
type BubbleMenuProps = {
|
type Props = {
|
||||||
editor: Editor
|
editor: Editor
|
||||||
ref: (el: HTMLDivElement) => void
|
ref: (el: HTMLElement) => void
|
||||||
}
|
}
|
||||||
|
|
||||||
export const ImageBubbleMenu = (props: BubbleMenuProps) => {
|
export const FigureBubbleMenu = (props: Props) => {
|
||||||
|
const { t } = useLocalize()
|
||||||
return (
|
return (
|
||||||
<div ref={props.ref} class={styles.ImageBubbleMenu}>
|
<div ref={props.ref} class={styles.FigureBubbleMenu}>
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
class={clsx(styles.bubbleMenuButton)}
|
class={clsx(styles.bubbleMenuButton)}
|
||||||
onClick={() => {
|
onClick={() => props.editor.chain().focus().setImageFloat('left').run()}
|
||||||
props.editor.chain().focus().setFloat('left').run()
|
|
||||||
}}
|
|
||||||
>
|
>
|
||||||
<Icon name="editor-image-align-left" />
|
<Icon name="editor-image-align-left" />
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
class={clsx(styles.bubbleMenuButton)}
|
class={clsx(styles.bubbleMenuButton)}
|
||||||
onClick={() => {
|
onClick={() => props.editor.chain().focus().setImageFloat(null).run()}
|
||||||
props.editor.chain().focus().setFloat(null).run()
|
|
||||||
}}
|
|
||||||
>
|
>
|
||||||
<Icon name="editor-image-align-center" />
|
<Icon name="editor-image-align-center" />
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
class={clsx(styles.bubbleMenuButton)}
|
class={clsx(styles.bubbleMenuButton)}
|
||||||
onClick={() => {
|
onClick={() => props.editor.chain().focus().setImageFloat('right').run()}
|
||||||
props.editor.chain().focus().setFloat('right').run()
|
|
||||||
}}
|
|
||||||
>
|
>
|
||||||
<Icon name="editor-image-align-right" />
|
<Icon name="editor-image-align-right" />
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
<div class={styles.delimiter} />
|
<div class={styles.delimiter} />
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
|
@ -46,7 +43,7 @@ export const ImageBubbleMenu = (props: BubbleMenuProps) => {
|
||||||
props.editor.chain().focus().imageToFigure().run()
|
props.editor.chain().focus().imageToFigure().run()
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<span style={{ color: 'white' }}>Добавить подпись</span>
|
<span style={{ color: 'white' }}>{t('Add signature')}</span>
|
||||||
</button>
|
</button>
|
||||||
<div class={styles.delimiter} />
|
<div class={styles.delimiter} />
|
||||||
<button type="button" class={clsx(styles.bubbleMenuButton)}>
|
<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 { createEffect, createSignal } from 'solid-js'
|
||||||
import { createTiptapEditor, useEditorHTML } from 'solid-tiptap'
|
import { createTiptapEditor, useEditorHTML } from 'solid-tiptap'
|
||||||
import { useLocalize } from '../../context/localize'
|
import { useLocalize } from '../../context/localize'
|
||||||
import { Blockquote } from '@tiptap/extension-blockquote'
|
|
||||||
import { Bold } from '@tiptap/extension-bold'
|
import { Bold } from '@tiptap/extension-bold'
|
||||||
import { BubbleMenu } from '@tiptap/extension-bubble-menu'
|
import { BubbleMenu } from '@tiptap/extension-bubble-menu'
|
||||||
import { Dropcursor } from '@tiptap/extension-dropcursor'
|
import { Dropcursor } from '@tiptap/extension-dropcursor'
|
||||||
|
@ -23,26 +22,27 @@ import { Link } from '@tiptap/extension-link'
|
||||||
import { Document } from '@tiptap/extension-document'
|
import { Document } from '@tiptap/extension-document'
|
||||||
import { Text } from '@tiptap/extension-text'
|
import { Text } from '@tiptap/extension-text'
|
||||||
import { CustomImage } from './extensions/CustomImage'
|
import { CustomImage } from './extensions/CustomImage'
|
||||||
|
import { CustomBlockquote } from './extensions/CustomBlockquote'
|
||||||
import { Figure } from './extensions/Figure'
|
import { Figure } from './extensions/Figure'
|
||||||
import { Paragraph } from '@tiptap/extension-paragraph'
|
import { Paragraph } from '@tiptap/extension-paragraph'
|
||||||
import Focus from '@tiptap/extension-focus'
|
import Focus from '@tiptap/extension-focus'
|
||||||
import * as Y from 'yjs'
|
import * as Y from 'yjs'
|
||||||
import { CollaborationCursor } from '@tiptap/extension-collaboration-cursor'
|
import { CollaborationCursor } from '@tiptap/extension-collaboration-cursor'
|
||||||
import { Collaboration } from '@tiptap/extension-collaboration'
|
import { Collaboration } from '@tiptap/extension-collaboration'
|
||||||
|
|
||||||
import { IndexeddbPersistence } from 'y-indexeddb'
|
import { IndexeddbPersistence } from 'y-indexeddb'
|
||||||
import { useSession } from '../../context/session'
|
import { useSession } from '../../context/session'
|
||||||
import uniqolor from 'uniqolor'
|
import uniqolor from 'uniqolor'
|
||||||
import { HocuspocusProvider } from '@hocuspocus/provider'
|
import { HocuspocusProvider } from '@hocuspocus/provider'
|
||||||
import { Embed } from './extensions/Embed'
|
import { Embed } from './extensions/Embed'
|
||||||
import { TextBubbleMenu } from './TextBubbleMenu'
|
import { TextBubbleMenu } from './TextBubbleMenu'
|
||||||
import { ImageBubbleMenu } from './ImageBubbleMenu'
|
import { FigureBubbleMenu, BlockquoteBubbleMenu, IncutBubbleMenu } from './BubbleMenu'
|
||||||
import { EditorFloatingMenu } from './EditorFloatingMenu'
|
import { EditorFloatingMenu } from './EditorFloatingMenu'
|
||||||
import { useEditorContext } from '../../context/editor'
|
import { useEditorContext } from '../../context/editor'
|
||||||
import { isTextSelection } from '@tiptap/core'
|
import { isTextSelection } from '@tiptap/core'
|
||||||
import type { Doc } from 'yjs/dist/src/utils/Doc'
|
import type { Doc } from 'yjs/dist/src/utils/Doc'
|
||||||
import './Prosemirror.scss'
|
import './Prosemirror.scss'
|
||||||
import { TrailingNode } from './extensions/TrailingNode'
|
import { TrailingNode } from './extensions/TrailingNode'
|
||||||
|
import Article from './extensions/Article'
|
||||||
|
|
||||||
type EditorProps = {
|
type EditorProps = {
|
||||||
shoutId: number
|
shoutId: number
|
||||||
|
@ -58,6 +58,7 @@ export const Editor = (props: EditorProps) => {
|
||||||
const { t } = useLocalize()
|
const { t } = useLocalize()
|
||||||
const { user } = useSession()
|
const { user } = useSession()
|
||||||
const [isCommonMarkup, setIsCommonMarkup] = createSignal(false)
|
const [isCommonMarkup, setIsCommonMarkup] = createSignal(false)
|
||||||
|
const [floatMenuRef, setFloatMenuRef] = createSignal<'blockquote' | 'image' | 'incut'>()
|
||||||
|
|
||||||
const docName = `shout-${props.shoutId}`
|
const docName = `shout-${props.shoutId}`
|
||||||
|
|
||||||
|
@ -89,8 +90,18 @@ export const Editor = (props: EditorProps) => {
|
||||||
current: null
|
current: null
|
||||||
}
|
}
|
||||||
|
|
||||||
const imageBubbleMenuRef: {
|
const incutBubbleMenuRef: {
|
||||||
current: HTMLDivElement
|
current: HTMLElement
|
||||||
|
} = {
|
||||||
|
current: null
|
||||||
|
}
|
||||||
|
const figureBubbleMenuRef: {
|
||||||
|
current: HTMLElement
|
||||||
|
} = {
|
||||||
|
current: null
|
||||||
|
}
|
||||||
|
const blockquoteBubbleMenuRef: {
|
||||||
|
current: HTMLElement
|
||||||
} = {
|
} = {
|
||||||
current: null
|
current: null
|
||||||
}
|
}
|
||||||
|
@ -108,7 +119,7 @@ export const Editor = (props: EditorProps) => {
|
||||||
Text,
|
Text,
|
||||||
Paragraph,
|
Paragraph,
|
||||||
Dropcursor,
|
Dropcursor,
|
||||||
Blockquote,
|
CustomBlockquote,
|
||||||
Bold,
|
Bold,
|
||||||
Italic,
|
Italic,
|
||||||
Strike,
|
Strike,
|
||||||
|
@ -163,16 +174,36 @@ export const Editor = (props: EditorProps) => {
|
||||||
shouldShow: ({ editor: e, view, state, from, to }) => {
|
shouldShow: ({ editor: e, view, state, from, to }) => {
|
||||||
const { doc, selection } = state
|
const { doc, selection } = state
|
||||||
const { empty } = selection
|
const { empty } = selection
|
||||||
|
|
||||||
const isEmptyTextBlock = doc.textBetween(from, to).length === 0 && isTextSelection(selection)
|
const isEmptyTextBlock = doc.textBetween(from, to).length === 0 && isTextSelection(selection)
|
||||||
|
|
||||||
setIsCommonMarkup(e.isActive('figure'))
|
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({
|
BubbleMenu.configure({
|
||||||
pluginKey: 'imageBubbleMenu',
|
pluginKey: 'imageBubbleMenu',
|
||||||
element: imageBubbleMenuRef.current,
|
element: figureBubbleMenuRef.current,
|
||||||
shouldShow: ({ editor: e, view }) => {
|
shouldShow: ({ editor: e, view }) => {
|
||||||
return view.hasFocus() && e.isActive('image')
|
return view.hasFocus() && e.isActive('image')
|
||||||
}
|
}
|
||||||
|
@ -183,7 +214,8 @@ export const Editor = (props: EditorProps) => {
|
||||||
},
|
},
|
||||||
element: floatingMenuRef.current
|
element: floatingMenuRef.current
|
||||||
}),
|
}),
|
||||||
TrailingNode
|
TrailingNode,
|
||||||
|
Article
|
||||||
]
|
]
|
||||||
}))
|
}))
|
||||||
|
|
||||||
|
@ -213,7 +245,24 @@ export const Editor = (props: EditorProps) => {
|
||||||
editor={editor()}
|
editor={editor()}
|
||||||
ref={(el) => (textBubbleMenuRef.current = el)}
|
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)} />
|
<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 {
|
.ProseMirror {
|
||||||
outline: none;
|
outline: none;
|
||||||
min-height: 300px;
|
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 {
|
.ProseMirror p.is-editor-empty:first-child::before {
|
||||||
|
@ -78,3 +70,110 @@ mark.highlight {
|
||||||
box-decoration-break: clone;
|
box-decoration-break: clone;
|
||||||
padding: 0.125em 0;
|
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()) {
|
if (textSizeBubbleOpen()) {
|
||||||
setTextSizeBubbleOpen(false)
|
setTextSizeBubbleOpen(false)
|
||||||
}
|
}
|
||||||
|
|
||||||
setListBubbleOpen((prev) => !prev)
|
setListBubbleOpen((prev) => !prev)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -153,7 +152,7 @@ export const TextBubbleMenu = (props: BubbleMenuProps) => {
|
||||||
[styles.bubbleMenuButtonActive]: isBlockQuote()
|
[styles.bubbleMenuButtonActive]: isBlockQuote()
|
||||||
})}
|
})}
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
props.editor.chain().focus().toggleBlockquote().run()
|
props.editor.chain().focus().toggleBlockquote('quote').run()
|
||||||
toggleTextSizePopup()
|
toggleTextSizePopup()
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
|
@ -165,13 +164,28 @@ export const TextBubbleMenu = (props: BubbleMenuProps) => {
|
||||||
[styles.bubbleMenuButtonActive]: isBlockQuote()
|
[styles.bubbleMenuButtonActive]: isBlockQuote()
|
||||||
})}
|
})}
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
props.editor.chain().focus().toggleBlockquote().run()
|
props.editor.chain().focus().toggleBlockquote('punchline').run()
|
||||||
toggleTextSizePopup()
|
toggleTextSizePopup()
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Icon name="editor-quote" />
|
<Icon name="editor-quote" />
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</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>
|
</div>
|
||||||
</Show>
|
</Show>
|
||||||
</div>
|
</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
|
* Add an image
|
||||||
*/
|
*/
|
||||||
setImage: (options: { src: string; alt?: string; title?: string }) => ReturnType
|
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
|
attrs: options
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
setFloat:
|
setImageFloat:
|
||||||
(value) =>
|
(value) =>
|
||||||
({ commands }) => {
|
({ commands }) => {
|
||||||
return commands.updateAttributes(this.name, { 'data-float': value })
|
return commands.updateAttributes(this.name, { 'data-float': value })
|
||||||
|
|
Loading…
Reference in New Issue
Block a user