/* 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 textarea] */ function editor_event_source(event) { let editor = undefined; /* Grab the the parent editor block. The onclick event itself usually reports the SVG in the button as the source */ let node = event.current || event.srcElement; while (node != document.body) { if (node.classList.contains("editor") && !editor) { editor = node; break; } node = node.parentNode; } if (!editor) return; const ta = editor.querySelector(".editor textarea"); return [editor, 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) { textarea.value = textarea.value.substring(0, start) + contents + textarea.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, 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(editor); } /* Event handler that modifies each line within the selection through a generic function. */ function editor_act_on_lines(event, fn) { const [editor, 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], i); let [start, end] = editor_replace_range(ta, firstLineIndex, lastLineIndex, lines.join('\n')); ta.selectionStart = start; ta.selectionEnd = end; preview(editor); } function editor_clear_modals(event, close = true) { // Stop the propagation of the event event.stopPropagation() const [editor, ta] = editor_event_source(event); // Reset all modal inputs editor.getElementsByClassName('media-alt-input')[0].value = ''; editor.getElementsByClassName('media-link-input')[0].value = ''; editor.getElementsByClassName('link-desc-input')[0].value = ''; editor.getElementsByClassName('link-link-input')[0].value = ''; const media_type = editor.getElementsByClassName("media-type")[0]; for(i = 0; i < media_type.length; i++) { media_type[i].checked = false; } // Close all modal if requested if (!close) { return } const modals = editor.getElementsByClassName('modal'); for (const i of modals) {i.style.display = 'none'}; } /* End-user functions */ function editor_inline(event, type, enable_preview = true) { tokens = { bold: "**", italic: "*", underline: "__", strike: "~~", inlinecode: "`", }; if (type in tokens) { editor_insert_around(event, tokens[type]); } if (enable_preview) { const [editor, ta] = editor_event_source(event); preview(editor); } } function editor_display_link_modal(event) { const [editor, 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; } editor_display_child_modal(event); } function editor_insert_link(event, link_id, text_id, media = false) { const [editor, ta] = editor_event_source(event); ta.focus(); let indexStart = ta.selectionStart; let indexEnd = ta.selectionEnd; const link = editor.getElementsByClassName(link_id)[0].value; const text = editor.getElementsByClassName(text_id)[0].value; let media_type = ""; const media_selector = editor.getElementsByClassName("media-type")[0]; 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, `${media ? "!" : ""}[${text.length === 0 ? ta.value.substring(indexStart, indexEnd) : text}](${link})${media ? media_type : ""}`); /* Restore selection */ if (indexStart != indexEnd) { ta.selectionStart = start; ta.selectionEnd = end; } else { ta.selectionStart = ta.selectionEnd = start + 1; } preview(editor); } 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, number) { 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 `${number + 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"); } function editor_display_child_modal(event) { editor_clear_modals(event); event.currentTarget.children[1].style = {'display': 'block'}; } const DISABLE_PREVIEW_ICON = ''; const ENABLE_PREVIEW_ICON = ''; function toggle_auto_preview() { const [editor, ta] = editor_event_source(event); let auto_preview; if (document.cookie.split(";").some((item) => item.trim().startsWith("auto-preview="))) { auto_preview = document.cookie.split(";").some((item) => item.includes("auto-preview=true")); } else { auto_preview = true; } document.cookie = `auto-preview=${!auto_preview}; max-age=31536000; SameSite=Strict; Secure` if (!auto_preview) { editor.getElementsByClassName("toggle_preview")[0].title = "Désactiver la prévisualisation"; editor.getElementsByClassName("toggle_preview")[0].innerHTML = DISABLE_PREVIEW_ICON; editor.getElementsByClassName("manual_preview")[0].style = "display: none"; } else { editor.getElementsByClassName("toggle_preview")[0].title = "Activer la prévisualisation"; editor.getElementsByClassName("toggle_preview")[0].innerHTML = ENABLE_PREVIEW_ICON; editor.getElementsByClassName("manual_preview")[0].style = "display: block"; } } /* This request the server to get a complete render of the current text in the textarea */ function preview(editor, manual=false) { // If auto-preview is disabled and the preview is not manually requested by the user if (document.cookie.split(";").some((item) => item.includes("auto-preview=false")) && !manual) { return; } const previewArea = editor.querySelector(".editor_content_preview"); const ta = editor.querySelector("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; } ); }); } /* Wrapper for user-requested preview refresh */ function manual_preview(editor_id) { const editor = document.getElementById("editor_" + editor_id); preview(editor, manual = true); } /* Add the event listener for the textarea hotkeys and auto-preview */ function editor_setup(editor_id) { const editor = document.getElementById("editor_" + editor_id); if (document.cookie.split(";").some((item) => item.trim().startsWith("auto-preview="))) { if (document.cookie.split(";").some((item) => item.includes("auto-preview=false"))) { editor.getElementsByClassName("toggle_preview")[0].title = "Activer la prévisualisation"; editor.getElementsByClassName("toggle_preview")[0].innerHTML = ENABLE_PREVIEW_ICON; editor.getElementsByClassName("manual_preview")[0].style = "display: block"; } } let previewTimeout = null; let ta = editor.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; } /* * Keybindings for buttons. The default action of the keybinding is prevented. * Ctrl+B adds bold * Ctrl+I adds italic * Ctrl+U adds underline * Ctrl+S adds strikethrough * Ctrl+H adds Header +1 * Ctrl+Enter send the form */ if (e.ctrlKey) { switch (keyCode) { case 13: let t = e.target; while(! (t instanceof HTMLFormElement)) { t = t.parentNode; } try { t.submit(); } catch(exception) { t.submit.click(); } e.preventDefault(); break; case 66: // B editor_inline(e, "bold", false); e.preventDefault(); break; case 72: // H editor_title(e, 0, +1); e.preventDefault(); break; case 73: // I editor_inline(e, "italic", false); e.preventDefault(); break; case 83: // S editor_inline(e, "strike", false); e.preventDefault(); break; case 85: // U editor_inline(e, "underline", false); e.preventDefault(); break; } } // Set a timeout for refreshing the preview clearTimeout(previewTimeout); previewTimeout = setTimeout(() => { preview(editor) }, 3000); }); editor.querySelector('emoji-picker').addEventListener('emoji-click', event => { editor_clear_modals(event); editor_insert_around(event, "", event.detail.unicode) preview(editor); }); }