// Media picker modal, insert image references into Markdown textareas. // Usage: mediaPickerOpen(textareaId) opens the picker, inserts at cursor on selection. (function() { 'use strict'; var modal = null; var targetTextareaId = null; function createModal() { if (modal) return modal; modal = document.createElement('div'); modal.id = 'media-picker-modal'; modal.style.cssText = 'display:none; position:fixed; inset:0; z-index:1000; background:rgba(0,0,0,0.5); align-items:center; justify-content:center;'; modal.innerHTML = '
' + '
' + '

Insert from Media Library

' + '' + '
' + '
' + '' + '' + '
' + '
' + '

Loading...

' + '
' + '
Click an image to insert its Markdown reference at the cursor.
' + '
'; document.body.appendChild(modal); document.getElementById('media-picker-close').addEventListener('click', mediaPickerClose); modal.addEventListener('click', function(e) { if (e.target === modal) mediaPickerClose(); }); document.getElementById('media-picker-search').addEventListener('input', filterPickerGrid); document.getElementById('media-picker-folder').addEventListener('change', filterPickerGrid); return modal; } function mediaPickerClose() { if (modal) modal.style.display = 'none'; } function filterPickerGrid() { var query = (document.getElementById('media-picker-search').value || '').toLowerCase(); var folder = document.getElementById('media-picker-folder').value; var cards = document.querySelectorAll('#media-picker-grid .mp-card'); for (var i = 0; i < cards.length; i++) { var name = cards[i].dataset.name || ''; var f = cards[i].dataset.folder || ''; var show = (!query || name.toLowerCase().indexOf(query) !== -1) && (!folder || f === folder); cards[i].style.display = show ? '' : 'none'; } } function insertAtCursor(textareaId, text) { var ta = document.getElementById(textareaId); if (!ta) return; ta.focus(); var start = ta.selectionStart; var end = ta.selectionEnd; ta.value = ta.value.substring(0, start) + text + ta.value.substring(end); ta.selectionStart = ta.selectionEnd = start + text.length; // Trigger input event for auto-save / word count ta.dispatchEvent(new Event('input', { bubbles: true })); } function renderGrid(files) { var grid = document.getElementById('media-picker-grid'); grid.textContent = ''; if (!files || files.length === 0) { var empty = document.createElement('p'); empty.className = 'form-hint'; empty.textContent = 'No media files yet. Upload images in your Media Library tab first.'; grid.appendChild(empty); return; } // Build the grid with DOM APIs (textContent / dataset / addEventListener), // never string-interpolated innerHTML: file_name / folder / cdn_url are // user-controlled and a crafted filename would otherwise be a stored XSS // (ultra-fuzz Run 13 Frontend). This also drops the inline on* handlers. for (var i = 0; i < files.length; i++) { var f = files[i]; var ref = f.markdown_ref || f.file_name || ''; var name = f.file_name || ''; var isImage = (f.content_type || '').indexOf('image') === 0; var card = document.createElement('div'); card.className = 'mp-card'; card.dataset.name = name; card.dataset.folder = f.folder || ''; card.style.cssText = 'cursor:pointer; border:1px solid var(--border-color); padding:0.5rem; background:var(--light-background); transition:outline 0.1s;'; (function(r) { card.addEventListener('click', function() { window._mediaPickerSelect(r); }); })(ref); card.addEventListener('mouseenter', function() { this.style.outline = '2px solid var(--accent-color)'; }); card.addEventListener('mouseleave', function() { this.style.outline = 'none'; }); var thumb = document.createElement('div'); if (isImage) { thumb.style.cssText = 'width:100%; height:80px; overflow:hidden; display:flex; align-items:center; justify-content:center; background:var(--surface-muted); margin-bottom:0.4rem;'; var img = document.createElement('img'); img.src = f.cdn_url || ''; img.alt = name; img.style.cssText = 'max-width:100%; max-height:80px; object-fit:contain;'; img.loading = 'lazy'; thumb.appendChild(img); } else { thumb.style.cssText = 'width:100%; height:80px; display:flex; align-items:center; justify-content:center; background:var(--surface-muted); margin-bottom:0.4rem; font-size:1.5rem; opacity:0.4;'; thumb.textContent = '\u{1F4C4}'; } card.appendChild(thumb); var label = document.createElement('div'); label.style.cssText = 'font-size:0.75rem; word-break:break-all; line-height:1.2;'; label.title = name; label.textContent = name; card.appendChild(label); grid.appendChild(card); } } window._mediaPickerSelect = function(ref) { if (targetTextareaId) { insertAtCursor(targetTextareaId, '![](' + ref + ')'); } mediaPickerClose(); }; window.mediaPickerOpen = function(textareaId) { targetTextareaId = textareaId; createModal(); modal.style.display = 'flex'; document.getElementById('media-picker-search').value = ''; document.getElementById('media-picker-grid').innerHTML = '

Loading...

'; fetch('/api/media', { credentials: 'same-origin', headers: csrfHeaders() }) .then(function(res) { return res.json(); }) .then(function(data) { // Populate folder dropdown var sel = document.getElementById('media-picker-folder'); sel.innerHTML = ''; (data.folders || []).forEach(function(f) { var opt = document.createElement('option'); opt.value = f; opt.textContent = f || '(root)'; sel.appendChild(opt); }); renderGrid(data.files || []); }) .catch(function() { document.getElementById('media-picker-grid').innerHTML = '

Failed to load media library.

'; }); }; })();