/* 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 the button's data-before and data-after attributes around the selection. */ function editor_insert_around(event) { const [editor, button, ta] = editor_event_source(event); ta.focus(); let indexStart = ta.selectionStart; let indexEnd = ta.selectionEnd; const before = button.dataset.before || ""; const after = button.dataset.after || ""; 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; } } /* 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; } /* * Create a modal used to get input from the user when generating some M⬇ */ function editor_create_modal(event, content = document.createElement("div"), fn = {}) { // The anchor is two levels above because the event.srcElement/event.target // are the path inside the button. const anchor = event.currentTarget; const modal = document.createElement("div"); const contentDiv = document.createElement("div"); const btnDiv = document.createElement("div"); const validateBtn = document.createElement("button"); const cancelBtn = document.createElement("button"); modal.classList.add("modal"); validateBtn.onclick = function() { modal.remove() fn(event, content); }; validateBtn.textContent = "Valider"; validateBtn.classList = ["bg-ok"]; cancelBtn.onclick = function() { modal.remove() }; cancelBtn.textContent = "Annuler"; cancelBtn.classList = ["bg-error"]; contentDiv.appendChild(content); modal.appendChild(contentDiv); btnDiv.appendChild(validateBtn); btnDiv.appendChild(cancelBtn); modal.appendChild(btnDiv); anchor.appendChild(modal); } function editor_set_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; }); } previewTimeout = null; ta = document.querySelector(".editor textarea"); ta.addEventListener('keydown', function(e) { // Tab insert some spaces // Ctrl+Enter send the form 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; } 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, 1000); }); 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 }; console.log(payload); fetch("/api/markdown", params).then( (response) => { response.text().then( (text) => { previewArea.innerHTML = text; } ); }); }