Add Link editor
This commit is contained in:
parent
293e7a06e4
commit
5b4b4e8f2d
864
package-lock.json
generated
864
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
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,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"
|
||||||
}
|
}
|
||||||
|
|
|
@ -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": "Неверный формат ссылки"
|
||||||
}
|
}
|
||||||
|
|
|
@ -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
|
||||||
}),
|
}),
|
||||||
|
|
|
@ -1,22 +1,51 @@
|
||||||
.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;
|
||||||
padding: 1rem;
|
padding: 1rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.bubbleMenuButtonActive {
|
.bubbleMenuButtonActive {
|
||||||
opacity: 1;
|
opacity: 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
.delimiter {
|
.delimiter {
|
||||||
background: #999;
|
background: #999;
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
height: 1.4em;
|
height: 1.4em;
|
||||||
margin: 0 0.2em;
|
margin: 0 0.2em;
|
||||||
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;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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,41 +13,111 @@ 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}>
|
<>
|
||||||
<button class={clsx(styles.bubbleMenuButton)}>
|
<div ref={props.ref} class={styles.bubbleMenu}>
|
||||||
<Icon name="editor-text-size" />
|
{linkEditorOpen() ? (
|
||||||
</button>
|
<>
|
||||||
<button
|
<form onSubmit={(e) => handleSubmitLink(e)} class={styles.linkForm}>
|
||||||
class={clsx(styles.bubbleMenuButton, {
|
<input
|
||||||
[styles.bubbleMenuButtonActive]: isBold()
|
type="text"
|
||||||
})}
|
placeholder={t('Enter URL address')}
|
||||||
onClick={(e) => {
|
autofocus
|
||||||
e.preventDefault()
|
value={prevUrl() ? prevUrl() : null}
|
||||||
props.editor.commands.toggleBold()
|
onChange={(e) => setUrl(e.currentTarget.value)}
|
||||||
}}
|
/>
|
||||||
>
|
<button type="submit">
|
||||||
<Icon name="editor-bold" />
|
<Icon name="status-done" />
|
||||||
</button>
|
</button>
|
||||||
<button class={styles.bubbleMenuButton}>
|
<button role="button" onClick={() => clearLinkForm()}>
|
||||||
<Icon name="editor-italic" />
|
<Icon name="status-cancel" />
|
||||||
</button>
|
</button>
|
||||||
<div class={styles.delimiter}></div>
|
</form>
|
||||||
<button class={styles.bubbleMenuButton}>
|
{linkError() && <div class={styles.linkError}>{linkError()}</div>}
|
||||||
<Icon name="editor-link" />
|
</>
|
||||||
</button>
|
) : (
|
||||||
<button class={styles.bubbleMenuButton}>
|
<>
|
||||||
<Icon name="editor-footnote" />
|
<button
|
||||||
</button>
|
onClick={(e) => {
|
||||||
<div class={styles.delimiter}></div>
|
e.preventDefault()
|
||||||
<button class={styles.bubbleMenuButton}>
|
setLinkEditorOpen(true)
|
||||||
<Icon name="editor-ul" />
|
}}
|
||||||
</button>
|
class={clsx(styles.bubbleMenuButton, {
|
||||||
</div>
|
[styles.bubbleMenuButtonActive]: isLink()
|
||||||
|
})}
|
||||||
|
>
|
||||||
|
<Icon name="editor-link" />
|
||||||
|
</button>
|
||||||
|
<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}>D</div>
|
||||||
|
<button class={styles.bubbleMenuButton}>
|
||||||
|
<Icon name="editor-link" />
|
||||||
|
</button>
|
||||||
|
<button class={styles.bubbleMenuButton}>
|
||||||
|
<Icon name="editor-footnote" />
|
||||||
|
</button>
|
||||||
|
<div class={styles.delimiter} />
|
||||||
|
<button class={styles.bubbleMenuButton}>
|
||||||
|
<Icon name="editor-ul" />
|
||||||
|
</button>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
@ -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 {
|
||||||
|
|
Loading…
Reference in New Issue
Block a user