link form setup

This commit is contained in:
ilya-bkv 2023-03-25 07:42:45 +03:00
parent d5a5e79daa
commit 3b3d4fecbe
8 changed files with 171 additions and 660 deletions

628
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -17,8 +17,6 @@ import { BulletList } from '@tiptap/extension-bullet-list'
import { OrderedList } from '@tiptap/extension-ordered-list'
import { ListItem } from '@tiptap/extension-list-item'
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 { Gapcursor } from '@tiptap/extension-gapcursor'
import { HardBreak } from '@tiptap/extension-hard-break'
@ -32,7 +30,7 @@ import { Image } from '@tiptap/extension-image'
import { Paragraph } from '@tiptap/extension-paragraph'
import Focus from '@tiptap/extension-focus'
import { TrailingNode } from './extensions/TrailingNode'
import { EditorBubbleMenu } from './EditorBubbleMenu'
import { EditorBubbleMenu } from './EditorBubbleMenu/EditorBubbleMenu'
import { EditorFloatingMenu } from './EditorFloatingMenu'
import './Prosemirror.scss'
@ -119,7 +117,6 @@ export const Editor = (props: EditorProps) => {
Heading,
Highlight,
Image,
Link,
Youtube,
TrailingNode
]

View File

@ -1,6 +1,6 @@
.bubbleMenu {
background: #fff;
box-shadow: 0 4px 10px rgba(0, 0, 0, 0.25);
box-shadow: 0 4px 10px rgba(#000, 0.25);
.bubbleMenuButton {
display: inline-flex;
@ -23,7 +23,6 @@
}
}
&:hover,
.bubbleMenuButtonActive {
opacity: 1;
}
@ -64,6 +63,12 @@
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 {

View File

@ -1,11 +1,12 @@
import { Switch, Match, createSignal, Show } from 'solid-js'
import type { Editor } from '@tiptap/core'
import styles from './EditorBubbleMenu.module.scss'
import { Icon } from '../_shared/Icon'
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 { useLocalize } from '../../../context/localize'
import { LinkForm } from './LinkForm'
import validateUrl from '../../../utils/validateUrl'
type BubbleMenuProps = {
editor: Editor
@ -18,17 +19,13 @@ export const EditorBubbleMenu = (props: BubbleMenuProps) => {
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 isActive = (name: string, attributes?: {}, checkPrevUrl?: boolean) =>
const isActive = (name: string, attributes?: {}) =>
createEditorTransaction(
() => props.editor,
(editor) => {
editor && editor.isActive(name, attributes)
if (checkPrevUrl) {
setPrevUrl(editor && editor.getAttributes('link').href)
}
return editor && editor.isActive(name, attributes)
}
)
@ -40,23 +37,46 @@ export const EditorBubbleMenu = (props: BubbleMenuProps) => {
const isBlockQuote = isActive('blockquote')
const isOrderedList = isActive('isOrderedList')
const isBulletList = isActive('isBulletList')
const isLink = isActive('link', {}, true)
const isLink = isActive('link')
//TODO: вынести логику линки в отдельный компонент
const toggleLinkForm = () => {
setLinkError(null)
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 handleSubmitLink = (e) => {
e.preventDefault()
if (url().length > 1 && validateUrl(url())) {
props.editor.chain().focus().toggleLink({ href: url() }).run()
clearLinkForm()
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 = () => {
if (listBubbleOpen()) setListBubbleOpen(false)
setTextSizeBubbleOpen((prev) => !prev)
@ -72,21 +92,23 @@ export const EditorBubbleMenu = (props: BubbleMenuProps) => {
<Switch>
<Match when={linkEditorOpen()}>
<>
<form onSubmit={(e) => handleSubmitLink(e)} class={styles.linkForm}>
{/*<LinkForm editor={props.editor} editorOpen={linkEditorOpen()} />*/}
<div class={styles.linkForm}>
<input
type="text"
placeholder={t('Enter URL address')}
autofocus
value={prevUrl() ? prevUrl() : null}
onChange={(e) => setUrl(e.currentTarget.value)}
value={currentUrl()}
onKeyPress={(e) => handleKeyPress(e)}
onChange={(e) => handleUrlChange(e.currentTarget.value)}
/>
<button type="submit">
<button type="button" onClick={() => handleSubmitLink()} disabled={linkError() !== null}>
<Icon name="status-done" />
</button>
<button type="button" onClick={() => clearLinkForm()}>
<Icon name="status-cancel" />
{currentUrl() ? 'Ж' : <Icon name="status-cancel" />}
</button>
</form>
</div>
{linkError() && <div class={styles.linkError}>{linkError()}</div>}
</>
</Match>
@ -181,9 +203,7 @@ export const EditorBubbleMenu = (props: BubbleMenuProps) => {
<div class={styles.delimiter} />
<button
type="button"
onClick={(e) => {
setLinkEditorOpen(true)
}}
onClick={toggleLinkForm}
class={clsx(styles.bubbleMenuButton, {
[styles.bubbleMenuButtonActive]: isLink()
})}

View File

@ -0,0 +1,38 @@
.LnkForm {
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;
&: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);
}
}

View File

@ -0,0 +1,78 @@
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'
type Props = {
editor: Editor
editorOpen: boolean
}
export const LinkForm = (props: Props) => {
const { t } = useLocalize()
const [editorOpen, setEditorOpen] = createSignal<boolean>(props.editorOpen)
const [url, setUrl] = createSignal<string>('')
const [linkError, setLinkError] = createSignal<string | null>(null)
createSignal(() => {
setEditorOpen(props.editorOpen)
})
const currentUrl = createEditorTransaction(
() => props.editor,
(editor) => {
return (editor && editor.getAttributes('link').href) || ''
}
)
const clearLinkForm = () => {
if (currentUrl()) {
props.editor.chain().focus().unsetLink().run()
}
setUrl('')
setEditorOpen(false)
}
const handleUrlChange = (value) => {
setUrl(value)
}
const handleSubmitLink = () => {
if (validateUrl(url())) {
props.editor.chain().focus().setLink({ href: url() }).run()
setEditorOpen(false)
} else {
setLinkError(t('Invalid url format'))
}
}
const handleKeyPress = (event) => {
const key = event.key
if (key === 'Enter') handleSubmitLink()
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)}
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>}
</div>
)
}

View File

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

View File

@ -17,7 +17,7 @@ export const TopicSelect = (props: TopicSelectProps) => {
const selectProps = createOptions(props.topics, {
key: 'title',
disable: (topic) => {
console.log({ selectedTopics: clone(props.selectedTopics) })
// console.log({ selectedTopics: clone(props.selectedTopics) })
return props.selectedTopics.some((selectedTopic) => selectedTopic.slug === topic.slug)
}
})