noteJS-react/src/components/notes/MarkdownToolbar.tsx

269 lines
7.8 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import React, { useState, useEffect, useRef } from "react";
import { Icon } from "@iconify/react";
import { useAppDispatch } from "../../store/hooks";
import { togglePreviewMode } from "../../store/slices/uiSlice";
interface MarkdownToolbarProps {
onInsert: (before: string, after?: string) => void;
onImageClick?: () => void;
onFileClick?: () => void;
onPreviewToggle?: () => void;
isPreviewMode?: boolean;
}
export const MarkdownToolbar: React.FC<MarkdownToolbarProps> = ({
onInsert,
onImageClick,
onFileClick,
onPreviewToggle,
isPreviewMode,
}) => {
const [showHeaderDropdown, setShowHeaderDropdown] = useState(false);
const dispatch = useAppDispatch();
const dropdownRef = useRef<HTMLDivElement>(null);
const buttonRef = useRef<HTMLButtonElement>(null);
const menuRef = useRef<HTMLDivElement>(null);
const containerRef = useRef<HTMLDivElement>(null);
const [isDragging, setIsDragging] = useState(false);
const [startX, setStartX] = useState(0);
const [scrollLeft, setScrollLeft] = useState(0);
const [menuPosition, setMenuPosition] = useState<{
top: number;
left: number;
} | null>(null);
useEffect(() => {
const handleClickOutside = (event: MouseEvent) => {
if (
dropdownRef.current &&
!dropdownRef.current.contains(event.target as Node) &&
menuRef.current &&
!menuRef.current.contains(event.target as Node)
) {
setShowHeaderDropdown(false);
setMenuPosition(null);
}
};
const updateMenuPosition = () => {
if (buttonRef.current && showHeaderDropdown) {
const rect = buttonRef.current.getBoundingClientRect();
setMenuPosition({
top: rect.bottom + window.scrollY + 2,
left: rect.left + window.scrollX,
});
}
};
if (showHeaderDropdown) {
updateMenuPosition();
// Используем небольшой таймаут, чтобы не перехватить клик на кнопке
const timeoutId = setTimeout(() => {
document.addEventListener("mousedown", handleClickOutside);
window.addEventListener("resize", updateMenuPosition);
window.addEventListener("scroll", updateMenuPosition);
}, 100);
return () => {
clearTimeout(timeoutId);
document.removeEventListener("mousedown", handleClickOutside);
window.removeEventListener("resize", updateMenuPosition);
window.removeEventListener("scroll", updateMenuPosition);
};
} else {
setMenuPosition(null);
}
}, [showHeaderDropdown]);
const handleMouseDown = (e: React.MouseEvent) => {
// Не начинаем перетаскивание если кликнули на кнопку
if ((e.target as HTMLElement).closest(".btnMarkdown")) return;
if (!containerRef.current) return;
setIsDragging(true);
setStartX(e.pageX - containerRef.current.offsetLeft);
setScrollLeft(containerRef.current.scrollLeft);
};
const handleMouseMove = (e: MouseEvent) => {
if (!isDragging || !containerRef.current) return;
e.preventDefault();
const x = e.pageX - containerRef.current.offsetLeft;
const walk = (x - startX) * 2; // Увеличиваем скорость прокрутки
containerRef.current.scrollLeft = scrollLeft - walk;
};
const handleMouseUp = () => {
setIsDragging(false);
};
// Обработчики для document чтобы отслеживать mouseMove и mouseUp даже вне элемента
useEffect(() => {
if (isDragging) {
document.addEventListener("mousemove", handleMouseMove);
document.addEventListener("mouseup", handleMouseUp);
} else {
document.removeEventListener("mousemove", handleMouseMove);
document.removeEventListener("mouseup", handleMouseUp);
}
return () => {
document.removeEventListener("mousemove", handleMouseMove);
document.removeEventListener("mouseup", handleMouseUp);
};
}, [isDragging]);
const buttons = [];
return (
<div
className="markdown-buttons"
ref={containerRef}
onMouseDown={handleMouseDown}
style={{
cursor: isDragging
? "grabbing"
: containerRef.current &&
containerRef.current.scrollWidth > containerRef.current.clientWidth
? "grab"
: "default",
}}
>
{buttons.map((btn) => (
<button
key={btn.id}
className="btnMarkdown"
onClick={() => {
if (btn.action) {
btn.action();
} else {
onInsert(btn.before!, btn.after);
}
}}
title={btn.title}
>
<Icon icon={btn.icon} />
</button>
))}
<div className="header-dropdown" ref={dropdownRef}>
<button
ref={buttonRef}
className="btnMarkdown"
onMouseDown={(e) => {
e.stopPropagation();
}}
onClick={(e) => {
e.stopPropagation();
setShowHeaderDropdown(!showHeaderDropdown);
}}
title="Заголовок"
>
<Icon icon="mdi:format-header-pound" />
<Icon
icon="mdi:menu-down"
style={{ fontSize: "10px", marginLeft: "-2px" }}
/>
</button>
{showHeaderDropdown && menuPosition && (
<div
ref={menuRef}
className="header-dropdown-menu"
style={{
position: "fixed",
top: `${menuPosition.top}px`,
left: `${menuPosition.left}px`,
}}
>
{[1, 2, 3, 4, 5].map((level) => (
<button
key={level}
onClick={(e) => {
e.stopPropagation();
onInsert("#".repeat(level) + " ", "");
setShowHeaderDropdown(false);
setMenuPosition(null);
}}
>
H{level}
</button>
))}
</div>
)}
</div>
<button
className="btnMarkdown"
onClick={() => onInsert("- ", "")}
title="Список"
>
<Icon icon="mdi:format-list-bulleted" />
</button>
<button
className="btnMarkdown"
onClick={() => onInsert("1. ", "")}
title="Нумерованный список"
>
<Icon icon="mdi:format-list-numbered" />
</button>
<button
className="btnMarkdown"
onClick={() => onInsert("> ", "")}
title="Цитата"
>
<Icon icon="mdi:format-quote-close" />
</button>
<button
className="btnMarkdown"
onClick={() => onInsert("`", "`")}
title="Код"
>
<Icon icon="mdi:code-tags" />
</button>
<button
className="btnMarkdown"
onClick={() => onInsert("[текст ссылки](", ")")}
title="Ссылка"
>
<Icon icon="mdi:link" />
</button>
<button
className="btnMarkdown"
onClick={() => onInsert("- [ ] ", "")}
title="To-Do список"
>
<Icon icon="mdi:checkbox-marked-outline" />
</button>
<button
className="btnMarkdown"
onClick={() => onImageClick?.()}
title="Загрузить изображения"
>
<Icon icon="mdi:image-plus" />
</button>
<button
className="btnMarkdown"
onClick={() => onFileClick?.()}
title="Прикрепить файлы"
>
<Icon icon="mdi:file-plus" />
</button>
<button
className={`btnMarkdown ${isPreviewMode ? "active" : ""}`}
onClick={onPreviewToggle || (() => dispatch(togglePreviewMode()))}
title="Предпросмотр"
>
<Icon icon="mdi:monitor-eye" />
</button>
</div>
);
};