Reputation: 3754
Few things.
First for some reason pasting over the selected text doesn't work in this site (it works when I debug it on my machine) - it gives selection over mouse positions of 0 and 0 always.
Second of all - all other interactions with the text must be fine except redo and undo.
I tried implementing a buffer to store their previous text replacement lengths because as you probably know redo and undo doesn't have ev.data
and I need to know the text I'm replacing - these events fire with the new text and previous selectionStart and selectionEnd.
I've replaced the post request with my own local server that simply modified the previous text "received" by the same "request".
I'm trying to implement live sharing functionality so each participant will send only the discrepancies between the new and the old text.
So for the most part it's currently working fine except I have no idea how to make the redo and undo work. Any help will be appreciated.
const codeelem = document.getElementById("code")
//here server simulated
//normally the postcode will send the adjustments to the server
//and previoustext will be the copy in the server
//I'll wait on a socket to recieve the update and apply it
let previoustext = "";
const postcode = (startpos, endpos, input, incaseerrormsg) => previoustext = codeelem.value = previoustext.slice(0, startpos) +
input +
previoustext.slice(endpos)
//mouse selection startpos and endpos plus a flag and a handler
let startpos,
endpos,
mouseshenanigans = false,
mouseshenaniganshandler = function (ev) {
// I actually don't know why am I checking if the start and end selection are equal here
//before setting the flag
// in any case it should not make a difference
this.selectionStart != this.selectionEnd && (mouseshenanigans = true);
(startpos = this.selectionStart), (endpos = codeelem.selectionEnd)
}
//detect if the mouse has selected a text
codeelem.addEventListener("select", mouseshenaniganshandler)
//or if the mouse has changed position in the text
//this is also reset on every input
codeelem.addEventListener("mouseup", function (ev) {
mouseshenanigans = false
})
//keep track of history
/** @type {[number, number, string][]} */
let historyredo = []
/** @type {Number} */
let currentredo = -1
//paste workaround so I don't need to prompt the user for
//copy and paste permission to see which is the new text copied
//I simply save last position before the paste
let lastselectionstart
document.addEventListener("paste", event => {
lastselectionstart = codeelem.selectionStart
})
codeelem.addEventListener("input", async function (ev) {
//if the mouse has selected text
//use that
const startopsinner = mouseshenanigans ? startpos : this.selectionStart,
endposinner = mouseshenanigans ? endpos : this.selectionEnd
//detailed diagnostics
console.log('\n')
console.log({
historyredolength: historyredo.length,
currentredo
})
console.log('\n\n\n')
console.log({
value: this.value,
previoustext,
eventtype: ev.inputType
})
console.log('\n'), console.log({
mouseshenanigans,
selectionStart: this.selectionStart,
selectionEnd: this.selectionEnd,
data: ev.data,
datansliceselection: this.value.slice(this.selectionStart, this.selectionEnd),
})
mouseshenanigans && (console.log('\n'), console.log({
startopsinner,
endposinner,
datasliceposinner: this.value.slice(startopsinner, endposinner)
}))
switch (ev.inputType) {
//if deleting or inserting text
case "deleteContentBackward":
case "insertText":
//if the last character and not deleting backwards
if (this.value.slice(startopsinner, endposinner) == "" && ev.inputType != "deleteContentBackward")
postcode(
this.selectionStart,
this.selectionEnd + 1,
ev.data || "",
"you have been violated"
)
//else depending if there is a mouse selction
//use the start and end position of those
//or else use the current selection
//since the current selection is of the replaced already text
else
postcode(
mouseshenanigans ? startpos : this.selectionStart,
mouseshenanigans ? endpos : this.selectionEnd,
ev.data || "",
"you have been violated"
)
break
//simillar situation for pasting text
//except we don't have the paste
//so we are using the last saved position from the paste event
//to slice it from the replaced text
case "insertFromPaste":
postcode(
mouseshenanigans ? startpos : lastselectionstart,
mouseshenanigans ? endpos : lastselectionstart,
this.value.slice(lastselectionstart, this.selectionEnd),
"you have been violated"
)
break
//now here I have no idea how to make this work
case "historyRedo":
case "historyUndo":
console.log('\n')
console.log({
historyredo0: historyredo[currentredo][0],
historyredo1: historyredo[currentredo][1],
historyredo2: historyredo[currentredo][2]
})
if (this.selectionStart != this.selectionEnd)
postcode(
this.selectionStart,
this.selectionStart + historyredo[currentredo][1],
this.value.slice(
this.selectionStart,
this.selectionEnd
),
"you have been violated"
)
//trying to save some of the previous data
ev.inputType == "historyUndo" ? (historyredo.push([startopsinner, historyredo[currentredo][1], previoustext.slice(startopsinner, endposinner)]), ++currentredo) : (--currentredo, historyredo.pop())
break
}
//trying to save some of the previous data
const isnotundoorredo = ev.inputType != "historyRedo" && ev.inputType != "historyUndo"
isnotundoorredo && (historyredo.push([startopsinner, ev.data.length, previoustext.slice(startopsinner, endposinner)]), ++currentredo)
//since we had typed no more mouse shenanigans
mouseshenanigans = false
})
<textarea id="code"></textarea>
Upvotes: 0
Views: 325
Reputation: 44086
A much more intuitive way to develop a text editor is to use contenteditable
attribute on a non-input type element and the .execCommand()
method.
const editor = document.forms.editor;
const exc = editor.elements;
exc.undo.onclick = function(e) { document.execCommand('undo', false, null) }
exc.redo.onclick = function(e) { document.execCommand('redo', false, null) }
:root, body {font: 400 3vw/1 Consolas}
#text {min-height: 20vh;word-wrap:wrap;word-break:break-word;padding: 5px;overflow:hidden;}
button {font-size: 2rem; line-height: 1;padding: 0;border:0;cursor:pointer}
<form id='editor'>
<fieldset id='text' contenteditable></fieldset>
<fieldset id='btns'>
<button id='undo' type='button'>🔄</button>
<button id='redo' type='button'>🔀</button>
</fieldset>
</form>
Upvotes: 2