anub
anub

Reputation: 527

Undo is working for only first replaced text?

Here I am using Ctrl+Z for undoing the replaced text, I have a scenario that, in text-area, I have a sentence with multiple words, where I select first word and replaced with stars,later I select another word and replaced with stars. But when did Ctrl+Z it is only working for latest selected word and not working for previous words.

JavaScript:

   var selection = {};

function undo(e) {
    var evtobj = window.event? window.event : e;
    if (evtobj.keyCode == 90 && evtobj.ctrlKey && selection.text) {
        evtobj.preventDefault();
        var txtarea = document.getElementById("mytextarea");
        var allText = txtarea.value;
        var newText = allText.substring(0, selection.start) + selection.text + allText.substring(selection.finish, allText.length);
        txtarea.value = newText;
    }
}

function getSel() {
    // obtain the object reference for the textarea>
    var txtarea = document.getElementById("mytextarea");
    // obtain the index of the first selected character
    var start = txtarea.selectionStart;
    // obtain the index of the last selected character
    var finish = txtarea.selectionEnd;
    //obtain all Text
    var allText = txtarea.value;

    selection.text = allText.substring(start, finish);
    selection.start = start;
    selection.finish = finish;

    // obtain the selected text
    var sel = allText.substring(start, finish);
    sel = sel.replace(/[\S]/g, "*"); //append te text;
    var newText = allText.substring(0, start) + sel + allText.substring(finish, allText.length);
    txtarea.value = newText;

    $('#newpost').offset({top: 0, left: 0}).hide();
}
function closePopUp() {
    $('#newpost').offset({top: 0, left: 0}).hide();
}

$(document).ready(function () {
    closePopUp();
    var newpost = $('#newpost');
    $('#mytextarea').on('select', function (e) {
        var txtarea = document.getElementById("mytextarea");
        var start = txtarea.selectionStart;
        var finish = txtarea.selectionEnd;
        newpost.offset(getCursorXY(txtarea, start, 20)).show();
        newpost.find('div').text('replace with stars');
    }).on('input', () => selection.text = null);
    document.onkeydown = undo;

});

var getCursorXY = function getCursorXY(input, selectionPoint, offset) {
    var inputX = input.offsetLeft,
        inputY = input.offsetTop;
    // create a dummy element that will be a clone of our input

    var div = document.createElement('div');
    // get the computed style of the input and clone it onto the dummy element
    var copyStyle = getComputedStyle(input);
    var _iteratorNormalCompletion = true;
    var _didIteratorError = false;
    var _iteratorError = undefined;

    try {
        for (var _iterator = copyStyle[Symbol.iterator](), _step; !(_iteratorNormalCompletion = (_step = _iterator.next()).done) ; _iteratorNormalCompletion = true) {
            var prop = _step.value;

            div.style[prop] = copyStyle[prop];
        }
        // we need a character that will replace whitespace when filling our dummy element 
        // if it's a single line <input/>
    } catch (err) {
        _didIteratorError = true;
        _iteratorError = err;
    } finally {
        try {
            if (!_iteratorNormalCompletion && _iterator.return) {
                _iterator.return();
            }
        } finally {
            if (_didIteratorError) {
                throw _iteratorError;
            }
        }
    }

    var swap = '.';
    var inputValue = input.tagName === 'INPUT' ? input.value.replace(/ /g, swap) : input.value;
    // set the div content to that of the textarea up until selection
    var textContent = inputValue.substr(0, selectionPoint);
    // set the text content of the dummy element div
    div.textContent = textContent;
    if (input.tagName === 'TEXTAREA') div.style.height = 'auto';
    // if a single line input then the div needs to be single line and not break out like a text area
    if (input.tagName === 'INPUT') div.style.width = 'auto';
    // create a marker element to obtain caret position
    var span = document.createElement('span');
    // give the span the textContent of remaining content so that the recreated dummy element 
    // is as close as possible
    span.textContent = inputValue.substr(selectionPoint) || '.';
    // append the span marker to the div
    div.appendChild(span);
    // append the dummy element to the body
    document.body.appendChild(div);
    // get the marker position, this is the caret position top and left relative to the input
    var spanX = span.offsetLeft,
        spanY = span.offsetTop;
    // lastly, remove that dummy element
    // NOTE:: can comment this out for debugging purposes if you want to see where that span is rendered

    document.body.removeChild(div);
    // return an object with the x and y of the caret. account for input positioning 
    // so that you don't need to wrap the input
    return {
        left: inputX + spanX,
        top: inputY + spanY + offset
    };
};

Here is my Plunker.

Upvotes: 2

Views: 196

Answers (2)

Kirill Simonov
Kirill Simonov

Reputation: 8491

You have to manually control all your textarea changes.

Here I created the edits array which is populated with textarea text on change every few seconds (you can control it using the saveInterval variable). You also can set the max length of this array using maxHistorySize. When the array is full, the old changes are lost.

var edits = [""];
var interval = true;
var maxHistorySize = 10;
var saveInterval = 3000;

function undo(e) {
      var evtobj = window.event? window.event : e;
      if (evtobj.keyCode == 90 && evtobj.ctrlKey) {
          evtobj.preventDefault();
          var txtarea = document.getElementById("mytextarea");
          var previousText = edits.length === 1 ? edits[0] : edits.pop();
          if (previousText !== undefined) {
              txtarea.value = previousText;
          }
      }
}

function getSel() {
    // obtain the object reference for the textarea>
    var txtarea = document.getElementById("mytextarea");
    // obtain the index of the first selected character
    var start = txtarea.selectionStart;
    // obtain the index of the last selected character
    var finish = txtarea.selectionEnd;
    //obtain all Text
    var allText = txtarea.value;
    
    edits.push(allText);
    if (edits.length > maxHistorySize) edits.shift();

    // obtain the selected text
    var sel = Array(finish - start + 1).join("*");
    //append te text;
    var newText = allText.substring(0, start) + sel + allText.substring(finish, allText.length);
    txtarea.value = newText;
    
    $('#newpost').offset({top: 0, left: 0}).hide();
}
function closePopUp() {
    $('#newpost').offset({top: 0, left: 0}).hide();
}

$(document).ready(function () {
    closePopUp();
    var newpost = $('#newpost');
    $('#mytextarea').on('select', function (e) {
        var txtarea = document.getElementById("mytextarea");
        var start = txtarea.selectionStart;
        var finish = txtarea.selectionEnd;
        newpost.offset(getCursorXY(txtarea, start, 20)).show();
        newpost.find('div').text(Array(finish - start + 1).join("*"));
    }).on('input', function() {
        if (interval) {
            interval = false;
            edits.push($(this).val());
            if (edits.length > maxHistorySize) edits.shift();
            setTimeout(() => interval = true, saveInterval);
        }
    });
    document.onkeydown = undo;
});

const getCursorXY = (input, selectionPoint, offset) => {
  const {
    offsetLeft: inputX,
    offsetTop: inputY,
  } = input
  // create a dummy element that will be a clone of our input
  const div = document.createElement('div')
  // get the computed style of the input and clone it onto the dummy element
  const copyStyle = getComputedStyle(input)
  for (const prop of copyStyle) {
    div.style[prop] = copyStyle[prop]
  }
  // we need a character that will replace whitespace when filling our dummy element 
  // if it's a single line <input/>
  const swap = '.'
  const inputValue = input.tagName === 'INPUT' ? input.value.replace(/ /g, swap) : input.value
  // set the div content to that of the textarea up until selection
  const textContent = inputValue.substr(0, selectionPoint)
  // set the text content of the dummy element div
  div.textContent = textContent
  if (input.tagName === 'TEXTAREA') div.style.height = 'auto'
  // if a single line input then the div needs to be single line and not break out like a text area
  if (input.tagName === 'INPUT') div.style.width = 'auto'
  // create a marker element to obtain caret position
  const span = document.createElement('span')
  // give the span the textContent of remaining content so that the recreated dummy element 
  // is as close as possible
  span.textContent = inputValue.substr(selectionPoint) || '.'
  // append the span marker to the div
  div.appendChild(span)
  // append the dummy element to the body
  document.body.appendChild(div)
  // get the marker position, this is the caret position top and left relative to the input
  const { offsetLeft: spanX, offsetTop: spanY } = span
  // lastly, remove that dummy element
  // NOTE:: can comment this out for debugging purposes if you want to see where that span is rendered
  document.body.removeChild(div)
  // return an object with the x and y of the caret. account for input positioning 
  // so that you don't need to wrap the input
  return {
    left: inputX + spanX,
    top: inputY + spanY + offset,
  }
}
#mytextarea {width: 600px; height: 200px; overflow:hidden; position:fixed}
#newpost {
    position:absolute;
    background-color:#ffffdc;
    border:1px solid #DCDCDC;
    border-radius:10px;
    padding-right:5px; 
    width: auto;
    height: 30px;
}
#newpost span {
    cursor:pointer;
    position: absolute;
    top: 0;
    right: 5px;
    font-size: 22px;
}
#newpost div {
    color:#0000ff;
    padding:10px;
    margin-right:10px;
    width: auto;
    cursor:pointer;
}
<script src="https://ajax.googleapis.com/ajax/libs/jquery/2.1.1/jquery.min.js"></script>
<html>
  <head>
  </head>
 <body>
    <textArea id="mytextarea"></textArea>
    <div id="newpost">
        <span onclick="closePopUp();">&#735;</span>
        <div onclick="getSel()"></div>
    </div>
</body>

</html>

Upvotes: 2

Adelin
Adelin

Reputation: 8219

You need to change your logic from using a single selection object to using an array of selection objects, like this:

  1. Initialize to selections instead of selection

    var selections = [];
    
  2. When getSel() is called, make sure you update to push to array:

    function getSel() {
        // obtain the object reference for the textarea>
        var txtarea = document.getElementById("mytextarea");
        // obtain the index of the first selected character
        var start = txtarea.selectionStart;
        // obtain the index of the last selected character
        var finish = txtarea.selectionEnd;
        //obtain all Text
        var allText = txtarea.value;
    
        selections.push({
          start: start,
          finish: finish,
          text: allText.substring(start, finish)
        });
        // obtain the selected text
        var sel = allText.substring(start, finish);
        sel = sel.replace(/[\S]/g, "*"); //append te text;
        var newText = allText.substring(0, start) + sel + allText.substring(finish, allText.length);
        txtarea.value = newText;
    
        $('#newpost').offset({top: 0, left: 0}).hide();
    }
    
  3. As such, we need to update undo function, as follows:

    function undo(e) {
        var evtobj = window.event? window.event : e;
    
        if (evtobj.keyCode == 90 && evtobj.ctrlKey) {
            evtobj.preventDefault();
    
            if (selections.length === 0) alert ("Can't do more undos");
            else {
                var thisSelection = selections.pop();
                var txtarea = document.getElementById("mytextarea");
                var allText = txtarea.value;
                var newText = allText.substring(0, thisSelection.start) + thisSelection.text + allText.substring(thisSelection.finish, allText.length);
                txtarea.value = newText;
            }
        }
    }
    
  4. Lastly, remove the on('input') event listener from $('#mytextarea') as I don't think it's needed

Here's the plunk with all the changes

Upvotes: 1

Related Questions