From d28b4f2ab3639b970bd35028c90f0ac7d5ba6c89 Mon Sep 17 00:00:00 2001 From: Untone Date: Wed, 9 Oct 2024 22:35:45 +0300 Subject: [PATCH] popper-fix+textbubble-revert --- package.json | 7 +- src/components/Article/FullArticle.tsx | 40 +- .../Editor/Toolbar/TextBubbleMenu.tsx | 716 ++++++++---------- src/lib/createTooltip.ts | 93 --- 4 files changed, 347 insertions(+), 509 deletions(-) delete mode 100644 src/lib/createTooltip.ts diff --git a/package.json b/package.json index d195cd13..8adbc886 100644 --- a/package.json +++ b/package.json @@ -133,12 +133,7 @@ "yjs": "13.6.19", "y-prosemirror": "1.2.12" }, - "trustedDependencies": [ - "@biomejs/biome", - "@swc/core", - "esbuild", - "protobufjs" - ], + "trustedDependencies": ["@biomejs/biome", "@swc/core", "esbuild", "protobufjs"], "dependencies": { "form-data": "^4.0.0", "idb": "^8.0.0", diff --git a/src/components/Article/FullArticle.tsx b/src/components/Article/FullArticle.tsx index f11d302d..f1f05205 100644 --- a/src/components/Article/FullArticle.tsx +++ b/src/components/Article/FullArticle.tsx @@ -4,6 +4,7 @@ import { A, useSearchParams } from '@solidjs/router' import { clsx } from 'clsx' import { For, Show, createEffect, createMemo, createSignal, on, onCleanup, onMount } from 'solid-js' import { isServer } from 'solid-js/web' +import usePopper from 'solid-popper' import { useFeed } from '~/context/feed' import { useLocalize } from '~/context/localize' @@ -13,7 +14,6 @@ import { DEFAULT_HEADER_OFFSET, useUI } from '~/context/ui' import type { Author, Maybe, Shout, Topic } from '~/graphql/schema/core.gen' import { processPrepositions } from '~/intl/prepositions' import { isCyrillic } from '~/intl/translate' -import { createTooltip } from '~/lib/createTooltip' import { getImageUrl } from '~/lib/getThumbUrl' import { MediaItem } from '~/types/mediaitem' import { capitalize } from '~/utils/capitalize' @@ -217,25 +217,25 @@ export const FullArticle = (props: Props) => { element.setAttribute('href', 'javascript: void(0)') } - const popperInstance = createTooltip(element, tooltip, { - placement: 'top', - modifiers: [ - { - name: 'eventListeners', - options: { scroll: false } - }, - { - name: 'offset', - options: { - offset: [0, 8] + const popperInstance = usePopper( + () => element, + () => tooltip, + { + placement: 'top', + modifiers: [ + { + name: 'offset', + options: { + offset: [0, 8] + } + }, + { + name: 'flip', + options: { fallbackPlacements: ['top'] } } - }, - { - name: 'flip', - options: { fallbackPlacements: ['top'] } - } - ] - }) + ] + } + ) tooltip.style.visibility = 'hidden' let isTooltipVisible = false @@ -248,7 +248,7 @@ export const FullArticle = (props: Props) => { isTooltipVisible = true } - popperInstance.update() + popperInstance()?.update() } const handleDocumentClick = (e: MouseEvent) => { diff --git a/src/components/Editor/Toolbar/TextBubbleMenu.tsx b/src/components/Editor/Toolbar/TextBubbleMenu.tsx index b129b309..23dfe8a3 100644 --- a/src/components/Editor/Toolbar/TextBubbleMenu.tsx +++ b/src/components/Editor/Toolbar/TextBubbleMenu.tsx @@ -1,10 +1,10 @@ import type { Editor } from '@tiptap/core' import { clsx } from 'clsx' -import { Match, Show, Switch, createEffect, createMemo, createSignal, onCleanup, onMount } from 'solid-js' +import { Match, Show, Switch, createEffect, createSignal, onCleanup, onMount } from 'solid-js' import { createEditorTransaction } from 'solid-tiptap' -import { Icon } from '~/components/_shared/Icon' -import { Popover } from '~/components/_shared/Popover' -import { useLocalize } from '~/context/localize' +import { useLocalize } from '../../../context/localize' +import { Icon } from '../../_shared/Icon' +import { Popover } from '../../_shared/Popover' import { MiniEditor } from '../MiniEditor' import { InsertLinkForm } from './InsertLinkForm' @@ -20,445 +20,381 @@ type BubbleMenuProps = { export const TextBubbleMenu = (props: BubbleMenuProps) => { const { t } = useLocalize() - const isActive = createMemo( - () => (name: string, attributes?: Record) => - props.editor?.isActive(name, attributes) - ) + const isActive = (name: string, attributes?: Record) => + createEditorTransaction( + () => props.editor, + (e) => e?.isActive(name, attributes) + ) - const [menuState, setMenuState] = createSignal({ - textSizeBubbleOpen: false, - listBubbleOpen: false, - linkEditorOpen: false, - footnoteEditorOpen: false, - footNote: undefined as string | undefined - }) + const [textSizeBubbleOpen, setTextSizeBubbleOpen] = createSignal(false) + const [listBubbleOpen, setListBubbleOpen] = createSignal(false) + const [linkEditorOpen, setLinkEditorOpen] = createSignal(false) + const [footnoteEditorOpen, setFootnoteEditorOpen] = createSignal(false) + const [footNote, setFootNote] = createSignal() createEffect(() => { if (!props.shouldShow) { - setMenuState((prev) => ({ - ...prev, - footNote: undefined, - footnoteEditorOpen: false, - linkEditorOpen: false, - textSizeBubbleOpen: false, - listBubbleOpen: false - })) + setFootNote() + setFootnoteEditorOpen(false) } }) - const activeStates = createMemo(() => ({ - bold: isActive()('bold'), - italic: isActive()('italic'), - h1: isActive()('heading', { level: 2 }), - h2: isActive()('heading', { level: 3 }), - h3: isActive()('heading', { level: 4 }), - quote: isActive()('blockquote', { 'data-type': 'quote' }), - punchLine: isActive()('blockquote', { 'data-type': 'punchline' }), - orderedList: isActive()('orderedList'), - bulletList: isActive()('bulletList'), - link: isActive()('link'), - highlight: isActive()('highlight'), - footnote: isActive()('footnote'), - incut: isActive()('article') - // underline: isActive()('underline'), - })) + const isBold = isActive('bold') + const isItalic = isActive('italic') + const isH1 = isActive('heading', { level: 2 }) + const isH2 = isActive('heading', { level: 3 }) + const isH3 = isActive('heading', { level: 4 }) + const isQuote = isActive('blockquote', { 'data-type': 'quote' }) + const isPunchLine = isActive('blockquote', { 'data-type': 'punchline' }) + const isOrderedList = isActive('isOrderedList') + const isBulletList = isActive('isBulletList') + const isLink = isActive('link') + const isHighlight = isActive('highlight') + const isFootnote = isActive('footnote') + const isIncut = isActive('article') - const togglePopup = (type: 'textSize' | 'list') => { - setMenuState((prev) => ({ - ...prev, - textSizeBubbleOpen: type === 'textSize' ? !prev.textSizeBubbleOpen : false, - listBubbleOpen: type === 'list' ? !prev.listBubbleOpen : false - })) + const toggleTextSizePopup = () => { + if (listBubbleOpen()) { + setListBubbleOpen(false) + } + setTextSizeBubbleOpen((prev) => !prev) + } + const toggleListPopup = () => { + if (textSizeBubbleOpen()) { + setTextSizeBubbleOpen(false) + } + setListBubbleOpen((prev) => !prev) } - const handleKeyDown = (event: KeyboardEvent) => { if (event.code === 'KeyK' && (event.metaKey || event.ctrlKey) && !props.editor.state.selection.empty) { event.preventDefault() - setMenuState((prev) => ({ ...prev, linkEditorOpen: true })) + setLinkEditorOpen(true) } } const updateCurrentFootnoteValue = createEditorTransaction( () => props.editor, (ed) => { - if (!activeStates().footnote) { + if (!isFootnote()) { return } const value = ed.getAttributes('footnote').value - setMenuState((prev) => ({ ...prev, footNote: value })) + setFootNote(value) } ) - const handleAddFootnote = (footnote: string) => { - if (menuState().footNote) { - props.editor?.chain().focus().updateFootnote({ value: footnote }).run() + const handleAddFootnote = (value: string) => { + if (footNote()) { + props.editor.chain().focus().updateFootnote({ value }).run() } else { - props.editor?.chain().focus().setFootnote({ value: footnote }).run() + props.editor.chain().focus().setFootnote({ value }).run() } - setMenuState((prev) => ({ - ...prev, - footNote: undefined, - linkEditorOpen: false, - footnoteEditorOpen: false - })) + setFootNote() + setFootnoteEditorOpen(false) } const handleOpenFootnoteEditor = () => { updateCurrentFootnoteValue() - setMenuState((prev) => ({ ...prev, linkEditorOpen: false, footnoteEditorOpen: true })) + setFootnoteEditorOpen(true) } const handleSetPunchline = () => { - if (activeStates().punchLine) { - props.editor?.chain().focus().toggleBlockquote('punchline').run() + if (isPunchLine()) { + props.editor.chain().focus().toggleBlockquote('punchline').run() } - props.editor?.chain().focus().toggleBlockquote('quote').run() - togglePopup('textSize') + props.editor.chain().focus().toggleBlockquote('quote').run() + toggleTextSizePopup() } const handleSetQuote = () => { - if (activeStates().quote) { - props.editor?.chain().focus().toggleBlockquote('quote').run() + if (isQuote()) { + props.editor.chain().focus().toggleBlockquote('quote').run() } - props.editor?.chain().focus().toggleBlockquote('punchline').run() - togglePopup('textSize') + props.editor.chain().focus().toggleBlockquote('punchline').run() + toggleTextSizePopup() } onMount(() => { window.addEventListener('keydown', handleKeyDown) onCleanup(() => { window.removeEventListener('keydown', handleKeyDown) - setMenuState((prev) => ({ ...prev, linkEditorOpen: false })) }) }) - const handleOpenLinkForm = () => { - props.editor?.chain().focus().addTextWrap({ class: 'highlight-fake-selection' }).run() - setMenuState((prev) => ({ ...prev, linkEditorOpen: true })) - } - - const handleCloseLinkForm = () => { - setMenuState((prev) => ({ ...prev, linkEditorOpen: false })) - props.editor?.chain().focus().removeTextWrap({ class: 'highlight-fake-selection' }).run() - } - const handleFormat = (type: 'Bold' | 'Italic' | 'Underline', _attributes?: Record) => { - props.editor?.chain().focus()[`toggle${type}`]().run() - } - - const ListBubbleMenu = (props: BubbleMenuProps) => { - return ( -
-
{t('Lists')}
-
- - {(triggerRef: (el: HTMLElement) => void) => ( - - )} - - - {(triggerRef: (el: HTMLElement) => void) => ( - - )} - -
-
- ) - } - - const CommonMarkupBubbleMenu = (props: BubbleMenuProps) => { - return ( - <> - - {(triggerRef: (el: HTMLElement) => void) => ( - - )} - -
-
- - - - -
- - ) - } - - const TextSizeBubbleMenu = (props: BubbleMenuProps) => { - return ( -
-
{t('Headers')}
-
- - {(triggerRef: (el: HTMLElement) => void) => ( - - )} - - - {(triggerRef: (el: HTMLElement) => void) => ( - - )} - - - {(triggerRef: (el: HTMLElement) => void) => ( - - )} - -
-
{t('Quotes')}
-
- - {(triggerRef: (el: HTMLElement) => void) => ( - - )} - - - {(triggerRef: (el: HTMLElement) => void) => ( - - )} - -
-
{t('squib')}
-
- - {(triggerRef: (el: HTMLElement) => void) => ( - - )} - -
-
- ) - } - - const BaseTextBubbleMenu = (props: BubbleMenuProps) => { - return ( - <> - - <> -
- - - - -
-
- - - - {(triggerRef: (el: HTMLElement) => void) => ( - - )} - - - {(triggerRef: (el: HTMLElement) => void) => ( - - )} - - {/* - {(triggerRef: (el: HTMLElement) => void) => ( - - )} - */} - - - {(triggerRef: (el: HTMLElement) => void) => ( - - )} - -
- - {t('Add url')}
}> - {(triggerRef: (el: HTMLElement) => void) => ( - - )} - - - - - - ) - } - return ( -
+
- - + + setLinkEditorOpen(false)} /> - + setMenuState((prev) => ({ ...prev, footnoteEditorOpen: false }))} + onSubmit={(value) => handleAddFootnote(value)} + content={footNote()} + onCancel={() => setFootnoteEditorOpen(false)} /> - - + + <> + + <> +
+ + +
+
{t('Headers')}
+
+ + {(triggerRef: (el: HTMLButtonElement) => void) => ( + + )} + + + {(triggerRef: (el: HTMLButtonElement) => void) => ( + + )} + + + {(triggerRef: (el: HTMLButtonElement) => void) => ( + + )} + +
+
{t('Quotes')}
+
+ + {(triggerRef: (el: HTMLButtonElement) => void) => ( + + )} + + + {(triggerRef: (el: HTMLButtonElement) => void) => ( + + )} + +
+
{t('squib')}
+
+ + {(triggerRef: (el: HTMLButtonElement) => void) => ( + + )} + +
+
+
+
+
+ + + + {(triggerRef: (el: HTMLButtonElement) => void) => ( + + )} + + + {(triggerRef: (el: HTMLButtonElement) => void) => ( + + )} + + + + + {(triggerRef: (el: HTMLButtonElement) => void) => ( + + )} + +
+ + {t('Add url')}
}> + {(triggerRef: (el: HTMLButtonElement) => void) => ( + + )} + + + <> + + {(triggerRef: (el: HTMLButtonElement) => void) => ( + + )} + +
+
+ + +
+
{t('Lists')}
+
+ + {(triggerRef: (el: HTMLButtonElement) => void) => ( + + )} + + + {(triggerRef: (el: HTMLButtonElement) => void) => ( + + )} + +
+
+
+
+ + +
diff --git a/src/lib/createTooltip.ts b/src/lib/createTooltip.ts deleted file mode 100644 index a6db23b3..00000000 --- a/src/lib/createTooltip.ts +++ /dev/null @@ -1,93 +0,0 @@ -export function createTooltip(referenceElement?: Element, tooltipElement?: HTMLElement, options = {}) { - const defaultOptions = { - placement: 'top', - offset: [0, 8], - flip: { - fallbackPlacements: ['top', 'bottom'] - } - } - const config = { ...defaultOptions, ...options } - - function updatePosition() { - if (!(referenceElement && tooltipElement)) return - - const rect = referenceElement.getBoundingClientRect() - const tooltipRect = tooltipElement.getBoundingClientRect() - const offsetX = config.offset[0] - const offsetY = config.offset[1] - - let placement = config.placement - let top = 0 - let left = 0 - - // Базовое позиционирование - switch (placement) { - case 'top': - top = rect.top - tooltipRect.height - offsetY - left = rect.left + (rect.width - tooltipRect.width) / 2 + offsetX - break - case 'bottom': - top = rect.bottom + offsetY - left = rect.left + (rect.width - tooltipRect.width) / 2 + offsetX - break - // Добавьте case для 'left' и 'right', если необходимо - } - - // Проверка на выход за границы окна и применение flip - if (top < 0 && config.flip.fallbackPlacements.includes('bottom')) { - top = rect.bottom + offsetY - placement = 'bottom' - } else if (top + tooltipRect.height > window.innerHeight && config.flip.fallbackPlacements.includes('top')) { - top = rect.top - tooltipRect.height - offsetY - placement = 'top' - } - - // Применение позиции - tooltipElement.style.position = 'absolute' - tooltipElement.style.top = `${top}px` - tooltipElement.style.left = `${left}px` - tooltipElement.style.visibility = 'visible' - - // Обновление класса для стрелки - tooltipElement.setAttribute('data-popper-placement', placement) - - // Позиционирование стрелки - const arrow = tooltipElement.querySelector('[data-popper-arrow]') as HTMLElement - if (arrow) { - const arrowRect = arrow.getBoundingClientRect() - if (placement === 'top') { - arrow.style.bottom = '-4px' - arrow.style.left = `${tooltipRect.width / 2 - arrowRect.width / 2}px` - } else if (placement === 'bottom') { - arrow.style.top = '-4px' - arrow.style.left = `${tooltipRect.width / 2 - arrowRect.width / 2}px` - } - } - } - - function showTooltip() { - if (tooltipElement) { - tooltipElement.style.visibility = 'visible' - updatePosition() - } - } - - function hideTooltip() { - if (tooltipElement) tooltipElement.style.visibility = 'hidden' - } - - referenceElement?.addEventListener('mouseenter', showTooltip) - referenceElement?.addEventListener('mouseleave', hideTooltip) - window.addEventListener('resize', updatePosition) - window.addEventListener('scroll', updatePosition) - - return { - update: updatePosition, - destroy() { - referenceElement?.removeEventListener('mouseenter', showTooltip) - referenceElement?.removeEventListener('mouseleave', hideTooltip) - window.removeEventListener('resize', updatePosition) - window.removeEventListener('scroll', updatePosition) - } - } -} \ No newline at end of file