Merge branch 'editor' of gitlab.com:discoursio/discoursio-webapp into editor
This commit is contained in:
commit
5cce55de44
864
package-lock.json
generated
864
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
3
public/icons/down-triangle.svg
Normal file
3
public/icons/down-triangle.svg
Normal file
|
@ -0,0 +1,3 @@
|
|||
<svg width="13" height="6" viewBox="0 0 13 6" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M6.5 6L0.870836 -9.53674e-07L12.1292 -9.53674e-07L6.5 6Z" fill="#898C94"/>
|
||||
</svg>
|
After Width: | Height: | Size: 185 B |
3
public/icons/editor-blockquote.svg
Normal file
3
public/icons/editor-blockquote.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="M2 16H0V0H2V16ZM4 5V3H16V5H4ZM4 7V9H16V7H4ZM4 13V11H16V13H4Z" fill="#898C94"/>
|
||||
</svg>
|
After Width: | Height: | Size: 231 B |
4
public/icons/editor-h3.svg
Normal file
4
public/icons/editor-h3.svg
Normal file
|
@ -0,0 +1,4 @@
|
|||
<svg width="21" height="12" viewBox="0 0 21 12" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M0 11.7647H2.52101V7.02521H7.79832V11.7647H10.3193V0H7.79832V4.7395H2.52101V0H0V11.7647Z" fill="currentColor"/>
|
||||
<path d="M16.3474 12C18.7004 12 20.9189 11.042 20.9189 8.63866C20.9189 6.95798 19.8936 6.06723 18.7172 5.71429C19.7928 5.34454 20.4483 4.43697 20.4483 3.2605C20.4483 1.17647 18.6836 0.100841 16.3138 0.100841C14.9189 0.100841 13.6079 0.436975 12.5827 0.991597V3.34454C13.7088 2.63865 14.9357 2.31933 15.9609 2.31933C17.339 2.31933 18.0617 2.78992 18.0617 3.61345C18.0617 4.40336 17.3558 4.82353 16.2466 4.80672L14.6668 4.78992L14.6499 6.97479H16.5323C17.6752 6.97479 18.5155 7.31092 18.5155 8.28571C18.5155 9.36134 17.4399 9.7647 16.1457 9.78151C14.8348 9.79832 13.692 9.59664 12.381 8.87395V11.2269C13.692 11.7647 14.8852 12 16.3474 12Z" fill="currentColor"/>
|
||||
</svg>
|
After Width: | Height: | Size: 884 B |
3
public/icons/editor-ol.svg
Normal file
3
public/icons/editor-ol.svg
Normal file
|
@ -0,0 +1,3 @@
|
|||
<svg width="19" height="16" viewBox="0 0 19 16" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M2.00002 4.00003H1.00001V1.00001H0V0H2.00002V4.00003ZM2.00002 13.5V13H0V12H3.00003V16H0V15H2.00002V14.5H1.00001V13.5H2.00002ZM0 6.99998H1.80002L0 9.1V10H3.00003V9H1.20001L3.00003 6.89998V5.99998H0V6.99998ZM4.9987 2.99967V0.999648H18.9988V2.99967H4.9987ZM4.9987 15.0001H18.9988V13.0001H4.9987V15.0001ZM18.9988 8.99987H4.9987V6.99986H18.9988V8.99987Z" fill="currentColor"/>
|
||||
</svg>
|
After Width: | Height: | Size: 524 B |
4
public/icons/status-cancel.svg
Normal file
4
public/icons/status-cancel.svg
Normal file
|
@ -0,0 +1,4 @@
|
|||
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M17.1512 4.42386L4.42326 17.1518L6.84763 19.5761L19.5756 6.84822L17.1512 4.42386Z" fill="#393840"/>
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M19.5755 17.1518L6.84763 4.42386L4.42326 6.84822L17.1512 19.5761L19.5755 17.1518Z" fill="#393840"/>
|
||||
</svg>
|
After Width: | Height: | Size: 401 B |
3
public/icons/status-done.svg
Normal file
3
public/icons/status-done.svg
Normal file
|
@ -0,0 +1,3 @@
|
|||
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M22 6.73787L19.2621 4L9.78964 13.4725L5.73787 9.42071L3 12.1586L9.78964 18.9482L22 6.73787Z" fill="#393840"/>
|
||||
</svg>
|
After Width: | Height: | Size: 262 B |
|
@ -233,5 +233,10 @@
|
|||
"By time": "By time",
|
||||
"New only": "New only",
|
||||
"Short opening": "Short opening",
|
||||
"Write an article": "Write an article"
|
||||
"Write an article": "Write an article",
|
||||
"Enter URL address": "Enter URL address",
|
||||
"Invalid url format": "Invalid url format",
|
||||
"Headers": "Headers",
|
||||
"Quotes": "Quotes",
|
||||
"Lists": "Lists"
|
||||
}
|
||||
|
|
|
@ -251,5 +251,10 @@
|
|||
"By time": "По порядку",
|
||||
"New only": "Только новые",
|
||||
"Short opening": "Небольшое вступление, чтобы заинтересовать читателя",
|
||||
"Write an article": "Написать статью"
|
||||
"Write an article": "Написать статью",
|
||||
"Enter URL address": "Введите адрес ссылки",
|
||||
"Invalid url format": "Неверный формат ссылки",
|
||||
"Headers": "Заголовки",
|
||||
"Quotes": "Цитаты",
|
||||
"Lists": "Списки"
|
||||
}
|
||||
|
|
|
@ -28,14 +28,12 @@ import { Youtube } from '@tiptap/extension-youtube'
|
|||
import { Document } from '@tiptap/extension-document'
|
||||
import { Text } from '@tiptap/extension-text'
|
||||
import { Image } from '@tiptap/extension-image'
|
||||
import { History } from '@tiptap/extension-history'
|
||||
import { Paragraph } from '@tiptap/extension-paragraph'
|
||||
import Focus from '@tiptap/extension-focus'
|
||||
import { TrailingNode } from './extensions/TrailingNode'
|
||||
import './Prosemirror.scss'
|
||||
import { EditorBubbleMenu } from './EditorBubbleMenu'
|
||||
import { EditorFloatingMenu } from './EditorFloatingMenu'
|
||||
import { createEffect } from 'solid-js'
|
||||
import './Prosemirror.scss'
|
||||
|
||||
type EditorProps = {
|
||||
initialContent?: string
|
||||
|
@ -80,6 +78,12 @@ export const Editor = (props: EditorProps) => {
|
|||
Strike,
|
||||
HorizontalRule,
|
||||
Underline,
|
||||
Link.configure({
|
||||
openOnClick: false
|
||||
}),
|
||||
Heading.configure({
|
||||
levels: [1, 2, 3]
|
||||
}),
|
||||
BubbleMenu.configure({
|
||||
element: bubbleMenuRef.current
|
||||
}),
|
||||
|
|
|
@ -1,22 +1,108 @@
|
|||
.bubbleMenu {
|
||||
background: #fff;
|
||||
box-shadow: 0 4px 10px rgba(0, 0, 0, 0.25);
|
||||
}
|
||||
|
||||
.bubbleMenuButton {
|
||||
opacity: 0.5;
|
||||
padding: 1rem;
|
||||
}
|
||||
.bubbleMenuButton {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
flex-wrap: nowrap;
|
||||
opacity: 0.5;
|
||||
padding: 1rem;
|
||||
|
||||
.bubbleMenuButtonActive {
|
||||
opacity: 1;
|
||||
}
|
||||
.triangle {
|
||||
margin-left: 4px;
|
||||
}
|
||||
}
|
||||
|
||||
.delimiter {
|
||||
background: #999;
|
||||
display: inline-block;
|
||||
height: 1.4em;
|
||||
margin: 0 0.2em;
|
||||
vertical-align: text-bottom;
|
||||
width: 1px;
|
||||
&:hover,
|
||||
.bubbleMenuButtonActive {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.delimiter {
|
||||
background: #999;
|
||||
display: inline-block;
|
||||
height: 1.4em;
|
||||
margin: 0 0.2em;
|
||||
vertical-align: text-bottom;
|
||||
width: 1px;
|
||||
}
|
||||
|
||||
.linkForm {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
flex-wrap: nowrap;
|
||||
padding: 6px 11px;
|
||||
|
||||
input {
|
||||
margin: 0 12px 0 0;
|
||||
padding: 0;
|
||||
flex: 1;
|
||||
border: none;
|
||||
min-width: 200px;
|
||||
|
||||
&:focus {
|
||||
outline: none;
|
||||
}
|
||||
|
||||
&::placeholder {
|
||||
color: rgba(#000, 0.3);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.linkError {
|
||||
padding: 6px 11px;
|
||||
color: red;
|
||||
font-size: 0.7em;
|
||||
}
|
||||
|
||||
.dropDownHolder {
|
||||
position: relative;
|
||||
cursor: pointer;
|
||||
display: inline-flex;
|
||||
flex-direction: row;
|
||||
flex-wrap: nowrap;
|
||||
align-items: center;
|
||||
|
||||
.dropDown {
|
||||
position: absolute;
|
||||
padding: 6px;
|
||||
top: calc(100% + 8px);
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
box-shadow: 0 4px 10px rgba(0, 0, 0, 0.25);
|
||||
background: #fff;
|
||||
color: #898c94;
|
||||
|
||||
& > 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;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,8 +1,12 @@
|
|||
import { createEffect, createSignal, Show } from 'solid-js'
|
||||
import type { Editor } from '@tiptap/core'
|
||||
import styles from './EditorBubbleMenu.module.scss'
|
||||
import { Icon } from '../_shared/Icon'
|
||||
import { clsx } from 'clsx'
|
||||
import { createEditorTransaction } from 'solid-tiptap'
|
||||
import { useLocalize } from '../../context/localize'
|
||||
import validateUrl from '../../utils/validateUrl'
|
||||
import list from '../Feed/List'
|
||||
|
||||
type BubbleMenuProps = {
|
||||
editor: Editor
|
||||
|
@ -10,41 +14,242 @@ type BubbleMenuProps = {
|
|||
}
|
||||
|
||||
export const EditorBubbleMenu = (props: BubbleMenuProps) => {
|
||||
const { t } = useLocalize()
|
||||
const [textSizeBubbleOpen, setTextSizeBubbleOpen] = createSignal<boolean>(false)
|
||||
const [listBubbleOpen, setListBubbleOpen] = createSignal<boolean>(false)
|
||||
const [linkEditorOpen, setLinkEditorOpen] = createSignal<boolean>(false)
|
||||
const [url, setUrl] = createSignal<string>('')
|
||||
const [prevUrl, setPrevUrl] = createSignal<string | null>(null)
|
||||
const [linkError, setLinkError] = createSignal<string | null>(null)
|
||||
|
||||
const isBold = createEditorTransaction(
|
||||
() => props.editor,
|
||||
(editor) => editor && editor.isActive('bold')
|
||||
)
|
||||
const isItalic = createEditorTransaction(
|
||||
() => props.editor,
|
||||
(editor) => editor && editor.isActive('italic')
|
||||
)
|
||||
|
||||
//props.editor.isActive('heading', { level: 1 }) - либо инлайново либо как-то возвращать что активно
|
||||
const isHOne = createEditorTransaction(
|
||||
() => props.editor,
|
||||
(editor) => editor && editor.isActive('heading', { level: 1 })
|
||||
)
|
||||
const isHTwo = createEditorTransaction(
|
||||
() => props.editor,
|
||||
(editor) => editor && editor.isActive('heading', { level: 2 })
|
||||
)
|
||||
const isHThree = createEditorTransaction(
|
||||
() => props.editor,
|
||||
(editor) => editor && editor.isActive('heading', { level: 3 })
|
||||
)
|
||||
const isBlockQuote = createEditorTransaction(
|
||||
() => props.editor,
|
||||
(editor) => editor && editor.isActive('blockquote')
|
||||
)
|
||||
const isOrderedList = createEditorTransaction(
|
||||
() => props.editor,
|
||||
(editor) => editor && editor.isActive('isOrderedList')
|
||||
)
|
||||
const isBulletList = createEditorTransaction(
|
||||
() => props.editor,
|
||||
(editor) => editor && editor.isActive('isBulletList')
|
||||
)
|
||||
|
||||
const isLink = createEditorTransaction(
|
||||
() => props.editor,
|
||||
(editor) => {
|
||||
editor && editor.isActive('link')
|
||||
setPrevUrl(editor && editor.getAttributes('link').href)
|
||||
}
|
||||
)
|
||||
|
||||
const clearLinkForm = () => {
|
||||
setUrl('')
|
||||
setLinkEditorOpen(false)
|
||||
}
|
||||
|
||||
const handleSubmitLink = (e) => {
|
||||
e.preventDefault()
|
||||
if (url().length === 0) {
|
||||
props.editor.chain().focus().unsetLink().run()
|
||||
clearLinkForm()
|
||||
return
|
||||
}
|
||||
|
||||
if (url().length > 1 && validateUrl(url())) {
|
||||
props.editor.commands.toggleLink({ href: url() })
|
||||
clearLinkForm()
|
||||
} else {
|
||||
setLinkError(t('Invalid url format'))
|
||||
}
|
||||
}
|
||||
|
||||
const toggleTextSizePopup = () => {
|
||||
if (listBubbleOpen()) setListBubbleOpen(false)
|
||||
setTextSizeBubbleOpen((prev) => !prev)
|
||||
}
|
||||
const toggleListPopup = () => {
|
||||
if (textSizeBubbleOpen()) setTextSizeBubbleOpen(false)
|
||||
setListBubbleOpen((prev) => !prev)
|
||||
}
|
||||
|
||||
return (
|
||||
<div ref={props.ref} class={styles.bubbleMenu}>
|
||||
<button class={clsx(styles.bubbleMenuButton)}>
|
||||
<Icon name="editor-text-size" />
|
||||
</button>
|
||||
<button
|
||||
class={clsx(styles.bubbleMenuButton, {
|
||||
[styles.bubbleMenuButtonActive]: isBold()
|
||||
})}
|
||||
onClick={(e) => {
|
||||
e.preventDefault()
|
||||
props.editor.commands.toggleBold()
|
||||
}}
|
||||
>
|
||||
<Icon name="editor-bold" />
|
||||
</button>
|
||||
<button class={styles.bubbleMenuButton}>
|
||||
<Icon name="editor-italic" />
|
||||
</button>
|
||||
<div class={styles.delimiter}></div>
|
||||
<button class={styles.bubbleMenuButton}>
|
||||
<Icon name="editor-link" />
|
||||
</button>
|
||||
<button class={styles.bubbleMenuButton}>
|
||||
<Icon name="editor-footnote" />
|
||||
</button>
|
||||
<div class={styles.delimiter}></div>
|
||||
<button class={styles.bubbleMenuButton}>
|
||||
<Icon name="editor-ul" />
|
||||
</button>
|
||||
</div>
|
||||
<>
|
||||
<div ref={props.ref} class={styles.bubbleMenu}>
|
||||
{linkEditorOpen() ? (
|
||||
<>
|
||||
<form onSubmit={(e) => handleSubmitLink(e)} class={styles.linkForm}>
|
||||
<input
|
||||
type="text"
|
||||
placeholder={t('Enter URL address')}
|
||||
autofocus
|
||||
value={prevUrl() ? prevUrl() : null}
|
||||
onChange={(e) => setUrl(e.currentTarget.value)}
|
||||
/>
|
||||
<button type="submit">
|
||||
<Icon name="status-done" />
|
||||
</button>
|
||||
<button type="button" onClick={() => clearLinkForm()}>
|
||||
<Icon name="status-cancel" />
|
||||
</button>
|
||||
</form>
|
||||
{linkError() && <div class={styles.linkError}>{linkError()}</div>}
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<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}>
|
||||
<button
|
||||
type="button"
|
||||
class={clsx(styles.bubbleMenuButton, {
|
||||
[styles.bubbleMenuButtonActive]: isHOne()
|
||||
})}
|
||||
onClick={() => props.editor.commands.toggleHeading({ level: 1 })}
|
||||
>
|
||||
<Icon name="editor-h1" />
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
class={clsx(styles.bubbleMenuButton, {
|
||||
[styles.bubbleMenuButtonActive]: isHTwo()
|
||||
})}
|
||||
onClick={() => props.editor.commands.toggleHeading({ level: 2 })}
|
||||
>
|
||||
<Icon name="editor-h2" />
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
class={clsx(styles.bubbleMenuButton, {
|
||||
[styles.bubbleMenuButtonActive]: isHThree()
|
||||
})}
|
||||
onClick={() => props.editor.commands.toggleHeading({ level: 3 })}
|
||||
>
|
||||
<Icon name="editor-h3" />
|
||||
</button>
|
||||
</div>
|
||||
<header>{t('Quotes')}</header>
|
||||
<div class={styles.actions}>
|
||||
<button
|
||||
type="button"
|
||||
class={clsx(styles.bubbleMenuButton, {
|
||||
[styles.bubbleMenuButtonActive]: isBlockQuote()
|
||||
})}
|
||||
onClick={() => props.editor.chain().focus().toggleBlockquote().run()}
|
||||
>
|
||||
<Icon name="editor-blockquote" />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</Show>
|
||||
</div>
|
||||
<div class={styles.delimiter} />
|
||||
<button
|
||||
type="button"
|
||||
class={clsx(styles.bubbleMenuButton, {
|
||||
[styles.bubbleMenuButtonActive]: isBold()
|
||||
})}
|
||||
onClick={() => props.editor.commands.toggleBold()}
|
||||
>
|
||||
<Icon name="editor-bold" />
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
class={clsx(styles.bubbleMenuButton, {
|
||||
[styles.bubbleMenuButtonActive]: isItalic()
|
||||
})}
|
||||
onClick={() => props.editor.commands.toggleItalic()}
|
||||
>
|
||||
<Icon name="editor-italic" />
|
||||
</button>
|
||||
<div class={styles.delimiter} />
|
||||
<button
|
||||
type="button"
|
||||
onClick={(e) => {
|
||||
setLinkEditorOpen(true)
|
||||
}}
|
||||
class={clsx(styles.bubbleMenuButton, {
|
||||
[styles.bubbleMenuButtonActive]: isLink()
|
||||
})}
|
||||
>
|
||||
<Icon name="editor-link" />
|
||||
</button>
|
||||
<button type="button" class={styles.bubbleMenuButton}>
|
||||
<Icon name="editor-footnote" />
|
||||
</button>
|
||||
<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}>
|
||||
<button
|
||||
type="button"
|
||||
class={clsx(styles.bubbleMenuButton, {
|
||||
[styles.bubbleMenuButtonActive]: isBulletList()
|
||||
})}
|
||||
onClick={() => props.editor.commands.toggleBulletList()}
|
||||
>
|
||||
<Icon name="editor-ul" />
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
class={clsx(styles.bubbleMenuButton, {
|
||||
[styles.bubbleMenuButtonActive]: isOrderedList()
|
||||
})}
|
||||
onClick={() => props.editor.commands.toggleOrderedList()}
|
||||
>
|
||||
<Icon name="editor-ol" />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</Show>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
|
|
@ -1,5 +1,13 @@
|
|||
.ProseMirror {
|
||||
outline: none;
|
||||
|
||||
blockquote {
|
||||
border-left: 2px solid;
|
||||
@include font-size(1.6rem);
|
||||
|
||||
margin: 1.5em 0;
|
||||
padding-left: 1.6em;
|
||||
}
|
||||
}
|
||||
|
||||
.ProseMirror p.is-editor-empty:first-child::before {
|
||||
|
|
|
@ -15,10 +15,4 @@
|
|||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
|
||||
&.enter,
|
||||
&.exitTo {
|
||||
height: 0;
|
||||
color: transparent;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -153,6 +153,7 @@ a:visited,
|
|||
a:link {
|
||||
border-bottom: 1px solid rgb(0 0 0 / 30%);
|
||||
text-decoration: none;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
a {
|
||||
|
|
Loading…
Reference in New Issue
Block a user