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 { 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 { Icon } from '~/components/_shared/Icon'
import { useLocalize } from '~/context/localize'
import { InlineForm } from '~/components/_shared/InlineForm/InlineForm'
import { Modal } from '~/components/_shared/Modal/Modal'
import { useUI } from '~/context/ui'
import { useOutsideClickHandler } from '~/lib/useOutsideClickHandler'
import { UploadedFile } from '~/types/upload'
import { UploadModalContent } from '../../Upload/UploadModalContent'
import { InlineForm } from '../../_shared/InlineForm'
import { Modal } from '../../_shared/Modal'
import { Menu } from './Menu'
import type { MenuItem } from './Menu/Menu'
import { useLocalize } from '../../../context/localize'
import { Icon } from '../../_shared/Icon'
import { Menu, type MenuItem } from './Menu/Menu'
import styles from './EditorFloatingMenu.module.scss'
@ -20,110 +18,99 @@ type FloatingMenuProps = {
ref: (el: HTMLDivElement) => void
}
const embedData = (data: string) => {
const element = document.createRange().createContextualFragment(data)
const { attributes } = element.firstChild as HTMLIFrameElement
const result: { src: string; width?: string; height?: string } = { src: '' }
if (isServer) return result
const embedData = (data: string): { [key: string]: string } | undefined => {
const parser = new DOMParser()
const doc = parser.parseFromString(data, 'text/html')
const iframe = doc.querySelector('iframe')
for (let i = 0; i < attributes.length; i++) {
const attribute = attributes.item(i)
if (attribute?.name) {
result[attribute.name as keyof typeof result] = attribute.value as string
}
if (!iframe) {
return undefined
}
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) => {
const { t } = useLocalize()
const { showModal, hideModal } = useUI()
const { showModal } = useUI()
const [selectedMenuItem, setSelectedMenuItem] = createSignal<MenuItem | undefined>()
const [menuOpen, setMenuOpen] = createSignal<boolean>(false)
let menuRef: HTMLDivElement | undefined
let plusButtonRef: HTMLButtonElement | undefined
const [menuRef, setMenuRef] = createSignal<HTMLDivElement | undefined>()
const [plusButtonRef, setPlusButtonRef] = createSignal<HTMLButtonElement | undefined>()
const handleEmbedFormSubmit = async (value: string) => {
// TODO: add support instagram embed (blockquote)
const emb = await embedData(value)
props.editor
?.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')
}
emb && props.editor.chain().focus().setIframe({ src: emb.src }).run()
}
createEffect(() => {
if (selectedMenuItem() === 'image') {
showModal('uploadImage')
return
}
if (selectedMenuItem() === 'horizontal-rule') {
props.editor?.chain().focus().setHorizontalRule().run()
setSelectedMenuItem()
return
switch (selectedMenuItem()) {
case 'image': {
showModal('uploadImage')
return
}
case 'horizontal-rule': {
props.editor.chain().focus().setHorizontalRule().run()
setSelectedMenuItem()
return
}
default: {
props.editor.chain().focus().run()
setSelectedMenuItem()
return
}
}
})
const closeUploadModalHandler = () => {
setSelectedMenuItem()
setMenuOpen(false)
setSelectedMenuItem()
}
useOutsideClickHandler({
containerRef: menuRef,
containerRef: menuRef()!,
handler: (e) => {
if (plusButtonRef?.contains(e.target)) {
if (plusButtonRef()?.contains(e.target as Node)) {
return
}
if (menuOpen()) {
setMenuOpen(false)
setSelectedMenuItem()
}
}
})
const handleUpload = (image: UploadedFile) => {
renderUploadedImage(props.editor, image)
hideModal()
}
return (
<>
<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" />
</button>
<Show when={menuOpen()}>
<div class={styles.menuHolder} ref={(el) => (menuRef = el)}>
<div class={styles.menuHolder} ref={setMenuRef}>
<Show when={!selectedMenuItem()}>
<Menu selectedItem={(value: string) => setSelectedMenuItem(value as MenuItem)} />
<Menu
selectedItem={(value: string) => {
setSelectedMenuItem(value as MenuItem)
}}
/>
</Show>
<Show when={selectedMenuItem() === 'embed'}>
<InlineForm
@ -131,7 +118,7 @@ export const EditorFloatingMenu = (props: FloatingMenuProps) => {
showInput={true}
onClose={closeUploadModalHandler}
onClear={() => setSelectedMenuItem()}
validate={(val) => validateEmbed(val) || ''}
validate={(value: string) => validateEmbed(value) ? t('Error') : ''}
onSubmit={handleEmbedFormSubmit}
/>
</Show>
@ -140,7 +127,7 @@ export const EditorFloatingMenu = (props: FloatingMenuProps) => {
</div>
<Modal variant="narrow" name="uploadImage" onClose={closeUploadModalHandler}>
<UploadModalContent
onClose={(value) => {
onClose={(value?: UploadedFile) => {
handleUpload(value as UploadedFile)
setSelectedMenuItem()
}}

View File

@ -1,6 +1,6 @@
.TextBubbleMenu {
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 {
min-width: 460px;
@ -36,7 +36,7 @@
}
.delimiter {
background: #999;
background: var(--secondary-color);
display: inline-block;
height: 1.4em;
margin: 0 0.2em;
@ -58,7 +58,7 @@
left: 50%;
transform: translateX(-50%);
box-shadow: 0 4px 10px rgb(0 0 0 / 25%);
background: var(--editor-bubble-menu-background);
background: var(--background-color);
color: var(--default-color);
& > header {

View File

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