floating-menu-restored
Some checks failed
deploy / testbuild (push) Failing after 46s
deploy / Update templates on Mailgun (push) Has been skipped

This commit is contained in:
Untone 2024-10-09 23:12:13 +03:00
parent d28b4f2ab3
commit 92292d70e5
3 changed files with 60 additions and 75 deletions

View File

@ -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()
}} }}

View File

@ -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 {

View File

@ -1,9 +1,7 @@
@import 'fonts'; @import 'fonts';
@import 'grid'; @import 'grid';
*, * {
*::before,
*::after {
box-sizing: border-box; box-sizing: border-box;
} }