floating-menu-restored
This commit is contained in:
parent
d28b4f2ab3
commit
92292d70e5
|
@ -1,17 +1,15 @@
|
||||||
import type { Editor } from '@tiptap/core'
|
import type { Editor } from '@tiptap/core'
|
||||||
import { Show, createEffect, createSignal } from 'solid-js'
|
import { Show, createEffect, createSignal } from 'solid-js'
|
||||||
import { isServer } from 'solid-js/web'
|
import { UploadModalContent } from '~/components/Upload/UploadModalContent/UploadModalContent'
|
||||||
import { renderUploadedImage } from '~/components/Upload/renderUploadedImage'
|
import { renderUploadedImage } from '~/components/Upload/renderUploadedImage'
|
||||||
import { Icon } from '~/components/_shared/Icon'
|
import { InlineForm } from '~/components/_shared/InlineForm/InlineForm'
|
||||||
import { useLocalize } from '~/context/localize'
|
import { Modal } from '~/components/_shared/Modal/Modal'
|
||||||
import { useUI } from '~/context/ui'
|
import { useUI } from '~/context/ui'
|
||||||
import { useOutsideClickHandler } from '~/lib/useOutsideClickHandler'
|
import { useOutsideClickHandler } from '~/lib/useOutsideClickHandler'
|
||||||
import { UploadedFile } from '~/types/upload'
|
import { UploadedFile } from '~/types/upload'
|
||||||
import { UploadModalContent } from '../../Upload/UploadModalContent'
|
import { useLocalize } from '../../../context/localize'
|
||||||
import { InlineForm } from '../../_shared/InlineForm'
|
import { Icon } from '../../_shared/Icon'
|
||||||
import { Modal } from '../../_shared/Modal'
|
import { Menu, type MenuItem } from './Menu/Menu'
|
||||||
import { Menu } from './Menu'
|
|
||||||
import type { MenuItem } from './Menu/Menu'
|
|
||||||
|
|
||||||
import styles from './EditorFloatingMenu.module.scss'
|
import styles from './EditorFloatingMenu.module.scss'
|
||||||
|
|
||||||
|
@ -20,110 +18,99 @@ type FloatingMenuProps = {
|
||||||
ref: (el: HTMLDivElement) => void
|
ref: (el: HTMLDivElement) => void
|
||||||
}
|
}
|
||||||
|
|
||||||
const embedData = (data: string) => {
|
const embedData = (data: string): { [key: string]: string } | undefined => {
|
||||||
const element = document.createRange().createContextualFragment(data)
|
const parser = new DOMParser()
|
||||||
const { attributes } = element.firstChild as HTMLIFrameElement
|
const doc = parser.parseFromString(data, 'text/html')
|
||||||
const result: { src: string; width?: string; height?: string } = { src: '' }
|
const iframe = doc.querySelector('iframe')
|
||||||
if (isServer) return result
|
|
||||||
|
|
||||||
for (let i = 0; i < attributes.length; i++) {
|
if (!iframe) {
|
||||||
const attribute = attributes.item(i)
|
return undefined
|
||||||
if (attribute?.name) {
|
}
|
||||||
result[attribute.name as keyof typeof result] = attribute.value as string
|
const attributes: { [key: string]: string } = {}
|
||||||
}
|
for (const attr of Array.from(iframe.attributes)) {
|
||||||
|
attributes[attr.name] = attr.value
|
||||||
}
|
}
|
||||||
|
|
||||||
return result
|
return attributes
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const validateEmbed = (value: string): boolean => {
|
||||||
|
const parser = new DOMParser()
|
||||||
|
const doc = parser.parseFromString(value, 'text/html')
|
||||||
|
const iframe = doc.querySelector('iframe')
|
||||||
|
|
||||||
|
return !iframe || !iframe.getAttribute('src')
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
export const EditorFloatingMenu = (props: FloatingMenuProps) => {
|
export const EditorFloatingMenu = (props: FloatingMenuProps) => {
|
||||||
const { t } = useLocalize()
|
const { t } = useLocalize()
|
||||||
const { showModal, hideModal } = useUI()
|
const { showModal } = useUI()
|
||||||
const [selectedMenuItem, setSelectedMenuItem] = createSignal<MenuItem | undefined>()
|
const [selectedMenuItem, setSelectedMenuItem] = createSignal<MenuItem | undefined>()
|
||||||
const [menuOpen, setMenuOpen] = createSignal<boolean>(false)
|
const [menuOpen, setMenuOpen] = createSignal<boolean>(false)
|
||||||
let menuRef: HTMLDivElement | undefined
|
const [menuRef, setMenuRef] = createSignal<HTMLDivElement | undefined>()
|
||||||
let plusButtonRef: HTMLButtonElement | undefined
|
const [plusButtonRef, setPlusButtonRef] = createSignal<HTMLButtonElement | undefined>()
|
||||||
const handleEmbedFormSubmit = async (value: string) => {
|
const handleEmbedFormSubmit = async (value: string) => {
|
||||||
// TODO: add support instagram embed (blockquote)
|
// TODO: add support instagram embed (blockquote)
|
||||||
const emb = await embedData(value)
|
const emb = await embedData(value)
|
||||||
props.editor
|
emb && props.editor.chain().focus().setIframe({ src: emb.src }).run()
|
||||||
?.chain()
|
|
||||||
.focus()
|
|
||||||
.insertContent({
|
|
||||||
type: 'figure',
|
|
||||||
attrs: { 'data-type': 'iframe' },
|
|
||||||
content: [
|
|
||||||
{
|
|
||||||
type: 'iframe',
|
|
||||||
attrs: {
|
|
||||||
src: emb.src,
|
|
||||||
width: emb.width,
|
|
||||||
height: emb.height
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
type: 'figcaption',
|
|
||||||
content: [{ type: 'text', text: t('Description') }]
|
|
||||||
}
|
|
||||||
]
|
|
||||||
})
|
|
||||||
.run()
|
|
||||||
}
|
|
||||||
|
|
||||||
const validateEmbed = (value: string) => {
|
|
||||||
const element = document.createRange().createContextualFragment(value)
|
|
||||||
if (element.firstChild?.nodeName !== 'IFRAME') {
|
|
||||||
return t('Error')
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
createEffect(() => {
|
createEffect(() => {
|
||||||
if (selectedMenuItem() === 'image') {
|
switch (selectedMenuItem()) {
|
||||||
showModal('uploadImage')
|
case 'image': {
|
||||||
return
|
showModal('uploadImage')
|
||||||
}
|
return
|
||||||
if (selectedMenuItem() === 'horizontal-rule') {
|
}
|
||||||
props.editor?.chain().focus().setHorizontalRule().run()
|
case 'horizontal-rule': {
|
||||||
setSelectedMenuItem()
|
props.editor.chain().focus().setHorizontalRule().run()
|
||||||
return
|
setSelectedMenuItem()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
default: {
|
||||||
|
props.editor.chain().focus().run()
|
||||||
|
setSelectedMenuItem()
|
||||||
|
return
|
||||||
|
}
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
const closeUploadModalHandler = () => {
|
const closeUploadModalHandler = () => {
|
||||||
setSelectedMenuItem()
|
setSelectedMenuItem()
|
||||||
setMenuOpen(false)
|
setMenuOpen(false)
|
||||||
setSelectedMenuItem()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
useOutsideClickHandler({
|
useOutsideClickHandler({
|
||||||
containerRef: menuRef,
|
containerRef: menuRef()!,
|
||||||
handler: (e) => {
|
handler: (e) => {
|
||||||
if (plusButtonRef?.contains(e.target)) {
|
if (plusButtonRef()?.contains(e.target as Node)) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if (menuOpen()) {
|
if (menuOpen()) {
|
||||||
setMenuOpen(false)
|
setMenuOpen(false)
|
||||||
setSelectedMenuItem()
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
const handleUpload = (image: UploadedFile) => {
|
const handleUpload = (image: UploadedFile) => {
|
||||||
renderUploadedImage(props.editor, image)
|
renderUploadedImage(props.editor, image)
|
||||||
hideModal()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<div ref={props.ref} class={styles.editorFloatingMenu}>
|
<div ref={props.ref} class={styles.editorFloatingMenu}>
|
||||||
<button ref={(el) => (plusButtonRef = el)} type="button" onClick={() => setMenuOpen(!menuOpen())}>
|
<button ref={setPlusButtonRef} type="button" onClick={() => setMenuOpen(!menuOpen())}>
|
||||||
<Icon name="editor-plus" />
|
<Icon name="editor-plus" />
|
||||||
</button>
|
</button>
|
||||||
<Show when={menuOpen()}>
|
<Show when={menuOpen()}>
|
||||||
<div class={styles.menuHolder} ref={(el) => (menuRef = el)}>
|
<div class={styles.menuHolder} ref={setMenuRef}>
|
||||||
<Show when={!selectedMenuItem()}>
|
<Show when={!selectedMenuItem()}>
|
||||||
<Menu selectedItem={(value: string) => setSelectedMenuItem(value as MenuItem)} />
|
<Menu
|
||||||
|
selectedItem={(value: string) => {
|
||||||
|
setSelectedMenuItem(value as MenuItem)
|
||||||
|
}}
|
||||||
|
/>
|
||||||
</Show>
|
</Show>
|
||||||
<Show when={selectedMenuItem() === 'embed'}>
|
<Show when={selectedMenuItem() === 'embed'}>
|
||||||
<InlineForm
|
<InlineForm
|
||||||
|
@ -131,7 +118,7 @@ export const EditorFloatingMenu = (props: FloatingMenuProps) => {
|
||||||
showInput={true}
|
showInput={true}
|
||||||
onClose={closeUploadModalHandler}
|
onClose={closeUploadModalHandler}
|
||||||
onClear={() => setSelectedMenuItem()}
|
onClear={() => setSelectedMenuItem()}
|
||||||
validate={(val) => validateEmbed(val) || ''}
|
validate={(value: string) => validateEmbed(value) ? t('Error') : ''}
|
||||||
onSubmit={handleEmbedFormSubmit}
|
onSubmit={handleEmbedFormSubmit}
|
||||||
/>
|
/>
|
||||||
</Show>
|
</Show>
|
||||||
|
@ -140,7 +127,7 @@ export const EditorFloatingMenu = (props: FloatingMenuProps) => {
|
||||||
</div>
|
</div>
|
||||||
<Modal variant="narrow" name="uploadImage" onClose={closeUploadModalHandler}>
|
<Modal variant="narrow" name="uploadImage" onClose={closeUploadModalHandler}>
|
||||||
<UploadModalContent
|
<UploadModalContent
|
||||||
onClose={(value) => {
|
onClose={(value?: UploadedFile) => {
|
||||||
handleUpload(value as UploadedFile)
|
handleUpload(value as UploadedFile)
|
||||||
setSelectedMenuItem()
|
setSelectedMenuItem()
|
||||||
}}
|
}}
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
.TextBubbleMenu {
|
.TextBubbleMenu {
|
||||||
background: var(--editor-bubble-menu-background);
|
background: var(--editor-bubble-menu-background);
|
||||||
box-shadow: 0 4px 10px rgba(#000, 0.25);
|
box-shadow: 0 4px 10px rgba(var(--primary-color), 0.25);
|
||||||
|
|
||||||
&.growWidth {
|
&.growWidth {
|
||||||
min-width: 460px;
|
min-width: 460px;
|
||||||
|
@ -36,7 +36,7 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
.delimiter {
|
.delimiter {
|
||||||
background: #999;
|
background: var(--secondary-color);
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
height: 1.4em;
|
height: 1.4em;
|
||||||
margin: 0 0.2em;
|
margin: 0 0.2em;
|
||||||
|
@ -58,7 +58,7 @@
|
||||||
left: 50%;
|
left: 50%;
|
||||||
transform: translateX(-50%);
|
transform: translateX(-50%);
|
||||||
box-shadow: 0 4px 10px rgb(0 0 0 / 25%);
|
box-shadow: 0 4px 10px rgb(0 0 0 / 25%);
|
||||||
background: var(--editor-bubble-menu-background);
|
background: var(--background-color);
|
||||||
color: var(--default-color);
|
color: var(--default-color);
|
||||||
|
|
||||||
& > header {
|
& > header {
|
||||||
|
|
|
@ -1,9 +1,7 @@
|
||||||
@import 'fonts';
|
@import 'fonts';
|
||||||
@import 'grid';
|
@import 'grid';
|
||||||
|
|
||||||
*,
|
* {
|
||||||
*::before,
|
|
||||||
*::after {
|
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
Loading…
Reference in New Issue
Block a user