link form setup
This commit is contained in:
parent
d5a5e79daa
commit
3b3d4fecbe
628
package-lock.json
generated
628
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
|
@ -17,8 +17,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'
|
||||||
|
@ -32,7 +30,7 @@ import { Image } from '@tiptap/extension-image'
|
||||||
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'
|
||||||
import { EditorBubbleMenu } from './EditorBubbleMenu'
|
import { EditorBubbleMenu } from './EditorBubbleMenu/EditorBubbleMenu'
|
||||||
import { EditorFloatingMenu } from './EditorFloatingMenu'
|
import { EditorFloatingMenu } from './EditorFloatingMenu'
|
||||||
import './Prosemirror.scss'
|
import './Prosemirror.scss'
|
||||||
|
|
||||||
|
@ -119,7 +117,6 @@ export const Editor = (props: EditorProps) => {
|
||||||
Heading,
|
Heading,
|
||||||
Highlight,
|
Highlight,
|
||||||
Image,
|
Image,
|
||||||
Link,
|
|
||||||
Youtube,
|
Youtube,
|
||||||
TrailingNode
|
TrailingNode
|
||||||
]
|
]
|
||||||
|
|
|
@ -1,6 +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(#000, 0.25);
|
||||||
|
|
||||||
.bubbleMenuButton {
|
.bubbleMenuButton {
|
||||||
display: inline-flex;
|
display: inline-flex;
|
||||||
|
@ -23,7 +23,6 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
&:hover,
|
|
||||||
.bubbleMenuButtonActive {
|
.bubbleMenuButtonActive {
|
||||||
opacity: 1;
|
opacity: 1;
|
||||||
}
|
}
|
||||||
|
@ -64,6 +63,12 @@
|
||||||
padding: 6px 11px;
|
padding: 6px 11px;
|
||||||
color: red;
|
color: red;
|
||||||
font-size: 0.7em;
|
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 {
|
|
@ -1,11 +1,12 @@
|
||||||
import { Switch, Match, createSignal, Show } from 'solid-js'
|
import { Switch, Match, createSignal, Show } from 'solid-js'
|
||||||
import type { Editor } from '@tiptap/core'
|
import type { Editor } from '@tiptap/core'
|
||||||
import styles from './EditorBubbleMenu.module.scss'
|
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 { useLocalize } from '../../context/localize'
|
import { useLocalize } from '../../../context/localize'
|
||||||
import validateUrl from '../../utils/validateUrl'
|
import { LinkForm } from './LinkForm'
|
||||||
|
import validateUrl from '../../../utils/validateUrl'
|
||||||
|
|
||||||
type BubbleMenuProps = {
|
type BubbleMenuProps = {
|
||||||
editor: Editor
|
editor: Editor
|
||||||
|
@ -18,17 +19,13 @@ export const EditorBubbleMenu = (props: BubbleMenuProps) => {
|
||||||
const [listBubbleOpen, setListBubbleOpen] = createSignal<boolean>(false)
|
const [listBubbleOpen, setListBubbleOpen] = createSignal<boolean>(false)
|
||||||
const [linkEditorOpen, setLinkEditorOpen] = createSignal<boolean>(false)
|
const [linkEditorOpen, setLinkEditorOpen] = createSignal<boolean>(false)
|
||||||
const [url, setUrl] = createSignal<string>('')
|
const [url, setUrl] = createSignal<string>('')
|
||||||
const [prevUrl, setPrevUrl] = createSignal<string | null>(null)
|
|
||||||
const [linkError, setLinkError] = createSignal<string | null>(null)
|
const [linkError, setLinkError] = createSignal<string | null>(null)
|
||||||
|
|
||||||
const isActive = (name: string, attributes?: {}, checkPrevUrl?: boolean) =>
|
const isActive = (name: string, attributes?: {}) =>
|
||||||
createEditorTransaction(
|
createEditorTransaction(
|
||||||
() => props.editor,
|
() => props.editor,
|
||||||
(editor) => {
|
(editor) => {
|
||||||
editor && editor.isActive(name, attributes)
|
return editor && editor.isActive(name, attributes)
|
||||||
if (checkPrevUrl) {
|
|
||||||
setPrevUrl(editor && editor.getAttributes('link').href)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -40,23 +37,46 @@ export const EditorBubbleMenu = (props: BubbleMenuProps) => {
|
||||||
const isBlockQuote = isActive('blockquote')
|
const isBlockQuote = isActive('blockquote')
|
||||||
const isOrderedList = isActive('isOrderedList')
|
const isOrderedList = isActive('isOrderedList')
|
||||||
const isBulletList = isActive('isBulletList')
|
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 = () => {
|
const clearLinkForm = () => {
|
||||||
|
if (currentUrl()) {
|
||||||
|
props.editor.chain().focus().unsetLink().run()
|
||||||
|
}
|
||||||
setUrl('')
|
setUrl('')
|
||||||
setLinkEditorOpen(false)
|
setLinkEditorOpen(false)
|
||||||
}
|
}
|
||||||
|
const handleUrlChange = (value) => {
|
||||||
const handleSubmitLink = (e) => {
|
setUrl(value)
|
||||||
e.preventDefault()
|
}
|
||||||
if (url().length > 1 && validateUrl(url())) {
|
const handleSubmitLink = () => {
|
||||||
props.editor.chain().focus().toggleLink({ href: url() }).run()
|
if (validateUrl(url())) {
|
||||||
clearLinkForm()
|
props.editor.chain().focus().setLink({ href: url() }).run()
|
||||||
|
setLinkEditorOpen(false)
|
||||||
} else {
|
} else {
|
||||||
setLinkError(t('Invalid url format'))
|
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)
|
||||||
|
@ -72,21 +92,23 @@ export const EditorBubbleMenu = (props: BubbleMenuProps) => {
|
||||||
<Switch>
|
<Switch>
|
||||||
<Match when={linkEditorOpen()}>
|
<Match when={linkEditorOpen()}>
|
||||||
<>
|
<>
|
||||||
<form onSubmit={(e) => handleSubmitLink(e)} class={styles.linkForm}>
|
{/*<LinkForm editor={props.editor} editorOpen={linkEditorOpen()} />*/}
|
||||||
|
<div class={styles.linkForm}>
|
||||||
<input
|
<input
|
||||||
type="text"
|
type="text"
|
||||||
placeholder={t('Enter URL address')}
|
placeholder={t('Enter URL address')}
|
||||||
autofocus
|
autofocus
|
||||||
value={prevUrl() ? prevUrl() : null}
|
value={currentUrl()}
|
||||||
onChange={(e) => setUrl(e.currentTarget.value)}
|
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" />
|
<Icon name="status-done" />
|
||||||
</button>
|
</button>
|
||||||
<button type="button" onClick={() => clearLinkForm()}>
|
<button type="button" onClick={() => clearLinkForm()}>
|
||||||
<Icon name="status-cancel" />
|
{currentUrl() ? 'Ж' : <Icon name="status-cancel" />}
|
||||||
</button>
|
</button>
|
||||||
</form>
|
</div>
|
||||||
{linkError() && <div class={styles.linkError}>{linkError()}</div>}
|
{linkError() && <div class={styles.linkError}>{linkError()}</div>}
|
||||||
</>
|
</>
|
||||||
</Match>
|
</Match>
|
||||||
|
@ -181,9 +203,7 @@ export const EditorBubbleMenu = (props: BubbleMenuProps) => {
|
||||||
<div class={styles.delimiter} />
|
<div class={styles.delimiter} />
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
onClick={(e) => {
|
onClick={toggleLinkForm}
|
||||||
setLinkEditorOpen(true)
|
|
||||||
}}
|
|
||||||
class={clsx(styles.bubbleMenuButton, {
|
class={clsx(styles.bubbleMenuButton, {
|
||||||
[styles.bubbleMenuButtonActive]: isLink()
|
[styles.bubbleMenuButtonActive]: isLink()
|
||||||
})}
|
})}
|
|
@ -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);
|
||||||
|
}
|
||||||
|
}
|
78
src/components/Editor/EditorBubbleMenu/LinkForm/LinkForm.tsx
Normal file
78
src/components/Editor/EditorBubbleMenu/LinkForm/LinkForm.tsx
Normal 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>
|
||||||
|
)
|
||||||
|
}
|
1
src/components/Editor/EditorBubbleMenu/LinkForm/index.ts
Normal file
1
src/components/Editor/EditorBubbleMenu/LinkForm/index.ts
Normal file
|
@ -0,0 +1 @@
|
||||||
|
export { LinkForm } from './LinkForm'
|
|
@ -17,7 +17,7 @@ export const TopicSelect = (props: TopicSelectProps) => {
|
||||||
const selectProps = createOptions(props.topics, {
|
const selectProps = createOptions(props.topics, {
|
||||||
key: 'title',
|
key: 'title',
|
||||||
disable: (topic) => {
|
disable: (topic) => {
|
||||||
console.log({ selectedTopics: clone(props.selectedTopics) })
|
// console.log({ selectedTopics: clone(props.selectedTopics) })
|
||||||
return props.selectedTopics.some((selectedTopic) => selectedTopic.slug === topic.slug)
|
return props.selectedTopics.some((selectedTopic) => selectedTopic.slug === topic.slug)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
Loading…
Reference in New Issue
Block a user