Add Link editor

This commit is contained in:
ilya-bkv 2023-03-20 12:19:14 +03:00
parent 293e7a06e4
commit 5b4b4e8f2d
9 changed files with 1028 additions and 53 deletions

864
package-lock.json generated

File diff suppressed because it is too large Load Diff

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,7 @@
"By time": "By time", "By time": "By time",
"New only": "New only", "New only": "New only",
"Short opening": "Short opening", "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"
} }

View File

@ -251,5 +251,7 @@
"By time": "По порядку", "By time": "По порядку",
"New only": "Только новые", "New only": "Только новые",
"Short opening": "Небольшое вступление, чтобы заинтересовать читателя", "Short opening": "Небольшое вступление, чтобы заинтересовать читателя",
"Write an article": "Написать статью" "Write an article": "Написать статью",
"Enter URL address": "Введите адрес ссылки",
"Invalid url format": "Неверный формат ссылки"
} }

View File

@ -1,11 +1,8 @@
import { createTiptapEditor } from 'solid-tiptap' import { createTiptapEditor } 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'
@ -16,8 +13,6 @@ import { BulletList } from '@tiptap/extension-bullet-list'
import { OrderedList } from '@tiptap/extension-ordered-list' import { OrderedList } from '@tiptap/extension-ordered-list'
import { ListItem } from '@tiptap/extension-list-item' import { ListItem } from '@tiptap/extension-list-item'
import { CharacterCount } from '@tiptap/extension-character-count' import { CharacterCount } from '@tiptap/extension-character-count'
import { Collaboration } from '@tiptap/extension-collaboration'
import { CollaborationCursor } from '@tiptap/extension-collaboration-cursor'
import { Placeholder } from '@tiptap/extension-placeholder' import { Placeholder } from '@tiptap/extension-placeholder'
import { Gapcursor } from '@tiptap/extension-gapcursor' import { Gapcursor } from '@tiptap/extension-gapcursor'
import { HardBreak } from '@tiptap/extension-hard-break' import { HardBreak } from '@tiptap/extension-hard-break'
@ -28,7 +23,6 @@ import { Youtube } from '@tiptap/extension-youtube'
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 { Image } from '@tiptap/extension-image' import { Image } from '@tiptap/extension-image'
import { History } from '@tiptap/extension-history'
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 { TrailingNode } from './extensions/TrailingNode' import { TrailingNode } from './extensions/TrailingNode'
@ -78,6 +72,9 @@ export const Editor = (props: EditorProps) => {
Strike, Strike,
HorizontalRule, HorizontalRule,
Underline, Underline,
Link.configure({
openOnClick: false
}),
BubbleMenu.configure({ BubbleMenu.configure({
element: bubbleMenuRef.current element: bubbleMenuRef.current
}), }),

View File

@ -1,7 +1,6 @@
.bubbleMenu { .bubbleMenu {
background: #fff; background: #fff;
box-shadow: 0 4px 10px rgba(0, 0, 0, 0.25); box-shadow: 0 4px 10px rgba(0, 0, 0, 0.25);
}
.bubbleMenuButton { .bubbleMenuButton {
opacity: 0.5; opacity: 0.5;
@ -20,3 +19,33 @@
vertical-align: text-bottom; vertical-align: text-bottom;
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;
}
}

View File

@ -3,6 +3,9 @@ import styles from './EditorBubbleMenu.module.scss'
import { Icon } from '../_shared/Icon' import { Icon } from '../_shared/Icon'
import { clsx } from 'clsx' import { clsx } from 'clsx'
import { createEditorTransaction } from 'solid-tiptap' import { createEditorTransaction } from 'solid-tiptap'
import { createSignal } from 'solid-js'
import { useLocalize } from '../../context/localize'
import validateUrl from '../../utils/validateUrl'
type BubbleMenuProps = { type BubbleMenuProps = {
editor: Editor editor: Editor
@ -10,13 +13,80 @@ type BubbleMenuProps = {
} }
export const EditorBubbleMenu = (props: BubbleMenuProps) => { export const EditorBubbleMenu = (props: BubbleMenuProps) => {
const { t } = useLocalize()
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( const isBold = createEditorTransaction(
() => props.editor, () => props.editor,
(editor) => editor && editor.isActive('bold') (editor) => editor && editor.isActive('bold')
) )
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'))
}
}
return ( return (
<>
<div ref={props.ref} class={styles.bubbleMenu}> <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 role="button" onClick={() => clearLinkForm()}>
<Icon name="status-cancel" />
</button>
</form>
{linkError() && <div class={styles.linkError}>{linkError()}</div>}
</>
) : (
<>
<button
onClick={(e) => {
e.preventDefault()
setLinkEditorOpen(true)
}}
class={clsx(styles.bubbleMenuButton, {
[styles.bubbleMenuButtonActive]: isLink()
})}
>
<Icon name="editor-link" />
</button>
<button class={clsx(styles.bubbleMenuButton)}> <button class={clsx(styles.bubbleMenuButton)}>
<Icon name="editor-text-size" /> <Icon name="editor-text-size" />
</button> </button>
@ -34,17 +104,20 @@ export const EditorBubbleMenu = (props: BubbleMenuProps) => {
<button class={styles.bubbleMenuButton}> <button class={styles.bubbleMenuButton}>
<Icon name="editor-italic" /> <Icon name="editor-italic" />
</button> </button>
<div class={styles.delimiter}></div> <div class={styles.delimiter}>D</div>
<button class={styles.bubbleMenuButton}> <button class={styles.bubbleMenuButton}>
<Icon name="editor-link" /> <Icon name="editor-link" />
</button> </button>
<button class={styles.bubbleMenuButton}> <button class={styles.bubbleMenuButton}>
<Icon name="editor-footnote" /> <Icon name="editor-footnote" />
</button> </button>
<div class={styles.delimiter}></div> <div class={styles.delimiter} />
<button class={styles.bubbleMenuButton}> <button class={styles.bubbleMenuButton}>
<Icon name="editor-ul" /> <Icon name="editor-ul" />
</button> </button>
</>
)}
</div> </div>
</>
) )
} }

View File

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