Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
seleniumhq
GitHub Repository: seleniumhq/selenium
Path: blob/trunk/third_party/closure/goog/events/pastehandler.js
2868 views
1
// Copyright 2009 The Closure Library Authors. All Rights Reserved.
2
//
3
// Licensed under the Apache License, Version 2.0 (the "License");
4
// you may not use this file except in compliance with the License.
5
// You may obtain a copy of the License at
6
//
7
// http://www.apache.org/licenses/LICENSE-2.0
8
//
9
// Unless required by applicable law or agreed to in writing, software
10
// distributed under the License is distributed on an "AS-IS" BASIS,
11
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
// See the License for the specific language governing permissions and
13
// limitations under the License.
14
15
/**
16
* @fileoverview Provides a 'paste' event detector that works consistently
17
* across different browsers.
18
*
19
* IE5, IE6, IE7, Safari3.0 and FF3.0 all fire 'paste' events on textareas.
20
* FF2 doesn't. This class uses 'paste' events when they are available
21
* and uses heuristics to detect the 'paste' event when they are not available.
22
*
23
* Known issue: will not detect paste events in FF2 if you pasted exactly the
24
* same existing text.
25
* Known issue: Opera + Mac doesn't work properly because of the meta key. We
26
* can probably fix that. TODO(user): {@link KeyboardShortcutHandler} does not
27
* work either very well with opera + mac. fix that.
28
*
29
* @see ../demos/pastehandler.html
30
*/
31
32
goog.provide('goog.events.PasteHandler');
33
goog.provide('goog.events.PasteHandler.EventType');
34
goog.provide('goog.events.PasteHandler.State');
35
36
goog.require('goog.Timer');
37
goog.require('goog.async.ConditionalDelay');
38
goog.require('goog.events.BrowserEvent');
39
goog.require('goog.events.EventHandler');
40
goog.require('goog.events.EventTarget');
41
goog.require('goog.events.EventType');
42
goog.require('goog.events.KeyCodes');
43
goog.require('goog.log');
44
goog.require('goog.userAgent');
45
46
47
48
/**
49
* A paste event detector. Gets an {@code element} as parameter and fires
50
* {@code goog.events.PasteHandler.EventType.PASTE} events when text is
51
* pasted in the {@code element}. Uses heuristics to detect paste events in FF2.
52
* See more details of the heuristic on {@link #handleEvent_}.
53
*
54
* @param {Element} element The textarea element we are listening on.
55
* @constructor
56
* @extends {goog.events.EventTarget}
57
*/
58
goog.events.PasteHandler = function(element) {
59
goog.events.EventTarget.call(this);
60
61
/**
62
* The element that you want to listen for paste events on.
63
* @type {Element}
64
* @private
65
*/
66
this.element_ = element;
67
68
/**
69
* The last known value of the element. Kept to check if things changed. See
70
* more details on {@link #handleEvent_}.
71
* @type {string}
72
* @private
73
*/
74
this.oldValue_ = this.element_.value;
75
76
/**
77
* Handler for events.
78
* @type {goog.events.EventHandler<!goog.events.PasteHandler>}
79
* @private
80
*/
81
this.eventHandler_ = new goog.events.EventHandler(this);
82
83
/**
84
* The last time an event occurred on the element. Kept to check whether the
85
* last event was generated by two input events or by multiple fast key events
86
* that got swallowed. See more details on {@link #handleEvent_}.
87
* @type {number}
88
* @private
89
*/
90
this.lastTime_ = goog.now();
91
92
if (goog.events.PasteHandler.SUPPORTS_NATIVE_PASTE_EVENT) {
93
// Most modern browsers support the paste event.
94
this.eventHandler_.listen(
95
element, goog.events.EventType.PASTE, this.dispatch_);
96
} else {
97
// But FF2 and Opera doesn't. we listen for a series of events to try to
98
// find out if a paste occurred. We enumerate and cover all known ways to
99
// paste text on textareas. See more details on {@link #handleEvent_}.
100
var events = [
101
goog.events.EventType.KEYDOWN, goog.events.EventType.BLUR,
102
goog.events.EventType.FOCUS, goog.events.EventType.MOUSEOVER, 'input'
103
];
104
this.eventHandler_.listen(element, events, this.handleEvent_);
105
}
106
107
/**
108
* ConditionalDelay used to poll for changes in the text element once users
109
* paste text. Browsers fire paste events BEFORE the text is actually present
110
* in the element.value property.
111
* @type {goog.async.ConditionalDelay}
112
* @private
113
*/
114
this.delay_ =
115
new goog.async.ConditionalDelay(goog.bind(this.checkUpdatedText_, this));
116
117
};
118
goog.inherits(goog.events.PasteHandler, goog.events.EventTarget);
119
120
121
/**
122
* The types of events fired by this class.
123
* @enum {string}
124
*/
125
goog.events.PasteHandler.EventType = {
126
/**
127
* Dispatched as soon as the paste event is detected, but before the pasted
128
* text has been added to the text element we're listening to.
129
*/
130
PASTE: 'paste',
131
132
/**
133
* Dispatched after detecting a change to the value of text element
134
* (within 200msec of receiving the PASTE event).
135
*/
136
AFTER_PASTE: 'after_paste'
137
};
138
139
140
/**
141
* The mandatory delay we expect between two {@code input} events, used to
142
* differentiated between non key paste events and key events.
143
* @type {number}
144
*/
145
goog.events.PasteHandler.MANDATORY_MS_BETWEEN_INPUT_EVENTS_TIE_BREAKER = 400;
146
147
148
/**
149
* Whether current UA supoprts the native "paste" event type.
150
* @const {boolean}
151
*/
152
goog.events.PasteHandler.SUPPORTS_NATIVE_PASTE_EVENT = goog.userAgent.WEBKIT ||
153
goog.userAgent.IE || goog.userAgent.EDGE ||
154
(goog.userAgent.GECKO && goog.userAgent.isVersionOrHigher('1.9'));
155
156
157
/**
158
* The period between each time we check whether the pasted text appears in the
159
* text element or not.
160
* @type {number}
161
* @private
162
*/
163
goog.events.PasteHandler.PASTE_POLLING_PERIOD_MS_ = 50;
164
165
166
/**
167
* The maximum amount of time we want to poll for changes.
168
* @type {number}
169
* @private
170
*/
171
goog.events.PasteHandler.PASTE_POLLING_TIMEOUT_MS_ = 200;
172
173
174
/**
175
* The states that this class can be found, on the paste detection algorithm.
176
* @enum {string}
177
*/
178
goog.events.PasteHandler.State = {
179
INIT: 'init',
180
FOCUSED: 'focused',
181
TYPING: 'typing'
182
};
183
184
185
/**
186
* The initial state of the paste detection algorithm.
187
* @type {goog.events.PasteHandler.State}
188
* @private
189
*/
190
goog.events.PasteHandler.prototype.state_ = goog.events.PasteHandler.State.INIT;
191
192
193
/**
194
* The previous event that caused us to be on the current state.
195
* @type {?string}
196
* @private
197
*/
198
goog.events.PasteHandler.prototype.previousEvent_;
199
200
201
/**
202
* A logger, used to help us debug the algorithm.
203
* @type {goog.log.Logger}
204
* @private
205
*/
206
goog.events.PasteHandler.prototype.logger_ =
207
goog.log.getLogger('goog.events.PasteHandler');
208
209
210
/** @override */
211
goog.events.PasteHandler.prototype.disposeInternal = function() {
212
goog.events.PasteHandler.superClass_.disposeInternal.call(this);
213
this.eventHandler_.dispose();
214
this.eventHandler_ = null;
215
this.delay_.dispose();
216
this.delay_ = null;
217
};
218
219
220
/**
221
* Returns the current state of the paste detection algorithm. Used mostly for
222
* testing.
223
* @return {goog.events.PasteHandler.State} The current state of the class.
224
*/
225
goog.events.PasteHandler.prototype.getState = function() {
226
return this.state_;
227
};
228
229
230
/**
231
* Returns the event handler.
232
* @return {goog.events.EventHandler<T>} The event handler.
233
* @protected
234
* @this {T}
235
* @template T
236
*/
237
goog.events.PasteHandler.prototype.getEventHandler = function() {
238
return this.eventHandler_;
239
};
240
241
242
/**
243
* Checks whether the element.value property was updated, and if so, dispatches
244
* the event that let clients know that the text is available.
245
* @return {boolean} Whether the polling should stop or not, based on whether
246
* we found a text change or not.
247
* @private
248
*/
249
goog.events.PasteHandler.prototype.checkUpdatedText_ = function() {
250
if (this.oldValue_ == this.element_.value) {
251
return false;
252
}
253
goog.log.info(this.logger_, 'detected textchange after paste');
254
this.dispatchEvent(goog.events.PasteHandler.EventType.AFTER_PASTE);
255
return true;
256
};
257
258
259
/**
260
* Dispatches the paste event.
261
* @param {goog.events.BrowserEvent} e The underlying browser event.
262
* @private
263
*/
264
goog.events.PasteHandler.prototype.dispatch_ = function(e) {
265
var event = new goog.events.BrowserEvent(e.getBrowserEvent());
266
event.type = goog.events.PasteHandler.EventType.PASTE;
267
this.dispatchEvent(event);
268
269
// Starts polling for updates in the element.value property so we can tell
270
// when do dispatch the AFTER_PASTE event. (We do an initial check after an
271
// async delay of 0 msec since some browsers update the text right away and
272
// our poller will always wait one period before checking).
273
goog.Timer.callOnce(function() {
274
if (!this.checkUpdatedText_()) {
275
this.delay_.start(
276
goog.events.PasteHandler.PASTE_POLLING_PERIOD_MS_,
277
goog.events.PasteHandler.PASTE_POLLING_TIMEOUT_MS_);
278
}
279
}, 0, this);
280
};
281
282
283
/**
284
* The main event handler which implements a state machine.
285
*
286
* To handle FF2, we enumerate and cover all the known ways a user can paste:
287
*
288
* 1) ctrl+v, shift+insert, cmd+v
289
* 2) right click -> paste
290
* 3) edit menu -> paste
291
* 4) drag and drop
292
* 5) middle click
293
*
294
* (1) is easy and can be detected by listening for key events and finding out
295
* which keys are pressed. (2), (3), (4) and (5) do not generate a key event,
296
* so we need to listen for more than that. (2-5) all generate 'input' events,
297
* but so does key events. So we need to have some sort of 'how did the input
298
* event was generated' history algorithm.
299
*
300
* (2) is an interesting case in Opera on a Mac: since Macs does not have two
301
* buttons, right clicking involves pressing the CTRL key. Even more interesting
302
* is the fact that opera does NOT set the e.ctrlKey bit. Instead, it sets
303
* e.keyCode = 0.
304
* {@link http://www.quirksmode.org/js/keys.html}
305
*
306
* (1) is also an interesting case in Opera on a Mac: Opera is the only browser
307
* covered by this class that can detect the cmd key (FF2 can't apparently). And
308
* it fires e.keyCode = 17, which is the CTRL key code.
309
* {@link http://www.quirksmode.org/js/keys.html}
310
*
311
* NOTE(user, pbarry): There is an interesting thing about (5): on Linux, (5)
312
* pastes the last thing that you highlighted, not the last thing that you
313
* ctrl+c'ed. This code will still generate a {@code PASTE} event though.
314
*
315
* We enumerate all the possible steps a user can take to paste text and we
316
* implemented the transition between the steps in a state machine. The
317
* following is the design of the state machine:
318
*
319
* matching paths:
320
*
321
* (1) happens on INIT -> FOCUSED -> TYPING -> [e.ctrlKey & e.keyCode = 'v']
322
* (2-3) happens on INIT -> FOCUSED -> [input event happened]
323
* (4) happens on INIT -> [mouseover && text changed]
324
*
325
* non matching paths:
326
*
327
* user is typing normally
328
* INIT -> FOCUS -> TYPING -> INPUT -> INIT
329
*
330
* @param {goog.events.BrowserEvent} e The underlying browser event.
331
* @private
332
*/
333
goog.events.PasteHandler.prototype.handleEvent_ = function(e) {
334
// transition between states happen at each browser event, and depend on the
335
// current state, the event that led to this state, and the event input.
336
switch (this.state_) {
337
case goog.events.PasteHandler.State.INIT: {
338
this.handleUnderInit_(e);
339
break;
340
}
341
case goog.events.PasteHandler.State.FOCUSED: {
342
this.handleUnderFocused_(e);
343
break;
344
}
345
case goog.events.PasteHandler.State.TYPING: {
346
this.handleUnderTyping_(e);
347
break;
348
}
349
default: {
350
goog.log.error(this.logger_, 'invalid ' + this.state_ + ' state');
351
}
352
}
353
this.lastTime_ = goog.now();
354
this.oldValue_ = this.element_.value;
355
goog.log.info(this.logger_, e.type + ' -> ' + this.state_);
356
this.previousEvent_ = e.type;
357
};
358
359
360
/**
361
* {@code goog.events.PasteHandler.EventType.INIT} is the first initial state
362
* the textarea is found. You can only leave this state by setting focus on the
363
* textarea, which is how users will input text. You can also paste things using
364
* drag and drop, which will not generate a {@code goog.events.EventType.FOCUS}
365
* event, but will generate a {@code goog.events.EventType.MOUSEOVER}.
366
*
367
* For browsers that support the 'paste' event, we match it and stay on the same
368
* state.
369
*
370
* @param {goog.events.BrowserEvent} e The underlying browser event.
371
* @private
372
*/
373
goog.events.PasteHandler.prototype.handleUnderInit_ = function(e) {
374
switch (e.type) {
375
case goog.events.EventType.BLUR: {
376
this.state_ = goog.events.PasteHandler.State.INIT;
377
break;
378
}
379
case goog.events.EventType.FOCUS: {
380
this.state_ = goog.events.PasteHandler.State.FOCUSED;
381
break;
382
}
383
case goog.events.EventType.MOUSEOVER: {
384
this.state_ = goog.events.PasteHandler.State.INIT;
385
if (this.element_.value != this.oldValue_) {
386
goog.log.info(this.logger_, 'paste by dragdrop while on init!');
387
this.dispatch_(e);
388
}
389
break;
390
}
391
default: {
392
goog.log.error(
393
this.logger_, 'unexpected event ' + e.type + 'during init');
394
}
395
}
396
};
397
398
399
/**
400
* {@code goog.events.PasteHandler.EventType.FOCUSED} is typically the second
401
* state the textarea will be, which is followed by the {@code INIT} state. On
402
* this state, users can paste in three different ways: edit -> paste,
403
* right click -> paste and drag and drop.
404
*
405
* The latter will generate a {@code goog.events.EventType.MOUSEOVER} event,
406
* which we match by making sure the textarea text changed. The first two will
407
* generate an 'input', which we match by making sure it was NOT generated by a
408
* key event (which also generates an 'input' event).
409
*
410
* Unfortunately, in Firefox, if you type fast, some KEYDOWN events are
411
* swallowed but an INPUT event may still happen. That means we need to
412
* differentiate between two consecutive INPUT events being generated either by
413
* swallowed key events OR by a valid edit -> paste -> edit -> paste action. We
414
* do this by checking a minimum time between the two events. This heuristic
415
* seems to work well, but it is obviously a heuristic :).
416
*
417
* @param {goog.events.BrowserEvent} e The underlying browser event.
418
* @private
419
*/
420
goog.events.PasteHandler.prototype.handleUnderFocused_ = function(e) {
421
switch (e.type) {
422
case 'input': {
423
// there are two different events that happen in practice that involves
424
// consecutive 'input' events. we use a heuristic to differentiate
425
// between the one that generates a valid paste action and the one that
426
// doesn't.
427
// @see testTypingReallyFastDispatchesTwoInputEventsBeforeTheKEYDOWNEvent
428
// and
429
// @see testRightClickRightClickAlsoDispatchesTwoConsecutiveInputEvents
430
// Notice that an 'input' event may be also triggered by a 'middle click'
431
// paste event, which is described in
432
// @see testMiddleClickWithoutFocusTriggersPasteEvent
433
var minimumMilisecondsBetweenInputEvents = this.lastTime_ +
434
goog.events.PasteHandler
435
.MANDATORY_MS_BETWEEN_INPUT_EVENTS_TIE_BREAKER;
436
if (goog.now() > minimumMilisecondsBetweenInputEvents ||
437
this.previousEvent_ == goog.events.EventType.FOCUS) {
438
goog.log.info(this.logger_, 'paste by textchange while focused!');
439
this.dispatch_(e);
440
}
441
break;
442
}
443
case goog.events.EventType.BLUR: {
444
this.state_ = goog.events.PasteHandler.State.INIT;
445
break;
446
}
447
case goog.events.EventType.KEYDOWN: {
448
goog.log.info(this.logger_, 'key down ... looking for ctrl+v');
449
// Opera + MAC does not set e.ctrlKey. Instead, it gives me e.keyCode = 0.
450
// http://www.quirksmode.org/js/keys.html
451
if (goog.userAgent.MAC && goog.userAgent.OPERA && e.keyCode == 0 ||
452
goog.userAgent.MAC && goog.userAgent.OPERA && e.keyCode == 17) {
453
break;
454
}
455
this.state_ = goog.events.PasteHandler.State.TYPING;
456
break;
457
}
458
case goog.events.EventType.MOUSEOVER: {
459
if (this.element_.value != this.oldValue_) {
460
goog.log.info(this.logger_, 'paste by dragdrop while focused!');
461
this.dispatch_(e);
462
}
463
break;
464
}
465
default: {
466
goog.log.error(
467
this.logger_, 'unexpected event ' + e.type + ' during focused');
468
}
469
}
470
};
471
472
473
/**
474
* {@code goog.events.PasteHandler.EventType.TYPING} is the third state
475
* this class can be. It exists because each KEYPRESS event will ALSO generate
476
* an INPUT event (because the textarea value changes), and we need to
477
* differentiate between an INPUT event generated by a key event and an INPUT
478
* event generated by edit -> paste actions.
479
*
480
* This is the state that we match the ctrl+v pattern.
481
*
482
* @param {goog.events.BrowserEvent} e The underlying browser event.
483
* @private
484
*/
485
goog.events.PasteHandler.prototype.handleUnderTyping_ = function(e) {
486
switch (e.type) {
487
case 'input': {
488
this.state_ = goog.events.PasteHandler.State.FOCUSED;
489
break;
490
}
491
case goog.events.EventType.BLUR: {
492
this.state_ = goog.events.PasteHandler.State.INIT;
493
break;
494
}
495
case goog.events.EventType.KEYDOWN: {
496
if (e.ctrlKey && e.keyCode == goog.events.KeyCodes.V ||
497
e.shiftKey && e.keyCode == goog.events.KeyCodes.INSERT ||
498
e.metaKey && e.keyCode == goog.events.KeyCodes.V) {
499
goog.log.info(this.logger_, 'paste by ctrl+v while keypressed!');
500
this.dispatch_(e);
501
}
502
break;
503
}
504
case goog.events.EventType.MOUSEOVER: {
505
if (this.element_.value != this.oldValue_) {
506
goog.log.info(this.logger_, 'paste by dragdrop while keypressed!');
507
this.dispatch_(e);
508
}
509
break;
510
}
511
default: {
512
goog.log.error(
513
this.logger_, 'unexpected event ' + e.type + ' during keypressed');
514
}
515
}
516
};
517
518