diff --git a/static/application.js b/static/application.js index a2961c4c..86bee330 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)) { @@ -737,6 +745,65 @@ function autofocus(event) { } } +function chunkOnTextInput(event) { + // The enter key does not behave correctly in almost all non-Firefox + // browsers, so we (attempt to) shim all enter keystrokes here to behave the + // same as in Firefox + if(event.originalEvent.data.slice(-1) === '\n') { + event.preventDefault(); + var s = getSelection(); // WARNING: Do not use rangy.getSelection() instead of getSelection() + var r = s.getRangeAt(0); + + // We prefer using document.execCommand here because it works best on + // 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).replace(/&/g, '&').replace(//g, '>').replace(/"/g, '"').replace(/'/g, ''').replace(/(?=\r|\n)\r?\n?/g, '
') + '
|'); + var t = $('#_EDITOR_SENTINEL_').contents().filter(function() { return this.nodeType === 3; })[0]; + } else { + var t = document.createTextNode('|'); + var b = document.createElement('br'); + b.id = "_EDITOR_LINEBREAK_"; + r.insertNode(b); + r.collapse(false); + r.insertNode(t); + } + + r.selectNodeContents(t); + s.removeAllRanges(); + s.addRange(r); + if(document.queryCommandSupported && document.execCommand && document.queryCommandSupported('forwardDelete')) { + r.collapse(true); + document.execCommand('forwardDelete'); + } else { + // deleteContents() sometimes breaks using the left + // arrow key in some browsers so we prefer the + // 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; + } +} + function chunkOnKeyDown(event) { // Make escape commit the changes (Originally we had Enter here to but its not required and nicer for users if we let them type freely // You can add the following after 27 if you want it back to committing on enter : || (!event.shiftKey && event.keyCode == 13) @@ -778,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)); @@ -852,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 { @@ -876,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 { @@ -889,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(); @@ -923,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() { @@ -970,10 +1041,20 @@ function chunkOnPaste(event) { } // If possible, intercept paste events into the editor in order to always // paste as plaintext - if(event.originalEvent.clipboardData && document.queryCommandSupported && document.execCommand && document.queryCommandSupported('insertText')) { + if(event.originalEvent.clipboardData && document.queryCommandSupported && document.execCommand && document.queryCommandSupported('insertHTML')) { event.preventDefault(); - document.execCommand('insertText', false, event.originalEvent.clipboardData.getData('text/plain')); - } + document.execCommand('insertHTML', false, event.originalEvent.clipboardData.getData('text/plain').replace(/&/g, '&').replace(//g, '>').replace(/"/g, '"').replace(/'/g, ''').replace(/(?=\r|\n)\r?\n?/g, '
')); + } else if (event.originalEvent.clipboardData) { + event.preventDefault(); + var s = getSelection(); // WARNING: Do not use rangy.getSelection() instead of getSelection() + var r = s.getRangeAt(0); + r.deleteContents(); + var nodes = Array.from($('' + event.originalEvent.clipboardData.getData('text/plain').replace(/&/g, '&').replace(//g, '>').replace(/"/g, '"').replace(/'/g, ''').replace(/(?=\r|\n)\r?\n?/g, '
') + '
')[0].childNodes); + for(var i = 0; i < nodes.length; i++) { + r.insertNode(nodes[i]); + r.collapse(false); + } + } } // This gets run every time the caret moves in the editor @@ -1165,9 +1246,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) { @@ -1179,26 +1263,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 @@ -1427,7 +1528,9 @@ $(document).ready(function(){ }); // Register editing events - game_text.on('keydown', + game_text.on('textInput', + chunkOnTextInput + ).on('keydown', chunkOnKeyDown ).on('paste', chunkOnPaste diff --git a/templates/index.html b/templates/index.html index 3e402eaf..1de19e28 100644 --- a/templates/index.html +++ b/templates/index.html @@ -6,7 +6,7 @@ - +