/* Add callbacks on text formatting buttons */ /* Locate the editor associated to an edition event. event: Global event emitted by one of the editor buttons Returns [the div.editor, the button, the textarea] */ function editor_event_source(event) { let button = undefined; let editor = undefined; /* Grab the button and the parent editor block. The onclick event itself usually reports the SVG in the button as the source */ let node = event.target || event.srcElement; while(node != document.body) { if(node.tagName == "BUTTON" && !button) { button = node; } if(node.classList.contains("editor") && !editor) { editor = node; break; } node = node.parentNode; } if(!button || !editor) return; const ta = editor.querySelector("textarea"); return [editor, button, ta]; } /* Replace the range [start:end) with the new contents, and returns the new interval [start:end) (ie. the range where the contents are now located). */ function editor_replace_range(textarea, start, end, contents) { ta.value = ta.value.substring(0, start) + contents + ta.value.substring(end); return [start, start + contents.length]; } /* Event handler that inserts specified tokens around the selection. after token is the same as before if not specified */ function editor_insert_around(event, before="", after=null) { const [editor, button, ta] = editor_event_source(event); ta.focus(); let indexStart = ta.selectionStart; let indexEnd = ta.selectionEnd; if(after === null) { after = before; } let [start, end] = editor_replace_range(ta, indexStart, indexEnd, before + ta.value.substring(indexStart, indexEnd) + after); /* Restore selection */ if(indexStart != indexEnd) { ta.selectionStart = start; ta.selectionEnd = end; } else { ta.selectionStart = ta.selectionEnd = start + before.length; } preview(); } /* Event handler that modifies each line within the selection through a generic function. */ function editor_act_on_lines(event, fn) { const [editor, button, ta] = editor_event_source(event); ta.focus(); let indexStart = ta.selectionStart; let indexEnd = ta.selectionEnd; let firstLineIndex = ta.value.substring(0, indexStart).lastIndexOf('\n'); if(firstLineIndex < 0) firstLineIndex = 0; else firstLineIndex += 1; let lastLineIndex = ta.value.substring(indexEnd).indexOf('\n'); if(lastLineIndex < 0) lastLineIndex = ta.value.length; else lastLineIndex += indexEnd; let lines = ta.value.substring(firstLineIndex, lastLineIndex).split('\n'); for(let i = 0; i < lines.length; i++) lines[i] = fn(lines[i]); let [start, end] = editor_replace_range(ta, firstLineIndex, lastLineIndex, lines.join('\n')); ta.selectionStart = start; ta.selectionEnd = end; preview(); } function editor_clear_modals(event, close = true) { // Stop the propagation of the event event.stopPropagation() // Reset all modal inputs document.getElementById('media-alt-input').value = ''; document.getElementById('media-link-input').value = ''; document.getElementById('link-desc-input').value = ''; document.getElementById('link-desc-input').value = ''; const media_type = document.getElementsByName("media-type"); for(i = 0; i < media_type.length; i++) { media_type[i].checked = false; } // Close all modal if requested if (!close) { return } const modals = document.getElementsByClassName('modal'); for (const i of modals) {i.style.display = 'none'}; } /* End-user functions */ function editor_inline(event, type) { tokens = { bold: "**", italic: "*", underline: "__", strike: "~~", inlinecode: "`", }; if(type in tokens){ editor_insert_around(event, tokens[type]); } preview(); } function editor_display_link_modal(event) { const [editor, button, ta] = editor_event_source(event); let indexStart = ta.selectionStart; let indexEnd = ta.selectionEnd; let selection = ta.value.substring(indexStart, indexEnd); // Assuming it's a link if(selection.match(/^https?:\/\/\S+/)) { event.currentTarget.querySelector("#link-link-input").value = selection; } // Or text else if(selection != "") { event.currentTarget.querySelector("#link-desc-input").value = selection; } // Or nothing selected else { event.currentTarget.querySelector("#link-desc-input").value = ""; event.currentTarget.querySelector("#link-link-input").value = ""; } event.currentTarget.children[1].style = {'display': 'block'}; } function editor_insert_link(event, link_id, text_id, media = false) { const [editor, button, ta] = editor_event_source(event); ta.focus(); let indexStart = ta.selectionStart; let indexEnd = ta.selectionEnd; const link = document.getElementById(link_id).value; const text = document.getElementById(text_id).value; let media_type = ""; const media_selector = document.getElementsByName("media-type"); for(i = 0; i < media_selector.length; i++) { if(media_selector[i].checked) { media_type = `{type=${media_selector[i].value}}`; } } editor_clear_modals(event); let [start, end] = editor_replace_range(ta, indexStart, indexEnd, `[${text.length === 0 ? ta.value.substring(indexStart, indexEnd) : text}]${media ? "!" : ""}(${link})${media ? media_type : ""}`); /* Restore selection */ if(indexStart != indexEnd) { ta.selectionStart = start; ta.selectionEnd = end; } else { ta.selectionStart = ta.selectionEnd = start + 1; } preview(); } function editor_title(event, level, diff) { editor_act_on_lines(event, function(line) { /* Strip all the initial # (and count them) */ let count = 0; while(count < line.length && line[count] == '#') count++; let contents_index = count; if(count < line.length && line[count] == ' ') contents_index++; let contents = line.slice(contents_index); if(level > 0 || count == 1 && diff == -1) { /* Remove the title if the corresponding level is re-requested */ if(count == level || count == 1 && diff == -1) return contents; /* Otherwise, add it */ else return '#'.repeat(level) + ' ' + contents; } else if(count > 0) { /* Apply the difference */ let new_level = Math.max(1, Math.min(6, count + diff)); return '#'.repeat(new_level) + ' ' + contents; } return line; }); } function editor_quote(event) { editor_act_on_lines(event, function(line) { /* Strip all the initial > (and count them) */ let count = 0; while(count < line.length && line[count] == '>') count++; let contents_index = count; if(count < line.length && line[count] == ' ') contents_index++; let contents = line.slice(contents_index); /* Apply the difference */ return '>'.repeat(count + 1) + ' ' + contents; }); } function editor_bullet_list(event) { editor_act_on_lines(event, function(line) { let ident_match = line.match(/^[\t]+/m) ?? ['']; let ident = ident_match[0]; let count = ident.length; const contents = line.slice(count); if((count < line.length || count == 0) && line[count] != '-') return '- ' + contents; return ident + "\t" + contents; }); } function editor_numbered_list(event) { editor_act_on_lines(event, function(line) { let ident_match = line.match(/^[\t]+/m) ?? ['']; let ident = ident_match[0]; let count = ident.length; const contents = line.slice(count); if((count < line.length || count == 0) && isNaN(line[count])) return '1. ' + contents; return ident + "\t" + contents; }); } function editor_table(event) { let table = `| Column 1 | Column 2 | Column 3 | | -------- | -------- | -------- | | Text | Text | Text |`; editor_insert_around(event, "", table); } function editor_separator(event) { editor_insert_around(event, "", "\n---\n"); } previewTimeout = null; ta = document.querySelector(".editor textarea"); ta.addEventListener('keydown', function(e) { // Tab insert some spaces let keyCode = e.keyCode || e.which; if (keyCode == 9) { // TODO Add one tab to selected text without replacing it e.preventDefault(); let start = e.target.selectionStart; let end = e.target.selectionEnd; // set textarea value to: text before caret + tab + text after caret e.target.value = e.target.value.substring(0, start) + "\t" + e.target.value.substring(end); e.target.selectionEnd = start + 1; } // TODO // Ctrl+B adds bold // Ctrl+I adds italic // Ctrl+U adds underline // Ctrl+Enter send the form if (e.ctrlKey && keyCode == 13) { let t = e.target; while(! (t instanceof HTMLFormElement)) { t = t.parentNode; } try { t.submit(); } catch(exception) { t.submit.click(); } } // Set a timeout for refreshing the preview if (previewTimeout != null) { clearTimeout(previewTimeout); } previewTimeout = setTimeout(preview, 3000); }); function preview() { const previewArea = document.querySelector("#editor_content_preview"); const textarea = document.querySelector(".editor textarea"); const payload = {text: ta.value}; const headers = new Headers(); headers.append("Content-Type", "application/json"); const params = { method: "POST", body: JSON.stringify(payload), headers }; fetch("/api/markdown", params).then( (response) => { response.text().then( (text) => { previewArea.innerHTML = text; } ); }); }