Compare commits
No commits in common. "80c42f8df01a1d4f5512d9109796ed391d528cbb" and "e3b98ea8d3dccf1158e17356f1cefbb620bd030d" have entirely different histories.
80c42f8df0
...
e3b98ea8d3
@ -502,28 +502,6 @@ function runMigrations() {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Проверяем существование колонки floating_toolbar_enabled
|
|
||||||
const hasFloatingToolbarEnabled = columns.some(
|
|
||||||
(col) => col.name === "floating_toolbar_enabled"
|
|
||||||
);
|
|
||||||
if (!hasFloatingToolbarEnabled) {
|
|
||||||
db.run(
|
|
||||||
"ALTER TABLE users ADD COLUMN floating_toolbar_enabled INTEGER DEFAULT 1",
|
|
||||||
(err) => {
|
|
||||||
if (err) {
|
|
||||||
console.error(
|
|
||||||
"Ошибка добавления колонки floating_toolbar_enabled:",
|
|
||||||
err.message
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
console.log(
|
|
||||||
"Колонка floating_toolbar_enabled добавлена в таблицу users"
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Проверяем существование колонки ai_enabled
|
// Проверяем существование колонки ai_enabled
|
||||||
const hasAiEnabled = columns.some((col) => col.name === "ai_enabled");
|
const hasAiEnabled = columns.some((col) => col.name === "ai_enabled");
|
||||||
if (!hasAiEnabled) {
|
if (!hasAiEnabled) {
|
||||||
@ -793,7 +771,7 @@ app.get("/api/user", requireApiAuth, (req, res) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const sql =
|
const sql =
|
||||||
"SELECT username, email, avatar, accent_color, show_edit_date, colored_icons, floating_toolbar_enabled FROM users WHERE id = ?";
|
"SELECT username, email, avatar, accent_color, show_edit_date, colored_icons FROM users WHERE id = ?";
|
||||||
db.get(sql, [req.session.userId], (err, user) => {
|
db.get(sql, [req.session.userId], (err, user) => {
|
||||||
if (err) {
|
if (err) {
|
||||||
console.error("Ошибка получения данных пользователя:", err.message);
|
console.error("Ошибка получения данных пользователя:", err.message);
|
||||||
@ -1622,7 +1600,6 @@ app.put("/api/user/profile", requireApiAuth, async (req, res) => {
|
|||||||
accent_color,
|
accent_color,
|
||||||
show_edit_date,
|
show_edit_date,
|
||||||
colored_icons,
|
colored_icons,
|
||||||
floating_toolbar_enabled,
|
|
||||||
} = req.body;
|
} = req.body;
|
||||||
const userId = req.session.userId;
|
const userId = req.session.userId;
|
||||||
|
|
||||||
@ -1698,11 +1675,6 @@ app.put("/api/user/profile", requireApiAuth, async (req, res) => {
|
|||||||
params.push(colored_icons ? 1 : 0);
|
params.push(colored_icons ? 1 : 0);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (floating_toolbar_enabled !== undefined) {
|
|
||||||
updateFields.push("floating_toolbar_enabled = ?");
|
|
||||||
params.push(floating_toolbar_enabled ? 1 : 0);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (newPassword) {
|
if (newPassword) {
|
||||||
const hashedPassword = await bcrypt.hash(newPassword, 10);
|
const hashedPassword = await bcrypt.hash(newPassword, 10);
|
||||||
updateFields.push("password = ?");
|
updateFields.push("password = ?");
|
||||||
|
|||||||
@ -82,7 +82,7 @@ define(['./workbox-9dc17825'], (function (workbox) { 'use strict';
|
|||||||
"revision": "3ca0b8505b4bec776b69afdba2768812"
|
"revision": "3ca0b8505b4bec776b69afdba2768812"
|
||||||
}, {
|
}, {
|
||||||
"url": "index.html",
|
"url": "index.html",
|
||||||
"revision": "0.2vg2p27g3bg"
|
"revision": "0.nsn25edhihg"
|
||||||
}], {});
|
}], {});
|
||||||
workbox.cleanupOutdatedCaches();
|
workbox.cleanupOutdatedCaches();
|
||||||
workbox.registerRoute(new workbox.NavigationRoute(workbox.createHandlerBoundToURL("index.html"), {
|
workbox.registerRoute(new workbox.NavigationRoute(workbox.createHandlerBoundToURL("index.html"), {
|
||||||
|
|||||||
@ -14,7 +14,6 @@ export const userApi = {
|
|||||||
accent_color?: string;
|
accent_color?: string;
|
||||||
show_edit_date?: boolean;
|
show_edit_date?: boolean;
|
||||||
colored_icons?: boolean;
|
colored_icons?: boolean;
|
||||||
floating_toolbar_enabled?: boolean;
|
|
||||||
}
|
}
|
||||||
) => {
|
) => {
|
||||||
const { data } = await axiosClient.put("/user/profile", profile);
|
const { data } = await axiosClient.put("/user/profile", profile);
|
||||||
|
|||||||
@ -5,14 +5,7 @@ interface FloatingToolbarProps {
|
|||||||
textareaRef: React.RefObject<HTMLTextAreaElement>;
|
textareaRef: React.RefObject<HTMLTextAreaElement>;
|
||||||
onFormat: (before: string, after: string) => void;
|
onFormat: (before: string, after: string) => void;
|
||||||
visible: boolean;
|
visible: boolean;
|
||||||
position: {
|
position: { top: number; left: number };
|
||||||
top: number;
|
|
||||||
left: number;
|
|
||||||
selectionTop?: number;
|
|
||||||
selectionBottom?: number;
|
|
||||||
selectionLeft?: number;
|
|
||||||
selectionRight?: number;
|
|
||||||
};
|
|
||||||
onHide?: () => void;
|
onHide?: () => void;
|
||||||
onInsertColor?: () => void;
|
onInsertColor?: () => void;
|
||||||
activeFormats?: {
|
activeFormats?: {
|
||||||
@ -20,7 +13,7 @@ interface FloatingToolbarProps {
|
|||||||
italic?: boolean;
|
italic?: boolean;
|
||||||
strikethrough?: boolean;
|
strikethrough?: boolean;
|
||||||
};
|
};
|
||||||
hasSelection?: boolean;
|
hasSelection?: boolean; // Есть ли выделение текста
|
||||||
}
|
}
|
||||||
|
|
||||||
export const FloatingToolbar: React.FC<FloatingToolbarProps> = ({
|
export const FloatingToolbar: React.FC<FloatingToolbarProps> = ({
|
||||||
@ -40,85 +33,53 @@ export const FloatingToolbar: React.FC<FloatingToolbarProps> = ({
|
|||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (visible && toolbarRef.current) {
|
if (visible && toolbarRef.current) {
|
||||||
|
// Небольшая задержка для корректного расчета размеров после рендера
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
if (!toolbarRef.current) return;
|
if (!toolbarRef.current) return;
|
||||||
|
|
||||||
const toolbar = toolbarRef.current;
|
const toolbar = toolbarRef.current;
|
||||||
const rect = toolbar.getBoundingClientRect();
|
const rect = toolbar.getBoundingClientRect();
|
||||||
const windowWidth = window.innerWidth;
|
const windowWidth = window.innerWidth;
|
||||||
const windowHeight = window.innerHeight;
|
const windowHeight = window.innerHeight;
|
||||||
const padding = 10;
|
const padding = 10;
|
||||||
const offset = 8; // Отступ от выделенного текста
|
|
||||||
|
|
||||||
// Получаем внутренний контейнер с кнопками
|
// Получаем внутренний контейнер с кнопками для определения реальной ширины
|
||||||
const toolbarContent = toolbar.querySelector(
|
const toolbarContent = toolbar.querySelector('.floating-toolbar') as HTMLElement;
|
||||||
".floating-toolbar"
|
const toolbarContentWidth = toolbarContent ? toolbarContent.scrollWidth : rect.width;
|
||||||
) as HTMLElement;
|
|
||||||
const toolbarContentWidth = toolbarContent
|
|
||||||
? toolbarContent.scrollWidth
|
|
||||||
: rect.width;
|
|
||||||
const availableWidth = windowWidth - padding * 2;
|
const availableWidth = windowWidth - padding * 2;
|
||||||
const toolbarHeight = rect.height;
|
|
||||||
|
|
||||||
// Используем границы выделения, если они есть
|
let top = position.top - rect.height - padding;
|
||||||
const selectionTop = position.selectionTop ?? position.top;
|
let left = position.left;
|
||||||
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
|
// Если контент шире доступного пространства, устанавливаем maxWidth для wrapper
|
||||||
if (toolbarContentWidth > availableWidth) {
|
if (toolbarContentWidth > availableWidth) {
|
||||||
toolbar.style.maxWidth = `${availableWidth}px`;
|
toolbar.style.maxWidth = `${availableWidth}px`;
|
||||||
left = padding; // Выравниваем по левому краю
|
}
|
||||||
} else {
|
|
||||||
// Проверяем границы экрана
|
// Если toolbar выходит за правую границу экрана
|
||||||
if (left + toolbarContentWidth > windowWidth - padding) {
|
if (left + rect.width > windowWidth - padding) {
|
||||||
// Выравниваем по правому краю
|
// Если контент шире экрана, используем прокрутку и позиционируем по левому краю
|
||||||
left = Math.max(
|
if (toolbarContentWidth > availableWidth) {
|
||||||
padding,
|
|
||||||
windowWidth - toolbarContentWidth - padding
|
|
||||||
);
|
|
||||||
}
|
|
||||||
if (left < padding) {
|
|
||||||
left = padding;
|
left = padding;
|
||||||
|
} else {
|
||||||
|
// Иначе выравниваем по правому краю
|
||||||
|
left = Math.max(padding, windowWidth - rect.width - padding);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Финальная проверка вертикальных границ
|
// Если toolbar выходит за левую границу экрана
|
||||||
if (top < padding) {
|
if (left < padding) {
|
||||||
top = padding;
|
left = padding;
|
||||||
}
|
}
|
||||||
if (top + toolbarHeight > windowHeight - padding) {
|
|
||||||
top = windowHeight - toolbarHeight - padding;
|
// Если toolbar выходит за верхнюю границу экрана
|
||||||
|
if (top < padding) {
|
||||||
|
top = position.top + 30; // Показываем снизу от выделения
|
||||||
|
}
|
||||||
|
|
||||||
|
// Проверяем нижнюю границу
|
||||||
|
if (top + rect.height > windowHeight - padding) {
|
||||||
|
top = windowHeight - rect.height - padding;
|
||||||
}
|
}
|
||||||
|
|
||||||
toolbar.style.top = `${top}px`;
|
toolbar.style.top = `${top}px`;
|
||||||
@ -127,9 +88,10 @@ export const FloatingToolbar: React.FC<FloatingToolbarProps> = ({
|
|||||||
}
|
}
|
||||||
}, [visible, position]);
|
}, [visible, position]);
|
||||||
|
|
||||||
|
|
||||||
const handleMouseDown = (e: React.MouseEvent) => {
|
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;
|
if (!toolbarRef.current) return;
|
||||||
setIsDragging(true);
|
setIsDragging(true);
|
||||||
@ -152,16 +114,16 @@ export const FloatingToolbar: React.FC<FloatingToolbarProps> = ({
|
|||||||
// Обработчики для document чтобы отслеживать mouseMove и mouseUp даже вне элемента
|
// Обработчики для document чтобы отслеживать mouseMove и mouseUp даже вне элемента
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (isDragging) {
|
if (isDragging) {
|
||||||
document.addEventListener("mousemove", handleMouseMove);
|
document.addEventListener('mousemove', handleMouseMove);
|
||||||
document.addEventListener("mouseup", handleMouseUp);
|
document.addEventListener('mouseup', handleMouseUp);
|
||||||
} else {
|
} else {
|
||||||
document.removeEventListener("mousemove", handleMouseMove);
|
document.removeEventListener('mousemove', handleMouseMove);
|
||||||
document.removeEventListener("mouseup", handleMouseUp);
|
document.removeEventListener('mouseup', handleMouseUp);
|
||||||
}
|
}
|
||||||
|
|
||||||
return () => {
|
return () => {
|
||||||
document.removeEventListener("mousemove", handleMouseMove);
|
document.removeEventListener('mousemove', handleMouseMove);
|
||||||
document.removeEventListener("mouseup", handleMouseUp);
|
document.removeEventListener('mouseup', handleMouseUp);
|
||||||
};
|
};
|
||||||
}, [isDragging]);
|
}, [isDragging]);
|
||||||
|
|
||||||
@ -187,11 +149,11 @@ export const FloatingToolbar: React.FC<FloatingToolbarProps> = ({
|
|||||||
|
|
||||||
const start = textarea.selectionStart;
|
const start = textarea.selectionStart;
|
||||||
const end = textarea.selectionEnd;
|
const end = textarea.selectionEnd;
|
||||||
|
|
||||||
if (start === end) return; // Нет выделения
|
if (start === end) return; // Нет выделения
|
||||||
|
|
||||||
const selectedText = textarea.value.substring(start, end);
|
const selectedText = textarea.value.substring(start, end);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await navigator.clipboard.writeText(selectedText);
|
await navigator.clipboard.writeText(selectedText);
|
||||||
// Можно добавить уведомление об успешном копировании
|
// Можно добавить уведомление об успешном копировании
|
||||||
@ -214,11 +176,11 @@ export const FloatingToolbar: React.FC<FloatingToolbarProps> = ({
|
|||||||
|
|
||||||
const start = textarea.selectionStart;
|
const start = textarea.selectionStart;
|
||||||
const end = textarea.selectionEnd;
|
const end = textarea.selectionEnd;
|
||||||
|
|
||||||
if (start === end) return; // Нет выделения
|
if (start === end) return; // Нет выделения
|
||||||
|
|
||||||
const selectedText = textarea.value.substring(start, end);
|
const selectedText = textarea.value.substring(start, end);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await navigator.clipboard.writeText(selectedText);
|
await navigator.clipboard.writeText(selectedText);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
@ -232,17 +194,16 @@ export const FloatingToolbar: React.FC<FloatingToolbarProps> = ({
|
|||||||
document.execCommand("copy");
|
document.execCommand("copy");
|
||||||
document.body.removeChild(textArea);
|
document.body.removeChild(textArea);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Удаляем выделенный текст
|
// Удаляем выделенный текст
|
||||||
const newValue =
|
const newValue = textarea.value.substring(0, start) + textarea.value.substring(end);
|
||||||
textarea.value.substring(0, start) + textarea.value.substring(end);
|
|
||||||
|
|
||||||
// Обновляем значение через React-совместимое событие
|
// Обновляем значение через React-совместимое событие
|
||||||
const nativeInputValueSetter = Object.getOwnPropertyDescriptor(
|
const nativeInputValueSetter = Object.getOwnPropertyDescriptor(
|
||||||
window.HTMLTextAreaElement.prototype,
|
window.HTMLTextAreaElement.prototype,
|
||||||
"value"
|
"value"
|
||||||
)?.set;
|
)?.set;
|
||||||
|
|
||||||
if (nativeInputValueSetter) {
|
if (nativeInputValueSetter) {
|
||||||
nativeInputValueSetter.call(textarea, newValue);
|
nativeInputValueSetter.call(textarea, newValue);
|
||||||
const inputEvent = new Event("input", { bubbles: true });
|
const inputEvent = new Event("input", { bubbles: true });
|
||||||
@ -252,7 +213,7 @@ export const FloatingToolbar: React.FC<FloatingToolbarProps> = ({
|
|||||||
const inputEvent = new Event("input", { bubbles: true });
|
const inputEvent = new Event("input", { bubbles: true });
|
||||||
textarea.dispatchEvent(inputEvent);
|
textarea.dispatchEvent(inputEvent);
|
||||||
}
|
}
|
||||||
|
|
||||||
textarea.setSelectionRange(start, start);
|
textarea.setSelectionRange(start, start);
|
||||||
textarea.focus();
|
textarea.focus();
|
||||||
};
|
};
|
||||||
@ -266,19 +227,19 @@ export const FloatingToolbar: React.FC<FloatingToolbarProps> = ({
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
const text = await navigator.clipboard.readText();
|
const text = await navigator.clipboard.readText();
|
||||||
|
|
||||||
// Вставляем текст в позицию курсора или заменяем выделенный текст
|
// Вставляем текст в позицию курсора или заменяем выделенный текст
|
||||||
const newValue =
|
const newValue =
|
||||||
textarea.value.substring(0, start) +
|
textarea.value.substring(0, start) +
|
||||||
text +
|
text +
|
||||||
textarea.value.substring(end);
|
textarea.value.substring(end);
|
||||||
|
|
||||||
// Обновляем значение через React-совместимое событие
|
// Обновляем значение через React-совместимое событие
|
||||||
const nativeInputValueSetter = Object.getOwnPropertyDescriptor(
|
const nativeInputValueSetter = Object.getOwnPropertyDescriptor(
|
||||||
window.HTMLTextAreaElement.prototype,
|
window.HTMLTextAreaElement.prototype,
|
||||||
"value"
|
"value"
|
||||||
)?.set;
|
)?.set;
|
||||||
|
|
||||||
if (nativeInputValueSetter) {
|
if (nativeInputValueSetter) {
|
||||||
nativeInputValueSetter.call(textarea, newValue);
|
nativeInputValueSetter.call(textarea, newValue);
|
||||||
const inputEvent = new Event("input", { bubbles: true });
|
const inputEvent = new Event("input", { bubbles: true });
|
||||||
@ -288,7 +249,7 @@ export const FloatingToolbar: React.FC<FloatingToolbarProps> = ({
|
|||||||
const inputEvent = new Event("input", { bubbles: true });
|
const inputEvent = new Event("input", { bubbles: true });
|
||||||
textarea.dispatchEvent(inputEvent);
|
textarea.dispatchEvent(inputEvent);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Устанавливаем курсор после вставленного текста
|
// Устанавливаем курсор после вставленного текста
|
||||||
const newCursorPos = start + text.length;
|
const newCursorPos = start + text.length;
|
||||||
textarea.setSelectionRange(newCursorPos, newCursorPos);
|
textarea.setSelectionRange(newCursorPos, newCursorPos);
|
||||||
@ -312,12 +273,7 @@ export const FloatingToolbar: React.FC<FloatingToolbarProps> = ({
|
|||||||
top: `${position.top}px`,
|
top: `${position.top}px`,
|
||||||
left: `${position.left}px`,
|
left: `${position.left}px`,
|
||||||
zIndex: 1000,
|
zIndex: 1000,
|
||||||
cursor: isDragging
|
cursor: isDragging ? 'grabbing' : (toolbarRef.current && toolbarRef.current.scrollWidth > toolbarRef.current.clientWidth ? 'grab' : 'default'),
|
||||||
? "grabbing"
|
|
||||||
: toolbarRef.current &&
|
|
||||||
toolbarRef.current.scrollWidth > toolbarRef.current.clientWidth
|
|
||||||
? "grab"
|
|
||||||
: "default",
|
|
||||||
}}
|
}}
|
||||||
onMouseDown={(e) => {
|
onMouseDown={(e) => {
|
||||||
// Предотвращаем потерю выделения при клике на toolbar
|
// Предотвращаем потерю выделения при клике на toolbar
|
||||||
@ -338,83 +294,97 @@ export const FloatingToolbar: React.FC<FloatingToolbarProps> = ({
|
|||||||
<Icon icon="mdi:close" />
|
<Icon icon="mdi:close" />
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
{hasSelection && (
|
{hasSelection && (
|
||||||
<>
|
<>
|
||||||
<button
|
<button
|
||||||
className="floating-toolbar-btn"
|
className="floating-toolbar-btn"
|
||||||
onClick={handleCopy}
|
onClick={handleCopy}
|
||||||
title="Копировать"
|
title="Копировать"
|
||||||
>
|
>
|
||||||
<Icon icon="mdi:content-copy" />
|
<Icon icon="mdi:content-copy" />
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
className="floating-toolbar-btn"
|
className="floating-toolbar-btn"
|
||||||
onClick={handleCut}
|
onClick={handleCut}
|
||||||
title="Вырезать"
|
title="Вырезать"
|
||||||
>
|
>
|
||||||
<Icon icon="mdi:content-cut" />
|
<Icon icon="mdi:content-cut" />
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
className="floating-toolbar-btn"
|
className="floating-toolbar-btn"
|
||||||
onClick={handlePaste}
|
onClick={handlePaste}
|
||||||
title="Вставить"
|
title="Вставить"
|
||||||
>
|
>
|
||||||
<Icon icon="mdi:content-paste" />
|
<Icon icon="mdi:content-paste" />
|
||||||
</button>
|
</button>
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{hasSelection && (
|
{hasSelection && (
|
||||||
<>
|
<>
|
||||||
<div className="floating-toolbar-separator" />
|
<div className="floating-toolbar-separator" />
|
||||||
|
|
||||||
<button
|
<button
|
||||||
className={`floating-toolbar-btn ${
|
className={`floating-toolbar-btn ${activeFormats.bold ? "active" : ""}`}
|
||||||
activeFormats.bold ? "active" : ""
|
onClick={() => handleFormat("**", "**")}
|
||||||
}`}
|
title="Жирный"
|
||||||
onClick={() => handleFormat("**", "**")}
|
>
|
||||||
title="Жирный"
|
<Icon icon="mdi:format-bold" />
|
||||||
>
|
</button>
|
||||||
<Icon icon="mdi:format-bold" />
|
<button
|
||||||
</button>
|
className={`floating-toolbar-btn ${
|
||||||
<button
|
activeFormats.italic ? "active" : ""
|
||||||
className={`floating-toolbar-btn ${
|
}`}
|
||||||
activeFormats.italic ? "active" : ""
|
onClick={() => handleFormat("*", "*")}
|
||||||
}`}
|
title="Курсив"
|
||||||
onClick={() => handleFormat("*", "*")}
|
>
|
||||||
title="Курсив"
|
<Icon icon="mdi:format-italic" />
|
||||||
>
|
</button>
|
||||||
<Icon icon="mdi:format-italic" />
|
<button
|
||||||
</button>
|
className={`floating-toolbar-btn ${
|
||||||
<button
|
activeFormats.strikethrough ? "active" : ""
|
||||||
className={`floating-toolbar-btn ${
|
}`}
|
||||||
activeFormats.strikethrough ? "active" : ""
|
onClick={() => handleFormat("~~", "~~")}
|
||||||
}`}
|
title="Зачеркнутый"
|
||||||
onClick={() => handleFormat("~~", "~~")}
|
>
|
||||||
title="Зачеркнутый"
|
<Icon icon="mdi:format-strikethrough" />
|
||||||
>
|
</button>
|
||||||
<Icon icon="mdi:format-strikethrough" />
|
|
||||||
</button>
|
|
||||||
|
|
||||||
<div className="floating-toolbar-separator" />
|
<div className="floating-toolbar-separator" />
|
||||||
|
|
||||||
<button
|
<button
|
||||||
className="floating-toolbar-btn"
|
className="floating-toolbar-btn"
|
||||||
onClick={() => onInsertColor?.()}
|
onClick={() => onInsertColor?.()}
|
||||||
title="Цвет текста"
|
title="Цвет текста"
|
||||||
>
|
>
|
||||||
<Icon icon="mdi:palette" />
|
<Icon icon="mdi:palette" />
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
<button
|
<button
|
||||||
className="floating-toolbar-btn"
|
className="floating-toolbar-btn"
|
||||||
onClick={() => handleFormat("||", "||")}
|
onClick={() => handleFormat("||", "||")}
|
||||||
title="Скрытый текст"
|
title="Скрытый текст"
|
||||||
>
|
>
|
||||||
<Icon icon="mdi:eye-off" />
|
<Icon icon="mdi:eye-off" />
|
||||||
</button>
|
</button>
|
||||||
</>
|
|
||||||
)}
|
<button
|
||||||
|
className="floating-toolbar-btn"
|
||||||
|
onClick={() => handleFormat("`", "`")}
|
||||||
|
title="Код"
|
||||||
|
>
|
||||||
|
<Icon icon="mdi:code-tags" />
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<button
|
||||||
|
className="floating-toolbar-btn"
|
||||||
|
onClick={() => handleFormat("> ", "")}
|
||||||
|
title="Цитата"
|
||||||
|
>
|
||||||
|
<Icon icon="mdi:format-quote-close" />
|
||||||
|
</button>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
@ -9,7 +9,6 @@ interface MarkdownToolbarProps {
|
|||||||
onFileClick?: () => void;
|
onFileClick?: () => void;
|
||||||
onPreviewToggle?: () => void;
|
onPreviewToggle?: () => void;
|
||||||
isPreviewMode?: boolean;
|
isPreviewMode?: boolean;
|
||||||
onInsertColor?: () => void;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export const MarkdownToolbar: React.FC<MarkdownToolbarProps> = ({
|
export const MarkdownToolbar: React.FC<MarkdownToolbarProps> = ({
|
||||||
@ -18,7 +17,6 @@ export const MarkdownToolbar: React.FC<MarkdownToolbarProps> = ({
|
|||||||
onFileClick,
|
onFileClick,
|
||||||
onPreviewToggle,
|
onPreviewToggle,
|
||||||
isPreviewMode,
|
isPreviewMode,
|
||||||
onInsertColor,
|
|
||||||
}) => {
|
}) => {
|
||||||
const [showHeaderDropdown, setShowHeaderDropdown] = useState(false);
|
const [showHeaderDropdown, setShowHeaderDropdown] = useState(false);
|
||||||
const dispatch = useAppDispatch();
|
const dispatch = useAppDispatch();
|
||||||
@ -210,46 +208,6 @@ export const MarkdownToolbar: React.FC<MarkdownToolbarProps> = ({
|
|||||||
<Icon icon="mdi:format-list-numbered" />
|
<Icon icon="mdi:format-list-numbered" />
|
||||||
</button>
|
</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
|
<button
|
||||||
className="btnMarkdown"
|
className="btnMarkdown"
|
||||||
onClick={() => onInsert("> ", "")}
|
onClick={() => onInsert("> ", "")}
|
||||||
|
|||||||
@ -4,7 +4,7 @@ import { FloatingToolbar } from "./FloatingToolbar";
|
|||||||
import { NotePreview } from "./NotePreview";
|
import { NotePreview } from "./NotePreview";
|
||||||
import { ImageUpload } from "./ImageUpload";
|
import { ImageUpload } from "./ImageUpload";
|
||||||
import { FileUpload } from "./FileUpload";
|
import { FileUpload } from "./FileUpload";
|
||||||
import { useAppSelector } from "../../store/hooks";
|
import { useAppSelector, useAppDispatch } from "../../store/hooks";
|
||||||
import { useNotification } from "../../hooks/useNotification";
|
import { useNotification } from "../../hooks/useNotification";
|
||||||
import { offlineNotesApi } from "../../api/offlineNotesApi";
|
import { offlineNotesApi } from "../../api/offlineNotesApi";
|
||||||
import { aiApi } from "../../api/aiApi";
|
import { aiApi } from "../../api/aiApi";
|
||||||
@ -31,13 +31,7 @@ export const NoteEditor: React.FC<NoteEditorProps> = ({ onSave }) => {
|
|||||||
const isPreviewMode = useAppSelector((state) => state.ui.isPreviewMode);
|
const isPreviewMode = useAppSelector((state) => state.ui.isPreviewMode);
|
||||||
const { showNotification } = useNotification();
|
const { showNotification } = useNotification();
|
||||||
const aiEnabled = useAppSelector((state) => state.profile.aiEnabled);
|
const aiEnabled = useAppSelector((state) => state.profile.aiEnabled);
|
||||||
const user = useAppSelector((state) => state.profile.user);
|
const dispatch = useAppDispatch();
|
||||||
|
|
||||||
// Проверяем, включена ли плавающая панель
|
|
||||||
const floatingToolbarEnabled =
|
|
||||||
user?.floating_toolbar_enabled !== undefined
|
|
||||||
? user.floating_toolbar_enabled === 1
|
|
||||||
: true;
|
|
||||||
|
|
||||||
const handleSave = async () => {
|
const handleSave = async () => {
|
||||||
if (!content.trim()) {
|
if (!content.trim()) {
|
||||||
@ -334,9 +328,7 @@ export const NoteEditor: React.FC<NoteEditorProps> = ({ onSave }) => {
|
|||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// Проверяем, является ли это форматированием списка или цитаты
|
// Проверяем, является ли это форматированием списка или цитаты
|
||||||
const isListFormatting = /^[-*+]\s|^\d+\.\s|^- \[ \]\s|^>\s/.test(
|
const isListFormatting = /^[-*+]\s|^\d+\.\s|^- \[ \]\s|^>\s/.test(before);
|
||||||
before
|
|
||||||
);
|
|
||||||
const isMultiline = selectedText.includes("\n");
|
const isMultiline = selectedText.includes("\n");
|
||||||
|
|
||||||
if (isListFormatting && isMultiline) {
|
if (isListFormatting && isMultiline) {
|
||||||
@ -349,7 +341,7 @@ export const NoteEditor: React.FC<NoteEditorProps> = ({ onSave }) => {
|
|||||||
for (let i = 0; i < lines.length; i++) {
|
for (let i = 0; i < lines.length; i++) {
|
||||||
const line = lines[i];
|
const line = lines[i];
|
||||||
const trimmedLine = line.trim();
|
const trimmedLine = line.trim();
|
||||||
|
|
||||||
// Пропускаем пустые строки
|
// Пропускаем пустые строки
|
||||||
if (trimmedLine === "") {
|
if (trimmedLine === "") {
|
||||||
processedLines.push(line);
|
processedLines.push(line);
|
||||||
@ -605,6 +597,13 @@ export const NoteEditor: React.FC<NoteEditorProps> = ({ onSave }) => {
|
|||||||
const end = textarea.selectionEnd;
|
const end = textarea.selectionEnd;
|
||||||
const hasSelection = start !== end;
|
const hasSelection = start !== end;
|
||||||
|
|
||||||
|
// Используем середину выделения или позицию курсора для позиционирования
|
||||||
|
const position = hasSelection ? Math.floor((start + end) / 2) : start;
|
||||||
|
const text = textarea.value.substring(0, position);
|
||||||
|
const lines = text.split("\n");
|
||||||
|
const lineNumber = lines.length - 1;
|
||||||
|
const currentLineText = lines[lines.length - 1];
|
||||||
|
|
||||||
// Получаем размеры textarea
|
// Получаем размеры textarea
|
||||||
const rect = textarea.getBoundingClientRect();
|
const rect = textarea.getBoundingClientRect();
|
||||||
const styles = window.getComputedStyle(textarea);
|
const styles = window.getComputedStyle(textarea);
|
||||||
@ -612,85 +611,30 @@ export const NoteEditor: React.FC<NoteEditorProps> = ({ onSave }) => {
|
|||||||
const paddingTop = parseInt(styles.paddingTop) || 0;
|
const paddingTop = parseInt(styles.paddingTop) || 0;
|
||||||
const paddingLeft = parseInt(styles.paddingLeft) || 0;
|
const paddingLeft = parseInt(styles.paddingLeft) || 0;
|
||||||
const fontSize = parseInt(styles.fontSize) || 14;
|
const fontSize = parseInt(styles.fontSize) || 14;
|
||||||
const scrollTop = textarea.scrollTop;
|
|
||||||
|
|
||||||
// Вычисляем координаты начала выделения
|
// Более точный расчет ширины символа
|
||||||
const textBeforeStart = textarea.value.substring(0, start);
|
// Создаем временный элемент для измерения
|
||||||
const linesBeforeStart = textBeforeStart.split("\n");
|
|
||||||
const startLineNumber = linesBeforeStart.length - 1;
|
|
||||||
const startLineText = linesBeforeStart[startLineNumber];
|
|
||||||
|
|
||||||
// Создаем временный элемент для измерения ширины текста
|
|
||||||
const measureEl = document.createElement("span");
|
const measureEl = document.createElement("span");
|
||||||
measureEl.style.position = "absolute";
|
measureEl.style.position = "absolute";
|
||||||
measureEl.style.visibility = "hidden";
|
measureEl.style.visibility = "hidden";
|
||||||
measureEl.style.whiteSpace = "pre";
|
measureEl.style.whiteSpace = "pre";
|
||||||
measureEl.style.font = styles.font;
|
measureEl.style.font = styles.font;
|
||||||
|
measureEl.textContent = currentLineText;
|
||||||
document.body.appendChild(measureEl);
|
document.body.appendChild(measureEl);
|
||||||
|
const textWidth = measureEl.offsetWidth;
|
||||||
measureEl.textContent = startLineText;
|
|
||||||
const startTextWidth = measureEl.offsetWidth;
|
|
||||||
|
|
||||||
// Вычисляем координаты конца выделения
|
|
||||||
const textBeforeEnd = textarea.value.substring(0, end);
|
|
||||||
const linesBeforeEnd = textBeforeEnd.split("\n");
|
|
||||||
const endLineNumber = linesBeforeEnd.length - 1;
|
|
||||||
const endLineText = linesBeforeEnd[endLineNumber];
|
|
||||||
|
|
||||||
measureEl.textContent = endLineText;
|
|
||||||
const endTextWidth = measureEl.offsetWidth;
|
|
||||||
|
|
||||||
document.body.removeChild(measureEl);
|
document.body.removeChild(measureEl);
|
||||||
|
|
||||||
// Вычисляем вертикальные координаты
|
// Вычисляем позицию (середина выделения или позиция курсора)
|
||||||
const startTop =
|
const top =
|
||||||
rect.top + paddingTop + startLineNumber * lineHeight - scrollTop;
|
rect.top + paddingTop + lineNumber * lineHeight + lineHeight / 2;
|
||||||
const endTop =
|
const left = rect.left + paddingLeft + textWidth;
|
||||||
rect.top + paddingTop + endLineNumber * lineHeight - scrollTop;
|
|
||||||
|
|
||||||
// Горизонтальные координаты
|
return { top, left, hasSelection };
|
||||||
const startLeft = rect.left + paddingLeft + startTextWidth;
|
|
||||||
const endLeft = rect.left + paddingLeft + endTextWidth;
|
|
||||||
|
|
||||||
// Если есть выделение, используем его границы
|
|
||||||
if (hasSelection) {
|
|
||||||
// Вычисляем середину выделения по горизонтали
|
|
||||||
const left = Math.min(startLeft, endLeft);
|
|
||||||
const right = Math.max(startLeft, endLeft);
|
|
||||||
const centerLeft = (left + right) / 2;
|
|
||||||
|
|
||||||
// Вертикальная позиция - середина выделения
|
|
||||||
const top = (startTop + endTop) / 2;
|
|
||||||
|
|
||||||
return {
|
|
||||||
top,
|
|
||||||
left: centerLeft,
|
|
||||||
hasSelection,
|
|
||||||
selectionTop: Math.min(startTop, endTop),
|
|
||||||
selectionBottom: Math.max(startTop, endTop) + lineHeight,
|
|
||||||
selectionLeft: left,
|
|
||||||
selectionRight: right,
|
|
||||||
};
|
|
||||||
} else {
|
|
||||||
// Для курсора без выделения
|
|
||||||
const top = startTop;
|
|
||||||
const left = startLeft;
|
|
||||||
|
|
||||||
return {
|
|
||||||
top,
|
|
||||||
left,
|
|
||||||
hasSelection,
|
|
||||||
selectionTop: top,
|
|
||||||
selectionBottom: top + lineHeight,
|
|
||||||
selectionLeft: left,
|
|
||||||
selectionRight: left,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
// Обработчик выделения текста
|
// Обработчик выделения текста
|
||||||
const handleSelection = useCallback(() => {
|
const handleSelection = useCallback(() => {
|
||||||
if (isPreviewMode || !floatingToolbarEnabled) {
|
if (isPreviewMode) {
|
||||||
setShowFloatingToolbar(false);
|
setShowFloatingToolbar(false);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@ -715,13 +659,7 @@ export const NoteEditor: React.FC<NoteEditorProps> = ({ onSave }) => {
|
|||||||
setHasSelection(false);
|
setHasSelection(false);
|
||||||
setActiveFormats({ bold: false, italic: false, strikethrough: false });
|
setActiveFormats({ bold: false, italic: false, strikethrough: false });
|
||||||
}
|
}
|
||||||
}, [
|
}, [isPreviewMode, content, getCursorPosition, getActiveFormats]);
|
||||||
isPreviewMode,
|
|
||||||
content,
|
|
||||||
getCursorPosition,
|
|
||||||
getActiveFormats,
|
|
||||||
floatingToolbarEnabled,
|
|
||||||
]);
|
|
||||||
|
|
||||||
// Отслеживание выделения текста
|
// Отслеживание выделения текста
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@ -919,7 +857,6 @@ export const NoteEditor: React.FC<NoteEditorProps> = ({ onSave }) => {
|
|||||||
onInsert={insertMarkdown}
|
onInsert={insertMarkdown}
|
||||||
onImageClick={handleImageButtonClick}
|
onImageClick={handleImageButtonClick}
|
||||||
onFileClick={handleFileButtonClick}
|
onFileClick={handleFileButtonClick}
|
||||||
onInsertColor={insertColorMarkdown}
|
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<input
|
<input
|
||||||
@ -967,18 +904,16 @@ export const NoteEditor: React.FC<NoteEditorProps> = ({ onSave }) => {
|
|||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
{floatingToolbarEnabled && (
|
<FloatingToolbar
|
||||||
<FloatingToolbar
|
textareaRef={textareaRef}
|
||||||
textareaRef={textareaRef}
|
onFormat={insertMarkdown}
|
||||||
onFormat={insertMarkdown}
|
visible={showFloatingToolbar}
|
||||||
visible={showFloatingToolbar}
|
position={toolbarPosition}
|
||||||
position={toolbarPosition}
|
onHide={() => setShowFloatingToolbar(false)}
|
||||||
onHide={() => setShowFloatingToolbar(false)}
|
onInsertColor={insertColorMarkdown}
|
||||||
onInsertColor={insertColorMarkdown}
|
activeFormats={activeFormats}
|
||||||
activeFormats={activeFormats}
|
hasSelection={hasSelection}
|
||||||
hasSelection={hasSelection}
|
/>
|
||||||
/>
|
|
||||||
)}
|
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
|||||||
@ -67,12 +67,6 @@ export const NoteItem: React.FC<NoteItemProps> = ({
|
|||||||
const { showNotification } = useNotification();
|
const { showNotification } = useNotification();
|
||||||
const dispatch = useAppDispatch();
|
const dispatch = useAppDispatch();
|
||||||
useMarkdown({ onNoteUpdate: onReload }); // Инициализируем обработчики спойлеров, внешних ссылок и чекбоксов
|
useMarkdown({ onNoteUpdate: onReload }); // Инициализируем обработчики спойлеров, внешних ссылок и чекбоксов
|
||||||
|
|
||||||
// Проверяем, включена ли плавающая панель
|
|
||||||
const floatingToolbarEnabled =
|
|
||||||
user?.floating_toolbar_enabled !== undefined
|
|
||||||
? user.floating_toolbar_enabled === 1
|
|
||||||
: true;
|
|
||||||
|
|
||||||
const handleEdit = () => {
|
const handleEdit = () => {
|
||||||
setIsEditing(true);
|
setIsEditing(true);
|
||||||
@ -600,7 +594,7 @@ export const NoteItem: React.FC<NoteItemProps> = ({
|
|||||||
|
|
||||||
// Обработчик выделения текста
|
// Обработчик выделения текста
|
||||||
const handleSelection = useCallback(() => {
|
const handleSelection = useCallback(() => {
|
||||||
if (localPreviewMode || !floatingToolbarEnabled) {
|
if (localPreviewMode) {
|
||||||
setShowFloatingToolbar(false);
|
setShowFloatingToolbar(false);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@ -625,7 +619,7 @@ export const NoteItem: React.FC<NoteItemProps> = ({
|
|||||||
setHasSelection(false);
|
setHasSelection(false);
|
||||||
setActiveFormats({ bold: false, italic: false, strikethrough: false });
|
setActiveFormats({ bold: false, italic: false, strikethrough: false });
|
||||||
}
|
}
|
||||||
}, [localPreviewMode, editContent, getCursorPosition, getActiveFormats, floatingToolbarEnabled]);
|
}, [localPreviewMode, editContent, getCursorPosition, getActiveFormats]);
|
||||||
|
|
||||||
const handleImageButtonClick = () => {
|
const handleImageButtonClick = () => {
|
||||||
imageInputRef.current?.click();
|
imageInputRef.current?.click();
|
||||||
@ -1227,18 +1221,16 @@ export const NoteItem: React.FC<NoteItemProps> = ({
|
|||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
{floatingToolbarEnabled && (
|
<FloatingToolbar
|
||||||
<FloatingToolbar
|
textareaRef={editTextareaRef}
|
||||||
textareaRef={editTextareaRef}
|
onFormat={insertMarkdown}
|
||||||
onFormat={insertMarkdown}
|
visible={showFloatingToolbar}
|
||||||
visible={showFloatingToolbar}
|
position={toolbarPosition}
|
||||||
position={toolbarPosition}
|
onHide={() => setShowFloatingToolbar(false)}
|
||||||
onHide={() => setShowFloatingToolbar(false)}
|
onInsertColor={insertColorMarkdown}
|
||||||
onInsertColor={insertColorMarkdown}
|
activeFormats={activeFormats}
|
||||||
activeFormats={activeFormats}
|
hasSelection={hasSelection}
|
||||||
hasSelection={hasSelection}
|
/>
|
||||||
/>
|
|
||||||
)}
|
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
|||||||
@ -46,7 +46,6 @@ const SettingsPage: React.FC = () => {
|
|||||||
const [selectedAccentColor, setSelectedAccentColor] = useState("#007bff");
|
const [selectedAccentColor, setSelectedAccentColor] = useState("#007bff");
|
||||||
const [showEditDate, setShowEditDate] = useState(true);
|
const [showEditDate, setShowEditDate] = useState(true);
|
||||||
const [coloredIcons, setColoredIcons] = useState(true);
|
const [coloredIcons, setColoredIcons] = useState(true);
|
||||||
const [floatingToolbarEnabled, setFloatingToolbarEnabled] = useState(true);
|
|
||||||
|
|
||||||
// AI settings
|
// AI settings
|
||||||
const [apiKey, setApiKey] = useState("");
|
const [apiKey, setApiKey] = useState("");
|
||||||
@ -135,11 +134,6 @@ const SettingsPage: React.FC = () => {
|
|||||||
: true;
|
: true;
|
||||||
setColoredIcons(coloredIconsValue);
|
setColoredIcons(coloredIconsValue);
|
||||||
updateColoredIconsClass(coloredIconsValue);
|
updateColoredIconsClass(coloredIconsValue);
|
||||||
const floatingToolbarValue =
|
|
||||||
userData.floating_toolbar_enabled !== undefined
|
|
||||||
? userData.floating_toolbar_enabled === 1
|
|
||||||
: true;
|
|
||||||
setFloatingToolbarEnabled(floatingToolbarValue);
|
|
||||||
|
|
||||||
// Загружаем AI настройки
|
// Загружаем AI настройки
|
||||||
try {
|
try {
|
||||||
@ -172,7 +166,6 @@ const SettingsPage: React.FC = () => {
|
|||||||
accent_color: selectedAccentColor,
|
accent_color: selectedAccentColor,
|
||||||
show_edit_date: showEditDate,
|
show_edit_date: showEditDate,
|
||||||
colored_icons: coloredIcons,
|
colored_icons: coloredIcons,
|
||||||
floating_toolbar_enabled: floatingToolbarEnabled,
|
|
||||||
});
|
});
|
||||||
dispatch(setAccentColorAction(selectedAccentColor));
|
dispatch(setAccentColorAction(selectedAccentColor));
|
||||||
setAccentColor(selectedAccentColor);
|
setAccentColor(selectedAccentColor);
|
||||||
@ -679,31 +672,6 @@ const SettingsPage: React.FC = () => {
|
|||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="form-group ai-toggle-group">
|
|
||||||
<label className="ai-toggle-label">
|
|
||||||
<div className="toggle-label-content">
|
|
||||||
<span className="toggle-text-main">
|
|
||||||
Плавающая панель редактирования
|
|
||||||
</span>
|
|
||||||
<span className="toggle-text-desc">
|
|
||||||
{floatingToolbarEnabled
|
|
||||||
? "Показывать плавающую панель инструментов при выделении текста в редакторе"
|
|
||||||
: "Скрывать плавающую панель инструментов при выделении текста"}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
<div className="toggle-switch-wrapper">
|
|
||||||
<input
|
|
||||||
type="checkbox"
|
|
||||||
id="floating-toolbar-toggle"
|
|
||||||
className="toggle-checkbox"
|
|
||||||
checked={floatingToolbarEnabled}
|
|
||||||
onChange={(e) => setFloatingToolbarEnabled(e.target.checked)}
|
|
||||||
/>
|
|
||||||
<span className="toggle-slider"></span>
|
|
||||||
</div>
|
|
||||||
</label>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<button className="btnSave" onClick={handleUpdateAppearance}>
|
<button className="btnSave" onClick={handleUpdateAppearance}>
|
||||||
Сохранить изменения
|
Сохранить изменения
|
||||||
</button>
|
</button>
|
||||||
|
|||||||
@ -671,8 +671,7 @@ header {
|
|||||||
|
|
||||||
/* Анимация пульсации для offline индикатора */
|
/* Анимация пульсации для offline индикатора */
|
||||||
@keyframes pulse {
|
@keyframes pulse {
|
||||||
0%,
|
0%, 100% {
|
||||||
100% {
|
|
||||||
opacity: 1;
|
opacity: 1;
|
||||||
transform: scale(1);
|
transform: scale(1);
|
||||||
}
|
}
|
||||||
@ -4410,11 +4409,7 @@ textarea:focus {
|
|||||||
|
|
||||||
/* Стили для скрытого текста (спойлеров) */
|
/* Стили для скрытого текста (спойлеров) */
|
||||||
.spoiler {
|
.spoiler {
|
||||||
background: linear-gradient(
|
background: linear-gradient(45deg, #f0f0f0, #e8e8e8);
|
||||||
45deg,
|
|
||||||
rgba(var(--accent-color-rgb), 0.15),
|
|
||||||
rgba(var(--accent-color-rgb), 0.25)
|
|
||||||
);
|
|
||||||
color: transparent;
|
color: transparent;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
border-radius: 4px;
|
border-radius: 4px;
|
||||||
@ -4422,15 +4417,11 @@ textarea:focus {
|
|||||||
user-select: none;
|
user-select: none;
|
||||||
transition: all 0.3s ease;
|
transition: all 0.3s ease;
|
||||||
position: relative;
|
position: relative;
|
||||||
display: inline-block;
|
border: 1px solid #ddd;
|
||||||
vertical-align: baseline;
|
|
||||||
border: 1px solid rgba(var(--accent-color-rgb), 0.3);
|
|
||||||
font-weight: 500;
|
font-weight: 500;
|
||||||
backdrop-filter: blur(2px);
|
backdrop-filter: blur(2px);
|
||||||
-webkit-backdrop-filter: blur(2px);
|
-webkit-backdrop-filter: blur(2px);
|
||||||
text-shadow: 0 0 8px rgba(var(--accent-color-rgb), 0.3);
|
text-shadow: 0 0 8px rgba(0, 0, 0, 0.3);
|
||||||
isolation: isolate;
|
|
||||||
z-index: 0;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.spoiler::before {
|
.spoiler::before {
|
||||||
@ -4440,38 +4431,27 @@ textarea:focus {
|
|||||||
left: 0;
|
left: 0;
|
||||||
right: 0;
|
right: 0;
|
||||||
bottom: 0;
|
bottom: 0;
|
||||||
background: rgba(var(--accent-color-rgb), 0.1);
|
background: rgba(255, 255, 255, 0.8);
|
||||||
border-radius: 4px;
|
border-radius: 4px;
|
||||||
filter: blur(1px);
|
filter: blur(1px);
|
||||||
z-index: -1;
|
z-index: -1;
|
||||||
box-sizing: border-box;
|
|
||||||
pointer-events: none;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.spoiler:hover {
|
.spoiler:hover {
|
||||||
background: linear-gradient(
|
background: linear-gradient(45deg, #e8e8e8, #d8d8d8);
|
||||||
45deg,
|
|
||||||
rgba(var(--accent-color-rgb), 0.25),
|
|
||||||
rgba(var(--accent-color-rgb), 0.35)
|
|
||||||
);
|
|
||||||
transform: scale(1.02);
|
transform: scale(1.02);
|
||||||
box-shadow: 0 2px 8px rgba(var(--accent-color-rgb), 0.25);
|
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.15);
|
||||||
border-color: rgba(var(--accent-color-rgb), 0.4);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.spoiler:hover::before {
|
.spoiler:hover::before {
|
||||||
background: rgba(var(--accent-color-rgb), 0.15);
|
background: rgba(255, 255, 255, 0.9);
|
||||||
}
|
}
|
||||||
|
|
||||||
.spoiler.revealed {
|
.spoiler.revealed {
|
||||||
background: linear-gradient(
|
background: linear-gradient(45deg, #e8f5e8, #d4edda);
|
||||||
45deg,
|
color: #155724;
|
||||||
rgba(var(--accent-color-rgb), 0.15),
|
border-color: #c3e6cb;
|
||||||
rgba(var(--accent-color-rgb), 0.2)
|
box-shadow: 0 0 0 2px rgba(40, 167, 69, 0.25);
|
||||||
);
|
|
||||||
color: var(--accent-color);
|
|
||||||
border-color: rgba(var(--accent-color-rgb), 0.4);
|
|
||||||
box-shadow: 0 0 0 2px rgba(var(--accent-color-rgb), 0.25);
|
|
||||||
text-shadow: none;
|
text-shadow: none;
|
||||||
user-select: text;
|
user-select: text;
|
||||||
-webkit-user-select: text;
|
-webkit-user-select: text;
|
||||||
@ -4483,12 +4463,8 @@ textarea:focus {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.spoiler.revealed:hover {
|
.spoiler.revealed:hover {
|
||||||
background: linear-gradient(
|
background: linear-gradient(45deg, #d4edda, #c3e6cb);
|
||||||
45deg,
|
box-shadow: 0 0 0 2px rgba(40, 167, 69, 0.35);
|
||||||
rgba(var(--accent-color-rgb), 0.2),
|
|
||||||
rgba(var(--accent-color-rgb), 0.25)
|
|
||||||
);
|
|
||||||
box-shadow: 0 0 0 2px rgba(var(--accent-color-rgb), 0.35);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Стили для файлов */
|
/* Стили для файлов */
|
||||||
@ -5102,44 +5078,3 @@ textarea:focus {
|
|||||||
[data-theme="dark"] .loading-content {
|
[data-theme="dark"] .loading-content {
|
||||||
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.6);
|
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.6);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Адаптивность для плавающей панели */
|
|
||||||
@media (max-width: 768px) {
|
|
||||||
.floating-toolbar {
|
|
||||||
padding: 8px;
|
|
||||||
gap: 6px;
|
|
||||||
border-radius: 12px;
|
|
||||||
/* Увеличиваем размер для удобства на touch-устройствах */
|
|
||||||
}
|
|
||||||
|
|
||||||
.floating-toolbar-btn {
|
|
||||||
min-width: 44px;
|
|
||||||
min-height: 44px;
|
|
||||||
padding: 8px 12px;
|
|
||||||
font-size: 18px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.floating-toolbar-btn .iconify {
|
|
||||||
font-size: 20px;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Улучшаем прокрутку на мобильных */
|
|
||||||
.floating-toolbar-wrapper {
|
|
||||||
-webkit-overflow-scrolling: touch;
|
|
||||||
touch-action: pan-x;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Анимация появления снизу для лучшего UX на мобильных */
|
|
||||||
@media (max-width: 768px) {
|
|
||||||
@keyframes fadeIn {
|
|
||||||
from {
|
|
||||||
opacity: 0;
|
|
||||||
transform: translateY(5px);
|
|
||||||
}
|
|
||||||
to {
|
|
||||||
opacity: 1;
|
|
||||||
transform: translateY(0);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
@ -5,7 +5,6 @@ export interface User {
|
|||||||
accent_color: string;
|
accent_color: string;
|
||||||
show_edit_date?: number;
|
show_edit_date?: number;
|
||||||
colored_icons?: number;
|
colored_icons?: number;
|
||||||
floating_toolbar_enabled?: number;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface AuthResponse {
|
export interface AuthResponse {
|
||||||
|
|||||||
@ -25,54 +25,49 @@ const spoilerExtension = {
|
|||||||
|
|
||||||
// Функция для рендеринга вложенных токенов
|
// Функция для рендеринга вложенных токенов
|
||||||
function renderTokens(tokens: any[], renderer: any): string {
|
function renderTokens(tokens: any[], renderer: any): string {
|
||||||
return tokens
|
return tokens.map((token) => {
|
||||||
.map((token) => {
|
// Используем кастомный renderer если он есть
|
||||||
// Используем кастомный renderer если он есть
|
if (renderer[token.type]) {
|
||||||
if (renderer[token.type]) {
|
return renderer[token.type](token);
|
||||||
return renderer[token.type](token);
|
}
|
||||||
|
// Fallback для встроенных типов токенов
|
||||||
|
if (token.type === 'text') {
|
||||||
|
return token.text || '';
|
||||||
|
}
|
||||||
|
if (token.type === 'strong') {
|
||||||
|
return `<strong>${renderTokens(token.tokens || [], renderer)}</strong>`;
|
||||||
|
}
|
||||||
|
if (token.type === 'em') {
|
||||||
|
return `<em>${renderTokens(token.tokens || [], renderer)}</em>`;
|
||||||
|
}
|
||||||
|
if (token.type === 'codespan') {
|
||||||
|
return `<code>${token.text || ''}</code>`;
|
||||||
|
}
|
||||||
|
if (token.type === 'del') {
|
||||||
|
return `<del>${renderTokens(token.tokens || [], renderer)}</del>`;
|
||||||
|
}
|
||||||
|
if (token.type === 'link') {
|
||||||
|
// Для ссылок используем кастомный renderer если доступен
|
||||||
|
if (renderer.link) {
|
||||||
|
return renderer.link(token);
|
||||||
}
|
}
|
||||||
// Fallback для встроенных типов токенов
|
// Fallback для встроенных ссылок
|
||||||
if (token.type === "text") {
|
const href = token.href || '';
|
||||||
return token.text || "";
|
const title = token.title ? ` title="${token.title}"` : '';
|
||||||
|
const text = token.tokens && token.tokens.length > 0
|
||||||
|
? renderTokens(token.tokens, renderer)
|
||||||
|
: (token.text || '');
|
||||||
|
return `<a href="${href}"${title}>${text}</a>`;
|
||||||
|
}
|
||||||
|
if (token.type === 'spoiler') {
|
||||||
|
// Для спойлеров используем кастомный renderer если доступен
|
||||||
|
if (renderer.spoiler) {
|
||||||
|
return renderer.spoiler(token);
|
||||||
}
|
}
|
||||||
if (token.type === "strong") {
|
return `<span class="spoiler" title="Нажмите, чтобы показать">${token.text || ''}</span>`;
|
||||||
return `<strong>${renderTokens(token.tokens || [], renderer)}</strong>`;
|
}
|
||||||
}
|
return token.text || '';
|
||||||
if (token.type === "em") {
|
}).join('');
|
||||||
return `<em>${renderTokens(token.tokens || [], renderer)}</em>`;
|
|
||||||
}
|
|
||||||
if (token.type === "codespan") {
|
|
||||||
return `<code>${token.text || ""}</code>`;
|
|
||||||
}
|
|
||||||
if (token.type === "del") {
|
|
||||||
return `<del>${renderTokens(token.tokens || [], renderer)}</del>`;
|
|
||||||
}
|
|
||||||
if (token.type === "link") {
|
|
||||||
// Для ссылок используем кастомный renderer если доступен
|
|
||||||
if (renderer.link) {
|
|
||||||
return renderer.link(token);
|
|
||||||
}
|
|
||||||
// Fallback для встроенных ссылок
|
|
||||||
const href = token.href || "";
|
|
||||||
const title = token.title ? ` title="${token.title}"` : "";
|
|
||||||
const text =
|
|
||||||
token.tokens && token.tokens.length > 0
|
|
||||||
? renderTokens(token.tokens, renderer)
|
|
||||||
: token.text || "";
|
|
||||||
return `<a href="${href}"${title}>${text}</a>`;
|
|
||||||
}
|
|
||||||
if (token.type === "spoiler") {
|
|
||||||
// Для спойлеров используем кастомный renderer если доступен
|
|
||||||
if (renderer.spoiler) {
|
|
||||||
return renderer.spoiler(token);
|
|
||||||
}
|
|
||||||
return `<span class="spoiler" title="Нажмите, чтобы показать">${
|
|
||||||
token.text || ""
|
|
||||||
}</span>`;
|
|
||||||
}
|
|
||||||
return token.text || "";
|
|
||||||
})
|
|
||||||
.join("");
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Кастомный renderer для внешних ссылок и чекбоксов
|
// Кастомный renderer для внешних ссылок и чекбоксов
|
||||||
@ -80,9 +75,9 @@ const renderer: any = {
|
|||||||
link(token: any) {
|
link(token: any) {
|
||||||
const href = token.href;
|
const href = token.href;
|
||||||
const title = token.title;
|
const title = token.title;
|
||||||
|
|
||||||
// Правильно обрабатываем вложенные токены для форматирования внутри ссылок
|
// Правильно обрабатываем вложенные токены для форматирования внутри ссылок
|
||||||
let text = "";
|
let text = '';
|
||||||
if (token.tokens && token.tokens.length > 0) {
|
if (token.tokens && token.tokens.length > 0) {
|
||||||
text = renderTokens(token.tokens, this);
|
text = renderTokens(token.tokens, this);
|
||||||
} else if (token.text) {
|
} else if (token.text) {
|
||||||
@ -106,9 +101,9 @@ const renderer: any = {
|
|||||||
listitem(token: any) {
|
listitem(token: any) {
|
||||||
const task = token.task;
|
const task = token.task;
|
||||||
const checked = token.checked;
|
const checked = token.checked;
|
||||||
|
|
||||||
// Правильно обрабатываем вложенные токены для форматирования
|
// Правильно обрабатываем вложенные токены для форматирования
|
||||||
let content = "";
|
let content = '';
|
||||||
if (token.tokens && token.tokens.length > 0) {
|
if (token.tokens && token.tokens.length > 0) {
|
||||||
// Рендерим вложенные токены используя наш renderer
|
// Рендерим вложенные токены используя наш renderer
|
||||||
content = renderTokens(token.tokens, this);
|
content = renderTokens(token.tokens, this);
|
||||||
@ -179,7 +174,7 @@ export const extractTags = (content: string): string[] => {
|
|||||||
|
|
||||||
const tag = match[1];
|
const tag = match[1];
|
||||||
// Проверяем, есть ли уже тег с таким же именем (регистронезависимо)
|
// Проверяем, есть ли уже тег с таким же именем (регистронезависимо)
|
||||||
if (!tags.some((t) => t.toLowerCase() === tag.toLowerCase())) {
|
if (!tags.some(t => t.toLowerCase() === tag.toLowerCase())) {
|
||||||
tags.push(tag);
|
tags.push(tag);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user