AnArrayOfFunctions
AnArrayOfFunctions

Reputation: 3754

How to make undo & redo work with my solution for text syncing?

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

Answers (1)

zer00ne
zer00ne

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.

Demo

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

Related Questions