ZDDC/transmittal/js/markdown-editor.js
2026-06-11 13:32:31 -05:00

222 lines
8.3 KiB
JavaScript

(function (app) {
'use strict';
var dom = app.dom;
var markdown = app.modules.markdown;
var editor = app.modules.markdownEditor = {};
var inputEl = null; // plain textarea
var toolbarEl = null; // button bar
var wrapperEl = null; // container for all editor elements
var initialized = false;
var renderClickBound = false;
var outsideClickBound = false;
// ── Textarea helpers ──────────────────────────────────────────────
function syncToHiddenTextarea() {
var remarks = dom.qs('#remarks');
if (remarks && inputEl) { remarks.value = inputEl.value; }
}
function insertText(before, after, placeholder) {
if (!inputEl) { return; }
inputEl.focus();
var start = inputEl.selectionStart;
var end = inputEl.selectionEnd;
var selected = inputEl.value.substring(start, end);
var insert = selected || placeholder || '';
var full = before + insert + (after || '');
// execCommand preserves undo stack in most browsers
document.execCommand('insertText', false, full);
// If we used placeholder, select it for easy replacement
if (!selected && insert) {
inputEl.selectionStart = start + before.length;
inputEl.selectionEnd = start + before.length + insert.length;
}
}
// ── Button bar ────────────────────────────────────────────────────
var buttons = [
{ label: 'B', title: 'Bold', wrap: ['**', '**'], placeholder: 'bold' },
{ label: 'I', title: 'Italic', wrap: ['*', '*'], placeholder: 'italic' },
{ label: 'H', title: 'Heading', wrap: ['## ', ''], placeholder: 'heading' },
{ label: '\u2022', title: 'Bullet list', wrap: ['- ', ''], placeholder: 'item' },
{ label: '1.', title: 'Numbered list', wrap: ['1. ', ''], placeholder: 'item' },
{ label: '\uD83D\uDD17', title: 'Link', wrap: ['[', '](url)'], placeholder: 'link text' },
{ label: '\u229E', title: 'Table', insert: '| Col 1 | Col 2 | Col 3 |\n| --- | --- | --- |\n| | | |\n' }
];
function onButtonClick(event) {
var btn = event.currentTarget;
var idx = parseInt(btn.getAttribute('data-idx'), 10);
var def = buttons[idx];
if (!def) { return; }
event.preventDefault();
if (def.wrap) {
insertText(def.wrap[0], def.wrap[1], def.placeholder);
} else if (def.insert) {
insertText(def.insert, '', '');
}
syncToHiddenTextarea();
app.markDirty();
}
function createToolbar() {
if (toolbarEl) { return toolbarEl; }
toolbarEl = document.createElement('div');
toolbarEl.className = 'md-toolbar';
buttons.forEach(function (def, i) {
var btn = document.createElement('button');
btn.type = 'button';
btn.className = 'md-toolbar-btn';
btn.textContent = def.label;
btn.title = def.title;
btn.setAttribute('data-idx', i);
btn.addEventListener('mousedown', function (e) { e.preventDefault(); });
btn.addEventListener('click', onButtonClick);
toolbarEl.appendChild(btn);
});
return toolbarEl;
}
// ── Visibility helpers ────────────────────────────────────────────
function refreshRender() {
var textarea = dom.qs('#remarks');
var renderEl = dom.qs('#remarks-render');
if (!textarea || !renderEl) { return; }
var value = textarea.value || '';
if (value.trim()) {
renderEl.innerHTML = markdown.render(value);
} else if (app.state.mode === 'edit' && !app.state.published) {
renderEl.innerHTML = '<span class="remarks-placeholder">Click to add remarks</span>';
} else {
renderEl.innerHTML = '';
}
}
function showRendered() {
if (wrapperEl) { wrapperEl.style.display = 'none'; }
var renderContainer = dom.qs('#remarks-render-container');
if (renderContainer) { renderContainer.hidden = false; }
syncToHiddenTextarea();
refreshRender();
}
function showEditor() {
var renderContainer = dom.qs('#remarks-render-container');
if (renderContainer) { renderContainer.hidden = true; }
if (!initialized) { editor.init(); }
if (wrapperEl) { wrapperEl.style.display = ''; }
var remarks = dom.qs('#remarks');
if (remarks && inputEl) {
inputEl.value = remarks.value || '';
}
if (inputEl) { inputEl.focus(); }
}
// ── Click-to-edit on rendered preview ─────────────────────────────
function onRenderClick() {
if (app.state.mode !== 'edit' || app.state.published) { return; }
showEditor();
}
function bindRenderClick() {
if (renderClickBound) { return; }
var renderContainer = dom.qs('#remarks-render-container');
if (!renderContainer) { return; }
renderContainer.addEventListener('click', onRenderClick);
renderClickBound = true;
}
function setRenderClickable(clickable) {
var renderContainer = dom.qs('#remarks-render-container');
if (!renderContainer) { return; }
if (clickable) {
renderContainer.classList.add('remarks-clickable');
} else {
renderContainer.classList.remove('remarks-clickable');
}
}
// ── Outside-click collapse ────────────────────────────────────────
function onDocumentMousedown(event) {
if (!wrapperEl) { return; }
if (wrapperEl.style.display === 'none') { return; }
if (wrapperEl.contains(event.target)) { return; }
showRendered();
}
function bindOutsideClick() {
if (outsideClickBound) { return; }
document.addEventListener('mousedown', onDocumentMousedown, true);
outsideClickBound = true;
}
// ── Public API ────────────────────────────────────────────────────
editor.init = function initEditor() {
if (initialized) { return; }
var remarksWrapper = dom.qs('#remarks-wrapper');
if (!remarksWrapper) { return; }
wrapperEl = document.createElement('div');
wrapperEl.id = 'remarks-editor';
wrapperEl.style.display = 'none';
wrapperEl.appendChild(createToolbar());
// Edit area container
var editArea = document.createElement('div');
editArea.className = 'md-edit-area';
// Plain textarea
inputEl = document.createElement('textarea');
inputEl.className = 'md-input';
inputEl.spellcheck = true;
inputEl.setAttribute('aria-label', 'Remarks');
editArea.appendChild(inputEl);
wrapperEl.appendChild(editArea);
remarksWrapper.appendChild(wrapperEl);
inputEl.addEventListener('input', function () {
syncToHiddenTextarea();
app.markDirty();
});
inputEl.addEventListener('keydown', function (e) {
if (e.key === 'Tab') {
e.preventDefault();
document.execCommand('insertText', false, ' ');
}
});
bindOutsideClick();
initialized = true;
};
editor.destroy = function destroyEditor() {
if (wrapperEl) { wrapperEl.style.display = 'none'; }
syncToHiddenTextarea();
};
editor.showRendered = showRendered;
editor.showEditor = showEditor;
editor.refreshRender = refreshRender;
editor.bindRenderClick = bindRenderClick;
editor.setRenderClickable = setRenderClickable;
editor.isActive = function isActive() {
return initialized && wrapperEl && wrapperEl.style.display !== 'none';
};
editor.getValue = function getValue() {
return inputEl ? inputEl.value : '';
};
})(window.transmittalApp);