Merge branch 'editor' of gitlab.com:discoursio/discoursio-webapp into editor

This commit is contained in:
bniwredyc 2023-03-23 18:19:55 +01:00
commit 5cce55de44
15 changed files with 1248 additions and 56 deletions

864
package-lock.json generated

File diff suppressed because it is too large Load Diff

View 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

View 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

View 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

View 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

View 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

View 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

View File

@ -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"
}

View File

@ -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": "Списки"
}

View File

@ -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
}),

View File

@ -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;
}
}

View File

@ -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>
</>
)
}

View File

@ -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 {

View File

@ -15,10 +15,4 @@
display: flex;
align-items: center;
justify-content: center;
&.enter,
&.exitTo {
height: 0;
color: transparent;
}
}

View File

@ -153,6 +153,7 @@ a:visited,
a:link {
border-bottom: 1px solid rgb(0 0 0 / 30%);
text-decoration: none;
cursor: pointer;
}
a {