311 lines
8.8 KiB
TypeScript
311 lines
8.8 KiB
TypeScript
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;
|
||
onInsertColor?: () => void;
|
||
}
|
||
|
||
export const MarkdownToolbar: React.FC<MarkdownToolbarProps> = ({
|
||
onInsert,
|
||
onImageClick,
|
||
onFileClick,
|
||
onPreviewToggle,
|
||
isPreviewMode,
|
||
onInsertColor,
|
||
}) => {
|
||
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-bold" />
|
||
</button>
|
||
|
||
<button
|
||
className="btnMarkdown"
|
||
onClick={() => onInsert("*", "*")}
|
||
title="Курсив"
|
||
>
|
||
<Icon icon="mdi:format-italic" />
|
||
</button>
|
||
|
||
<button
|
||
className="btnMarkdown"
|
||
onClick={() => onInsert("~~", "~~")}
|
||
title="Зачеркнутый"
|
||
>
|
||
<Icon icon="mdi:format-strikethrough" />
|
||
</button>
|
||
|
||
<button
|
||
className="btnMarkdown"
|
||
onClick={() => onInsertColor?.()}
|
||
title="Цвет текста"
|
||
>
|
||
<Icon icon="mdi:palette" />
|
||
</button>
|
||
|
||
<button
|
||
className="btnMarkdown"
|
||
onClick={() => onInsert("||", "||")}
|
||
title="Скрытый текст"
|
||
>
|
||
<Icon icon="mdi:eye-off" />
|
||
</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>
|
||
);
|
||
};
|