/** * Markdown Editor Module - Notion/Obsidian Style * UPDATED: Removed slash command feature (redundant with new-block-line buttons) * UPDATED: Chapter marker no longer auto-creates empty text block * UPDATED: Image upload in new-block-line buttons */ // ============================================ // Editor State // ============================================ let editorBlocks = []; let activeBlockId = null; let isToolbarClick = false; // ============================================ // Initialization // ============================================ function initMarkdownEditor() { const editor = document.getElementById('markdownEditor'); editor.addEventListener('click', function(e) { if (e.target === editor || e.target.id === 'emptyEditorMessage') { if (editorBlocks.length === 0) { addChapterMarker(1); } } }); document.addEventListener('keydown', handleGlobalKeydown); document.addEventListener('mousedown', function(e) { if (e.target.closest('.md-block-toolbar') || e.target.closest('.dropdown-menu') || e.target.closest('.toolbar-btn')) { isToolbarClick = true; } else { isToolbarClick = false; } }); console.log('📝 Markdown editor initialized'); } // ============================================ // New Block Line Helper // ============================================ function createNewBlockLine() { const line = document.createElement('div'); line.className = 'new-block-line'; const lineId = 'line_' + Date.now() + '_' + Math.random().toString(36).substr(2, 5); line.innerHTML = `
`; return line; } function triggerImageAtLine(lineId) { const fileInput = document.getElementById(`imageLineInput_${lineId}`); if (fileInput) { fileInput.click(); } } function handleImageAtLine(event, inputEl) { const file = event.target.files[0]; if (!file || !file.type.startsWith('image/')) return; const line = inputEl.closest('.new-block-line'); if (!line) return; const reader = new FileReader(); reader.onload = function(e) { const base64Data = e.target.result.split(',')[1]; const format = file.type.split('/')[1] === 'jpeg' ? 'jpeg' : file.type.split('/')[1]; const images = [{ data: base64Data, format: format, alt_text: file.name, position: 'before' }]; const content = `![${file.name}](embedded-image.${format})`; const blockId = addBlock('image', content, line, images); const blockEl = document.getElementById(blockId); if (blockEl) { const contentDiv = blockEl.querySelector('.md-block-content'); if (contentDiv) { contentDiv.innerHTML = `
${file.name}
`; } } repairAllNewBlockLines(); showNotification('Image added', 'success'); }; reader.readAsDataURL(file); event.target.value = ''; } function ensureNewBlockLineAfter(element) { if (!element) return; const next = element.nextElementSibling; if (next && next.classList.contains('new-block-line')) { return; } element.after(createNewBlockLine()); } function repairAllNewBlockLines() { const editor = document.getElementById('markdownEditor'); if (!editor) return; const children = Array.from(editor.children); for (let i = 0; i < children.length; i++) { const el = children[i]; if (el.classList.contains('new-block-line')) { const prev = el.previousElementSibling; if (!prev || prev.classList.contains('new-block-line')) { el.remove(); } } } const updatedChildren = Array.from(editor.children); for (let i = 0; i < updatedChildren.length; i++) { const el = updatedChildren[i]; if (el.classList.contains('md-block') || el.classList.contains('chapter-marker')) { ensureNewBlockLineAfter(el); } } const finalChildren = Array.from(editor.children); for (let i = 1; i < finalChildren.length; i++) { if (finalChildren[i].classList.contains('new-block-line') && finalChildren[i-1].classList.contains('new-block-line')) { finalChildren[i].remove(); } } } // ============================================ // Chapter Markers // ============================================ function addChapterMarker(chapterNumber = 1, voice = 'af_heart', afterElement = null) { const markerId = 'chapter_' + Date.now(); const marker = document.createElement('div'); marker.className = 'chapter-marker'; marker.id = markerId; marker.dataset.chapterNumber = chapterNumber; marker.dataset.voice = voice; marker.innerHTML = `
Chapter
`; const editor = document.getElementById('markdownEditor'); const emptyMessage = document.getElementById('emptyEditorMessage'); if (emptyMessage) { emptyMessage.style.display = 'none'; } if (afterElement) { afterElement.after(marker); } else { editor.appendChild(marker); } ensureNewBlockLineAfter(marker); return markerId; } function getNextChapterNumber() { const chapterMarkers = document.querySelectorAll('.chapter-marker'); let maxChapter = 0; chapterMarkers.forEach(m => { const num = parseInt(m.dataset.chapterNumber) || 0; if (num > maxChapter) maxChapter = num; }); return maxChapter + 1; } function updateChapterNumber(markerId, value) { const marker = document.getElementById(markerId); if (marker) { marker.dataset.chapterNumber = value; } } function updateChapterVoice(markerId, value) { const marker = document.getElementById(markerId); if (marker) { marker.dataset.voice = value; } } function deleteChapter(markerId) { const marker = document.getElementById(markerId); if (!marker) return; const nextLine = marker.nextElementSibling; if (nextLine && nextLine.classList.contains('new-block-line')) { nextLine.remove(); } marker.remove(); repairAllNewBlockLines(); checkEmptyEditor(); } // ============================================ // Block Deletion // ============================================ function deleteBlock(blockId) { const block = document.getElementById(blockId); if (!block) return; if (activeBlockId === blockId) { activeBlockId = null; } const nextLine = block.nextElementSibling; if (nextLine && nextLine.classList.contains('new-block-line')) { nextLine.remove(); } block.remove(); editorBlocks = editorBlocks.filter(b => b.id !== blockId); repairAllNewBlockLines(); checkEmptyEditor(); } // ============================================ // Block Management // ============================================ function addBlock(type = 'paragraph', content = '', afterElement = null, images = []) { const blockId = 'block_' + Date.now() + '_' + Math.random().toString(36).substr(2, 5); const block = document.createElement('div'); block.className = 'md-block'; block.id = blockId; block.dataset.blockId = blockId; block.dataset.blockType = type; block.dataset.ttsText = ''; const renderedContent = renderBlockContent(type, content, blockId, images); const isImageBlock = (type === 'image'); if (isImageBlock) { block.innerHTML = `
${renderedContent}
`; } else { block.innerHTML = `
${renderedContent}
`; } const editor = document.getElementById('markdownEditor'); if (afterElement) { if (afterElement.classList.contains('new-block-line')) { afterElement.after(block); } else { const nextSibling = afterElement.nextElementSibling; if (nextSibling && nextSibling.classList.contains('new-block-line')) { nextSibling.after(block); } else { afterElement.after(block); } } } else { editor.appendChild(block); } editorBlocks.push({ id: blockId, type: type, content: content, images: images }); if (!content && type !== 'image') { setTimeout(() => enterEditMode(blockId), 100); } return blockId; } function addBlockAtLine(button) { const line = button.closest('.new-block-line'); const blockId = addBlock('paragraph', '', line); repairAllNewBlockLines(); } function addChapterAtLine(button) { const line = button.closest('.new-block-line'); const nextChapterNum = getNextChapterNumber(); addChapterMarker(nextChapterNum, 'af_heart', line); repairAllNewBlockLines(); } function renderBlockContent(type, content, blockId, images) { if (!content) { return `Click to edit`; } if (type === 'image') { if (images && images.length > 0) { const img = images[0]; if (img.data) { return `
${img.alt_text || 'Image'}
`; } } if (blockId) { const blockData = editorBlocks.find(b => b.id === blockId); if (blockData && blockData.images && blockData.images.length > 0) { const img = blockData.images[0]; if (img.data) { return `
${img.alt_text || 'Image'}
`; } } } const dataUriMatch = content.match(/!\[([^\]]*)\]\((data:image\/[^;]+;base64,[^)]+)\)/); if (dataUriMatch) { return `
${dataUriMatch[1] || 'Image'}
`; } return `

Click to upload an image

`; } let safeContent = content; if (content.length > 10000 && content.includes('base64,')) { safeContent = content.replace( /!\[([^\]]*)\]\(data:image\/[^;]+;base64,[^)]+\)/g, '![$1](embedded-image)' ); } try { const html = marked.parse(safeContent, { breaks: true, gfm: true }); return html; } catch (e) { console.warn('marked.js parse error, rendering as plain text:', e.message); return `

${escapeHtml(content)}

`; } } function getBlockContent(blockId) { const block = document.getElementById(blockId); if (block) { const textarea = block.querySelector('.md-block-textarea'); return textarea ? textarea.value : ''; } return ''; } // ============================================ // Edit Mode // ============================================ function enterEditMode(blockId) { if (activeBlockId && activeBlockId !== blockId) { exitEditMode(activeBlockId); } const block = document.getElementById(blockId); if (!block) return; block.classList.add('editing'); activeBlockId = blockId; const textarea = block.querySelector('.md-block-textarea'); if (textarea) { textarea.focus(); textarea.setSelectionRange(textarea.value.length, textarea.value.length); autoResizeTextarea(textarea); } } function handleBlockBlur(event, blockId) { if (isToolbarClick) { isToolbarClick = false; setTimeout(() => { const block = document.getElementById(blockId); if (block && block.classList.contains('editing')) { const textarea = block.querySelector('.md-block-textarea'); if (textarea) { textarea.focus(); } } }, 10); return; } exitEditMode(blockId); } function exitEditMode(blockId) { const block = document.getElementById(blockId); if (!block) return; block.classList.remove('editing'); const textarea = block.querySelector('.md-block-textarea'); const contentDiv = block.querySelector('.md-block-content'); if (textarea && contentDiv) { const content = textarea.value.trim(); const blockType = block.dataset.blockType || 'paragraph'; const blockData = editorBlocks.find(b => b.id === blockId); const images = blockData ? (blockData.images || []) : []; if (content === '') { contentDiv.innerHTML = renderBlockContent(blockType, '', blockId, images); if (blockData) { blockData.content = ''; } } else { contentDiv.innerHTML = renderBlockContent(blockType, content, blockId, images); if (blockData) { blockData.content = content; } } } if (activeBlockId === blockId) { activeBlockId = null; } repairAllNewBlockLines(); } function autoResizeTextarea(textarea) { textarea.style.height = 'auto'; textarea.style.height = Math.max(textarea.scrollHeight, 60) + 'px'; } // ============================================ // Input Handling // ============================================ function handleBlockInput(event, blockId) { const textarea = event.target; autoResizeTextarea(textarea); } function handleBlockKeydown(event, blockId) { const textarea = event.target; if (event.key === 'Escape') { event.preventDefault(); textarea.blur(); return; } if (event.key === 'Enter' && !event.shiftKey) { event.preventDefault(); const block = document.getElementById(blockId); exitEditMode(blockId); addBlock('paragraph', '', block); repairAllNewBlockLines(); return; } if (event.key === 'Backspace' && textarea.value === '') { event.preventDefault(); const block = document.getElementById(blockId); let prevBlock = block.previousElementSibling; while (prevBlock && !prevBlock.classList.contains('md-block')) { prevBlock = prevBlock.previousElementSibling; } const nextLine = block.nextElementSibling; if (nextLine && nextLine.classList.contains('new-block-line')) { nextLine.remove(); } block.remove(); editorBlocks = editorBlocks.filter(b => b.id !== blockId); activeBlockId = null; repairAllNewBlockLines(); if (prevBlock) { enterEditMode(prevBlock.id); } checkEmptyEditor(); return; } } function handleGlobalKeydown(event) { if ((event.ctrlKey || event.metaKey) && event.key === 's') { event.preventDefault(); saveProject(); return; } } // ============================================ // Text Formatting // ============================================ function applyFormat(format) { if (!activeBlockId) return; const block = document.getElementById(activeBlockId); const textarea = block.querySelector('.md-block-textarea'); const start = textarea.selectionStart; const end = textarea.selectionEnd; const selectedText = textarea.value.substring(start, end); if (start === end) return; let wrapper = ''; switch (format) { case 'bold': wrapper = '**'; break; case 'italic': wrapper = '*'; break; } const newText = textarea.value.substring(0, start) + wrapper + selectedText + wrapper + textarea.value.substring(end); textarea.value = newText; textarea.setSelectionRange(start + wrapper.length, end + wrapper.length); textarea.focus(); } function changeCase(caseType) { if (!activeBlockId) return; const block = document.getElementById(activeBlockId); const textarea = block.querySelector('.md-block-textarea'); const start = textarea.selectionStart; const end = textarea.selectionEnd; let selectedText = textarea.value.substring(start, end); if (start === end) return; switch (caseType) { case 'lower': selectedText = selectedText.toLowerCase(); break; case 'upper': selectedText = selectedText.toUpperCase(); break; case 'sentence': selectedText = selectedText.toLowerCase().replace(/(^\w|\.\s+\w)/g, c => c.toUpperCase()); break; case 'title': selectedText = selectedText.toLowerCase().replace(/\b\w/g, c => c.toUpperCase()); break; } textarea.value = textarea.value.substring(0, start) + selectedText + textarea.value.substring(end); textarea.setSelectionRange(start, start + selectedText.length); textarea.focus(); } // ============================================ // Image Block Handling // ============================================ function convertToImageBlock(blockId) { const block = document.getElementById(blockId); if (!block) return; block.dataset.blockType = 'image'; const actionsIndicator = block.querySelector('.block-actions-indicator'); if (actionsIndicator) { const editBtn = actionsIndicator.querySelector('.edit-block-btn'); if (editBtn) editBtn.remove(); } const audioIndicator = block.querySelector('.audio-indicator'); if (audioIndicator) audioIndicator.remove(); const toolbar = block.querySelector('.md-block-toolbar'); if (toolbar) toolbar.remove(); const contentDiv = block.querySelector('.md-block-content'); contentDiv.innerHTML = `

Click to upload or drag & drop an image

`; const imageBlock = contentDiv.querySelector('.image-block'); imageBlock.addEventListener('dragover', (e) => { e.preventDefault(); imageBlock.classList.add('drag-over'); }); imageBlock.addEventListener('dragleave', () => { imageBlock.classList.remove('drag-over'); }); imageBlock.addEventListener('drop', (e) => { e.preventDefault(); imageBlock.classList.remove('drag-over'); const files = e.dataTransfer.files; if (files.length > 0 && files[0].type.startsWith('image/')) { processImageFile(files[0], blockId); } }); } function triggerImageUpload(blockId) { document.getElementById(`imageInput_${blockId}`).click(); } function handleImageUpload(event, blockId) { const file = event.target.files[0]; if (file && file.type.startsWith('image/')) { processImageFile(file, blockId); } } function processImageFile(file, blockId) { const reader = new FileReader(); reader.onload = function(e) { const base64Data = e.target.result.split(',')[1]; const format = file.type.split('/')[1]; const block = document.getElementById(blockId); const textarea = block.querySelector('.md-block-textarea'); const contentDiv = block.querySelector('.md-block-content'); textarea.value = `![${file.name}](embedded-image.${format})`; contentDiv.innerHTML = `
${file.name}
`; const blockData = editorBlocks.find(b => b.id === blockId); if (blockData) { blockData.content = textarea.value; blockData.images = [{ data: base64Data, format: format, alt_text: file.name, position: 'before' }]; } }; reader.readAsDataURL(file); } // ============================================ // Utility Functions // ============================================ function checkEmptyEditor() { const editor = document.getElementById('markdownEditor'); const blocks = editor.querySelectorAll('.md-block, .chapter-marker'); const emptyMessage = document.getElementById('emptyEditorMessage'); if (blocks.length === 0) { editor.querySelectorAll('.new-block-line').forEach(line => line.remove()); if (emptyMessage) { emptyMessage.style.display = 'block'; } } else { if (emptyMessage) { emptyMessage.style.display = 'none'; } } } // ============================================ // Content Collection // ============================================ function collectEditorContent() { const editor = document.getElementById('markdownEditor'); const chapters = []; let currentChapter = null; let blockOrder = 0; const elements = editor.children; for (let i = 0; i < elements.length; i++) { const el = elements[i]; if (el.classList.contains('chapter-marker')) { if (currentChapter) { chapters.push(currentChapter); } currentChapter = { chapter_number: parseInt(el.dataset.chapterNumber) || 1, voice: el.dataset.voice || 'af_heart', blocks: [] }; blockOrder = 0; } else if (el.classList.contains('md-block')) { if (!currentChapter) { currentChapter = { chapter_number: 1, voice: 'af_heart', blocks: [] }; } blockOrder++; const textarea = el.querySelector('.md-block-textarea'); const content = textarea ? textarea.value : ''; const blockType = el.dataset.blockType || 'paragraph'; const blockData = editorBlocks.find(b => b.id === el.id); const hasImages = blockData && blockData.images && blockData.images.length > 0; if (content.trim() || (blockType === 'image' && hasImages)) { currentChapter.blocks.push({ block_order: blockOrder, block_type: blockType, content: content, tts_text: el.dataset.ttsText || '', audio_data: blockData?.audio_data || '', audio_format: blockData?.audio_format || 'mp3', transcription: blockData?.transcription || [], images: blockData?.images || [] }); } } } if (currentChapter && currentChapter.blocks.length > 0) { chapters.push(currentChapter); } return chapters; } function renderProjectInEditor(projectData) { const editor = document.getElementById('markdownEditor'); editor.innerHTML = ''; editorBlocks = []; const emptyMessage = document.getElementById('emptyEditorMessage'); if (emptyMessage) { emptyMessage.style.display = 'none'; } for (const chapter of projectData.chapters) { addChapterMarker(chapter.chapter_number, chapter.voice); for (const block of chapter.blocks) { const lastChild = editor.lastElementChild; const blockId = addBlock( block.block_type, block.content, lastChild, block.images || [] ); const blockData = editorBlocks.find(b => b.id === blockId); if (blockData) { blockData.audio_data = block.audio_data; blockData.audio_format = block.audio_format; blockData.transcription = block.transcription; blockData.tts_text = block.tts_text; } const blockEl = document.getElementById(blockId); if (blockEl && block.tts_text) { blockEl.dataset.ttsText = block.tts_text; } if (block.block_type === 'image' && block.images && block.images.length > 0) { const img = block.images[0]; if (img.data) { const contentDiv = blockEl.querySelector('.md-block-content'); if (contentDiv) { contentDiv.innerHTML = `
${img.alt_text || 'Image'}
`; } } } if (block.audio_data && block.block_type !== 'image') { const indicator = blockEl.querySelector('.audio-indicator'); if (indicator) { indicator.classList.remove('no-audio'); indicator.classList.add('has-audio'); indicator.title = 'Audio generated'; } } ensureNewBlockLineAfter(blockEl); } } repairAllNewBlockLines(); }