| 1 |
(function() { |
| 2 |
var __cfg = document.getElementById('partial-item-text-editor-cfg'); |
| 3 |
var ITEM_ID = __cfg ? __cfg.dataset.itemId : ''; |
| 4 |
|
| 5 |
function switchEditorTab(tab) { |
| 6 |
document.querySelectorAll('.editor-tab').forEach(t => t.classList.remove('is-selected')); |
| 7 |
document.querySelector('.editor-tab[data-tab="' + tab + '"]').classList.add('is-selected'); |
| 8 |
|
| 9 |
document.querySelectorAll('.editor-panel').forEach(p => p.classList.remove('active')); |
| 10 |
document.getElementById(tab + '-panel').classList.add('active'); |
| 11 |
|
| 12 |
if (tab === 'preview') { |
| 13 |
renderMarkdownPreview(); |
| 14 |
} |
| 15 |
} |
| 16 |
|
| 17 |
function renderMarkdownPreview() { |
| 18 |
const body = document.getElementById('text-body').value; |
| 19 |
const preview = document.getElementById('markdown-preview'); |
| 20 |
|
| 21 |
|
| 22 |
let html = body |
| 23 |
.split('\n\n').map(p => p.trim()).filter(p => p).map(p => { |
| 24 |
|
| 25 |
if (p.startsWith('### ')) return '<h3>' + escapeHtml(p.slice(4)) + '</h3>'; |
| 26 |
if (p.startsWith('## ')) return '<h2>' + escapeHtml(p.slice(3)) + '</h2>'; |
| 27 |
if (p.startsWith('# ')) return '<h1>' + escapeHtml(p.slice(2)) + '</h1>'; |
| 28 |
|
| 29 |
let text = escapeHtml(p); |
| 30 |
text = text.replace(/\*\*(.+?)\*\*/g, '<strong>$1</strong>'); |
| 31 |
text = text.replace(/\*(.+?)\*/g, '<em>$1</em>'); |
| 32 |
text = text.replace(/`(.+?)`/g, '<code>$1</code>'); |
| 33 |
return '<p>' + text + '</p>'; |
| 34 |
}).join('\n'); |
| 35 |
|
| 36 |
preview.innerHTML = html || '<p class="text-editor-placeholder">Nothing to preview...</p>'; |
| 37 |
} |
| 38 |
|
| 39 |
|
| 40 |
function updateWordCount() { |
| 41 |
const body = document.getElementById('text-body').value; |
| 42 |
const words = body.trim().split(/\s+/).filter(w => w).length; |
| 43 |
const readingTime = Math.max(1, Math.ceil(words / 200)); |
| 44 |
document.getElementById('word-count').textContent = words + ' words'; |
| 45 |
document.querySelector('.reading-time').textContent = readingTime + ' min read'; |
| 46 |
} |
| 47 |
|
| 48 |
function saveTextContent() { |
| 49 |
const body = document.getElementById('text-body').value; |
| 50 |
const btn = document.getElementById('save-text-btn'); |
| 51 |
const status = document.getElementById('text-save-status'); |
| 52 |
const itemId = ITEM_ID; |
| 53 |
|
| 54 |
btn.disabled = true; |
| 55 |
status.innerHTML = ''; |
| 56 |
|
| 57 |
fetch('/api/items/' + itemId + '/text', { |
| 58 |
method: 'PUT', |
| 59 |
headers: { 'Content-Type': 'application/json', ...csrfHeaders() }, |
| 60 |
body: JSON.stringify({ body: body }) |
| 61 |
}) |
| 62 |
.then(res => res.json()) |
| 63 |
.then(data => { |
| 64 |
status.innerHTML = '<span class="save-status success">Saved</span>'; |
| 65 |
if (data.word_count !== undefined) { |
| 66 |
document.getElementById('word-count').textContent = data.word_count + ' words'; |
| 67 |
} |
| 68 |
}) |
| 69 |
.catch(err => { |
| 70 |
status.innerHTML = '<span class="save-status error">Error saving</span>'; |
| 71 |
}) |
| 72 |
.finally(() => { |
| 73 |
btn.disabled = false; |
| 74 |
}); |
| 75 |
} |
| 76 |
|
| 77 |
document.getElementById('text-body').addEventListener('input', updateWordCount); |
| 78 |
|
| 79 |
|
| 80 |
var autoSaveTimer = null; |
| 81 |
var autoSaveStatus = document.getElementById('text-save-status'); |
| 82 |
|
| 83 |
document.getElementById('text-body').addEventListener('input', function() { |
| 84 |
clearTimeout(autoSaveTimer); |
| 85 |
autoSaveTimer = setTimeout(function() { |
| 86 |
autoSaveStatus.innerHTML = '<span class="save-status saving">Saving...</span>'; |
| 87 |
var body = document.getElementById('text-body').value; |
| 88 |
fetch('/api/items/' + ITEM_ID + '/text', { |
| 89 |
method: 'PUT', |
| 90 |
headers: { 'Content-Type': 'application/json', ...csrfHeaders() }, |
| 91 |
body: JSON.stringify({ body: body }) |
| 92 |
}) |
| 93 |
.then(function(res) { return res.json(); }) |
| 94 |
.then(function(data) { |
| 95 |
autoSaveStatus.innerHTML = '<span class="save-status success">Auto-saved</span>'; |
| 96 |
if (data.word_count !== undefined) { |
| 97 |
document.getElementById('word-count').textContent = data.word_count + ' words'; |
| 98 |
} |
| 99 |
setTimeout(function() { |
| 100 |
if (autoSaveStatus.textContent === 'Auto-saved') autoSaveStatus.innerHTML = ''; |
| 101 |
}, 3000); |
| 102 |
}) |
| 103 |
.catch(function() { |
| 104 |
autoSaveStatus.innerHTML = '<span class="save-status error">Auto-save failed</span>'; |
| 105 |
}); |
| 106 |
}, 30000); |
| 107 |
}); |
| 108 |
|
| 109 |
window.switchEditorTab = switchEditorTab; |
| 110 |
window.renderMarkdownPreview = renderMarkdownPreview; |
| 111 |
window.escapeHtml = escapeHtml; |
| 112 |
window.updateWordCount = updateWordCount; |
| 113 |
window.saveTextContent = saveTextContent; |
| 114 |
})(); |
| 115 |
|