144bfb2ba7
This commit introduces a complete overhaul of the application, replacing the previous simple Markdown editor with a powerful, Google Docs-style editor with a live preview and ODT export. The new features include: - A new three-pane UI with a Markdown editor, a live preview, and a styles panel. - A powerful styling engine that allows users to create, edit, and apply custom styles to the document. - A persistent storage mechanism for styles using a JSON file. - Support for advanced Markdown features, including code blocks and blockquotes. - A hyperlinked table of contents that is automatically generated from the headings in the document. - Auto-captioning for tables and images. - Pagination for large tables. - Secure image embedding with a domain whitelist and size limits to prevent SSRF and DoS attacks. - An improved live preview that correctly renders multi-line Markdown elements.
158 lines
6.4 KiB
JavaScript
158 lines
6.4 KiB
JavaScript
document.addEventListener('DOMContentLoaded', () => {
|
|
const markdownInput = document.getElementById('markdown-input');
|
|
const previewOutput = document.getElementById('preview-output');
|
|
const stylesList = document.getElementById('styles-list');
|
|
const exportButton = document.getElementById('export-odt');
|
|
const addTocButton = document.getElementById('add-toc');
|
|
const addStyleButton = document.getElementById('add-style');
|
|
const styleEditorModal = document.getElementById('style-editor-modal');
|
|
const styleEditorForm = document.getElementById('style-editor-form');
|
|
const styleModalTitle = document.getElementById('style-modal-title');
|
|
const styleNameInput = document.getElementById('style-name-input');
|
|
const fontFamilyInput = document.getElementById('font-family-input');
|
|
const fontSizeInput = document.getElementById('font-size-input');
|
|
const fontWeightInput = document.getElementById('font-weight-input');
|
|
const fontStyleInput = document.getElementById('font-style-input');
|
|
const textAlignInput = document.getElementById('text-align-input');
|
|
const colorInput = document.getElementById('color-input');
|
|
const backgroundColorInput = document.getElementById('background-color-input');
|
|
const paddingInput = document.getElementById('padding-input');
|
|
const borderLeftInput = document.getElementById('border-left-input');
|
|
const paddingLeftInput = document.getElementById('padding-left-input');
|
|
const marginBottomInput = document.getElementById('margin-bottom-input');
|
|
const closeButton = document.querySelector('.close-button');
|
|
const boldButton = document.getElementById('bold-button');
|
|
const italicButton = document.getElementById('italic-button');
|
|
const h1Button = document.getElementById('h1-button');
|
|
const h2Button = document.getElementById('h2-button');
|
|
|
|
let styles = {};
|
|
let appliedStyles = {};
|
|
|
|
async function fetchStyles() {
|
|
const response = await fetch('/styles');
|
|
styles = await response.json();
|
|
renderStyles();
|
|
}
|
|
|
|
function renderStyles() {
|
|
stylesList.innerHTML = '';
|
|
for (const name in styles) {
|
|
const styleItem = document.createElement('div');
|
|
styleItem.className = 'style-item';
|
|
styleItem.textContent = name;
|
|
styleItem.addEventListener('click', () => applyStyleToSelection(name));
|
|
stylesList.appendChild(styleItem);
|
|
}
|
|
}
|
|
|
|
function applyStyleToSelection(styleName) {
|
|
const start = markdownInput.selectionStart;
|
|
const end = markdownInput.selectionEnd;
|
|
const selectedText = markdownInput.value.substring(start, end);
|
|
const lines = markdownInput.value.substring(0, start).split('\n');
|
|
const startLine = lines.length - 1;
|
|
|
|
for (let i = 0; i < selectedText.split('\n').length; i++) {
|
|
appliedStyles[startLine + i] = styleName;
|
|
}
|
|
updatePreview();
|
|
}
|
|
|
|
function applyMarkdownFormatting(prefix, suffix = prefix) {
|
|
const start = markdownInput.selectionStart;
|
|
const end = markdownInput.selectionEnd;
|
|
const selectedText = markdownInput.value.substring(start, end);
|
|
const newText = prefix + selectedText + suffix;
|
|
markdownInput.value = markdownInput.value.substring(0, start) + newText + markdownInput.value.substring(end);
|
|
updatePreview();
|
|
}
|
|
|
|
boldButton.addEventListener('click', () => applyMarkdownFormatting('**'));
|
|
italicButton.addEventListener('click', () => applyMarkdownFormatting('*'));
|
|
h1Button.addEventListener('click', () => applyMarkdownFormatting('# '));
|
|
h2Button.addEventListener('click', () => applyMarkdownFormatting('## '));
|
|
|
|
async function updatePreview() {
|
|
const markdown = markdownInput.value;
|
|
const response = await fetch('/preview', {
|
|
method: 'POST',
|
|
headers: {
|
|
'Content-Type': 'application/json',
|
|
},
|
|
body: JSON.stringify({ markdown, styles: appliedStyles }),
|
|
});
|
|
const data = await response.json();
|
|
previewOutput.innerHTML = data.html;
|
|
}
|
|
|
|
markdownInput.addEventListener('input', updatePreview);
|
|
|
|
exportButton.addEventListener('click', async () => {
|
|
const markdown = markdownInput.value;
|
|
const response = await fetch('/export', {
|
|
method: 'POST',
|
|
headers: {
|
|
'Content-Type': 'application/json',
|
|
},
|
|
body: JSON.stringify({ markdown, styles: appliedStyles }),
|
|
});
|
|
const blob = await response.blob();
|
|
const url = window.URL.createObjectURL(blob);
|
|
const a = document.createElement('a');
|
|
a.href = url;
|
|
a.download = 'document.odt';
|
|
document.body.appendChild(a);
|
|
a.click();
|
|
a.remove();
|
|
});
|
|
|
|
addTocButton.addEventListener('click', () => {
|
|
const cursorPos = markdownInput.selectionStart;
|
|
const textBefore = markdownInput.value.substring(0, cursorPos);
|
|
const textAfter = markdownInput.value.substring(cursorPos);
|
|
markdownInput.value = textBefore + '[TOC]\n' + textAfter;
|
|
updatePreview();
|
|
});
|
|
|
|
addStyleButton.addEventListener('click', () => {
|
|
styleModalTitle.textContent = 'Add New Style';
|
|
styleEditorForm.reset();
|
|
styleNameInput.value = '';
|
|
styleEditorModal.style.display = 'block';
|
|
});
|
|
|
|
closeButton.addEventListener('click', () => {
|
|
styleEditorModal.style.display = 'none';
|
|
});
|
|
|
|
styleEditorForm.addEventListener('submit', async (e) => {
|
|
e.preventDefault();
|
|
const name = styleNameInput.value;
|
|
const style = {
|
|
'font-family': fontFamilyInput.value,
|
|
'font-size': fontSizeInput.value,
|
|
'font-weight': fontWeightInput.value,
|
|
'font-style': fontStyleInput.value,
|
|
'text-align': textAlignInput.value,
|
|
'color': colorInput.value,
|
|
'background-color': backgroundColorInput.value,
|
|
'padding': paddingInput.value,
|
|
'border-left': borderLeftInput.value,
|
|
'padding-left': paddingLeftInput.value,
|
|
'margin-bottom': marginBottomInput.value,
|
|
};
|
|
await fetch(`/styles/${name}`, {
|
|
method: 'POST',
|
|
headers: {
|
|
'Content-Type': 'application/json',
|
|
},
|
|
body: JSON.stringify(style),
|
|
});
|
|
styleEditorModal.style.display = 'none';
|
|
fetchStyles();
|
|
});
|
|
|
|
fetchStyles();
|
|
});
|