3

I currently have bound my textarea to a couple of events which seems to work. However, the problem is that the events overlap and fire several times, which in turn reduces performance by a factor of too much.

What I want to do is pretty much catch any change to the textarea: clicking, paste, keyup, keydown, right click context menu editing (right click, cut/delete/paste), drag and drop, etc. This has to work cross-browser and at least down to IE8. The events have to fire when you move the caret around in the textarea using arrowkeys or similar (I handle changes based on caret position, among other things).

I can't use any major delays. As soon as you do something with the textarea, the events have to fire and execute whatever code I have there immediately.

I am currently using jQuery to bind the event, but I am fine with a pure javascript solution as long as it works cross browser and does what I want.

Here's the code I currently use:

var deadKeycodes = [16, 17, 18, 19, 20, 
                    27, 33, 34, 35, 36,
                    38, 40, 44, //37 = left arrow and 39 = right arrow removed, it needs to trigger on those
                    45, 112, 113, 114, 115,
                    116, 117, 118, 119, 120,
                    121, 122, 123, 144, 145];

$(original).bind('propertychange keyup keydown input click', function(e) { 
    if (!Array.prototype.indexOf || deadKeycodes.indexOf(e.keyCode) == -1) { // prevent execution when pressing a 'dead' key
         //do stuff here
    }
});

If anything is unclear just ask and I'll clarify it for you :)

Brian Tompsett - 汤莱恩
  • 5,753
  • 72
  • 57
  • 129
Firas Dib
  • 2,743
  • 19
  • 38
  • Already answered http://stackoverflow.com/questions/2823733/textarea-onchange-detection – bchhun Jan 03 '13 at 21:57
  • keydown and click are all you really need, maybe the paste and cut events too if click doesn't cover that. – Kevin B Jan 03 '13 at 22:01
  • 1
    Keyup and keydown are fired at different times. Hold a key down in a textarea and see (and look when the markdown preview updates). In general keyup and keydown are not great for determining when input has changed (though you can use both for something acceptable). – Alex Churchill Jan 03 '13 at 22:03
  • keydown paired with a setTimeout set to 0 works very well for detecting changes instantly on keydown rather than waiting for the keyup. – Kevin B Jan 03 '13 at 22:07

4 Answers4

1

I don't think you can really "solve" the problem in the sense of stopping multiple events from firing. Keyup and keydown really are different events and happen at different times. If you want to respond to both (which you probably do, since keying down and keying up will both potentially change the textarea), both events need to be included. However, most of the time they will fire almost simultaneously (and many times in a row), which as you point out can pose a performance problem.

Instead, you should probably consider firing a throttled or debounced callback. A throttled callback only will fire once every n milliseconds (good for functions that might get called too much). A debounced callback will only fire after a stream of events is done; after n milliseconds have elapsed since the last callback.

You can easily accomplish this using underscore's debounce and throttle functions.

Something like:

debouncedFn = _.debounce(function(e) { 
    if (!Array.prototype.indexOf || deadKeycodes.indexOf(e.keyCode) == -1) { // prevent execution when pressing a 'dead' key
         //do stuff here
    }
}, 100);

$(original).bind('propertychange keyup keydown input click', debouncedFn);
Alex Churchill
  • 4,887
  • 5
  • 30
  • 42
  • Hmm, I'll use this as a last resort. I don't want to add an unnecessary library if I don't _really_ have to. – Firas Dib Jan 04 '13 at 00:26
  • If so, hack it yourself instead of using the library. It should take max 20 lines. Just use the `Date` object and subtract. – Alex Churchill Jan 04 '13 at 06:25
  • But seriously it's better to use the library. Underscore is super-compact (especially compared to jQuery -- it's like 20 times smaller) and you should be using it (or something fulfilling a similar purpose, like [sugar](http://sugarjs.com/)) for any javascript project longer than a few hundred lines. Having an aversion to core libraries is generally not a good idea. – Alex Churchill Jan 04 '13 at 06:28
1

Your original is overkill. All you need are the input and propertychange events.

2016 Update

The originally linked page has now disappeared. Here's a slightly suboptimal snapshot of it:

http://web.archive.org/web/20140810185102/http://whattheheadsaid.com/2010/09/effectively-detecting-user-input-in-javascript

Here's an answer demonstrating using the input event and falling back to propertychange in IE <= 8:

Catch only keypresses that change input?

Community
  • 1
  • 1
Tim Down
  • 318,141
  • 75
  • 454
  • 536
0

This seems to solve it in IE7-9 and Chrome, haven't tested the rest. Only one console.log happens per change regardless of what the change was. If there were no changes, nothing is logged: http://jsfiddle.net/SJN6J/2/

var timer;
$("textarea").on("keydown paste cut", function(){
    clearTimeout(timer);
    var origvalue = this.value, self = this;
    timer = setTimeout(function(){
        if ( origvalue !== self.value ) {
            console.log("it changed!");
            // do stuff because content changed
        }
    },0);
});

Note: my IE7-8 testing was with IE9 changing browser mode, so you may want to do real IE7-8 testing.

Kevin B
  • 94,570
  • 16
  • 163
  • 180
  • This _kind of_ works, but I need to trigger on input because if you select and right click -> delete, it does not trigger. However, when I add 'input' it breaks. – Firas Dib Jan 03 '13 at 23:14
0

To prevent the event overlap, you could store the time of the last call. Before you execute an event's callback, you test if enough time has passed to make sense to fire again.

$(original).bind('propertychange keyup keydown input click', (function () {
    var lastCallTime = 0;

    return function (e) {
        if (Date.now() - lastCallTime < 50) { return; } // Too little time has passed--exit

        if (!Array.prototype.indexOf || deadKeycodes.indexOf(e.keyCode) == -1) {
            lastCallTime = Date.now();

            // your code...
        }
    };
}()));
Brian Ustas
  • 62,713
  • 3
  • 28
  • 21