var string = ""; var escapeSequenceRegex = /(%.{2})+/gi; onload = function() { document.getElementById("raw").addEventListener("keydown", onKeydown) document.getElementById("formatted").addEventListener("keydown", onKeydown) function onKeydown(event) { if (event.which === 13) { string += "%0A"; update(event.target); event.preventDefault(); } } } function rawUpdated() { string = document.getElementById("raw").innerText; update(document.getElementById("raw")); } function formattedUpdated() { string = encodeURIComponent(document.getElementById("formatted").innerText); update(document.getElementById("formatted")); } function update(input) { // Save caret position var caretPos = getCaretPos(input); // Create HTML for raw input document.getElementById("raw").innerHTML = string .replace(escapeSequenceRegex, function(str) { try { decodeURIComponent(str); return "" + str + ""; } catch(e) { return "" + str + ""; } }) .replace(/%.?$/, "$&"); // Unfinished escape sequence // Create HTML for formatted input var formatted = ""; for (var i = 0; i < string.length; i++) { // URL escape sequence reached if (string[i] === "%") { var sequence = string.substr(i).match(escapeSequenceRegex); // Contains valid characters if (sequence) { sequence = sequence[0]; try { var decoded = decodeURIComponent(sequence) .replace(/\n/g, "
"); formatted += "" + decoded + ""; } catch (e) { formatted += "" + sequence + ""; } i += sequence.length - 1; // Unfinished escape sequence } else { formatted += "" + string.substr(i, 3) + ""; i += 2; } } else { formatted += string[i]; } } document.getElementById("formatted").innerHTML = formatted; // Restore caret position setCaretPos(input, caretPos); document.querySelectorAll("#raw > span, #formatted > span").forEach(function(escaped) { escaped.onmouseenter = highlightSegment; escaped.onmouseleave = unhighlightAll; }); } function highlightSegment(event) { // Get node index (https://stackoverflow.com/questions/5913927/get-child-node-index#comment39914731_5913984) var node = event.target; for (var i = 0; (node = node.previousElementSibling); i++); document.getElementById("formatted").children[i].className += " highlighted"; document.getElementById("raw").children[i].className += " highlighted"; } function unhighlightAll() { [].slice.call(document.getElementsByClassName("highlighted")).forEach(function(elem) { elem.className = elem.className.replace("highlighted", ""); }); } // why is manipulating the caret in a contenteditable div so painful // https://stackoverflow.com/a/12500791 function getCaretPos(element) { var range = window.getSelection().getRangeAt(0); var selected = range.toString().length; var preCaretRange = range.cloneRange(); preCaretRange.selectNodeContents(element); preCaretRange.setEnd(range.endContainer, range.endOffset); return preCaretRange.toString().length - selected; } // https://stackoverflow.com/a/36953852 function setCaretPos(element, pos) { // Loop through all child nodes for (var i = 0; i < element.childNodes.length; i++){ var node = element.childNodes[i]; if (node.nodeType === Node.TEXT_NODE) { if (node.length >= pos){ // finally add our range var range = document.createRange(), sel = window.getSelection(); range.setStart(node, pos); range.collapse(true); sel.removeAllRanges(); sel.addRange(range); return -1; // we are done } else { pos -= node.length; } } else { pos = setCaretPos(node, pos); if (pos === -1) { return -1; // no need to finish the for loop } } } return pos; // needed because of recursion stuff }