How can I "undo" the programmatic insertion of text into a textarea?
Asked Answered
S

12

32

I have a textarea and a button. Clicking the button causes text to be inserted into the textarea.

Is there a way to allow a user to press Ctrl/Cmd+z to undo the insertion of text and revert the textarea to its previous state?

Softshoe answered 28/11, 2012 at 2:54 Comment(0)
E
12

I think the easiest approach to leverage browser's undo stack instead of capturing events.

To do this you need to use different codes for different browsers. Luckily out of all major browsers only Firefox has a different approach.

// https://mcmap.net/q/42092/-how-to-detect-safari-chrome-ie-firefox-and-opera-browsers
// Opera 8.0+
var isOpera = (!!window.opr && !!opr.addons) || !!window.opera || navigator.userAgent.indexOf(' OPR/') >= 0;

// Firefox 1.0+
var isFirefox = typeof InstallTrigger !== 'undefined';

// Safari 3.0+ "[object HTMLElementConstructor]" 
var isSafari = Object.prototype.toString.call(window.HTMLElement).indexOf('Constructor') > 0 || (function(p) {
  return p.toString() === "[object SafariRemoteNotification]";
})(!window['safari'] || safari.pushNotification);

// Internet Explorer 6-11
var isIE = /*@cc_on!@*/ false || !!document.documentMode;

// Edge 20+
var isEdge = !isIE && !!window.StyleMedia;

// Chrome 1+
var isChrome = !!window.chrome && !!window.chrome.webstore;



var position = 0;

// text to anser
var text = 'Inserted Text';

// Just for fun :)
if (isFirefox)
  text = "  __ Firefox __ ";
else if (isIE)
  text = "  __ IE __ ";
else if (isEdge)
  text = "  __ Edge __ ";
else if (isSafari)
  text = "  __ Safari __ ";
else if (isOpera)
  text = "  __ Opera __ ";
else if (isChrome)
  text = "  __ Chrome __ ";

/* Adding text on click based on browser */
jQuery(".addText").on("click", function() {
  if (isFirefox) {
    // Firefox
    var val = jQuery(".textArea").val();

    var firstText = val.substring(0, position);
    var secondText = val.substring(position);

    jQuery(".textArea").val(firstText + text + secondText);
  } else {

    jQuery(".textArea").focus();
    var val = jQuery(".textArea").val();

    jQuery(".textArea")[0].selectionStart = position;
    jQuery(".textArea")[0].selectionEnd = position;

    document.execCommand('insertText', false, text);
  }
});

jQuery(".textArea").on("focusout", function(e) {
  position = jQuery(this)[0].selectionStart;
});
textarea {
  padding: 10px;
  font-family: Calibri;
  font-size: 18px;
  line-height: 1.1;
  resize: none;
}
.addText {
  padding: 5px 15px;
  transition: all 0.5s;
  border: 1px solid black;
  border-radius: 2px;
  background-color: #169c16;
  width: 70px;
  margin: 10px 0;
  color: white;
  cursor: pointer;
}
.addText:hover {
  background-color: #2776b9;
}
<script src="https://ajax.googleapis.com/ajax/libs/jquery/2.1.1/jquery.min.js"></script>
<textarea name='textArea' class='textArea' rows="7" cols="50">Suspendisse convallis, metus at laoreet congue, sapien dui ornare magna, a porttitor felis purus a ipsum. Morbi vulputate erat rhoncus, luctus neque ut, lacinia orci. Pellentesque habitant morbi tristique senectus et netus et malesuada fames ac turpis
  egestas. Fusce aliquam, nulla nec fringilla ultrices, ipsum lectus maximus nisl, ut laoreet purus lectus eget nisl. Duis blandit cursus nulla. Vestibulum consectetur, nunc non viverra condimentum, neque neque tincidunt diam, nec vestibulum neque nisi
  ac sem. Integer aliquam a leo id laoreet. Mauris ultrices mauris lorem, eu hendrerit odio volutpat ut. Nam eget hendrerit metus.</textarea>
<div class='addText'>
  Add Text
</div>

Tested on:

  • FF: 50
  • Chrome: 55
  • Edge 38
  • Safari: 5.1
  • Opera: 42
Enactment answered 19/1, 2017 at 2:33 Comment(4)
Thanks, this is the perfect answer. Other users suggested to rewrite browser's native undo functionality which was not good for me.Jayjaycee
Apparently FF now nukes the undo history for textareas upon manipulating the value. There is a related bugreport in FF which suggests to implement the insertText command properly: bugzilla.mozilla.org/show_bug.cgi?id=1220696 So hopefully we can simply use the insertText command for all browsers in at some point in future.Blanks
According to MDN, execCommand() is deprecated now. See related #44472199Taper
Great answer, but how can I remove text from the textarea, while preserving undo history?Alric
B
8

You need to insert the text in a special way so the user can use the normal undo/redo behaviour.

var textEvent = document.createEvent('TextEvent');

textEvent.initTextEvent('textInput', true, true, null, "new text");

document.getElementById("your-textarea").dispatchEvent(textEvent);
Begin answered 28/11, 2012 at 2:58 Comment(7)
This isn't working for me in Firefox (does work in Chrome though). Otherwise, this is perfect. The only caveat here is you need to call focus() on the textarea before you call dispatchEvent() on it, otherwise nothing happens. Any idea how I can get it to work in Firefox?Softshoe
@Softshoe Doesn't have the best browser support yet, but this is the way of the future. You could create your own history buffer and capture the undo/redo events and pull the text from there.Begin
It seems like Mozilla disabled this functionality for security reasons. Le sigh... :(Softshoe
The way of the future should be UndoManager but little progress seems to have been made in recent times.Tanbark
Where can I find documentation for this? I must be searching incorrectly, as I've come up with very little.Batts
The above code was inserting the text at the beginning of the textbox. I had to select everything first to get the contents to be replaced using: textbox.setSelectionRange(0, textbox.value.length);Aleksandrovsk
According to Chrome this method soon won't work anymore: chromestatus.com/feature/5718803933560832Aleksandrovsk
F
3

Save the original value of the textarea in its data:

var $textarea = $('textarea');

$('button').on('click', function () {
    var val = $textarea.val();

    $textarea.data('old-val', val).val(val + ' some text');
});

If you want an array of data (as @ahren suggested), use this:

var $textarea = $('textarea');

$('button').on('click', function () {
    var val = $textarea.val();

    if ( ! $textarea.data('old-val')) {
        $textarea.data('old-val', []);
    }

    $textarea.data('old-val').push(val);

    $textarea.val(val + ' some text');
});
Formative answered 28/11, 2012 at 2:55 Comment(1)
or even as an array and then you've built yourself a 'history'.Catalogue
S
0

Even if this question is a year ago old, I want to share my way too.

You can do it like this:

    $(function () {

  // Check to see if an array is not already defined.
  if (!$('#t').data('old-val')) {

    // If the check returns True we proceed to create an array in old-val.
    $('#t').data('old-val', []);

  }

  // Get the current content value.
  inputValue = $('#t').val();

  // Push it to the old-val array.
  $('#t').data('old-val').push(inputValue);

  // We start with a current array position of 0.
  curArrPos = 0;


  $('#c').click(function () {
    // Append a string to the #t.
    $('#t').val(' ==this is the 2nd appended text==');


    // Save the current content value.
    inputValue = $('#t').val();
    // Push it to the array.
    $('#t').data('old-val').push(inputValue);
    // Increment current array position.
    ++curArrPos;

  });


  $('#b').click(function () {
    // Append a string to the #t.
    $('#t').val(' ==this is the 1st appended text==');


    // Save the current content value.
    inputValue = $('#t').val();
    // Push it to the array.
    $('#t').data('old-val').push(inputValue);
    // Increment current array position.
    ++curArrPos;

  });

  $('#undo').click(function () {
    // First check that the old-val array length is greater than 1 (It's the initial position. No need undoing to a blank state) and current array position greater than 0 (for the same reason).
    if ($('#t').data('old-val').length > 1 && curArrPos > 0) {

      // Set current #t value to the one in the current array position, minus one.
      // Minus one gets you to the previous array position (ex. current=5; previous= current - 1 = 4).
      $('#t').val($('#t').data('old-val')[curArrPos - 1]);

      // Decrease current array position, because we effectively shifted back by 1 position.
      --curArrPos;
    }
  });

  $('#redo').click(function () {
    if (currentArrayPos < $('#c').data('old-val').length - 1) {

      $('#t').val($('#t').data('old-val')[curArrPos + 1]);

      // Increase current array position, because we effectively shifted forward by 1 position.
      ++curArrPos;

    }
  });

});

Here's a fiddle if you want to experiment with it http://jsfiddle.net/45Jwz/1/

I wrote the code like this for good comprehension, but you should of course write the actual code better and less verbose than this.

Snout answered 4/5, 2014 at 12:6 Comment(0)
B
0

Concrete libs and technologies strongly depends on your stack.

There are 2 general ways i can think of instantly.

First: Save the previous state in your controller. Hook into the shortcut and replace the content to the previous state. If other modifications are made delete the hook. More or less your 2013 approach

This is a quick way and does not play nice if you want a stack with more than a one time edit history.

Second: Watch the textinput and save the state on a stack periodicly. Hook into the shortcut. (Take over the whole process). This is a cleaner way because your modification is conceptually the same as user modifications.

This could be pretty straightforward with a redux/flux architecture http://redux.js.org/docs/recipes/ImplementingUndoHistory.html

For capturing cmd/ctrl+z you could look into https://github.com/madrobby/keymaster

If you elaborate a bit more about your stack/requirements i would be happy to expand this answer.

Beret answered 16/1, 2017 at 12:52 Comment(0)
G
0

Here is a thought:

If we can generate the keyborad events just as if the user is typing in the textarea, then browser will automatically be able to handle Undo event. So instead of just appending / changing the value of textarea, we should try to simulate keyboard events for the text that we want to insert.

As per the documentation at MDN (links given below), we can use KeyboardEvent object to generate the events like:

  var e1 = new KeyboardEvent(<type>, <details>);
  var b1 = <textbox>.dispatchEvent(e1);

Where:

  • <type> represents the event type such as keydown, keypress or keyup
  • <details> represents the object having event details such key, code
  • <textbox> represents the target textbox on which we want to trigger the event

Here is a JSFiddle where I've tried to simulate keydown, keypress and keyup events for each character in a given string. Though its firing the appropriate event handlers, somehow the chars are not getting displayed / added to textbox.

What I've noticed is that there are some differences in the event object generated when I type a in the textbox verses when I simulate the 3 events for a using my code. The differences are (when tested in Firefox 50.1.0):

  1. explicitOriginalTarget is different than originalTarget when I simulate the events; and when I type in textbox both have same value
  2. rangeParent and rangeOffset values are null / 0 when I type in textbox; and when I simulate the events they have some values
  3. isTrusted property is true when I type in textbox; and when I simulate the events its false (For any event generated using script it will have false)

MDN links:

Gallery answered 18/1, 2017 at 3:55 Comment(2)
This is interesting approach and I would be more happy to simulate key presses than rewriting undo functionality as other people suggestJayjaycee
"somehow the chars are not getting displayed / added to textbox" yea that's a security measure, browsers purposely don't let you do thatLightness
A
0

The simplest way seems this:

  • save previous textarea contents on button click, in an attribute of DOM textarea itself.
  • intercept keyboard at the BODY level (you don't know where the focus will be when and if Ctrl-Z is hit)
  • (optional) also intercept as many events from different objects as needed: input, keyPress, and so on, also at the BODY level.

Now:

  • when the user clicks the button, old contents is saved and a custom "dirty" attribute of textarea is set to true
  • if the user hits Ctrl-Z or Cmd-Z, the undo routine is straightforward (in jQuery that would be $('#idOfTextarea').val($('#idOfTextarea').attr('prevContents')); . The undo routine also clears the "dirty" attribute so that undo is not called twice.
  • (optional) if the user alters another field, clicks somewhere else etc., the dirty attribute of the textarea is also cleared, and the change becomes undoable.
  • (optional) objects with an undo function of their own can/must stop the event propagation to avoid other undo's having two effects.

You may want, or not, to also intercept onChange on the textarea. If onChange fires and dirty is not set, that is the initial button click and may be ignored. Otherwise it might indicate that the user has added some changes of his own to the text clicked in. In that case you may want to disable the undo to preserve those changes, or ask the user for confirmation.

Anther answered 18/1, 2017 at 12:43 Comment(2)
While yes this is simple idea, you are going to rewrite native browser functionality. I wonder is there a way to tell browser "hey we changed text, add it up to your undo history"Jayjaycee
I had searched this on Google, but apparently it is not supported (for security reasons?). If it is not part of the standard then you would have to cope with several alternative ways and you would still need a path of last resort...Anther
P
0

You can do like this,

HTML

<div id="text-content">
 <textarea rows="10" cols="50"></textarea>
   <input type="button" name="Insert" value="Insert" />
</div>

Jquery

var oldText = [],ctrl=false;
   $(function(){
      $("input[name=Insert]").click(function(){
          var text = oldText.length+1;
          oldText.push(text);
          var textAreaText = $('textarea').val();
          textAreaText +=text;
          $('textarea').val(textAreaText);
          $("input[name=insertText]").val('');
     });
     $('textarea').keydown(function(e){
            if(e.which == 17){ ctrl = true;}
     });
     $('textarea').keyup(function(e){
         var c = e.which;
         if(c == 17){ ctrl = false;}
         if(ctrl==true && c==90 ){
            oldText.pop(); 
            var text = '';
            $.each(oldText,function(i,ele){
               text += ele;
            });
            $('textarea').val(text);
        }
     })
  })

You can also check and experiment with the fiddle.

Prevost answered 18/1, 2017 at 14:16 Comment(0)
T
0

Add html first and you can use keypress event for undo

you can try here also here http://jsfiddle.net/surendra786/1v5jxaa0/

            <input type="text" class="reset actor_input" name="actor" value="add actors"></input>

            <input type="text" name="actors"></input>

            <div class="found_actors"></div>

            <div id="add" class="button_content">ADD</div>

            <div id="undo" class="button_content">UNDO</div>

            <div class="actors_list"><textarea readonly style="resize: none;" rows="20" cols="20" name="actors-list"></textarea></div>

        </div>

**then add jquery **

    var items = [];

$("#add").click(function() {
    // Push the new actor in the array
    items.push($("[name='actor']").val());
    populate();
});



$(document).keydown(function(e){
       if( e.which === 90 && e.ctrlKey ){
         console.log('control + z'); 
         if (items.length > 0) {
        // remove last element of the array
        items.splice(-1,1);
        populate();
    }
      }          
}); 

populate = function() {
    $("[name='actors-list']").text('');
    $("[name='actors-list']").append(items.join('&#13;&#10;'));
    $("[name='actors']").val(items.join(','));   
}

you can try here http://jsfiddle.net/surendra786/1v5jxaa0/

this is working for me

Thermoluminescence answered 18/1, 2017 at 16:47 Comment(0)
G
0

There are alot of applicable answers here that would work for what you want. Here is what I would do in your circumstance. This allows for the changing of a text variable that you would most likely use, you can set it or the user can set it with another field, etc.

codepen here

the jist of it would be something like this.

$(document).ready(function(){
  function deployText(){
    var textArray = [];
    var textToAdd = 'let\'s go ahead and add some more text';
    var textarea = $('textarea');
    var origValue = textarea.text();
    textArray.push(origValue);

    $('button').on('click', function(e){
      textArray.push(textToAdd);
      textarea.text(textArray);
      console.log(textArray.length);
      console.log(textArray);
    });

    $(document).on('keypress', function(e){
      var zKey = 26;
      if(e.ctrlKey && e.which === zKey){
        removePreviousText();
      }
    })

    function removePreviousText(){
      console.log(textArray);
      if(textArray.length > 1){
        textArray.pop();
        $('textarea').text(textArray);
      }
    }
  }
  deployText()
})
Greenstein answered 19/1, 2017 at 0:5 Comment(0)
J
0

This is what I used in 2023. Tested on Chrome, FireFox, and Safari. It works!

3 lines of code that matter.

  1. textarea.select(); ⬅️ selects all the text, so that it can be deleted.
  2. document.execCommand("delete", false, null); ⬅️ deletes selected text. (allowing you to undo that removal, and whatever happened before it).
  3. document.execCommand("insertText", false, phrase_to_insert); ⬅️ updates your textarea with the phrase_to_insert

And yes, document.execCommand() is now "obsolete", whatever that means... There is no alternative yet the "obsolete" function works. Either dictionaries are going to need to redefine "obsolete", or browsers are going to need to get their act together?

Here's the code. Replace your ID/selector for the textarea, as well as the phrase_to_insert.

function insert_text(){
  var textarea = document.getElementById('my_textarea'); // use your own textarea!
  var phrase_to_insert = "Hello World"; // use your own phrase!

  // to select everything in the text
  textarea.select();

  // remove existing selected text in textarea (needed for undo.)
  document.execCommand("delete", false, null);

  // insertText, while making undo (⌘/ctrl + z) possible.
  document.execCommand("insertText", false, phrase_to_insert);
}
<p>1. Try writing in the textarea.</p>
<p>2. Push the button.</p>
<p>3. Undo, and mess around!</p>
<textarea id="my_textarea"></textarea>
<br>
<button onclick="insert_text();">Push me</button>
Jemappes answered 16/2, 2023 at 3:33 Comment(3)
in FF you must undo twice to get the original textMomism
But it works?! I didn't realize that this would work in FF. Awesome! Sounds like my solution is best :) I take upvotes as paymentJemappes
execCommand is deprecatedLightness
M
-1
$("#target").keypress(function(event) {
  if ( event.which == 'Z' && first press == 'Cmd' && second press == 'Ctrl') {//check for key press
     event.preventDefault();
    text.value = defaultValueForTextField
   }
});

That should be what you are looking for. First and Second press will need to be saved as you want combo presses. You will indeed need to save a default text value though.

Mononucleosis answered 28/11, 2012 at 3:1 Comment(1)
I'm not entirely sure how to use this snippet, since it doesn't seem to me like you are detecting two concurrent keypresses here. Am I wrong?Softshoe

© 2022 - 2024 — McMap. All rights reserved.