Path: blob/trunk/third_party/closure/goog/events/pastehandler.js
2868 views
// Copyright 2009 The Closure Library Authors. All Rights Reserved.1//2// Licensed under the Apache License, Version 2.0 (the "License");3// you may not use this file except in compliance with the License.4// You may obtain a copy of the License at5//6// http://www.apache.org/licenses/LICENSE-2.07//8// Unless required by applicable law or agreed to in writing, software9// distributed under the License is distributed on an "AS-IS" BASIS,10// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.11// See the License for the specific language governing permissions and12// limitations under the License.1314/**15* @fileoverview Provides a 'paste' event detector that works consistently16* across different browsers.17*18* IE5, IE6, IE7, Safari3.0 and FF3.0 all fire 'paste' events on textareas.19* FF2 doesn't. This class uses 'paste' events when they are available20* and uses heuristics to detect the 'paste' event when they are not available.21*22* Known issue: will not detect paste events in FF2 if you pasted exactly the23* same existing text.24* Known issue: Opera + Mac doesn't work properly because of the meta key. We25* can probably fix that. TODO(user): {@link KeyboardShortcutHandler} does not26* work either very well with opera + mac. fix that.27*28* @see ../demos/pastehandler.html29*/3031goog.provide('goog.events.PasteHandler');32goog.provide('goog.events.PasteHandler.EventType');33goog.provide('goog.events.PasteHandler.State');3435goog.require('goog.Timer');36goog.require('goog.async.ConditionalDelay');37goog.require('goog.events.BrowserEvent');38goog.require('goog.events.EventHandler');39goog.require('goog.events.EventTarget');40goog.require('goog.events.EventType');41goog.require('goog.events.KeyCodes');42goog.require('goog.log');43goog.require('goog.userAgent');44454647/**48* A paste event detector. Gets an {@code element} as parameter and fires49* {@code goog.events.PasteHandler.EventType.PASTE} events when text is50* pasted in the {@code element}. Uses heuristics to detect paste events in FF2.51* See more details of the heuristic on {@link #handleEvent_}.52*53* @param {Element} element The textarea element we are listening on.54* @constructor55* @extends {goog.events.EventTarget}56*/57goog.events.PasteHandler = function(element) {58goog.events.EventTarget.call(this);5960/**61* The element that you want to listen for paste events on.62* @type {Element}63* @private64*/65this.element_ = element;6667/**68* The last known value of the element. Kept to check if things changed. See69* more details on {@link #handleEvent_}.70* @type {string}71* @private72*/73this.oldValue_ = this.element_.value;7475/**76* Handler for events.77* @type {goog.events.EventHandler<!goog.events.PasteHandler>}78* @private79*/80this.eventHandler_ = new goog.events.EventHandler(this);8182/**83* The last time an event occurred on the element. Kept to check whether the84* last event was generated by two input events or by multiple fast key events85* that got swallowed. See more details on {@link #handleEvent_}.86* @type {number}87* @private88*/89this.lastTime_ = goog.now();9091if (goog.events.PasteHandler.SUPPORTS_NATIVE_PASTE_EVENT) {92// Most modern browsers support the paste event.93this.eventHandler_.listen(94element, goog.events.EventType.PASTE, this.dispatch_);95} else {96// But FF2 and Opera doesn't. we listen for a series of events to try to97// find out if a paste occurred. We enumerate and cover all known ways to98// paste text on textareas. See more details on {@link #handleEvent_}.99var events = [100goog.events.EventType.KEYDOWN, goog.events.EventType.BLUR,101goog.events.EventType.FOCUS, goog.events.EventType.MOUSEOVER, 'input'102];103this.eventHandler_.listen(element, events, this.handleEvent_);104}105106/**107* ConditionalDelay used to poll for changes in the text element once users108* paste text. Browsers fire paste events BEFORE the text is actually present109* in the element.value property.110* @type {goog.async.ConditionalDelay}111* @private112*/113this.delay_ =114new goog.async.ConditionalDelay(goog.bind(this.checkUpdatedText_, this));115116};117goog.inherits(goog.events.PasteHandler, goog.events.EventTarget);118119120/**121* The types of events fired by this class.122* @enum {string}123*/124goog.events.PasteHandler.EventType = {125/**126* Dispatched as soon as the paste event is detected, but before the pasted127* text has been added to the text element we're listening to.128*/129PASTE: 'paste',130131/**132* Dispatched after detecting a change to the value of text element133* (within 200msec of receiving the PASTE event).134*/135AFTER_PASTE: 'after_paste'136};137138139/**140* The mandatory delay we expect between two {@code input} events, used to141* differentiated between non key paste events and key events.142* @type {number}143*/144goog.events.PasteHandler.MANDATORY_MS_BETWEEN_INPUT_EVENTS_TIE_BREAKER = 400;145146147/**148* Whether current UA supoprts the native "paste" event type.149* @const {boolean}150*/151goog.events.PasteHandler.SUPPORTS_NATIVE_PASTE_EVENT = goog.userAgent.WEBKIT ||152goog.userAgent.IE || goog.userAgent.EDGE ||153(goog.userAgent.GECKO && goog.userAgent.isVersionOrHigher('1.9'));154155156/**157* The period between each time we check whether the pasted text appears in the158* text element or not.159* @type {number}160* @private161*/162goog.events.PasteHandler.PASTE_POLLING_PERIOD_MS_ = 50;163164165/**166* The maximum amount of time we want to poll for changes.167* @type {number}168* @private169*/170goog.events.PasteHandler.PASTE_POLLING_TIMEOUT_MS_ = 200;171172173/**174* The states that this class can be found, on the paste detection algorithm.175* @enum {string}176*/177goog.events.PasteHandler.State = {178INIT: 'init',179FOCUSED: 'focused',180TYPING: 'typing'181};182183184/**185* The initial state of the paste detection algorithm.186* @type {goog.events.PasteHandler.State}187* @private188*/189goog.events.PasteHandler.prototype.state_ = goog.events.PasteHandler.State.INIT;190191192/**193* The previous event that caused us to be on the current state.194* @type {?string}195* @private196*/197goog.events.PasteHandler.prototype.previousEvent_;198199200/**201* A logger, used to help us debug the algorithm.202* @type {goog.log.Logger}203* @private204*/205goog.events.PasteHandler.prototype.logger_ =206goog.log.getLogger('goog.events.PasteHandler');207208209/** @override */210goog.events.PasteHandler.prototype.disposeInternal = function() {211goog.events.PasteHandler.superClass_.disposeInternal.call(this);212this.eventHandler_.dispose();213this.eventHandler_ = null;214this.delay_.dispose();215this.delay_ = null;216};217218219/**220* Returns the current state of the paste detection algorithm. Used mostly for221* testing.222* @return {goog.events.PasteHandler.State} The current state of the class.223*/224goog.events.PasteHandler.prototype.getState = function() {225return this.state_;226};227228229/**230* Returns the event handler.231* @return {goog.events.EventHandler<T>} The event handler.232* @protected233* @this {T}234* @template T235*/236goog.events.PasteHandler.prototype.getEventHandler = function() {237return this.eventHandler_;238};239240241/**242* Checks whether the element.value property was updated, and if so, dispatches243* the event that let clients know that the text is available.244* @return {boolean} Whether the polling should stop or not, based on whether245* we found a text change or not.246* @private247*/248goog.events.PasteHandler.prototype.checkUpdatedText_ = function() {249if (this.oldValue_ == this.element_.value) {250return false;251}252goog.log.info(this.logger_, 'detected textchange after paste');253this.dispatchEvent(goog.events.PasteHandler.EventType.AFTER_PASTE);254return true;255};256257258/**259* Dispatches the paste event.260* @param {goog.events.BrowserEvent} e The underlying browser event.261* @private262*/263goog.events.PasteHandler.prototype.dispatch_ = function(e) {264var event = new goog.events.BrowserEvent(e.getBrowserEvent());265event.type = goog.events.PasteHandler.EventType.PASTE;266this.dispatchEvent(event);267268// Starts polling for updates in the element.value property so we can tell269// when do dispatch the AFTER_PASTE event. (We do an initial check after an270// async delay of 0 msec since some browsers update the text right away and271// our poller will always wait one period before checking).272goog.Timer.callOnce(function() {273if (!this.checkUpdatedText_()) {274this.delay_.start(275goog.events.PasteHandler.PASTE_POLLING_PERIOD_MS_,276goog.events.PasteHandler.PASTE_POLLING_TIMEOUT_MS_);277}278}, 0, this);279};280281282/**283* The main event handler which implements a state machine.284*285* To handle FF2, we enumerate and cover all the known ways a user can paste:286*287* 1) ctrl+v, shift+insert, cmd+v288* 2) right click -> paste289* 3) edit menu -> paste290* 4) drag and drop291* 5) middle click292*293* (1) is easy and can be detected by listening for key events and finding out294* which keys are pressed. (2), (3), (4) and (5) do not generate a key event,295* so we need to listen for more than that. (2-5) all generate 'input' events,296* but so does key events. So we need to have some sort of 'how did the input297* event was generated' history algorithm.298*299* (2) is an interesting case in Opera on a Mac: since Macs does not have two300* buttons, right clicking involves pressing the CTRL key. Even more interesting301* is the fact that opera does NOT set the e.ctrlKey bit. Instead, it sets302* e.keyCode = 0.303* {@link http://www.quirksmode.org/js/keys.html}304*305* (1) is also an interesting case in Opera on a Mac: Opera is the only browser306* covered by this class that can detect the cmd key (FF2 can't apparently). And307* it fires e.keyCode = 17, which is the CTRL key code.308* {@link http://www.quirksmode.org/js/keys.html}309*310* NOTE(user, pbarry): There is an interesting thing about (5): on Linux, (5)311* pastes the last thing that you highlighted, not the last thing that you312* ctrl+c'ed. This code will still generate a {@code PASTE} event though.313*314* We enumerate all the possible steps a user can take to paste text and we315* implemented the transition between the steps in a state machine. The316* following is the design of the state machine:317*318* matching paths:319*320* (1) happens on INIT -> FOCUSED -> TYPING -> [e.ctrlKey & e.keyCode = 'v']321* (2-3) happens on INIT -> FOCUSED -> [input event happened]322* (4) happens on INIT -> [mouseover && text changed]323*324* non matching paths:325*326* user is typing normally327* INIT -> FOCUS -> TYPING -> INPUT -> INIT328*329* @param {goog.events.BrowserEvent} e The underlying browser event.330* @private331*/332goog.events.PasteHandler.prototype.handleEvent_ = function(e) {333// transition between states happen at each browser event, and depend on the334// current state, the event that led to this state, and the event input.335switch (this.state_) {336case goog.events.PasteHandler.State.INIT: {337this.handleUnderInit_(e);338break;339}340case goog.events.PasteHandler.State.FOCUSED: {341this.handleUnderFocused_(e);342break;343}344case goog.events.PasteHandler.State.TYPING: {345this.handleUnderTyping_(e);346break;347}348default: {349goog.log.error(this.logger_, 'invalid ' + this.state_ + ' state');350}351}352this.lastTime_ = goog.now();353this.oldValue_ = this.element_.value;354goog.log.info(this.logger_, e.type + ' -> ' + this.state_);355this.previousEvent_ = e.type;356};357358359/**360* {@code goog.events.PasteHandler.EventType.INIT} is the first initial state361* the textarea is found. You can only leave this state by setting focus on the362* textarea, which is how users will input text. You can also paste things using363* drag and drop, which will not generate a {@code goog.events.EventType.FOCUS}364* event, but will generate a {@code goog.events.EventType.MOUSEOVER}.365*366* For browsers that support the 'paste' event, we match it and stay on the same367* state.368*369* @param {goog.events.BrowserEvent} e The underlying browser event.370* @private371*/372goog.events.PasteHandler.prototype.handleUnderInit_ = function(e) {373switch (e.type) {374case goog.events.EventType.BLUR: {375this.state_ = goog.events.PasteHandler.State.INIT;376break;377}378case goog.events.EventType.FOCUS: {379this.state_ = goog.events.PasteHandler.State.FOCUSED;380break;381}382case goog.events.EventType.MOUSEOVER: {383this.state_ = goog.events.PasteHandler.State.INIT;384if (this.element_.value != this.oldValue_) {385goog.log.info(this.logger_, 'paste by dragdrop while on init!');386this.dispatch_(e);387}388break;389}390default: {391goog.log.error(392this.logger_, 'unexpected event ' + e.type + 'during init');393}394}395};396397398/**399* {@code goog.events.PasteHandler.EventType.FOCUSED} is typically the second400* state the textarea will be, which is followed by the {@code INIT} state. On401* this state, users can paste in three different ways: edit -> paste,402* right click -> paste and drag and drop.403*404* The latter will generate a {@code goog.events.EventType.MOUSEOVER} event,405* which we match by making sure the textarea text changed. The first two will406* generate an 'input', which we match by making sure it was NOT generated by a407* key event (which also generates an 'input' event).408*409* Unfortunately, in Firefox, if you type fast, some KEYDOWN events are410* swallowed but an INPUT event may still happen. That means we need to411* differentiate between two consecutive INPUT events being generated either by412* swallowed key events OR by a valid edit -> paste -> edit -> paste action. We413* do this by checking a minimum time between the two events. This heuristic414* seems to work well, but it is obviously a heuristic :).415*416* @param {goog.events.BrowserEvent} e The underlying browser event.417* @private418*/419goog.events.PasteHandler.prototype.handleUnderFocused_ = function(e) {420switch (e.type) {421case 'input': {422// there are two different events that happen in practice that involves423// consecutive 'input' events. we use a heuristic to differentiate424// between the one that generates a valid paste action and the one that425// doesn't.426// @see testTypingReallyFastDispatchesTwoInputEventsBeforeTheKEYDOWNEvent427// and428// @see testRightClickRightClickAlsoDispatchesTwoConsecutiveInputEvents429// Notice that an 'input' event may be also triggered by a 'middle click'430// paste event, which is described in431// @see testMiddleClickWithoutFocusTriggersPasteEvent432var minimumMilisecondsBetweenInputEvents = this.lastTime_ +433goog.events.PasteHandler434.MANDATORY_MS_BETWEEN_INPUT_EVENTS_TIE_BREAKER;435if (goog.now() > minimumMilisecondsBetweenInputEvents ||436this.previousEvent_ == goog.events.EventType.FOCUS) {437goog.log.info(this.logger_, 'paste by textchange while focused!');438this.dispatch_(e);439}440break;441}442case goog.events.EventType.BLUR: {443this.state_ = goog.events.PasteHandler.State.INIT;444break;445}446case goog.events.EventType.KEYDOWN: {447goog.log.info(this.logger_, 'key down ... looking for ctrl+v');448// Opera + MAC does not set e.ctrlKey. Instead, it gives me e.keyCode = 0.449// http://www.quirksmode.org/js/keys.html450if (goog.userAgent.MAC && goog.userAgent.OPERA && e.keyCode == 0 ||451goog.userAgent.MAC && goog.userAgent.OPERA && e.keyCode == 17) {452break;453}454this.state_ = goog.events.PasteHandler.State.TYPING;455break;456}457case goog.events.EventType.MOUSEOVER: {458if (this.element_.value != this.oldValue_) {459goog.log.info(this.logger_, 'paste by dragdrop while focused!');460this.dispatch_(e);461}462break;463}464default: {465goog.log.error(466this.logger_, 'unexpected event ' + e.type + ' during focused');467}468}469};470471472/**473* {@code goog.events.PasteHandler.EventType.TYPING} is the third state474* this class can be. It exists because each KEYPRESS event will ALSO generate475* an INPUT event (because the textarea value changes), and we need to476* differentiate between an INPUT event generated by a key event and an INPUT477* event generated by edit -> paste actions.478*479* This is the state that we match the ctrl+v pattern.480*481* @param {goog.events.BrowserEvent} e The underlying browser event.482* @private483*/484goog.events.PasteHandler.prototype.handleUnderTyping_ = function(e) {485switch (e.type) {486case 'input': {487this.state_ = goog.events.PasteHandler.State.FOCUSED;488break;489}490case goog.events.EventType.BLUR: {491this.state_ = goog.events.PasteHandler.State.INIT;492break;493}494case goog.events.EventType.KEYDOWN: {495if (e.ctrlKey && e.keyCode == goog.events.KeyCodes.V ||496e.shiftKey && e.keyCode == goog.events.KeyCodes.INSERT ||497e.metaKey && e.keyCode == goog.events.KeyCodes.V) {498goog.log.info(this.logger_, 'paste by ctrl+v while keypressed!');499this.dispatch_(e);500}501break;502}503case goog.events.EventType.MOUSEOVER: {504if (this.element_.value != this.oldValue_) {505goog.log.info(this.logger_, 'paste by dragdrop while keypressed!');506this.dispatch_(e);507}508break;509}510default: {511goog.log.error(512this.logger_, 'unexpected event ' + e.type + ' during keypressed');513}514}515};516517518