- Web-based Markdown formatter with real-time conversion - Microsoft Azure AD authentication with PKCE flow - Server-side JWT validation with httpOnly cookies - Clipboard functionality for HTML/text output - PHP backend with Composer dependency management - Comprehensive README with installation instructions 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
137 lines
No EOL
5.1 KiB
JavaScript
Executable file
137 lines
No EOL
5.1 KiB
JavaScript
Executable file
document.addEventListener('DOMContentLoaded', function() {
|
|
const markdownInput = document.getElementById('markdown-input');
|
|
const htmlOutput = document.getElementById('html-output');
|
|
const selectAllBtn = document.getElementById('select-all');
|
|
const copyBtn = document.getElementById('copy');
|
|
|
|
function updateOutput() {
|
|
const markdown = markdownInput.value;
|
|
|
|
if (!markdown.trim()) {
|
|
htmlOutput.innerHTML = '';
|
|
return;
|
|
}
|
|
|
|
// Convert markdown to HTML client-side
|
|
const html = marked(markdown);
|
|
htmlOutput.innerHTML = html;
|
|
|
|
// Apply alternating column colors to tables
|
|
const tables = htmlOutput.querySelectorAll('table');
|
|
tables.forEach(table => {
|
|
const rows = table.querySelectorAll('tr');
|
|
rows.forEach(row => {
|
|
const cells = row.querySelectorAll('td');
|
|
cells.forEach((cell, index) => {
|
|
if (index % 2 === 0) {
|
|
cell.style.backgroundColor = 'rgba(0, 0, 0, 0.25)';
|
|
}
|
|
});
|
|
});
|
|
});
|
|
}
|
|
|
|
markdownInput.addEventListener('input', updateOutput);
|
|
|
|
selectAllBtn.addEventListener('click', function() {
|
|
if (!htmlOutput.innerHTML.trim()) {
|
|
alert('No content to select');
|
|
return;
|
|
}
|
|
|
|
const range = document.createRange();
|
|
range.selectNodeContents(htmlOutput);
|
|
const selection = window.getSelection();
|
|
selection.removeAllRanges();
|
|
selection.addRange(range);
|
|
});
|
|
|
|
copyBtn.addEventListener('click', function() {
|
|
if (!htmlOutput.innerHTML.trim()) {
|
|
alert('No content to copy');
|
|
return;
|
|
}
|
|
|
|
// Use modern clipboard API if available
|
|
if (navigator.clipboard && window.isSecureContext) {
|
|
// Copy both HTML and plain text formats
|
|
const htmlContent = htmlOutput.innerHTML;
|
|
const textContent = htmlOutput.textContent || htmlOutput.innerText;
|
|
|
|
const clipboardItem = new ClipboardItem({
|
|
'text/html': new Blob([htmlContent], { type: 'text/html' }),
|
|
'text/plain': new Blob([textContent], { type: 'text/plain' })
|
|
});
|
|
|
|
navigator.clipboard.write([clipboardItem]).then(() => {
|
|
showNotification('Rich text copied to clipboard!');
|
|
}).catch(err => {
|
|
console.error('Failed to copy rich text:', err);
|
|
// Fallback to plain text if rich text copying fails
|
|
navigator.clipboard.writeText(textContent).then(() => {
|
|
showNotification('Text copied to clipboard!');
|
|
}).catch(() => {
|
|
fallbackCopy();
|
|
});
|
|
});
|
|
} else {
|
|
fallbackCopy();
|
|
}
|
|
});
|
|
|
|
function fallbackCopy() {
|
|
const range = document.createRange();
|
|
range.selectNodeContents(htmlOutput);
|
|
const selection = window.getSelection();
|
|
selection.removeAllRanges();
|
|
selection.addRange(range);
|
|
|
|
try {
|
|
document.execCommand('copy');
|
|
showNotification('Rich text copied to clipboard!');
|
|
} catch (err) {
|
|
console.error('Fallback copy failed:', err);
|
|
showNotification('Failed to copy to clipboard', 'error');
|
|
}
|
|
|
|
selection.removeAllRanges();
|
|
}
|
|
|
|
function showNotification(message, type = 'success') {
|
|
// Create notification element
|
|
const notification = document.createElement('div');
|
|
notification.textContent = message;
|
|
|
|
// Set styles individually to avoid template literal issues
|
|
notification.style.position = 'fixed';
|
|
notification.style.top = '20px';
|
|
notification.style.right = '20px';
|
|
notification.style.background = type === 'error' ? '#f44336' : '#4caf50';
|
|
notification.style.color = 'white';
|
|
notification.style.padding = '12px 20px';
|
|
notification.style.borderRadius = '4px';
|
|
notification.style.fontFamily = "'Montserrat', Arial, sans-serif";
|
|
notification.style.zIndex = '1000';
|
|
notification.style.animation = 'slideIn 0.3s ease';
|
|
|
|
// Add CSS animation
|
|
const style = document.createElement('style');
|
|
style.textContent = '@keyframes slideIn { from { transform: translateX(100%); opacity: 0; } to { transform: translateX(0); opacity: 1; } }';
|
|
document.head.appendChild(style);
|
|
|
|
document.body.appendChild(notification);
|
|
|
|
// Remove notification after 3 seconds
|
|
setTimeout(() => {
|
|
notification.remove();
|
|
style.remove();
|
|
}, 3000);
|
|
}
|
|
|
|
// Handle authentication errors gracefully
|
|
window.addEventListener('unhandledrejection', function(event) {
|
|
if (event.reason && event.reason.message && event.reason.message.includes('authentication')) {
|
|
showNotification('Authentication error. Please refresh and login again.', 'error');
|
|
}
|
|
});
|
|
}); |