diff --git a/src/components/Editor/Toolbar/EditorFloatingMenu.tsx b/src/components/Editor/Toolbar/EditorFloatingMenu.tsx index 99c93d0c..13e91cea 100644 --- a/src/components/Editor/Toolbar/EditorFloatingMenu.tsx +++ b/src/components/Editor/Toolbar/EditorFloatingMenu.tsx @@ -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() const [menuOpen, setMenuOpen] = createSignal(false) - let menuRef: HTMLDivElement | undefined - let plusButtonRef: HTMLButtonElement | undefined + const [menuRef, setMenuRef] = createSignal() + const [plusButtonRef, setPlusButtonRef] = createSignal() 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 ( <>
- -
(menuRef = el)}> +
- setSelectedMenuItem(value as MenuItem)} /> + { + setSelectedMenuItem(value as MenuItem) + }} + /> { showInput={true} onClose={closeUploadModalHandler} onClear={() => setSelectedMenuItem()} - validate={(val) => validateEmbed(val) || ''} + validate={(value: string) => validateEmbed(value) ? t('Error') : ''} onSubmit={handleEmbedFormSubmit} /> @@ -140,7 +127,7 @@ export const EditorFloatingMenu = (props: FloatingMenuProps) => {
{ + onClose={(value?: UploadedFile) => { handleUpload(value as UploadedFile) setSelectedMenuItem() }} diff --git a/src/components/Editor/Toolbar/TextBubbleMenu.module.scss b/src/components/Editor/Toolbar/TextBubbleMenu.module.scss index 9f022771..533a8412 100644 --- a/src/components/Editor/Toolbar/TextBubbleMenu.module.scss +++ b/src/components/Editor/Toolbar/TextBubbleMenu.module.scss @@ -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 { diff --git a/src/styles/app.scss b/src/styles/app.scss index 47e438e6..646174a4 100644 --- a/src/styles/app.scss +++ b/src/styles/app.scss @@ -1,9 +1,7 @@ @import 'fonts'; @import 'grid'; -*, -*::before, -*::after { +* { box-sizing: border-box; }