Merge branch 'feature/editor_refactoring_link_form' into 'editor'

Init Refactor

See merge request discoursio/discoursio-webapp!44
This commit is contained in:
ilia tapazukk 2023-03-26 16:56:49 +00:00
commit 2a1c7ce9c1
8 changed files with 139 additions and 94 deletions

View File

@ -1,12 +1,9 @@
import { createEffect } from 'solid-js' import { createEffect } from 'solid-js'
import { createTiptapEditor, useEditorHTML } from 'solid-tiptap' import { createTiptapEditor, useEditorHTML } from 'solid-tiptap'
import { clsx } from 'clsx'
import { useLocalize } from '../../context/localize' import { useLocalize } from '../../context/localize'
import { Blockquote } from '@tiptap/extension-blockquote' 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 * as Y from 'yjs'
import { WebrtcProvider } from 'y-webrtc'
import { Dropcursor } from '@tiptap/extension-dropcursor' import { Dropcursor } from '@tiptap/extension-dropcursor'
import { Italic } from '@tiptap/extension-italic' import { Italic } from '@tiptap/extension-italic'
import { Strike } from '@tiptap/extension-strike' import { Strike } from '@tiptap/extension-strike'

View File

@ -36,41 +36,6 @@
width: 1px; 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;
position: absolute;
bottom: -3rem;
left: 0;
right: 0;
background: #fff;
box-shadow: 0 4px 10px rgba(#000, 0.25);
}
.dropDownHolder { .dropDownHolder {
position: relative; position: relative;
cursor: pointer; cursor: pointer;

View File

@ -5,7 +5,7 @@ import { Icon } from '../../_shared/Icon'
import { clsx } from 'clsx' import { clsx } from 'clsx'
import { createEditorTransaction } from 'solid-tiptap' import { createEditorTransaction } from 'solid-tiptap'
import { useLocalize } from '../../../context/localize' import { useLocalize } from '../../../context/localize'
import validateUrl from '../../../utils/validateUrl' import { LinkForm } from './LinkForm'
type BubbleMenuProps = { type BubbleMenuProps = {
editor: Editor editor: Editor
@ -38,44 +38,10 @@ export const EditorBubbleMenu = (props: BubbleMenuProps) => {
const isBulletList = isActive('isBulletList') const isBulletList = isActive('isBulletList')
const isLink = isActive('link') const isLink = isActive('link')
//TODO: вынести логику линки в отдельный компонент
const toggleLinkForm = () => { const toggleLinkForm = () => {
setLinkError(null)
setLinkEditorOpen(true) setLinkEditorOpen(true)
} }
const currentUrl = createEditorTransaction(
() => props.editor,
(editor) => {
return (editor && editor.getAttributes('link').href) || ''
}
)
const clearLinkForm = () => {
if (currentUrl()) {
props.editor.chain().focus().unsetLink().run()
}
setUrl('')
setLinkEditorOpen(false)
}
const handleUrlChange = (value) => {
setUrl(value)
}
const handleSubmitLink = () => {
if (validateUrl(url())) {
props.editor.chain().focus().setLink({ href: url() }).run()
setLinkEditorOpen(false)
} else {
setLinkError(t('Invalid url format'))
}
}
const handleKeyPress = (event) => {
const key = event.key
if (key === 'Enter') handleSubmitLink()
if (key === 'Esc') clearLinkForm()
}
const toggleTextSizePopup = () => { const toggleTextSizePopup = () => {
if (listBubbleOpen()) setListBubbleOpen(false) if (listBubbleOpen()) setListBubbleOpen(false)
setTextSizeBubbleOpen((prev) => !prev) setTextSizeBubbleOpen((prev) => !prev)
@ -90,25 +56,7 @@ export const EditorBubbleMenu = (props: BubbleMenuProps) => {
<div ref={props.ref} class={styles.bubbleMenu}> <div ref={props.ref} class={styles.bubbleMenu}>
<Switch> <Switch>
<Match when={linkEditorOpen()}> <Match when={linkEditorOpen()}>
<> <LinkForm editor={props.editor} onClose={() => setLinkEditorOpen(false)} />
<div class={styles.linkForm}>
<input
type="text"
placeholder={t('Enter URL address')}
autofocus
value={currentUrl()}
onKeyPress={(e) => handleKeyPress(e)}
onChange={(e) => handleUrlChange(e.currentTarget.value)}
/>
<button type="button" onClick={() => handleSubmitLink()} disabled={linkError() !== null}>
<Icon name="status-done" />
</button>
<button type="button" onClick={() => clearLinkForm()}>
{currentUrl() ? 'Ж' : <Icon name="status-cancel" />}
</button>
</div>
{linkError() && <div class={styles.linkError}>{linkError()}</div>}
</>
</Match> </Match>
<Match when={!linkEditorOpen()}> <Match when={!linkEditorOpen()}>
<> <>

View File

@ -0,0 +1,46 @@
.LinkForm {
position: relative;
.form {
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;
display: block;
&::placeholder {
color: rgba(#000, 0.3);
}
&:focus {
outline: none;
}
}
}
.linkError {
padding: 6px 11px;
color: red;
font-size: 0.7em;
position: absolute;
bottom: -3rem;
left: 0;
right: 0;
height: 0;
background: #fff;
box-shadow: 0 4px 10px rgba(#000, 0.25);
opacity: 0;
transition: height 0.3s ease-in-out, opacity 0.3s ease-in-out;
&.visible {
height: 32px;
opacity: 1;
}
}
}

View File

@ -0,0 +1,84 @@
import styles from './LinkForm.module.scss'
import { Icon } from '../../../_shared/Icon'
import { createEditorTransaction } from 'solid-tiptap'
import validateUrl from '../../../../utils/validateUrl'
import type { Editor } from '@tiptap/core'
import { createSignal } from 'solid-js'
import { useLocalize } from '../../../../context/localize'
import { clsx } from 'clsx'
type Props = {
editor: Editor
onClose: () => void
}
export const LinkForm = (props: Props) => {
const { t } = useLocalize()
const [url, setUrl] = createSignal('')
const [linkError, setLinkError] = createSignal('')
const currentUrl = createEditorTransaction(
() => props.editor,
(editor) => {
return (editor && editor.getAttributes('link').href) || ''
}
)
const clearLinkForm = () => {
if (currentUrl()) {
props.editor.chain().focus().unsetLink().run()
}
setUrl('')
props.onClose()
}
const handleUrlInput = (value) => {
setUrl(value)
}
const handleSaveButtonClick = () => {
if (!validateUrl(url())) {
setLinkError(t('Invalid url format'))
return
}
props.editor.chain().focus().setLink({ href: url() }).run()
props.onClose()
}
const handleKeyPress = (event) => {
setLinkError('')
const key = event.key
if (key === 'Enter') {
handleSaveButtonClick()
}
if (key === 'Esc') {
clearLinkForm()
}
}
return (
<div class={styles.LinkForm}>
<div class={styles.form}>
<input
type="text"
placeholder={t('Enter URL address')}
autofocus
value={currentUrl()}
onKeyPress={(e) => handleKeyPress(e)}
onInput={(e) => handleUrlInput(e.currentTarget.value)}
/>
<button type="button" onClick={handleSaveButtonClick} disabled={linkError() !== ''}>
<Icon name="status-done" />
</button>
<button type="button" onClick={() => clearLinkForm()}>
{currentUrl() ? 'Ж' : <Icon name="status-cancel" />}
</button>
</div>
<div class={clsx(styles.linkError, { [styles.visible]: Boolean(linkError()) })}>{linkError()}</div>
</div>
)
}

View File

@ -0,0 +1 @@
export { LinkForm } from './LinkForm'

View File

@ -4,6 +4,10 @@
padding: 1rem 1.2rem; padding: 1rem 1.2rem;
} }
.formHolder {
padding: 0 4rem;
}
.saveBlock { .saveBlock {
background: #f1f1f1; background: #f1f1f1;
line-height: 1.4; line-height: 1.4;

View File

@ -53,7 +53,7 @@ export const CreateView = () => {
<div class="wide-container"> <div class="wide-container">
<div class="shift-content"> <div class="shift-content">
<div class="row"> <div class="row">
<div class="col-md-10 col-lg-9 col-xl-8"> <div class="col-md-20 col-lg-18 col-xl-16">
<h4>Slug</h4> <h4>Slug</h4>
<div class="pretty-form__item"> <div class="pretty-form__item">
<input <input
@ -152,7 +152,7 @@ export const CreateView = () => {
Выберите заглавное изображение для статьи, тут сразу можно увидеть как карточка будет Выберите заглавное изображение для статьи, тут сразу можно увидеть как карточка будет
выглядеть на&nbsp;главной странице выглядеть на&nbsp;главной странице
</p> </p>
<div class={styles.articlePreview}></div> <div class={styles.articlePreview} />
<div class={styles.saveBlock}> <div class={styles.saveBlock}>
<p> <p>