Compare commits
2 Commits
06400d6e97
...
14cef88a6b
| Author | SHA1 | Date | |
|---|---|---|---|
| 14cef88a6b | |||
| 05a9275253 |
@ -82,7 +82,7 @@ define(['./workbox-47da91e0'], (function (workbox) { 'use strict';
|
|||||||
"revision": "3ca0b8505b4bec776b69afdba2768812"
|
"revision": "3ca0b8505b4bec776b69afdba2768812"
|
||||||
}, {
|
}, {
|
||||||
"url": "/index.html",
|
"url": "/index.html",
|
||||||
"revision": "0.9eood2uf828"
|
"revision": "0.q2r0i2lj5ro"
|
||||||
}], {
|
}], {
|
||||||
"ignoreURLParametersMatching": [/^utm_/, /^fbclid$/]
|
"ignoreURLParametersMatching": [/^utm_/, /^fbclid$/]
|
||||||
});
|
});
|
||||||
|
|||||||
10
package-lock.json
generated
10
package-lock.json
generated
@ -14,6 +14,7 @@
|
|||||||
"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",
|
||||||
@ -4616,6 +4617,15 @@
|
|||||||
"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,6 +19,7 @@
|
|||||||
"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} disabled={isLoading}>
|
<button className="btn-secondary" onClick={onClose}>
|
||||||
Отмена
|
Отмена
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@ -151,7 +151,7 @@ export const ImproveTextModal: React.FC<ImproveTextModalProps> = ({
|
|||||||
>
|
>
|
||||||
Применить
|
Применить
|
||||||
</button>
|
</button>
|
||||||
<button className="btn-secondary" onClick={onClose} disabled={isLoading}>
|
<button className="btn-secondary" onClick={onClose}>
|
||||||
Отмена
|
Отмена
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@ -21,6 +21,13 @@ 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);
|
||||||
@ -51,8 +58,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 + window.scrollY + 2,
|
top: rect.bottom + 2,
|
||||||
left: rect.left + window.scrollX,
|
left: rect.left,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@ -115,6 +122,118 @@ 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;
|
||||||
@ -184,9 +303,10 @@ export const MarkdownToolbar: React.FC<MarkdownToolbarProps> = ({
|
|||||||
left: `${menuPosition.left}px`,
|
left: `${menuPosition.left}px`,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{[1, 2, 3, 4, 5].map((level) => (
|
{[1, 2, 3, 4, 5, 6].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) + " ", "");
|
||||||
@ -194,7 +314,7 @@ export const MarkdownToolbar: React.FC<MarkdownToolbarProps> = ({
|
|||||||
setMenuPosition(null);
|
setMenuPosition(null);
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
H{level}
|
Заголовок {level}
|
||||||
</button>
|
</button>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
@ -267,7 +387,7 @@ export const MarkdownToolbar: React.FC<MarkdownToolbarProps> = ({
|
|||||||
|
|
||||||
<button
|
<button
|
||||||
className="btnMarkdown"
|
className="btnMarkdown"
|
||||||
onClick={() => onInsert("`", "`")}
|
onClick={handleCodeButtonClick}
|
||||||
title="Код"
|
title="Код"
|
||||||
>
|
>
|
||||||
<Icon icon="mdi:code-tags" />
|
<Icon icon="mdi:code-tags" />
|
||||||
@ -275,7 +395,7 @@ export const MarkdownToolbar: React.FC<MarkdownToolbarProps> = ({
|
|||||||
|
|
||||||
<button
|
<button
|
||||||
className="btnMarkdown"
|
className="btnMarkdown"
|
||||||
onClick={() => onInsert("[текст ссылки](", ")")}
|
onClick={handleLinkButtonClick}
|
||||||
title="Ссылка"
|
title="Ссылка"
|
||||||
>
|
>
|
||||||
<Icon icon="mdi:link" />
|
<Icon icon="mdi:link" />
|
||||||
@ -312,6 +432,113 @@ 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: () => void;
|
onSave: (noteId?: number | string) => 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();
|
onSave(note.id);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Ошибка сохранения заметки:", error);
|
console.error("Ошибка сохранения заметки:", error);
|
||||||
showNotification("Ошибка сохранения заметки", "error");
|
showNotification("Ошибка сохранения заметки", "error");
|
||||||
@ -106,7 +106,9 @@ export const NoteEditor: React.FC<NoteEditorProps> = ({ onSave }) => {
|
|||||||
console.error("Ошибка улучшения текста:", error);
|
console.error("Ошибка улучшения текста:", error);
|
||||||
setImproveError(true);
|
setImproveError(true);
|
||||||
setImproveErrorMessage(
|
setImproveErrorMessage(
|
||||||
error.response?.data?.error || error.message || "Ошибка улучшения текста"
|
error.response?.data?.error ||
|
||||||
|
error.message ||
|
||||||
|
"Ошибка улучшения текста"
|
||||||
);
|
);
|
||||||
} finally {
|
} finally {
|
||||||
setIsAiLoading(false);
|
setIsAiLoading(false);
|
||||||
@ -147,7 +149,10 @@ 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 = error.response?.data?.error || error.message || "Ошибка генерации тегов";
|
const errorMessage =
|
||||||
|
error.response?.data?.error ||
|
||||||
|
error.message ||
|
||||||
|
"Ошибка генерации тегов";
|
||||||
showNotification(errorMessage, "error");
|
showNotification(errorMessage, "error");
|
||||||
} finally {
|
} finally {
|
||||||
setIsGeneratingTags(false);
|
setIsGeneratingTags(false);
|
||||||
@ -159,13 +164,19 @@ export const NoteEditor: React.FC<NoteEditorProps> = ({ onSave }) => {
|
|||||||
|
|
||||||
const existingTags = extractTags(content);
|
const existingTags = extractTags(content);
|
||||||
const tagsToAdd = tags
|
const tagsToAdd = tags
|
||||||
.filter((tag) => !existingTags.some((existing) => existing.toLowerCase() === tag.toLowerCase()))
|
.filter(
|
||||||
|
(tag) =>
|
||||||
|
!existingTags.some(
|
||||||
|
(existing) => existing.toLowerCase() === tag.toLowerCase()
|
||||||
|
)
|
||||||
|
)
|
||||||
.map((tag) => `#${tag}`)
|
.map((tag) => `#${tag}`)
|
||||||
.join(" ");
|
.join(" ");
|
||||||
|
|
||||||
if (tagsToAdd) {
|
if (tagsToAdd) {
|
||||||
// Добавляем теги в конец заметки
|
// Добавляем теги в конец заметки
|
||||||
const newContent = content.trim() + (content.trim() ? "\n\n" : "") + tagsToAdd;
|
const newContent =
|
||||||
|
content.trim() + (content.trim() ? "\n\n" : "") + tagsToAdd;
|
||||||
setContent(newContent);
|
setContent(newContent);
|
||||||
showNotification(`Добавлено тегов: ${tags.length}`, "success");
|
showNotification(`Добавлено тегов: ${tags.length}`, "success");
|
||||||
} else {
|
} else {
|
||||||
@ -1108,6 +1119,7 @@ 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}
|
||||||
@ -1123,6 +1135,7 @@ 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,6 +21,7 @@ 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 {
|
||||||
@ -31,6 +32,7 @@ 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> = ({
|
||||||
@ -41,6 +43,7 @@ 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);
|
||||||
@ -65,6 +68,10 @@ 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);
|
||||||
@ -138,6 +145,13 @@ 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");
|
||||||
@ -179,18 +193,36 @@ export const NoteItem: React.FC<NoteItemProps> = ({
|
|||||||
}
|
}
|
||||||
|
|
||||||
setIsAiLoading(true);
|
setIsAiLoading(true);
|
||||||
|
setImproveError(false);
|
||||||
|
setImproveErrorMessage("");
|
||||||
|
setImprovedText("");
|
||||||
|
setShowImproveModal(true);
|
||||||
try {
|
try {
|
||||||
const improvedText = await aiApi.improveText(editContent);
|
const improved = await aiApi.improveText(editContent);
|
||||||
setEditContent(improvedText);
|
setImprovedText(improved);
|
||||||
showNotification("Текст улучшен!", "success");
|
setImproveError(false);
|
||||||
} catch (error) {
|
} catch (error: any) {
|
||||||
console.error("Ошибка улучшения текста:", error);
|
console.error("Ошибка улучшения текста:", error);
|
||||||
showNotification("Ошибка улучшения текста", "error");
|
setImproveError(true);
|
||||||
|
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");
|
||||||
@ -1620,6 +1652,7 @@ 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}
|
||||||
@ -1627,6 +1660,23 @@ 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,6 +8,7 @@ import { extractTags } from "../../utils/markdown";
|
|||||||
|
|
||||||
export interface NotesListRef {
|
export interface NotesListRef {
|
||||||
reloadNotes: () => void;
|
reloadNotes: () => void;
|
||||||
|
focusNote: (noteId: number | string) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface NotesListProps {
|
interface NotesListProps {
|
||||||
@ -81,8 +82,34 @@ 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) => {
|
||||||
@ -167,6 +194,7 @@ 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,10 +63,32 @@ const NotesPage: React.FC = () => {
|
|||||||
|
|
||||||
const activeFilters = getActiveFilters();
|
const activeFilters = getActiveFilters();
|
||||||
|
|
||||||
const handleNoteSave = () => {
|
const handleNoteSave = (noteId?: number | string) => {
|
||||||
// Вызываем перезагрузку заметок после создания новой заметки
|
// Вызываем перезагрузку заметок после создания новой заметки
|
||||||
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,6 +1424,94 @@ 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;
|
||||||
@ -1655,8 +1743,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: 60px;
|
min-width: 140px;
|
||||||
max-width: 120px;
|
max-width: 250px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.header-dropdown-menu button {
|
.header-dropdown-menu button {
|
||||||
@ -1667,10 +1755,9 @@ 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 {
|
||||||
@ -1685,6 +1772,50 @@ 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;
|
||||||
@ -3240,6 +3371,90 @@ 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,4 +1,8 @@
|
|||||||
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 = {
|
||||||
@ -75,7 +79,72 @@ function renderTokens(tokens: any[], renderer: any): string {
|
|||||||
.join("");
|
.join("");
|
||||||
}
|
}
|
||||||
|
|
||||||
// Кастомный renderer для внешних ссылок и чекбоксов
|
// Функция для экранирования HTML
|
||||||
|
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;
|
||||||
@ -123,6 +192,20 @@ 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