From aaa0c3374ef0ff106a3e3e98a9e1c894a7cfc020 Mon Sep 17 00:00:00 2001 From: Gnome Ann <> Date: Wed, 13 Oct 2021 00:42:03 -0400 Subject: [PATCH] Fix problems with stories that end in newlines Today I learned that the editor only works properly when the last tag has a
inside it at the end. This last
is invisible and is automatically created by all major browsers when you use the enter key to type a newline at the end of a story to "prevent the element from collapsing". When there's more than one
at the end of the last , only the last of those
s is invisible, so if you have three
s, they are rendered as two newlines. This only applies to the last , so if the second last has three
s at the end, they are still rendered as three newlines. Since the browser is really insistent on doing this, this commit mostly deals with dynamically creating and deleting
tags at the ends of tags as needed to provide a consistent experience, and making sure that all
tags actually go inside of tags to prevent breaking the editor. The latter behaviour was exhibited by Chrome and caused a bug when you added a newline at the end of your story using the editor. --- static/application.js | 85 +++++++++++++++++++++++++++++++++++-------- 1 file changed, 69 insertions(+), 16 deletions(-) diff --git a/static/application.js b/static/application.js index 44fa349f..5bbf8d32 100644 --- a/static/application.js +++ b/static/application.js @@ -496,6 +496,14 @@ function returnWiList(ar) { socket.send({'cmd': 'sendwilist', 'data': list}); } +function formatChunkInnerText(chunk) { + var text = chunk.innerText.replace(/\u00a0/g, " "); + if((chunk.nextSibling === null || chunk.nextSibling.nodeType !== 1 || chunk.nextSibling.tagName !== "CHUNK") && text.slice(-1) === '\n') { + return text.slice(0, -1); + } + return text; +} + function dosubmit() { var txt = input_text.val().replace(/\u00a0/g, " "); if(!memorymode && !gamestarted && ((!adventure || !action_mode) && txt.trim().length == 0)) { @@ -750,11 +758,13 @@ function chunkOnTextInput(event) { // mobile devices, but the other method is also here as // a fallback if(document.queryCommandSupported && document.execCommand && document.queryCommandSupported('insertHTML')) { - document.execCommand('insertHTML', false, event.originalEvent.data.slice(0, -1) + '
|'); + document.execCommand('insertHTML', false, event.originalEvent.data.slice(0, -1) + '
|'); var t = $('#_EDITOR_SENTINEL_').contents().filter(function() { return this.nodeType === 3; })[0]; } else { var t = document.createTextNode('|'); - r.insertNode(document.createElement('br')); + var b = document.createElement('br'); + b.id = "_EDITOR_LINEBREAK_"; + r.insertNode(b); r.collapse(false); r.insertNode(t); } @@ -771,6 +781,25 @@ function chunkOnTextInput(event) { // document.execCommand method r.deleteContents(); } + + // In Chrome the added
will go outside of the chunks if we press + // enter at the end of the story in the editor, so this is here + // to put the
back in the right place + var br = $("#_EDITOR_LINEBREAK_")[0]; + if(br.parentNode === game_text[0]) { + if(br.previousSibling.nodeType !== 1) { + br.previousSibling.previousSibling.appendChild(br.previousSibling); + } + br.previousSibling.appendChild(br); + r.selectNodeContents(br.parentNode); + s.removeAllRanges(); + s.addRange(r); + r.collapse(false); + } + br.id = ""; + if(game_text[0].lastChild.tagName === "BR") { + br.parentNode.appendChild(game_text[0].lastChild); + } return; } } @@ -816,6 +845,10 @@ function downloadStory(format) { for(var i = 0; i < actionlist.length; i++) { actionlist_compiled.push(actionlist[i].innerText.replace(/\u00a0/g, " ")); } + var last = actionlist_compiled[actionlist_compiled.length-1]; + if(last.slice(-1) === '\n') { + actionlist_compiled[actionlist_compiled.length-1] = last.slice(0, -1); + } if(format == "plaintext") { var objectURL = URL.createObjectURL(new Blob(actionlist_compiled)); @@ -890,10 +923,10 @@ function applyChunkDeltas(nodes) { var selected_chunks = buildChunkSetFromNodeArray(getSelectedNodes()); for(var i = 0; i < chunks.length; i++) { var chunk = document.getElementById("n" + chunks[i]); - if(chunk && chunk.innerText.length != 0 && chunks[i] != '0') { + if(chunk && formatChunkInnerText(chunk).length != 0 && chunks[i] != '0') { if(!selected_chunks.has(chunks[i])) { modified_chunks.delete(chunks[i]); - socket.send({'cmd': 'inlineedit', 'chunk': chunks[i], 'data': chunk.innerText.replace(/\u00a0/g, " ")}); + socket.send({'cmd': 'inlineedit', 'chunk': chunks[i], 'data': formatChunkInnerText(chunk)}); } empty_chunks.delete(chunks[i]); } else { @@ -914,7 +947,7 @@ function syncAllModifiedChunks(including_selected_chunks=false) { if(including_selected_chunks || !selected_chunks.has(chunks[i])) { modified_chunks.delete(chunks[i]); var chunk = document.getElementById("n" + chunks[i]); - var data = chunk ? document.getElementById("n" + chunks[i]).innerText.replace(/\u00a0/g, " ") : ""; + var data = chunk ? formatChunkInnerText(document.getElementById("n" + chunks[i])) : ""; if(data.length == 0) { empty_chunks.add(chunks[i]); } else { @@ -927,7 +960,7 @@ function syncAllModifiedChunks(including_selected_chunks=false) { function restorePrompt() { if(game_text[0].firstChild && game_text[0].firstChild.nodeType === 3) { - saved_prompt = game_text[0].firstChild.textContent.replace(/\u00a0/g, " "); + saved_prompt = formatChunkInnerText(game_text[0].firstChild); unbindGametext(); game_text[0].innerText = ""; bindGametext(); @@ -961,9 +994,9 @@ function deleteEmptyChunks() { } if(modified_chunks.has('0')) { modified_chunks.delete(chunks[i]); - socket.send({'cmd': 'inlineedit', 'chunk': chunks[i], 'data': document.getElementById("n0").innerText.replace(/\u00a0/g, " ")}); + socket.send({'cmd': 'inlineedit', 'chunk': chunks[i], 'data': formatChunkInnerText(document.getElementById("n0"))}); } - saved_prompt = $("#n0")[0].innerText.replace(/\u00a0/g, " "); + saved_prompt = formatChunkInnerText($("#n0")[0]); } function highlightEditingChunks() { @@ -1203,9 +1236,12 @@ $(document).ready(function(){ modified_chunks = new Set(); empty_chunks = new Set(); game_text.html(msg.data); + if(game_text[0].lastChild !== null && game_text[0].lastChild.tagName === "CHUNK") { + game_text[0].lastChild.appendChild(document.createElement("br")); + } bindGametext(); if(gamestarted) { - saved_prompt = $("#n0")[0].innerText.replace(/\u00a0/g, " "); + saved_prompt = formatChunkInnerText($("#n0")[0]); } // Scroll to bottom of text if(newly_loaded) { @@ -1217,26 +1253,43 @@ $(document).ready(function(){ scrollToBottom(); } else if(msg.cmd == "updatechunk") { hideMessage(); - const {index, html} = msg.data; - const existingChunk = game_text.children(`#n${index}`) - const newChunk = $(html); + var index = msg.data.index; + var html = msg.data.html; + var existingChunk = game_text.children('#n' + index); + var newChunk = $(html); unbindGametext(); if (existingChunk.length > 0) { // Update existing chunk + if(existingChunk[0].nextSibling === null || existingChunk[0].nextSibling.nodeType !== 1 || existingChunk[0].nextSibling.tagName !== "CHUNK") { + newChunk[0].appendChild(document.createElement("br")); + } existingChunk.before(newChunk); existingChunk.remove(); } else if (!empty_chunks.has(index.toString())) { // Append at the end + unbindGametext(); + var lc = game_text[0].lastChild; + if(lc.tagName === "CHUNK" && lc.lastChild !== null && lc.lastChild.tagName === "BR") { + lc.removeChild(lc.lastChild); + } + newChunk[0].appendChild(document.createElement("br")); game_text.append(newChunk); + bindGametext(); } bindGametext(); hide([$('#curtain')]); } else if(msg.cmd == "removechunk") { hideMessage(); - let index = msg.data; - unbindGametext(); - game_text.children(`#n${index}`).remove() // Remove the chunk - bindGametext(); + var index = msg.data; + var element = game_text.children('#n' + index); + if(element.length) { + unbindGametext(); + if((element[0].nextSibling === null || element[0].nextSibling.nodeType !== 1 || element[0].nextSibling.tagName !== "CHUNK") && element[0].previousSibling !== null && element[0].previousSibling.tagName === "CHUNK") { + element[0].previousSibling.appendChild(document.createElement("br")); + } + element.remove(); // Remove the chunk + bindGametext(); + } hide([$('#curtain')]); } else if(msg.cmd == "setgamestate") { // Enable or Disable buttons