Compare commits
No commits in common. "14cef88a6b4ea40cd2d060cefa3341f6ac46d041" and "06400d6e97448b929d4dcd4fb0afd8b21dda853a" have entirely different histories.
14cef88a6b
...
06400d6e97
@ -82,7 +82,7 @@ define(['./workbox-47da91e0'], (function (workbox) { 'use strict';
|
|||||||
"revision": "3ca0b8505b4bec776b69afdba2768812"
|
"revision": "3ca0b8505b4bec776b69afdba2768812"
|
||||||
}, {
|
}, {
|
||||||
"url": "/index.html",
|
"url": "/index.html",
|
||||||
"revision": "0.q2r0i2lj5ro"
|
"revision": "0.9eood2uf828"
|
||||||
}], {
|
}], {
|
||||||
"ignoreURLParametersMatching": [/^utm_/, /^fbclid$/]
|
"ignoreURLParametersMatching": [/^utm_/, /^fbclid$/]
|
||||||
});
|
});
|
||||||
|
|||||||
10
package-lock.json
generated
10
package-lock.json
generated
@ -14,7 +14,6 @@
|
|||||||
"axios": "^1.13.1",
|
"axios": "^1.13.1",
|
||||||
"clsx": "^2.1.1",
|
"clsx": "^2.1.1",
|
||||||
"date-fns": "^2.30.0",
|
"date-fns": "^2.30.0",
|
||||||
"highlight.js": "^11.11.1",
|
|
||||||
"marked": "^16.4.1",
|
"marked": "^16.4.1",
|
||||||
"react": "^18.2.0",
|
"react": "^18.2.0",
|
||||||
"react-dom": "^18.2.0",
|
"react-dom": "^18.2.0",
|
||||||
@ -4617,15 +4616,6 @@
|
|||||||
"node": ">= 0.4"
|
"node": ">= 0.4"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/highlight.js": {
|
|
||||||
"version": "11.11.1",
|
|
||||||
"resolved": "https://registry.npmjs.org/highlight.js/-/highlight.js-11.11.1.tgz",
|
|
||||||
"integrity": "sha512-Xwwo44whKBVCYoliBQwaPvtd/2tYFkRQtXDWj1nackaV2JPXx3L0+Jvd8/qCJ2p+ML0/XVkJ2q+Mr+UVdpJK5w==",
|
|
||||||
"license": "BSD-3-Clause",
|
|
||||||
"engines": {
|
|
||||||
"node": ">=12.0.0"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/idb": {
|
"node_modules/idb": {
|
||||||
"version": "7.1.1",
|
"version": "7.1.1",
|
||||||
"resolved": "https://registry.npmjs.org/idb/-/idb-7.1.1.tgz",
|
"resolved": "https://registry.npmjs.org/idb/-/idb-7.1.1.tgz",
|
||||||
|
|||||||
@ -19,7 +19,6 @@
|
|||||||
"axios": "^1.13.1",
|
"axios": "^1.13.1",
|
||||||
"clsx": "^2.1.1",
|
"clsx": "^2.1.1",
|
||||||
"date-fns": "^2.30.0",
|
"date-fns": "^2.30.0",
|
||||||
"highlight.js": "^11.11.1",
|
|
||||||
"marked": "^16.4.1",
|
"marked": "^16.4.1",
|
||||||
"react": "^18.2.0",
|
"react": "^18.2.0",
|
||||||
"react-dom": "^18.2.0",
|
"react-dom": "^18.2.0",
|
||||||
|
|||||||
@ -254,7 +254,7 @@ export const GenerateTagsModal: React.FC<GenerateTagsModalProps> = ({
|
|||||||
>
|
>
|
||||||
Применить ({selectedTags.length})
|
Применить ({selectedTags.length})
|
||||||
</button>
|
</button>
|
||||||
<button className="btn-secondary" onClick={onClose}>
|
<button className="btn-secondary" onClick={onClose} disabled={isLoading}>
|
||||||
Отмена
|
Отмена
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@ -151,7 +151,7 @@ export const ImproveTextModal: React.FC<ImproveTextModalProps> = ({
|
|||||||
>
|
>
|
||||||
Применить
|
Применить
|
||||||
</button>
|
</button>
|
||||||
<button className="btn-secondary" onClick={onClose}>
|
<button className="btn-secondary" onClick={onClose} disabled={isLoading}>
|
||||||
Отмена
|
Отмена
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@ -21,13 +21,6 @@ export const MarkdownToolbar: React.FC<MarkdownToolbarProps> = ({
|
|||||||
onInsertColor,
|
onInsertColor,
|
||||||
}) => {
|
}) => {
|
||||||
const [showHeaderDropdown, setShowHeaderDropdown] = useState(false);
|
const [showHeaderDropdown, setShowHeaderDropdown] = useState(false);
|
||||||
const [showCodeModal, setShowCodeModal] = useState(false);
|
|
||||||
const [codeLanguage, setCodeLanguage] = useState("");
|
|
||||||
const codeLanguageInputRef = useRef<HTMLInputElement>(null);
|
|
||||||
const [showLinkModal, setShowLinkModal] = useState(false);
|
|
||||||
const [linkText, setLinkText] = useState("");
|
|
||||||
const [linkUrl, setLinkUrl] = useState("");
|
|
||||||
const linkTextInputRef = useRef<HTMLInputElement>(null);
|
|
||||||
const dispatch = useAppDispatch();
|
const dispatch = useAppDispatch();
|
||||||
const dropdownRef = useRef<HTMLDivElement>(null);
|
const dropdownRef = useRef<HTMLDivElement>(null);
|
||||||
const buttonRef = useRef<HTMLButtonElement>(null);
|
const buttonRef = useRef<HTMLButtonElement>(null);
|
||||||
@ -58,8 +51,8 @@ export const MarkdownToolbar: React.FC<MarkdownToolbarProps> = ({
|
|||||||
if (buttonRef.current && showHeaderDropdown) {
|
if (buttonRef.current && showHeaderDropdown) {
|
||||||
const rect = buttonRef.current.getBoundingClientRect();
|
const rect = buttonRef.current.getBoundingClientRect();
|
||||||
setMenuPosition({
|
setMenuPosition({
|
||||||
top: rect.bottom + 2,
|
top: rect.bottom + window.scrollY + 2,
|
||||||
left: rect.left,
|
left: rect.left + window.scrollX,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@ -122,118 +115,6 @@ export const MarkdownToolbar: React.FC<MarkdownToolbarProps> = ({
|
|||||||
};
|
};
|
||||||
}, [isDragging]);
|
}, [isDragging]);
|
||||||
|
|
||||||
// Фокус на поле ввода при открытии модального окна
|
|
||||||
useEffect(() => {
|
|
||||||
if (showCodeModal && codeLanguageInputRef.current) {
|
|
||||||
setTimeout(() => {
|
|
||||||
codeLanguageInputRef.current?.focus();
|
|
||||||
}, 100);
|
|
||||||
}
|
|
||||||
}, [showCodeModal]);
|
|
||||||
|
|
||||||
// Фокус на поле ввода текста ссылки при открытии модального окна
|
|
||||||
useEffect(() => {
|
|
||||||
if (showLinkModal && linkTextInputRef.current) {
|
|
||||||
setTimeout(() => {
|
|
||||||
linkTextInputRef.current?.focus();
|
|
||||||
}, 100);
|
|
||||||
}
|
|
||||||
}, [showLinkModal]);
|
|
||||||
|
|
||||||
// Обработка Escape для закрытия модального окна
|
|
||||||
useEffect(() => {
|
|
||||||
const handleEscape = (e: KeyboardEvent) => {
|
|
||||||
if (e.key === "Escape") {
|
|
||||||
if (showCodeModal) {
|
|
||||||
setShowCodeModal(false);
|
|
||||||
setCodeLanguage("");
|
|
||||||
}
|
|
||||||
if (showLinkModal) {
|
|
||||||
setShowLinkModal(false);
|
|
||||||
setLinkText("");
|
|
||||||
setLinkUrl("");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
if (showCodeModal || showLinkModal) {
|
|
||||||
document.addEventListener("keydown", handleEscape);
|
|
||||||
}
|
|
||||||
|
|
||||||
return () => {
|
|
||||||
document.removeEventListener("keydown", handleEscape);
|
|
||||||
};
|
|
||||||
}, [showCodeModal, showLinkModal]);
|
|
||||||
|
|
||||||
const handleCodeButtonClick = () => {
|
|
||||||
setShowCodeModal(true);
|
|
||||||
setCodeLanguage("");
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleCodeModalConfirm = () => {
|
|
||||||
const language = codeLanguage.trim();
|
|
||||||
if (language) {
|
|
||||||
onInsert("```" + language + "\n", "\n```");
|
|
||||||
} else {
|
|
||||||
onInsert("```\n", "\n```");
|
|
||||||
}
|
|
||||||
setShowCodeModal(false);
|
|
||||||
setCodeLanguage("");
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleCodeModalClose = () => {
|
|
||||||
setShowCodeModal(false);
|
|
||||||
setCodeLanguage("");
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleCodeModalKeyDown = (e: React.KeyboardEvent<HTMLInputElement>) => {
|
|
||||||
if (e.key === "Enter") {
|
|
||||||
e.preventDefault();
|
|
||||||
handleCodeModalConfirm();
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleLinkButtonClick = () => {
|
|
||||||
setShowLinkModal(true);
|
|
||||||
setLinkText("");
|
|
||||||
setLinkUrl("");
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleLinkModalConfirm = () => {
|
|
||||||
const text = linkText.trim() || "текст ссылки";
|
|
||||||
const url = linkUrl.trim() || "#";
|
|
||||||
onInsert(`[${text}](`, `${url})`);
|
|
||||||
setShowLinkModal(false);
|
|
||||||
setLinkText("");
|
|
||||||
setLinkUrl("");
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleLinkModalClose = () => {
|
|
||||||
setShowLinkModal(false);
|
|
||||||
setLinkText("");
|
|
||||||
setLinkUrl("");
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleLinkTextKeyDown = (e: React.KeyboardEvent<HTMLInputElement>) => {
|
|
||||||
if (e.key === "Enter") {
|
|
||||||
e.preventDefault();
|
|
||||||
// Переход к полю URL
|
|
||||||
const urlInput = document.querySelector(
|
|
||||||
".link-modal-url-input"
|
|
||||||
) as HTMLInputElement;
|
|
||||||
if (urlInput) {
|
|
||||||
urlInput.focus();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleLinkUrlKeyDown = (e: React.KeyboardEvent<HTMLInputElement>) => {
|
|
||||||
if (e.key === "Enter") {
|
|
||||||
e.preventDefault();
|
|
||||||
handleLinkModalConfirm();
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const buttons: Array<{
|
const buttons: Array<{
|
||||||
id: string;
|
id: string;
|
||||||
icon: string;
|
icon: string;
|
||||||
@ -303,10 +184,9 @@ export const MarkdownToolbar: React.FC<MarkdownToolbarProps> = ({
|
|||||||
left: `${menuPosition.left}px`,
|
left: `${menuPosition.left}px`,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{[1, 2, 3, 4, 5, 6].map((level) => (
|
{[1, 2, 3, 4, 5].map((level) => (
|
||||||
<button
|
<button
|
||||||
key={level}
|
key={level}
|
||||||
className={`header-dropdown-item header-level-${level}`}
|
|
||||||
onClick={(e) => {
|
onClick={(e) => {
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
onInsert("#".repeat(level) + " ", "");
|
onInsert("#".repeat(level) + " ", "");
|
||||||
@ -314,7 +194,7 @@ export const MarkdownToolbar: React.FC<MarkdownToolbarProps> = ({
|
|||||||
setMenuPosition(null);
|
setMenuPosition(null);
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
Заголовок {level}
|
H{level}
|
||||||
</button>
|
</button>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
@ -387,7 +267,7 @@ export const MarkdownToolbar: React.FC<MarkdownToolbarProps> = ({
|
|||||||
|
|
||||||
<button
|
<button
|
||||||
className="btnMarkdown"
|
className="btnMarkdown"
|
||||||
onClick={handleCodeButtonClick}
|
onClick={() => onInsert("`", "`")}
|
||||||
title="Код"
|
title="Код"
|
||||||
>
|
>
|
||||||
<Icon icon="mdi:code-tags" />
|
<Icon icon="mdi:code-tags" />
|
||||||
@ -395,7 +275,7 @@ export const MarkdownToolbar: React.FC<MarkdownToolbarProps> = ({
|
|||||||
|
|
||||||
<button
|
<button
|
||||||
className="btnMarkdown"
|
className="btnMarkdown"
|
||||||
onClick={handleLinkButtonClick}
|
onClick={() => onInsert("[текст ссылки](", ")")}
|
||||||
title="Ссылка"
|
title="Ссылка"
|
||||||
>
|
>
|
||||||
<Icon icon="mdi:link" />
|
<Icon icon="mdi:link" />
|
||||||
@ -432,113 +312,6 @@ export const MarkdownToolbar: React.FC<MarkdownToolbarProps> = ({
|
|||||||
>
|
>
|
||||||
<Icon icon="mdi:monitor-eye" />
|
<Icon icon="mdi:monitor-eye" />
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
{/* Модальное окно для ввода языка программирования */}
|
|
||||||
{showCodeModal && (
|
|
||||||
<div
|
|
||||||
className="modal"
|
|
||||||
style={{ display: "block" }}
|
|
||||||
onClick={handleCodeModalClose}
|
|
||||||
>
|
|
||||||
<div
|
|
||||||
className="modal-content"
|
|
||||||
onClick={(e) => e.stopPropagation()}
|
|
||||||
>
|
|
||||||
<div className="modal-header">
|
|
||||||
<h3>Введите язык программирования</h3>
|
|
||||||
<span className="modal-close" onClick={handleCodeModalClose}>
|
|
||||||
×
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
<div className="modal-body">
|
|
||||||
<input
|
|
||||||
ref={codeLanguageInputRef}
|
|
||||||
type="text"
|
|
||||||
className="modal-password-input"
|
|
||||||
placeholder="Например: javascript, python, typescript..."
|
|
||||||
value={codeLanguage}
|
|
||||||
onChange={(e) => setCodeLanguage(e.target.value)}
|
|
||||||
onKeyDown={handleCodeModalKeyDown}
|
|
||||||
style={{ width: "100%" }}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div className="modal-footer">
|
|
||||||
<button
|
|
||||||
className="btn-primary"
|
|
||||||
onClick={handleCodeModalConfirm}
|
|
||||||
>
|
|
||||||
Вставить
|
|
||||||
</button>
|
|
||||||
<button className="btn-secondary" onClick={handleCodeModalClose}>
|
|
||||||
Отмена
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{/* Модальное окно для ввода ссылки */}
|
|
||||||
{showLinkModal && (
|
|
||||||
<div
|
|
||||||
className="modal"
|
|
||||||
style={{ display: "block" }}
|
|
||||||
onClick={handleLinkModalClose}
|
|
||||||
>
|
|
||||||
<div
|
|
||||||
className="modal-content"
|
|
||||||
onClick={(e) => e.stopPropagation()}
|
|
||||||
>
|
|
||||||
<div className="modal-header">
|
|
||||||
<h3>Введите ссылку</h3>
|
|
||||||
<span className="modal-close" onClick={handleLinkModalClose}>
|
|
||||||
×
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
<div className="modal-body">
|
|
||||||
<div style={{ marginBottom: "15px" }}>
|
|
||||||
<label style={{ display: "block", marginBottom: "5px", fontWeight: "500" }}>
|
|
||||||
Текст ссылки:
|
|
||||||
</label>
|
|
||||||
<input
|
|
||||||
ref={linkTextInputRef}
|
|
||||||
type="text"
|
|
||||||
className="modal-password-input"
|
|
||||||
placeholder="Текст ссылки"
|
|
||||||
value={linkText}
|
|
||||||
onChange={(e) => setLinkText(e.target.value)}
|
|
||||||
onKeyDown={handleLinkTextKeyDown}
|
|
||||||
style={{ width: "100%" }}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<label style={{ display: "block", marginBottom: "5px", fontWeight: "500" }}>
|
|
||||||
URL:
|
|
||||||
</label>
|
|
||||||
<input
|
|
||||||
type="text"
|
|
||||||
className="modal-password-input link-modal-url-input"
|
|
||||||
placeholder="https://example.com"
|
|
||||||
value={linkUrl}
|
|
||||||
onChange={(e) => setLinkUrl(e.target.value)}
|
|
||||||
onKeyDown={handleLinkUrlKeyDown}
|
|
||||||
style={{ width: "100%" }}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div className="modal-footer">
|
|
||||||
<button
|
|
||||||
className="btn-primary"
|
|
||||||
onClick={handleLinkModalConfirm}
|
|
||||||
>
|
|
||||||
Вставить
|
|
||||||
</button>
|
|
||||||
<button className="btn-secondary" onClick={handleLinkModalClose}>
|
|
||||||
Отмена
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@ -14,7 +14,7 @@ import { ImproveTextModal } from "./ImproveTextModal";
|
|||||||
import { extractTags } from "../../utils/markdown";
|
import { extractTags } from "../../utils/markdown";
|
||||||
|
|
||||||
interface NoteEditorProps {
|
interface NoteEditorProps {
|
||||||
onSave: (noteId?: number | string) => void;
|
onSave: () => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const NoteEditor: React.FC<NoteEditorProps> = ({ onSave }) => {
|
export const NoteEditor: React.FC<NoteEditorProps> = ({ onSave }) => {
|
||||||
@ -80,7 +80,7 @@ export const NoteEditor: React.FC<NoteEditorProps> = ({ onSave }) => {
|
|||||||
setContent("");
|
setContent("");
|
||||||
setImages([]);
|
setImages([]);
|
||||||
setFiles([]);
|
setFiles([]);
|
||||||
onSave(note.id);
|
onSave();
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Ошибка сохранения заметки:", error);
|
console.error("Ошибка сохранения заметки:", error);
|
||||||
showNotification("Ошибка сохранения заметки", "error");
|
showNotification("Ошибка сохранения заметки", "error");
|
||||||
@ -106,9 +106,7 @@ export const NoteEditor: React.FC<NoteEditorProps> = ({ onSave }) => {
|
|||||||
console.error("Ошибка улучшения текста:", error);
|
console.error("Ошибка улучшения текста:", error);
|
||||||
setImproveError(true);
|
setImproveError(true);
|
||||||
setImproveErrorMessage(
|
setImproveErrorMessage(
|
||||||
error.response?.data?.error ||
|
error.response?.data?.error || error.message || "Ошибка улучшения текста"
|
||||||
error.message ||
|
|
||||||
"Ошибка улучшения текста"
|
|
||||||
);
|
);
|
||||||
} finally {
|
} finally {
|
||||||
setIsAiLoading(false);
|
setIsAiLoading(false);
|
||||||
@ -149,10 +147,7 @@ export const NoteEditor: React.FC<NoteEditorProps> = ({ onSave }) => {
|
|||||||
console.error("Детали ошибки:", error.response?.data);
|
console.error("Детали ошибки:", error.response?.data);
|
||||||
setTagsGenerationError(true);
|
setTagsGenerationError(true);
|
||||||
setShowTagsModal(false);
|
setShowTagsModal(false);
|
||||||
const errorMessage =
|
const errorMessage = error.response?.data?.error || error.message || "Ошибка генерации тегов";
|
||||||
error.response?.data?.error ||
|
|
||||||
error.message ||
|
|
||||||
"Ошибка генерации тегов";
|
|
||||||
showNotification(errorMessage, "error");
|
showNotification(errorMessage, "error");
|
||||||
} finally {
|
} finally {
|
||||||
setIsGeneratingTags(false);
|
setIsGeneratingTags(false);
|
||||||
@ -164,19 +159,13 @@ export const NoteEditor: React.FC<NoteEditorProps> = ({ onSave }) => {
|
|||||||
|
|
||||||
const existingTags = extractTags(content);
|
const existingTags = extractTags(content);
|
||||||
const tagsToAdd = tags
|
const tagsToAdd = tags
|
||||||
.filter(
|
.filter((tag) => !existingTags.some((existing) => existing.toLowerCase() === tag.toLowerCase()))
|
||||||
(tag) =>
|
|
||||||
!existingTags.some(
|
|
||||||
(existing) => existing.toLowerCase() === tag.toLowerCase()
|
|
||||||
)
|
|
||||||
)
|
|
||||||
.map((tag) => `#${tag}`)
|
.map((tag) => `#${tag}`)
|
||||||
.join(" ");
|
.join(" ");
|
||||||
|
|
||||||
if (tagsToAdd) {
|
if (tagsToAdd) {
|
||||||
// Добавляем теги в конец заметки
|
// Добавляем теги в конец заметки
|
||||||
const newContent =
|
const newContent = content.trim() + (content.trim() ? "\n\n" : "") + tagsToAdd;
|
||||||
content.trim() + (content.trim() ? "\n\n" : "") + tagsToAdd;
|
|
||||||
setContent(newContent);
|
setContent(newContent);
|
||||||
showNotification(`Добавлено тегов: ${tags.length}`, "success");
|
showNotification(`Добавлено тегов: ${tags.length}`, "success");
|
||||||
} else {
|
} else {
|
||||||
@ -1119,7 +1108,6 @@ export const NoteEditor: React.FC<NoteEditorProps> = ({ onSave }) => {
|
|||||||
setShowTagsModal(false);
|
setShowTagsModal(false);
|
||||||
setSuggestedTags([]);
|
setSuggestedTags([]);
|
||||||
setTagsGenerationError(false);
|
setTagsGenerationError(false);
|
||||||
setIsGeneratingTags(false);
|
|
||||||
}}
|
}}
|
||||||
onSelectTags={handleSelectTags}
|
onSelectTags={handleSelectTags}
|
||||||
suggestedTags={suggestedTags}
|
suggestedTags={suggestedTags}
|
||||||
@ -1135,7 +1123,6 @@ export const NoteEditor: React.FC<NoteEditorProps> = ({ onSave }) => {
|
|||||||
setImprovedText("");
|
setImprovedText("");
|
||||||
setImproveError(false);
|
setImproveError(false);
|
||||||
setImproveErrorMessage("");
|
setImproveErrorMessage("");
|
||||||
setIsAiLoading(false);
|
|
||||||
}}
|
}}
|
||||||
onApply={handleApplyImprovedText}
|
onApply={handleApplyImprovedText}
|
||||||
originalText={content}
|
originalText={content}
|
||||||
|
|||||||
@ -21,7 +21,6 @@ import { ImageUpload } from "./ImageUpload";
|
|||||||
import { FileUpload } from "./FileUpload";
|
import { FileUpload } from "./FileUpload";
|
||||||
import { aiApi } from "../../api/aiApi";
|
import { aiApi } from "../../api/aiApi";
|
||||||
import { GenerateTagsModal } from "./GenerateTagsModal";
|
import { GenerateTagsModal } from "./GenerateTagsModal";
|
||||||
import { ImproveTextModal } from "./ImproveTextModal";
|
|
||||||
import { extractTags } from "../../utils/markdown";
|
import { extractTags } from "../../utils/markdown";
|
||||||
|
|
||||||
interface NoteItemProps {
|
interface NoteItemProps {
|
||||||
@ -32,7 +31,6 @@ interface NoteItemProps {
|
|||||||
onReload: () => void;
|
onReload: () => void;
|
||||||
isSelected?: boolean;
|
isSelected?: boolean;
|
||||||
onSelect?: (id: number | string) => void;
|
onSelect?: (id: number | string) => void;
|
||||||
onFocusNote?: (id: number | string) => void;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export const NoteItem: React.FC<NoteItemProps> = ({
|
export const NoteItem: React.FC<NoteItemProps> = ({
|
||||||
@ -43,7 +41,6 @@ export const NoteItem: React.FC<NoteItemProps> = ({
|
|||||||
onReload,
|
onReload,
|
||||||
isSelected = false,
|
isSelected = false,
|
||||||
onSelect,
|
onSelect,
|
||||||
onFocusNote,
|
|
||||||
}) => {
|
}) => {
|
||||||
const [isEditing, setIsEditing] = useState(false);
|
const [isEditing, setIsEditing] = useState(false);
|
||||||
const [editContent, setEditContent] = useState(note.content);
|
const [editContent, setEditContent] = useState(note.content);
|
||||||
@ -68,10 +65,6 @@ export const NoteItem: React.FC<NoteItemProps> = ({
|
|||||||
const [suggestedTags, setSuggestedTags] = useState<string[]>([]);
|
const [suggestedTags, setSuggestedTags] = useState<string[]>([]);
|
||||||
const [isGeneratingTags, setIsGeneratingTags] = useState(false);
|
const [isGeneratingTags, setIsGeneratingTags] = useState(false);
|
||||||
const [tagsGenerationError, setTagsGenerationError] = useState(false);
|
const [tagsGenerationError, setTagsGenerationError] = useState(false);
|
||||||
const [showImproveModal, setShowImproveModal] = useState(false);
|
|
||||||
const [improvedText, setImprovedText] = useState("");
|
|
||||||
const [improveError, setImproveError] = useState(false);
|
|
||||||
const [improveErrorMessage, setImproveErrorMessage] = useState<string>("");
|
|
||||||
const editTextareaRef = useRef<HTMLTextAreaElement>(null);
|
const editTextareaRef = useRef<HTMLTextAreaElement>(null);
|
||||||
const imageInputRef = useRef<HTMLInputElement>(null);
|
const imageInputRef = useRef<HTMLInputElement>(null);
|
||||||
const fileInputRef = useRef<HTMLInputElement>(null);
|
const fileInputRef = useRef<HTMLInputElement>(null);
|
||||||
@ -145,13 +138,6 @@ export const NoteItem: React.FC<NoteItemProps> = ({
|
|||||||
setDeletedImageIds([]);
|
setDeletedImageIds([]);
|
||||||
setDeletedFileIds([]);
|
setDeletedFileIds([]);
|
||||||
onReload();
|
onReload();
|
||||||
// Устанавливаем фокус на заметку после сохранения
|
|
||||||
if (onFocusNote) {
|
|
||||||
// Небольшая задержка для завершения обновления DOM
|
|
||||||
setTimeout(() => {
|
|
||||||
onFocusNote(note.id);
|
|
||||||
}, 100);
|
|
||||||
}
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Ошибка обновления заметки:", error);
|
console.error("Ошибка обновления заметки:", error);
|
||||||
showNotification("Ошибка обновления заметки", "error");
|
showNotification("Ошибка обновления заметки", "error");
|
||||||
@ -193,36 +179,18 @@ export const NoteItem: React.FC<NoteItemProps> = ({
|
|||||||
}
|
}
|
||||||
|
|
||||||
setIsAiLoading(true);
|
setIsAiLoading(true);
|
||||||
setImproveError(false);
|
|
||||||
setImproveErrorMessage("");
|
|
||||||
setImprovedText("");
|
|
||||||
setShowImproveModal(true);
|
|
||||||
try {
|
try {
|
||||||
const improved = await aiApi.improveText(editContent);
|
const improvedText = await aiApi.improveText(editContent);
|
||||||
setImprovedText(improved);
|
setEditContent(improvedText);
|
||||||
setImproveError(false);
|
showNotification("Текст улучшен!", "success");
|
||||||
} catch (error: any) {
|
} catch (error) {
|
||||||
console.error("Ошибка улучшения текста:", error);
|
console.error("Ошибка улучшения текста:", error);
|
||||||
setImproveError(true);
|
showNotification("Ошибка улучшения текста", "error");
|
||||||
setImproveErrorMessage(
|
|
||||||
error.response?.data?.error ||
|
|
||||||
error.message ||
|
|
||||||
"Ошибка улучшения текста"
|
|
||||||
);
|
|
||||||
} finally {
|
} finally {
|
||||||
setIsAiLoading(false);
|
setIsAiLoading(false);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleApplyImprovedText = (text: string) => {
|
|
||||||
setEditContent(text);
|
|
||||||
showNotification("Текст улучшен!", "success");
|
|
||||||
setShowImproveModal(false);
|
|
||||||
setImprovedText("");
|
|
||||||
setImproveError(false);
|
|
||||||
setImproveErrorMessage("");
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleGenerateTags = async () => {
|
const handleGenerateTags = async () => {
|
||||||
if (!editContent.trim()) {
|
if (!editContent.trim()) {
|
||||||
showNotification("Введите текст для генерации тегов", "warning");
|
showNotification("Введите текст для генерации тегов", "warning");
|
||||||
@ -1652,7 +1620,6 @@ export const NoteItem: React.FC<NoteItemProps> = ({
|
|||||||
setShowTagsModal(false);
|
setShowTagsModal(false);
|
||||||
setSuggestedTags([]);
|
setSuggestedTags([]);
|
||||||
setTagsGenerationError(false);
|
setTagsGenerationError(false);
|
||||||
setIsGeneratingTags(false);
|
|
||||||
}}
|
}}
|
||||||
onSelectTags={handleSelectTags}
|
onSelectTags={handleSelectTags}
|
||||||
suggestedTags={suggestedTags}
|
suggestedTags={suggestedTags}
|
||||||
@ -1660,23 +1627,6 @@ export const NoteItem: React.FC<NoteItemProps> = ({
|
|||||||
isLoading={isGeneratingTags}
|
isLoading={isGeneratingTags}
|
||||||
hasError={tagsGenerationError}
|
hasError={tagsGenerationError}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<ImproveTextModal
|
|
||||||
isOpen={showImproveModal}
|
|
||||||
onClose={() => {
|
|
||||||
setShowImproveModal(false);
|
|
||||||
setImprovedText("");
|
|
||||||
setImproveError(false);
|
|
||||||
setImproveErrorMessage("");
|
|
||||||
setIsAiLoading(false);
|
|
||||||
}}
|
|
||||||
onApply={handleApplyImprovedText}
|
|
||||||
originalText={editContent}
|
|
||||||
improvedText={improvedText}
|
|
||||||
isLoading={isAiLoading}
|
|
||||||
hasError={improveError}
|
|
||||||
errorMessage={improveErrorMessage}
|
|
||||||
/>
|
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@ -8,7 +8,6 @@ import { extractTags } from "../../utils/markdown";
|
|||||||
|
|
||||||
export interface NotesListRef {
|
export interface NotesListRef {
|
||||||
reloadNotes: () => void;
|
reloadNotes: () => void;
|
||||||
focusNote: (noteId: number | string) => void;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
interface NotesListProps {
|
interface NotesListProps {
|
||||||
@ -82,34 +81,8 @@ export const NotesList = forwardRef<NotesListRef, NotesListProps>(
|
|||||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
}, [userId, searchQuery, selectedDate, selectedTag]);
|
}, [userId, searchQuery, selectedDate, selectedTag]);
|
||||||
|
|
||||||
const focusNote = (noteId: number | string) => {
|
|
||||||
// Используем data-атрибут для поиска элемента заметки
|
|
||||||
const noteElement = document.querySelector(
|
|
||||||
`[data-note-id="${noteId}"]`
|
|
||||||
) as HTMLDivElement | null;
|
|
||||||
|
|
||||||
if (noteElement) {
|
|
||||||
// Прокручиваем к верхней части заметки
|
|
||||||
noteElement.scrollIntoView({ behavior: "smooth", block: "start", inline: "nearest" });
|
|
||||||
// Добавляем визуальное выделение на короткое время
|
|
||||||
// Очищаем предыдущий таймер, если он был установлен
|
|
||||||
const existingTimeout = (noteElement as any).__focusTimeout;
|
|
||||||
if (existingTimeout) {
|
|
||||||
clearTimeout(existingTimeout);
|
|
||||||
}
|
|
||||||
noteElement.style.transition = "box-shadow 0.3s ease";
|
|
||||||
noteElement.style.boxShadow = "0 0 0 3px rgba(33, 150, 243, 0.5)";
|
|
||||||
const timeout = setTimeout(() => {
|
|
||||||
noteElement.style.boxShadow = "";
|
|
||||||
(noteElement as any).__focusTimeout = null;
|
|
||||||
}, 2000);
|
|
||||||
(noteElement as any).__focusTimeout = timeout;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
useImperativeHandle(ref, () => ({
|
useImperativeHandle(ref, () => ({
|
||||||
reloadNotes: loadNotes,
|
reloadNotes: loadNotes,
|
||||||
focusNote: focusNote,
|
|
||||||
}));
|
}));
|
||||||
|
|
||||||
const handleDelete = async (id: number | string) => {
|
const handleDelete = async (id: number | string) => {
|
||||||
@ -194,7 +167,6 @@ export const NotesList = forwardRef<NotesListRef, NotesListProps>(
|
|||||||
onReload={loadNotes}
|
onReload={loadNotes}
|
||||||
isSelected={selectedNoteIds.includes(note.id)}
|
isSelected={selectedNoteIds.includes(note.id)}
|
||||||
onSelect={onNoteSelect}
|
onSelect={onNoteSelect}
|
||||||
onFocusNote={focusNote}
|
|
||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@ -63,32 +63,10 @@ const NotesPage: React.FC = () => {
|
|||||||
|
|
||||||
const activeFilters = getActiveFilters();
|
const activeFilters = getActiveFilters();
|
||||||
|
|
||||||
const handleNoteSave = (noteId?: number | string) => {
|
const handleNoteSave = () => {
|
||||||
// Вызываем перезагрузку заметок после создания новой заметки
|
// Вызываем перезагрузку заметок после создания новой заметки
|
||||||
if (notesListRef.current) {
|
if (notesListRef.current) {
|
||||||
notesListRef.current.reloadNotes();
|
notesListRef.current.reloadNotes();
|
||||||
// Устанавливаем фокус на сохраненную заметку после небольшой задержки,
|
|
||||||
// чтобы дать время для перезагрузки и рендеринга заметок
|
|
||||||
if (noteId) {
|
|
||||||
let attempts = 0;
|
|
||||||
const maxAttempts = 15;
|
|
||||||
const attemptFocus = () => {
|
|
||||||
attempts++;
|
|
||||||
if (notesListRef.current) {
|
|
||||||
// Проверяем, есть ли элемент заметки в DOM
|
|
||||||
const noteElement = document.querySelector(
|
|
||||||
`[data-note-id="${noteId}"]`
|
|
||||||
);
|
|
||||||
if (noteElement || attempts >= maxAttempts) {
|
|
||||||
notesListRef.current.focusNote(noteId);
|
|
||||||
} else if (attempts < maxAttempts) {
|
|
||||||
// Пытаемся еще раз, если заметка еще не появилась в DOM
|
|
||||||
setTimeout(attemptFocus, 100);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
setTimeout(attemptFocus, 200);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@ -1424,94 +1424,6 @@ textarea:focus {
|
|||||||
word-break: break-word;
|
word-break: break-word;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Стили для блоков кода с подсветкой синтаксиса в заметках */
|
|
||||||
.textNote .code-block {
|
|
||||||
position: relative;
|
|
||||||
margin: 15px 0;
|
|
||||||
background: #2d2d2d;
|
|
||||||
border-radius: 6px;
|
|
||||||
overflow: hidden;
|
|
||||||
border: 1px solid var(--border-primary);
|
|
||||||
}
|
|
||||||
|
|
||||||
.textNote .code-block code {
|
|
||||||
display: block;
|
|
||||||
padding: 15px;
|
|
||||||
overflow-x: auto;
|
|
||||||
font-family: "Courier New", "Consolas", "Monaco", monospace;
|
|
||||||
font-size: 14px;
|
|
||||||
line-height: 1.5;
|
|
||||||
color: #f8f8f2;
|
|
||||||
background: transparent;
|
|
||||||
border: none;
|
|
||||||
border-radius: 0;
|
|
||||||
word-wrap: normal;
|
|
||||||
overflow-wrap: normal;
|
|
||||||
word-break: normal;
|
|
||||||
white-space: pre;
|
|
||||||
}
|
|
||||||
|
|
||||||
.textNote .code-block .code-language {
|
|
||||||
position: absolute;
|
|
||||||
top: 8px;
|
|
||||||
right: 12px;
|
|
||||||
font-size: 11px;
|
|
||||||
font-weight: 600;
|
|
||||||
color: #888;
|
|
||||||
text-transform: uppercase;
|
|
||||||
letter-spacing: 0.5px;
|
|
||||||
background: rgba(0, 0, 0, 0.3);
|
|
||||||
padding: 4px 8px;
|
|
||||||
border-radius: 4px;
|
|
||||||
font-family: "Open Sans", sans-serif;
|
|
||||||
z-index: 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Светлая тема для блоков кода в заметках */
|
|
||||||
[data-theme="light"] .textNote .code-block,
|
|
||||||
.textNote .code-block {
|
|
||||||
background: #f5f5f5;
|
|
||||||
border-color: var(--border-primary);
|
|
||||||
}
|
|
||||||
|
|
||||||
[data-theme="light"] .textNote .code-block code,
|
|
||||||
.textNote .code-block code {
|
|
||||||
color: #333;
|
|
||||||
}
|
|
||||||
|
|
||||||
[data-theme="light"] .textNote .code-block .code-language,
|
|
||||||
.textNote .code-block .code-language {
|
|
||||||
color: #666;
|
|
||||||
background: rgba(255, 255, 255, 0.7);
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Темная тема для блоков кода в заметках */
|
|
||||||
[data-theme="dark"] .textNote .code-block {
|
|
||||||
background: #1e1e1e;
|
|
||||||
border-color: var(--border-primary);
|
|
||||||
}
|
|
||||||
|
|
||||||
[data-theme="dark"] .textNote .code-block code {
|
|
||||||
color: #d4d4d4;
|
|
||||||
}
|
|
||||||
|
|
||||||
[data-theme="dark"] .textNote .code-block .code-language {
|
|
||||||
color: #999;
|
|
||||||
background: rgba(0, 0, 0, 0.5);
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Стили для inline кода в заметках (не блоки) */
|
|
||||||
.textNote code:not(.code-block code) {
|
|
||||||
background-color: var(--bg-tertiary);
|
|
||||||
color: var(--text-primary);
|
|
||||||
padding: 2px 4px;
|
|
||||||
border-radius: 5px;
|
|
||||||
font-size: 14px;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Стили highlight.js уже включены через импорт CSS файлов */
|
|
||||||
/* Дополнительные стили для адаптации к теме приложения */
|
|
||||||
|
|
||||||
/* Стили для чекбоксов (to-do списков) */
|
/* Стили для чекбоксов (to-do списков) */
|
||||||
.textNote input[type="checkbox"] {
|
.textNote input[type="checkbox"] {
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
@ -1743,8 +1655,8 @@ textarea:focus {
|
|||||||
border-radius: 5px;
|
border-radius: 5px;
|
||||||
box-shadow: 0 2px 8px var(--shadow-medium);
|
box-shadow: 0 2px 8px var(--shadow-medium);
|
||||||
z-index: 1001;
|
z-index: 1001;
|
||||||
min-width: 140px;
|
min-width: 60px;
|
||||||
max-width: 250px;
|
max-width: 120px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.header-dropdown-menu button {
|
.header-dropdown-menu button {
|
||||||
@ -1755,9 +1667,10 @@ textarea:focus {
|
|||||||
background: none;
|
background: none;
|
||||||
text-align: left;
|
text-align: left;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
|
font-size: 14px;
|
||||||
|
font-weight: 500;
|
||||||
color: var(--text-primary);
|
color: var(--text-primary);
|
||||||
transition: background 0.2s ease;
|
transition: background 0.2s ease;
|
||||||
white-space: nowrap;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.header-dropdown-menu button:hover {
|
.header-dropdown-menu button:hover {
|
||||||
@ -1772,50 +1685,6 @@ textarea:focus {
|
|||||||
border-radius: 0 0 5px 5px;
|
border-radius: 0 0 5px 5px;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Стили для визуальных примеров заголовков */
|
|
||||||
.header-dropdown-menu .header-level-1 {
|
|
||||||
font-size: 32px;
|
|
||||||
font-weight: 700;
|
|
||||||
line-height: 1.2;
|
|
||||||
padding: 12px 16px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.header-dropdown-menu .header-level-2 {
|
|
||||||
font-size: 28px;
|
|
||||||
font-weight: 700;
|
|
||||||
line-height: 1.2;
|
|
||||||
padding: 11px 16px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.header-dropdown-menu .header-level-3 {
|
|
||||||
font-size: 24px;
|
|
||||||
font-weight: 600;
|
|
||||||
line-height: 1.3;
|
|
||||||
padding: 10px 16px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.header-dropdown-menu .header-level-4 {
|
|
||||||
font-size: 20px;
|
|
||||||
font-weight: 600;
|
|
||||||
line-height: 1.3;
|
|
||||||
padding: 9px 16px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.header-dropdown-menu .header-level-5 {
|
|
||||||
font-size: 18px;
|
|
||||||
font-weight: 600;
|
|
||||||
line-height: 1.4;
|
|
||||||
padding: 8px 16px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.header-dropdown-menu .header-level-6 {
|
|
||||||
font-size: 16px;
|
|
||||||
font-weight: 600;
|
|
||||||
line-height: 1.4;
|
|
||||||
padding: 8px 16px;
|
|
||||||
color: var(--text-secondary);
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Плавающая панель форматирования */
|
/* Плавающая панель форматирования */
|
||||||
.floating-toolbar-wrapper {
|
.floating-toolbar-wrapper {
|
||||||
overflow-x: auto;
|
overflow-x: auto;
|
||||||
@ -3371,90 +3240,6 @@ textarea:focus {
|
|||||||
border-radius: 0;
|
border-radius: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Стили для блоков кода с подсветкой синтаксиса */
|
|
||||||
.note-preview-content .code-block {
|
|
||||||
position: relative;
|
|
||||||
margin: 15px 0;
|
|
||||||
background: #2d2d2d;
|
|
||||||
border-radius: 6px;
|
|
||||||
overflow: hidden;
|
|
||||||
border: 1px solid var(--border-primary);
|
|
||||||
}
|
|
||||||
|
|
||||||
.note-preview-content .code-block code {
|
|
||||||
display: block;
|
|
||||||
padding: 15px;
|
|
||||||
overflow-x: auto;
|
|
||||||
font-family: "Courier New", "Consolas", "Monaco", monospace;
|
|
||||||
font-size: 14px;
|
|
||||||
line-height: 1.5;
|
|
||||||
color: #f8f8f2;
|
|
||||||
background: transparent;
|
|
||||||
border: none;
|
|
||||||
border-radius: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.note-preview-content .code-block .code-language {
|
|
||||||
position: absolute;
|
|
||||||
top: 8px;
|
|
||||||
right: 12px;
|
|
||||||
font-size: 11px;
|
|
||||||
font-weight: 600;
|
|
||||||
color: #888;
|
|
||||||
text-transform: uppercase;
|
|
||||||
letter-spacing: 0.5px;
|
|
||||||
background: rgba(0, 0, 0, 0.3);
|
|
||||||
padding: 4px 8px;
|
|
||||||
border-radius: 4px;
|
|
||||||
font-family: "Open Sans", sans-serif;
|
|
||||||
z-index: 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Светлая тема для блоков кода */
|
|
||||||
[data-theme="light"] .note-preview-content .code-block,
|
|
||||||
.note-preview-content .code-block {
|
|
||||||
background: #f5f5f5;
|
|
||||||
border-color: var(--border-primary);
|
|
||||||
}
|
|
||||||
|
|
||||||
[data-theme="light"] .note-preview-content .code-block code,
|
|
||||||
.note-preview-content .code-block code {
|
|
||||||
color: #333;
|
|
||||||
}
|
|
||||||
|
|
||||||
[data-theme="light"] .note-preview-content .code-block .code-language,
|
|
||||||
.note-preview-content .code-block .code-language {
|
|
||||||
color: #666;
|
|
||||||
background: rgba(255, 255, 255, 0.7);
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Темная тема для блоков кода */
|
|
||||||
[data-theme="dark"] .note-preview-content .code-block {
|
|
||||||
background: #1e1e1e;
|
|
||||||
border-color: var(--border-primary);
|
|
||||||
}
|
|
||||||
|
|
||||||
[data-theme="dark"] .note-preview-content .code-block code {
|
|
||||||
color: #d4d4d4;
|
|
||||||
}
|
|
||||||
|
|
||||||
[data-theme="dark"] .note-preview-content .code-block .code-language {
|
|
||||||
color: #999;
|
|
||||||
background: rgba(0, 0, 0, 0.5);
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Стили для inline кода (не блоки) */
|
|
||||||
.note-preview-content code:not(.code-block code) {
|
|
||||||
background: var(--bg-quaternary);
|
|
||||||
color: var(--text-primary);
|
|
||||||
padding: 2px 6px;
|
|
||||||
border-radius: 4px;
|
|
||||||
font-family: "Courier New", monospace;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Стили highlight.js уже включены через импорт CSS файлов */
|
|
||||||
/* Дополнительные стили для адаптации к теме приложения */
|
|
||||||
|
|
||||||
.note-preview-content blockquote {
|
.note-preview-content blockquote {
|
||||||
border-left: 4px solid var(--accent-color, #007bff);
|
border-left: 4px solid var(--accent-color, #007bff);
|
||||||
padding-left: 15px;
|
padding-left: 15px;
|
||||||
|
|||||||
@ -1,8 +1,4 @@
|
|||||||
import { marked } from "marked";
|
import { marked } from "marked";
|
||||||
import hljs from "highlight.js";
|
|
||||||
// Импортируем стили
|
|
||||||
import "highlight.js/styles/github-dark.css";
|
|
||||||
import "highlight.js/styles/github.css";
|
|
||||||
|
|
||||||
// Расширение для спойлеров
|
// Расширение для спойлеров
|
||||||
const spoilerExtension = {
|
const spoilerExtension = {
|
||||||
@ -79,72 +75,7 @@ function renderTokens(tokens: any[], renderer: any): string {
|
|||||||
.join("");
|
.join("");
|
||||||
}
|
}
|
||||||
|
|
||||||
// Функция для экранирования HTML
|
// Кастомный renderer для внешних ссылок и чекбоксов
|
||||||
const escapeHtml = (text: string): string => {
|
|
||||||
const div = document.createElement("div");
|
|
||||||
div.textContent = text;
|
|
||||||
return div.innerHTML;
|
|
||||||
};
|
|
||||||
|
|
||||||
// Маппинг алиасов языков
|
|
||||||
const languageAliases: Record<string, string> = {
|
|
||||||
js: "javascript",
|
|
||||||
ts: "typescript",
|
|
||||||
py: "python",
|
|
||||||
sh: "bash",
|
|
||||||
shell: "bash",
|
|
||||||
zsh: "bash",
|
|
||||||
yml: "yaml",
|
|
||||||
md: "markdown",
|
|
||||||
html: "markup",
|
|
||||||
xml: "markup",
|
|
||||||
svg: "markup",
|
|
||||||
mathml: "markup",
|
|
||||||
ssml: "markup",
|
|
||||||
atom: "markup",
|
|
||||||
rss: "markup",
|
|
||||||
};
|
|
||||||
|
|
||||||
// Функция для нормализации названия языка
|
|
||||||
const normalizeLanguage = (lang: string): string => {
|
|
||||||
const normalized = lang.toLowerCase().trim();
|
|
||||||
// Проверяем алиасы
|
|
||||||
return languageAliases[normalized] || normalized;
|
|
||||||
};
|
|
||||||
|
|
||||||
// Функция для подсветки синтаксиса кода
|
|
||||||
const highlightCode = (code: string, lang?: string): string => {
|
|
||||||
if (!lang) {
|
|
||||||
// Если язык не указан, просто экранируем HTML
|
|
||||||
return escapeHtml(code);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Нормализуем название языка
|
|
||||||
const normalizedLang = normalizeLanguage(lang);
|
|
||||||
|
|
||||||
try {
|
|
||||||
// Используем highlight.js для подсветки синтаксиса
|
|
||||||
const highlighted = hljs.highlight(code, { language: normalizedLang });
|
|
||||||
return highlighted.value;
|
|
||||||
} catch (e: any) {
|
|
||||||
// Если произошла ошибка, пробуем автоопределение
|
|
||||||
try {
|
|
||||||
const autoHighlighted = hljs.highlightAuto(code);
|
|
||||||
return autoHighlighted.value;
|
|
||||||
} catch (autoError: any) {
|
|
||||||
// Если автоопределение тоже не сработало, логируем и экранируем
|
|
||||||
console.warn(
|
|
||||||
"Highlight.js error:",
|
|
||||||
e?.message || e,
|
|
||||||
"Language:",
|
|
||||||
normalizedLang
|
|
||||||
);
|
|
||||||
return escapeHtml(code);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// Кастомный renderer для внешних ссылок, чекбоксов и блоков кода
|
|
||||||
const renderer: any = {
|
const renderer: any = {
|
||||||
link(token: any) {
|
link(token: any) {
|
||||||
const href = token.href;
|
const href = token.href;
|
||||||
@ -192,20 +123,6 @@ const renderer: any = {
|
|||||||
}
|
}
|
||||||
return `<li>${content}</li>\n`;
|
return `<li>${content}</li>\n`;
|
||||||
},
|
},
|
||||||
// Кастомный renderer для блоков кода с подсветкой синтаксиса
|
|
||||||
code(token: any) {
|
|
||||||
const code = token.text || "";
|
|
||||||
// В marked токен блока кода имеет поле lang
|
|
||||||
const lang = (token.lang || token.language || "").trim();
|
|
||||||
|
|
||||||
const highlightedCode = highlightCode(code, lang);
|
|
||||||
|
|
||||||
// Если язык указан, добавляем класс для стилизации (highlight.js использует класс hljs)
|
|
||||||
const langClass = lang ? `language-${lang}` : "";
|
|
||||||
const langLabel = lang ? `<span class="code-language">${lang}</span>` : "";
|
|
||||||
|
|
||||||
return `<pre class="code-block">${langLabel}<code class="hljs ${langClass}">${highlightedCode}</code></pre>`;
|
|
||||||
},
|
|
||||||
};
|
};
|
||||||
|
|
||||||
// Настройка marked
|
// Настройка marked
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user