diff --git a/dev-dist/sw.js b/dev-dist/sw.js index 4f10989..1a0abe6 100644 --- a/dev-dist/sw.js +++ b/dev-dist/sw.js @@ -82,7 +82,7 @@ define(['./workbox-9dc17825'], (function (workbox) { 'use strict'; "revision": "3ca0b8505b4bec776b69afdba2768812" }, { "url": "index.html", - "revision": "0.b1jpidvaji" + "revision": "0.b944c6vblpo" }], {}); workbox.cleanupOutdatedCaches(); workbox.registerRoute(new workbox.NavigationRoute(workbox.createHandlerBoundToURL("index.html"), { diff --git a/src/components/notes/FloatingToolbar.tsx b/src/components/notes/FloatingToolbar.tsx index e1fa7b6..087857e 100644 --- a/src/components/notes/FloatingToolbar.tsx +++ b/src/components/notes/FloatingToolbar.tsx @@ -5,7 +5,14 @@ interface FloatingToolbarProps { textareaRef: React.RefObject; onFormat: (before: string, after: string) => void; visible: boolean; - position: { top: number; left: number }; + position: { + top: number; + left: number; + selectionTop?: number; + selectionBottom?: number; + selectionLeft?: number; + selectionRight?: number; + }; onHide?: () => void; onInsertColor?: () => void; activeFormats?: { @@ -13,7 +20,7 @@ interface FloatingToolbarProps { italic?: boolean; strikethrough?: boolean; }; - hasSelection?: boolean; // Есть ли выделение текста + hasSelection?: boolean; } export const FloatingToolbar: React.FC = ({ @@ -33,53 +40,85 @@ export const FloatingToolbar: React.FC = ({ useEffect(() => { if (visible && toolbarRef.current) { - // Небольшая задержка для корректного расчета размеров после рендера setTimeout(() => { if (!toolbarRef.current) return; - + const toolbar = toolbarRef.current; const rect = toolbar.getBoundingClientRect(); const windowWidth = window.innerWidth; const windowHeight = window.innerHeight; const padding = 10; + const offset = 8; // Отступ от выделенного текста - // Получаем внутренний контейнер с кнопками для определения реальной ширины - const toolbarContent = toolbar.querySelector('.floating-toolbar') as HTMLElement; - const toolbarContentWidth = toolbarContent ? toolbarContent.scrollWidth : rect.width; + // Получаем внутренний контейнер с кнопками + const toolbarContent = toolbar.querySelector( + ".floating-toolbar" + ) as HTMLElement; + const toolbarContentWidth = toolbarContent + ? toolbarContent.scrollWidth + : rect.width; const availableWidth = windowWidth - padding * 2; + const toolbarHeight = rect.height; - let top = position.top - rect.height - padding; - let left = position.left; + // Используем границы выделения, если они есть + const selectionTop = position.selectionTop ?? position.top; + const selectionBottom = position.selectionBottom ?? position.top + 20; + + // Вычисляем пространство сверху и снизу от выделения + const spaceAbove = selectionTop - padding; + const spaceBelow = windowHeight - selectionBottom - padding; + + // Определяем, где больше места и где не будет перекрытия + let top: number; + + // Проверяем, помещается ли панель сверху + if (spaceAbove >= toolbarHeight + offset) { + // Помещается сверху - размещаем над выделением + top = selectionTop - toolbarHeight - offset; + } else if (spaceBelow >= toolbarHeight + offset) { + // Помещается снизу - размещаем под выделением + top = selectionBottom + offset; + } else { + // Не помещается ни сверху, ни снизу - выбираем сторону с большим пространством + if (spaceAbove > spaceBelow) { + // Больше места сверху, но панель может частично выйти за границы + top = Math.max(padding, selectionTop - toolbarHeight - offset); + } else { + // Больше места снизу + top = Math.min( + windowHeight - toolbarHeight - padding, + selectionBottom + offset + ); + } + } + + // Горизонтальное позиционирование + let left = position.left - toolbarContentWidth / 2; // Центрируем относительно середины выделения // Если контент шире доступного пространства, устанавливаем maxWidth для wrapper if (toolbarContentWidth > availableWidth) { toolbar.style.maxWidth = `${availableWidth}px`; - } - - // Если toolbar выходит за правую границу экрана - if (left + rect.width > windowWidth - padding) { - // Если контент шире экрана, используем прокрутку и позиционируем по левому краю - if (toolbarContentWidth > availableWidth) { + left = padding; // Выравниваем по левому краю + } else { + // Проверяем границы экрана + if (left + toolbarContentWidth > windowWidth - padding) { + // Выравниваем по правому краю + left = Math.max( + padding, + windowWidth - toolbarContentWidth - padding + ); + } + if (left < padding) { left = padding; - } else { - // Иначе выравниваем по правому краю - left = Math.max(padding, windowWidth - rect.width - padding); } } - // Если toolbar выходит за левую границу экрана - if (left < padding) { - left = padding; - } - - // Если toolbar выходит за верхнюю границу экрана + // Финальная проверка вертикальных границ if (top < padding) { - top = position.top + 30; // Показываем снизу от выделения + top = padding; } - - // Проверяем нижнюю границу - if (top + rect.height > windowHeight - padding) { - top = windowHeight - rect.height - padding; + if (top + toolbarHeight > windowHeight - padding) { + top = windowHeight - toolbarHeight - padding; } toolbar.style.top = `${top}px`; @@ -88,10 +127,9 @@ export const FloatingToolbar: React.FC = ({ } }, [visible, position]); - const handleMouseDown = (e: React.MouseEvent) => { // Не начинаем перетаскивание если кликнули на кнопку - if ((e.target as HTMLElement).closest('.floating-toolbar-btn')) return; + if ((e.target as HTMLElement).closest(".floating-toolbar-btn")) return; if (!toolbarRef.current) return; setIsDragging(true); @@ -114,16 +152,16 @@ export const FloatingToolbar: React.FC = ({ // Обработчики для document чтобы отслеживать mouseMove и mouseUp даже вне элемента useEffect(() => { if (isDragging) { - document.addEventListener('mousemove', handleMouseMove); - document.addEventListener('mouseup', handleMouseUp); + document.addEventListener("mousemove", handleMouseMove); + document.addEventListener("mouseup", handleMouseUp); } else { - document.removeEventListener('mousemove', handleMouseMove); - document.removeEventListener('mouseup', handleMouseUp); + document.removeEventListener("mousemove", handleMouseMove); + document.removeEventListener("mouseup", handleMouseUp); } return () => { - document.removeEventListener('mousemove', handleMouseMove); - document.removeEventListener('mouseup', handleMouseUp); + document.removeEventListener("mousemove", handleMouseMove); + document.removeEventListener("mouseup", handleMouseUp); }; }, [isDragging]); @@ -149,11 +187,11 @@ export const FloatingToolbar: React.FC = ({ const start = textarea.selectionStart; const end = textarea.selectionEnd; - + if (start === end) return; // Нет выделения const selectedText = textarea.value.substring(start, end); - + try { await navigator.clipboard.writeText(selectedText); // Можно добавить уведомление об успешном копировании @@ -176,11 +214,11 @@ export const FloatingToolbar: React.FC = ({ const start = textarea.selectionStart; const end = textarea.selectionEnd; - + if (start === end) return; // Нет выделения const selectedText = textarea.value.substring(start, end); - + try { await navigator.clipboard.writeText(selectedText); } catch (err) { @@ -194,16 +232,17 @@ export const FloatingToolbar: React.FC = ({ document.execCommand("copy"); document.body.removeChild(textArea); } - + // Удаляем выделенный текст - const newValue = textarea.value.substring(0, start) + textarea.value.substring(end); - + const newValue = + textarea.value.substring(0, start) + textarea.value.substring(end); + // Обновляем значение через React-совместимое событие const nativeInputValueSetter = Object.getOwnPropertyDescriptor( window.HTMLTextAreaElement.prototype, "value" )?.set; - + if (nativeInputValueSetter) { nativeInputValueSetter.call(textarea, newValue); const inputEvent = new Event("input", { bubbles: true }); @@ -213,7 +252,7 @@ export const FloatingToolbar: React.FC = ({ const inputEvent = new Event("input", { bubbles: true }); textarea.dispatchEvent(inputEvent); } - + textarea.setSelectionRange(start, start); textarea.focus(); }; @@ -227,19 +266,19 @@ export const FloatingToolbar: React.FC = ({ try { const text = await navigator.clipboard.readText(); - + // Вставляем текст в позицию курсора или заменяем выделенный текст - const newValue = - textarea.value.substring(0, start) + - text + + const newValue = + textarea.value.substring(0, start) + + text + textarea.value.substring(end); - + // Обновляем значение через React-совместимое событие const nativeInputValueSetter = Object.getOwnPropertyDescriptor( window.HTMLTextAreaElement.prototype, "value" )?.set; - + if (nativeInputValueSetter) { nativeInputValueSetter.call(textarea, newValue); const inputEvent = new Event("input", { bubbles: true }); @@ -249,7 +288,7 @@ export const FloatingToolbar: React.FC = ({ const inputEvent = new Event("input", { bubbles: true }); textarea.dispatchEvent(inputEvent); } - + // Устанавливаем курсор после вставленного текста const newCursorPos = start + text.length; textarea.setSelectionRange(newCursorPos, newCursorPos); @@ -273,7 +312,12 @@ export const FloatingToolbar: React.FC = ({ top: `${position.top}px`, left: `${position.left}px`, zIndex: 1000, - cursor: isDragging ? 'grabbing' : (toolbarRef.current && toolbarRef.current.scrollWidth > toolbarRef.current.clientWidth ? 'grab' : 'default'), + cursor: isDragging + ? "grabbing" + : toolbarRef.current && + toolbarRef.current.scrollWidth > toolbarRef.current.clientWidth + ? "grab" + : "default", }} onMouseDown={(e) => { // Предотвращаем потерю выделения при клике на toolbar @@ -294,97 +338,83 @@ export const FloatingToolbar: React.FC = ({ - {hasSelection && ( - <> - - - - - )} + {hasSelection && ( + <> + + + + + )} - {hasSelection && ( - <> -
+ {hasSelection && ( + <> +
- - - + + + -
+
- + - - - - - - - )} + + + )}
); diff --git a/src/components/notes/MarkdownToolbar.tsx b/src/components/notes/MarkdownToolbar.tsx index e6d7817..db83a45 100644 --- a/src/components/notes/MarkdownToolbar.tsx +++ b/src/components/notes/MarkdownToolbar.tsx @@ -9,6 +9,7 @@ interface MarkdownToolbarProps { onFileClick?: () => void; onPreviewToggle?: () => void; isPreviewMode?: boolean; + onInsertColor?: () => void; } export const MarkdownToolbar: React.FC = ({ @@ -17,6 +18,7 @@ export const MarkdownToolbar: React.FC = ({ onFileClick, onPreviewToggle, isPreviewMode, + onInsertColor, }) => { const [showHeaderDropdown, setShowHeaderDropdown] = useState(false); const dispatch = useAppDispatch(); @@ -208,6 +210,46 @@ export const MarkdownToolbar: React.FC = ({ + + + + + + + + + +