vault backup: 2025-12-26 02:09:22
All checks were successful
Deploy Quartz site to GitHub Pages / build (push) Successful in 2m29s
All checks were successful
Deploy Quartz site to GitHub Pages / build (push) Successful in 2m29s
This commit is contained in:
130
stroma/file/frontend/src/editor.js
Normal file
130
stroma/file/frontend/src/editor.js
Normal file
@@ -0,0 +1,130 @@
|
||||
import { Editor } from '@tiptap/core';
|
||||
import StarterKit from '@tiptap/starter-kit';
|
||||
import { Markdown } from 'tiptap-markdown';
|
||||
import Placeholder from '@tiptap/extension-placeholder';
|
||||
import BubbleMenu from '@tiptap/extension-bubble-menu';
|
||||
import './styles.css';
|
||||
import 'remixicon/fonts/remixicon.css';
|
||||
|
||||
// DOM Elements
|
||||
const saveBtn = document.getElementById('save-btn');
|
||||
const editorContainer = document.getElementById('editor-container');
|
||||
const bubbleMenuEl = document.getElementById('bubble-menu');
|
||||
|
||||
// Editor Instance
|
||||
let editor;
|
||||
let currentFileId = null;
|
||||
|
||||
// CSRF Helper
|
||||
function getCookie(name) {
|
||||
let cookieValue = null;
|
||||
if (document.cookie && document.cookie !== '') {
|
||||
const cookies = document.cookie.split(';');
|
||||
for (let i = 0; i < cookies.length; i++) {
|
||||
const cookie = cookies[i].trim();
|
||||
if (cookie.substring(0, name.length + 1) === (name + '=')) {
|
||||
cookieValue = decodeURIComponent(cookie.substring(name.length + 1));
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
return cookieValue;
|
||||
}
|
||||
|
||||
// Initialize Editor (only once)
|
||||
function initEditor() {
|
||||
// Bubble Menu Buttons
|
||||
const btnBold = document.getElementById('btn-bold');
|
||||
const btnItalic = document.getElementById('btn-italic');
|
||||
const btnStrike = document.getElementById('btn-strike');
|
||||
const btnCode = document.getElementById('btn-code');
|
||||
|
||||
editor = new Editor({
|
||||
element: document.querySelector('#editor'),
|
||||
extensions: [
|
||||
StarterKit,
|
||||
Markdown,
|
||||
Placeholder.configure({
|
||||
placeholder: "Type '/' for commands...",
|
||||
}),
|
||||
BubbleMenu.configure({
|
||||
element: bubbleMenuEl,
|
||||
tippyOptions: { duration: 100 },
|
||||
}),
|
||||
],
|
||||
content: '',
|
||||
editorProps: {
|
||||
attributes: {
|
||||
class: 'prose prose-sm sm:prose lg:prose-lg max-w-none focus:outline-none',
|
||||
},
|
||||
},
|
||||
onSelectionUpdate({ editor }) {
|
||||
btnBold?.classList.toggle('is-active', editor.isActive('bold'));
|
||||
btnItalic?.classList.toggle('is-active', editor.isActive('italic'));
|
||||
btnStrike?.classList.toggle('is-active', editor.isActive('strike'));
|
||||
btnCode?.classList.toggle('is-active', editor.isActive('code'));
|
||||
}
|
||||
});
|
||||
|
||||
// Bubble Menu Listeners
|
||||
if (btnBold) btnBold.addEventListener('click', () => editor.chain().focus().toggleBold().run());
|
||||
if (btnItalic) btnItalic.addEventListener('click', () => editor.chain().focus().toggleItalic().run());
|
||||
if (btnStrike) btnStrike.addEventListener('click', () => editor.chain().focus().toggleStrike().run());
|
||||
if (btnCode) btnCode.addEventListener('click', () => editor.chain().focus().toggleCode().run());
|
||||
|
||||
// Container Focus
|
||||
if (editorContainer) {
|
||||
editorContainer.addEventListener('click', () => {
|
||||
if (editor && !editor.isFocused) {
|
||||
editor.chain().focus().run();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Expose to global scope for SPA usage
|
||||
window.editorInstance = editor;
|
||||
window.setEditorFileId = (fileId) => { currentFileId = fileId; };
|
||||
}
|
||||
|
||||
// Save Content
|
||||
if (saveBtn) {
|
||||
saveBtn.addEventListener('click', async () => {
|
||||
if (!editor || !currentFileId) return;
|
||||
|
||||
saveBtn.disabled = true;
|
||||
saveBtn.textContent = 'Saving...';
|
||||
|
||||
const markdownContent = editor.storage.markdown.getMarkdown();
|
||||
const saveUrl = `/file/content/${currentFileId}/save/`;
|
||||
|
||||
try {
|
||||
const response = await fetch(saveUrl, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'X-CSRFToken': getCookie('csrftoken')
|
||||
},
|
||||
body: JSON.stringify({ content: markdownContent })
|
||||
});
|
||||
|
||||
if (response.ok) {
|
||||
saveBtn.textContent = 'Saved!';
|
||||
setTimeout(() => {
|
||||
saveBtn.textContent = 'Save Changes';
|
||||
saveBtn.disabled = false;
|
||||
}, 1000);
|
||||
} else {
|
||||
throw new Error('Save failed');
|
||||
}
|
||||
} catch (err) {
|
||||
alert('Failed to save file');
|
||||
saveBtn.textContent = 'Save Changes';
|
||||
saveBtn.disabled = false;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Initialize editor on load
|
||||
if (document.querySelector('#editor')) {
|
||||
initEditor();
|
||||
}
|
||||
2
stroma/file/frontend/src/styles.css
Normal file
2
stroma/file/frontend/src/styles.css
Normal file
@@ -0,0 +1,2 @@
|
||||
@import "tailwindcss";
|
||||
@import 'remixicon/fonts/remixicon.css';
|
||||
Reference in New Issue
Block a user