Добавлен функционал для создания таблиц в компонент MarkdownToolbar. Реализованы модальные окна для ввода количества строк и столбцов, а также добавлены соответствующие стили для отображения таблиц в заметках. Обновлены обработчики событий для управления модальными окнами и валидации ввода. Улучшено взаимодействие с пользователем через фокусировку на полях ввода.

This commit is contained in:
Fovway 2025-11-08 14:56:36 +07:00
parent c4327d31d1
commit 9a1ee8629f
9 changed files with 472 additions and 17 deletions

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -158,8 +158,8 @@
<!-- Manifest -->
<link rel="manifest" href="/manifest.json" />
<script type="module" crossorigin src="/assets/index-DgyuSC5D.js"></script>
<link rel="stylesheet" crossorigin href="/assets/index-caJGErnu.css">
<script type="module" crossorigin src="/assets/index-BLHAueVj.js"></script>
<link rel="stylesheet" crossorigin href="/assets/index-PWucW8RD.css">
<link rel="manifest" href="/manifest.webmanifest"><script id="vite-plugin-pwa:register-sw" src="/registerSW.js"></script></head>
<body>
<div id="root">

View File

@ -1 +1 @@
if(!self.define){let e,n={};const i=(i,c)=>(i=new URL(i+".js",c).href,n[i]||new Promise(n=>{if("document"in self){const e=document.createElement("script");e.src=i,e.onload=n,document.head.appendChild(e)}else e=i,importScripts(i),n()}).then(()=>{let e=n[i];if(!e)throw new Error(`Module ${i} didnt register its module`);return e}));self.define=(c,s)=>{const a=e||("document"in self?document.currentScript.src:"")||location.href;if(n[a])return;let o={};const d=e=>i(e,a),r={module:{uri:a},exports:o,require:d};n[a]=Promise.all(c.map(e=>r[e]||d(e))).then(e=>(s(...e),o))}}define(["./workbox-40c80ae4"],function(e){"use strict";self.skipWaiting(),e.clientsClaim(),e.precacheAndRoute([{url:"assets/index-caJGErnu.css",revision:"b5c1c05bdfd2d7b59a5d3e9caefed39e"},{url:"assets/index-DgyuSC5D.js",revision:"77a1bd3893cfacfb7a63395ad09f9145"},{url:"icon.svg",revision:"537ae73d8f9e90e6a01816aa6d527d16"},{url:"icons/icon-128x128.png",revision:"fa71db17e345406d5f7d847f88c65ac4"},{url:"icons/icon-144x144.png",revision:"e790ff42758ea1a2a46eb84201630757"},{url:"icons/icon-152x152.png",revision:"88f2400f6617a32cc9cd62c70fb49a05"},{url:"icons/icon-16x16.png",revision:"101c13808e9fd0956f247bc446a8ac1e"},{url:"icons/icon-192x192.png",revision:"7d86d2d2ada99d7cee015dff0fdcb497"},{url:"icons/icon-32x32.png",revision:"22ee5d42535bc339ab0e19cb496378a5"},{url:"icons/icon-384x384.png",revision:"c601fa602952a903389e5e8f8a699617"},{url:"icons/icon-48x48.png",revision:"cfdd3bebd931375f2e0277d638ec8781"},{url:"icons/icon-512x512.png",revision:"8731edef999b9e7deba310d72a739925"},{url:"icons/icon-72x72.png",revision:"6b3cb1b2537ec91921698260a9c2f47c"},{url:"icons/icon-96x96.png",revision:"7efd757a81217207d981de88ef199d86"},{url:"index.html",revision:"5609b5d50cef9e4061e094b7b8d3eb8f"},{url:"logo.svg",revision:"11616ede8898b4c24203e331b3ec6dc3"},{url:"registerSW.js",revision:"1872c500de691dce40960bb85481de07"},{url:"icon.svg",revision:"537ae73d8f9e90e6a01816aa6d527d16"},{url:"icons/icon-192x192.png",revision:"7d86d2d2ada99d7cee015dff0fdcb497"},{url:"icons/icon-512x512.png",revision:"8731edef999b9e7deba310d72a739925"},{url:"icons/icon-72x72.png",revision:"6b3cb1b2537ec91921698260a9c2f47c"},{url:"icons/icon-96x96.png",revision:"7efd757a81217207d981de88ef199d86"},{url:"icons/icon-128x128.png",revision:"fa71db17e345406d5f7d847f88c65ac4"},{url:"icons/icon-144x144.png",revision:"e790ff42758ea1a2a46eb84201630757"},{url:"icons/icon-152x152.png",revision:"88f2400f6617a32cc9cd62c70fb49a05"},{url:"icons/icon-384x384.png",revision:"c601fa602952a903389e5e8f8a699617"},{url:"manifest.webmanifest",revision:"1c071cadebd7a1b0dc1eeb0270e73fb8"}],{ignoreURLParametersMatching:[/^utm_/,/^fbclid$/]}),e.cleanupOutdatedCaches(),e.registerRoute(new e.NavigationRoute(e.createHandlerBoundToURL("/index.html"),{denylist:[/^\/api/,/^\/uploads/]})),e.registerRoute(({request:e})=>"navigate"===e.mode,new e.CacheFirst({cacheName:"pages-cache",plugins:[new e.ExpirationPlugin({maxEntries:10,maxAgeSeconds:604800}),new e.CacheableResponsePlugin({statuses:[0,200]})]}),"GET"),e.registerRoute(/\.html$/,new e.CacheFirst({cacheName:"html-cache",plugins:[new e.ExpirationPlugin({maxEntries:10,maxAgeSeconds:604800}),new e.CacheableResponsePlugin({statuses:[0,200]})]}),"GET"),e.registerRoute(/^https:\/\/api\./,new e.NetworkFirst({cacheName:"api-cache",plugins:[new e.ExpirationPlugin({maxEntries:50,maxAgeSeconds:3600})]}),"GET"),e.registerRoute(/\/api\//,new e.NetworkFirst({cacheName:"api-cache-local",networkTimeoutSeconds:10,plugins:[new e.ExpirationPlugin({maxEntries:100,maxAgeSeconds:86400})]}),"GET"),e.registerRoute(/\/uploads\//,new e.CacheFirst({cacheName:"uploads-cache",plugins:[new e.ExpirationPlugin({maxEntries:200,maxAgeSeconds:2592e3})]}),"GET"),e.registerRoute(/\.(?:png|jpg|jpeg|svg|gif|webp)$/,new e.CacheFirst({cacheName:"images-cache",plugins:[new e.ExpirationPlugin({maxEntries:100,maxAgeSeconds:2592e3})]}),"GET")});
if(!self.define){let e,n={};const i=(i,c)=>(i=new URL(i+".js",c).href,n[i]||new Promise(n=>{if("document"in self){const e=document.createElement("script");e.src=i,e.onload=n,document.head.appendChild(e)}else e=i,importScripts(i),n()}).then(()=>{let e=n[i];if(!e)throw new Error(`Module ${i} didnt register its module`);return e}));self.define=(c,s)=>{const o=e||("document"in self?document.currentScript.src:"")||location.href;if(n[o])return;let a={};const r=e=>i(e,o),d={module:{uri:o},exports:a,require:r};n[o]=Promise.all(c.map(e=>d[e]||r(e))).then(e=>(s(...e),a))}}define(["./workbox-40c80ae4"],function(e){"use strict";self.skipWaiting(),e.clientsClaim(),e.precacheAndRoute([{url:"assets/index-BLHAueVj.js",revision:"a554119eeb8e2517fbf5b56f3056f821"},{url:"assets/index-PWucW8RD.css",revision:"d9c0b2036bfdcfb70738c2c7fc47cd92"},{url:"icon.svg",revision:"537ae73d8f9e90e6a01816aa6d527d16"},{url:"icons/icon-128x128.png",revision:"fa71db17e345406d5f7d847f88c65ac4"},{url:"icons/icon-144x144.png",revision:"e790ff42758ea1a2a46eb84201630757"},{url:"icons/icon-152x152.png",revision:"88f2400f6617a32cc9cd62c70fb49a05"},{url:"icons/icon-16x16.png",revision:"101c13808e9fd0956f247bc446a8ac1e"},{url:"icons/icon-192x192.png",revision:"7d86d2d2ada99d7cee015dff0fdcb497"},{url:"icons/icon-32x32.png",revision:"22ee5d42535bc339ab0e19cb496378a5"},{url:"icons/icon-384x384.png",revision:"c601fa602952a903389e5e8f8a699617"},{url:"icons/icon-48x48.png",revision:"cfdd3bebd931375f2e0277d638ec8781"},{url:"icons/icon-512x512.png",revision:"8731edef999b9e7deba310d72a739925"},{url:"icons/icon-72x72.png",revision:"6b3cb1b2537ec91921698260a9c2f47c"},{url:"icons/icon-96x96.png",revision:"7efd757a81217207d981de88ef199d86"},{url:"index.html",revision:"9a7779e0b82393809d2b49570f7f35a0"},{url:"logo.svg",revision:"11616ede8898b4c24203e331b3ec6dc3"},{url:"registerSW.js",revision:"1872c500de691dce40960bb85481de07"},{url:"icon.svg",revision:"537ae73d8f9e90e6a01816aa6d527d16"},{url:"icons/icon-192x192.png",revision:"7d86d2d2ada99d7cee015dff0fdcb497"},{url:"icons/icon-512x512.png",revision:"8731edef999b9e7deba310d72a739925"},{url:"icons/icon-72x72.png",revision:"6b3cb1b2537ec91921698260a9c2f47c"},{url:"icons/icon-96x96.png",revision:"7efd757a81217207d981de88ef199d86"},{url:"icons/icon-128x128.png",revision:"fa71db17e345406d5f7d847f88c65ac4"},{url:"icons/icon-144x144.png",revision:"e790ff42758ea1a2a46eb84201630757"},{url:"icons/icon-152x152.png",revision:"88f2400f6617a32cc9cd62c70fb49a05"},{url:"icons/icon-384x384.png",revision:"c601fa602952a903389e5e8f8a699617"},{url:"manifest.webmanifest",revision:"1c071cadebd7a1b0dc1eeb0270e73fb8"}],{ignoreURLParametersMatching:[/^utm_/,/^fbclid$/]}),e.cleanupOutdatedCaches(),e.registerRoute(new e.NavigationRoute(e.createHandlerBoundToURL("/index.html"),{denylist:[/^\/api/,/^\/uploads/]})),e.registerRoute(({request:e})=>"navigate"===e.mode,new e.CacheFirst({cacheName:"pages-cache",plugins:[new e.ExpirationPlugin({maxEntries:10,maxAgeSeconds:604800}),new e.CacheableResponsePlugin({statuses:[0,200]})]}),"GET"),e.registerRoute(/\.html$/,new e.CacheFirst({cacheName:"html-cache",plugins:[new e.ExpirationPlugin({maxEntries:10,maxAgeSeconds:604800}),new e.CacheableResponsePlugin({statuses:[0,200]})]}),"GET"),e.registerRoute(/^https:\/\/api\./,new e.NetworkFirst({cacheName:"api-cache",plugins:[new e.ExpirationPlugin({maxEntries:50,maxAgeSeconds:3600})]}),"GET"),e.registerRoute(/\/api\//,new e.NetworkFirst({cacheName:"api-cache-local",networkTimeoutSeconds:10,plugins:[new e.ExpirationPlugin({maxEntries:100,maxAgeSeconds:86400})]}),"GET"),e.registerRoute(/\/uploads\//,new e.CacheFirst({cacheName:"uploads-cache",plugins:[new e.ExpirationPlugin({maxEntries:200,maxAgeSeconds:2592e3})]}),"GET"),e.registerRoute(/\.(?:png|jpg|jpeg|svg|gif|webp)$/,new e.CacheFirst({cacheName:"images-cache",plugins:[new e.ExpirationPlugin({maxEntries:100,maxAgeSeconds:2592e3})]}),"GET")});

View File

@ -82,7 +82,7 @@ define(['./workbox-47da91e0'], (function (workbox) { 'use strict';
"revision": "3ca0b8505b4bec776b69afdba2768812"
}, {
"url": "/index.html",
"revision": "0.dlpr89s9d7"
"revision": "0.1kpib446v2o"
}], {
"ignoreURLParametersMatching": [/^utm_/, /^fbclid$/]
});

View File

@ -28,6 +28,10 @@ export const MarkdownToolbar: React.FC<MarkdownToolbarProps> = ({
const [linkText, setLinkText] = useState("");
const [linkUrl, setLinkUrl] = useState("");
const linkTextInputRef = useRef<HTMLInputElement>(null);
const [showTableModal, setShowTableModal] = useState(false);
const [tableRows, setTableRows] = useState("2");
const [tableCols, setTableCols] = useState("3");
const tableRowsInputRef = useRef<HTMLInputElement>(null);
const dispatch = useAppDispatch();
const dropdownRef = useRef<HTMLDivElement>(null);
const buttonRef = useRef<HTMLButtonElement>(null);
@ -140,6 +144,16 @@ export const MarkdownToolbar: React.FC<MarkdownToolbarProps> = ({
}
}, [showLinkModal]);
// Фокус на поле ввода строк при открытии модального окна таблицы
useEffect(() => {
if (showTableModal && tableRowsInputRef.current) {
setTimeout(() => {
tableRowsInputRef.current?.focus();
tableRowsInputRef.current?.select();
}, 100);
}
}, [showTableModal]);
// Обработка Escape для закрытия модального окна
useEffect(() => {
const handleEscape = (e: KeyboardEvent) => {
@ -153,17 +167,22 @@ export const MarkdownToolbar: React.FC<MarkdownToolbarProps> = ({
setLinkText("");
setLinkUrl("");
}
if (showTableModal) {
setShowTableModal(false);
setTableRows("2");
setTableCols("3");
}
}
};
if (showCodeModal || showLinkModal) {
if (showCodeModal || showLinkModal || showTableModal) {
document.addEventListener("keydown", handleEscape);
}
return () => {
document.removeEventListener("keydown", handleEscape);
};
}, [showCodeModal, showLinkModal]);
}, [showCodeModal, showLinkModal, showTableModal]);
const handleCodeButtonClick = () => {
setShowCodeModal(true);
@ -234,6 +253,70 @@ export const MarkdownToolbar: React.FC<MarkdownToolbarProps> = ({
}
};
const handleTableButtonClick = () => {
setShowTableModal(true);
setTableRows("2");
setTableCols("3");
};
const generateTableMarkdown = (rows: number, cols: number): string => {
// Генерируем заголовок
const headerRow = "| " + Array(cols).fill("Заголовок").join(" | ") + " |\n";
// Генерируем разделитель
const separatorRow = "| " + Array(cols).fill("---").join(" | ") + " |\n";
// Генерируем строки данных
const dataRows = Array(rows)
.fill("")
.map(() => "| " + Array(cols).fill("Ячейка").join(" | ") + " |\n")
.join("");
// Добавляем пустую строку перед таблицей для лучшей читаемости
return "\n" + headerRow + separatorRow + dataRows;
};
const handleTableModalConfirm = () => {
const rows = parseInt(tableRows) || 2;
const cols = parseInt(tableCols) || 3;
// Валидация: минимум 1, максимум 20 строк и 10 столбцов
const validRows = Math.max(1, Math.min(20, rows));
const validCols = Math.max(1, Math.min(10, cols));
const tableMarkdown = generateTableMarkdown(validRows, validCols);
onInsert(tableMarkdown, "");
setShowTableModal(false);
setTableRows("2");
setTableCols("3");
};
const handleTableModalClose = () => {
setShowTableModal(false);
setTableRows("2");
setTableCols("3");
};
const handleTableRowsKeyDown = (e: React.KeyboardEvent<HTMLInputElement>) => {
if (e.key === "Enter") {
e.preventDefault();
// Переход к полю столбцов
const colsInput = document.querySelector(
".table-modal-cols-input"
) as HTMLInputElement;
if (colsInput) {
colsInput.focus();
colsInput.select();
}
}
};
const handleTableColsKeyDown = (e: React.KeyboardEvent<HTMLInputElement>) => {
if (e.key === "Enter") {
e.preventDefault();
handleTableModalConfirm();
}
};
const buttons: Array<{
id: string;
icon: string;
@ -401,6 +484,14 @@ export const MarkdownToolbar: React.FC<MarkdownToolbarProps> = ({
<Icon icon="mdi:link" />
</button>
<button
className="btnMarkdown"
onClick={handleTableButtonClick}
title="Таблица"
>
<Icon icon="mdi:table" />
</button>
<button
className="btnMarkdown"
onClick={() => onInsert("- [ ] ", "")}
@ -539,6 +630,83 @@ export const MarkdownToolbar: React.FC<MarkdownToolbarProps> = ({
</div>
</div>
)}
{/* Модальное окно для создания таблицы */}
{showTableModal && (
<div
className="modal"
style={{ display: "block" }}
onClick={handleTableModalClose}
>
<div
className="modal-content"
onClick={(e) => e.stopPropagation()}
>
<div className="modal-header">
<h3>Создать таблицу</h3>
<span className="modal-close" onClick={handleTableModalClose}>
&times;
</span>
</div>
<div className="modal-body">
<div style={{ marginBottom: "15px" }}>
<label style={{ display: "block", marginBottom: "5px", fontWeight: "500" }}>
Количество строк (1-20):
</label>
<input
ref={tableRowsInputRef}
type="number"
className="modal-password-input"
placeholder="2"
min="1"
max="20"
value={tableRows}
onChange={(e) => {
const val = e.target.value;
if (val === "" || (parseInt(val) >= 1 && parseInt(val) <= 20)) {
setTableRows(val);
}
}}
onKeyDown={handleTableRowsKeyDown}
style={{ width: "100%" }}
/>
</div>
<div>
<label style={{ display: "block", marginBottom: "5px", fontWeight: "500" }}>
Количество столбцов (1-10):
</label>
<input
type="number"
className="modal-password-input table-modal-cols-input"
placeholder="3"
min="1"
max="10"
value={tableCols}
onChange={(e) => {
const val = e.target.value;
if (val === "" || (parseInt(val) >= 1 && parseInt(val) <= 10)) {
setTableCols(val);
}
}}
onKeyDown={handleTableColsKeyDown}
style={{ width: "100%" }}
/>
</div>
</div>
<div className="modal-footer">
<button
className="btn-primary"
onClick={handleTableModalConfirm}
>
Вставить
</button>
<button className="btn-secondary" onClick={handleTableModalClose}>
Отмена
</button>
</div>
</div>
</div>
)}
</div>
);
};

View File

@ -1,4 +1,4 @@
import React, { useState, useEffect } from "react";
import React, { useState } from "react";
import { Icon } from "@iconify/react";
import { userApi } from "../../api/userApi";
import { useNotification } from "../../hooks/useNotification";

View File

@ -50,19 +50,19 @@ const LoginPage: React.FC = () => {
const data = await authApi.login(username, password);
console.log("Login response:", data);
if (data.success) {
// Проверяем, требуется ли 2FA
if (data.requires2FA) {
setRequires2FA(true);
showNotification("Введите код двухфакторной аутентификации", "info");
setIsLoading(false);
return;
}
// Проверяем, требуется ли 2FA
if (data.requires2FA) {
setRequires2FA(true);
showNotification("Введите код двухфакторной аутентификации", "info");
setIsLoading(false);
return;
}
// Если 2FA не требуется, продолжаем как обычно
// Если 2FA не требуется и аутентификация успешна, продолжаем как обычно
if (data.authenticated) {
await completeLogin();
} else {
showNotification(data.error || "Ошибка входа", "error");
showNotification(data.message || "Ошибка входа", "error");
}
} catch (error: any) {
console.error("Login error details:", error);

View File

@ -1606,6 +1606,63 @@ textarea:focus {
margin-left: -24px;
}
/* Стили для таблиц в заметках */
.textNote table {
width: 100%;
max-width: 100%;
border-collapse: collapse;
margin: 10px 0;
background: var(--bg-secondary);
border-radius: 6px;
box-shadow: 0 1px 3px var(--shadow-light);
transition: background-color 0.3s ease, box-shadow 0.3s ease;
table-layout: auto;
}
.textNote th,
.textNote td {
padding: 8px 12px;
text-align: left;
border-bottom: 1px solid var(--border-primary);
color: var(--text-primary);
transition: color 0.3s ease, border-color 0.3s ease;
word-wrap: break-word;
overflow-wrap: break-word;
}
.textNote th {
background: var(--bg-tertiary);
font-weight: bold;
color: var(--text-primary);
transition: background-color 0.3s ease, color 0.3s ease;
}
.textNote tr:hover {
background: var(--bg-quaternary);
transition: background-color 0.3s ease;
}
/* Стили для таблиц в темной теме */
[data-theme="dark"] .textNote table {
background: var(--bg-secondary);
box-shadow: 0 1px 3px var(--shadow-light);
}
[data-theme="dark"] .textNote th {
background: var(--bg-tertiary);
color: var(--text-primary);
}
[data-theme="dark"] .textNote th,
[data-theme="dark"] .textNote td {
border-bottom-color: var(--border-primary);
color: var(--text-primary);
}
[data-theme="dark"] .textNote tr:hover {
background: var(--bg-quaternary);
}
.notes-container {
width: 100%;
display: flex;
@ -3184,6 +3241,37 @@ textarea:focus {
min-height: 100px;
}
/* Адаптация таблиц для мобильных устройств - обертка для горизонтальной прокрутки */
.textNote table,
.note-preview-content table {
display: block;
width: 100%;
overflow-x: auto;
-webkit-overflow-scrolling: touch;
max-width: 100%;
}
.textNote thead,
.textNote tbody,
.textNote tr,
.note-preview-content thead,
.note-preview-content tbody,
.note-preview-content tr {
display: table;
width: 100%;
table-layout: fixed;
}
.textNote th,
.textNote td,
.note-preview-content th,
.note-preview-content td {
min-width: 80px;
white-space: normal;
word-wrap: break-word;
overflow-wrap: break-word;
}
/* Адаптируем кнопку сохранения */
.save-button-container {
width: 100%;