516 lines
17 KiB
JavaScript
516 lines
17 KiB
JavaScript
// Copyright 2009 The Closure Library Authors. All Rights Reserved.
|
|
//
|
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
|
// you may not use this file except in compliance with the License.
|
|
// You may obtain a copy of the License at
|
|
//
|
|
// http://www.apache.org/licenses/LICENSE-2.0
|
|
//
|
|
// Unless required by applicable law or agreed to in writing, software
|
|
// distributed under the License is distributed on an "AS-IS" BASIS,
|
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
// See the License for the specific language governing permissions and
|
|
// limitations under the License.
|
|
|
|
/**
|
|
* @fileoverview Provides a 'paste' event detector that works consistently
|
|
* across different browsers.
|
|
*
|
|
* IE5, IE6, IE7, Safari3.0 and FF3.0 all fire 'paste' events on textareas.
|
|
* FF2 doesn't. This class uses 'paste' events when they are available
|
|
* and uses heuristics to detect the 'paste' event when they are not available.
|
|
*
|
|
* Known issue: will not detect paste events in FF2 if you pasted exactly the
|
|
* same existing text.
|
|
* Known issue: Opera + Mac doesn't work properly because of the meta key. We
|
|
* can probably fix that. TODO(user): {@link KeyboardShortcutHandler} does not
|
|
* work either very well with opera + mac. fix that.
|
|
*
|
|
* @supported IE5, IE6, IE7, Safari3.0, Chrome, FF2.0 (linux) and FF3.0 and
|
|
* Opera (mac and windows).
|
|
*
|
|
* @see ../demos/pastehandler.html
|
|
*/
|
|
|
|
goog.provide('goog.events.PasteHandler');
|
|
goog.provide('goog.events.PasteHandler.EventType');
|
|
goog.provide('goog.events.PasteHandler.State');
|
|
|
|
goog.require('goog.Timer');
|
|
goog.require('goog.async.ConditionalDelay');
|
|
goog.require('goog.events.BrowserEvent');
|
|
goog.require('goog.events.EventHandler');
|
|
goog.require('goog.events.EventTarget');
|
|
goog.require('goog.events.EventType');
|
|
goog.require('goog.events.KeyCodes');
|
|
goog.require('goog.log');
|
|
goog.require('goog.userAgent');
|
|
|
|
|
|
|
|
/**
|
|
* A paste event detector. Gets an {@code element} as parameter and fires
|
|
* {@code goog.events.PasteHandler.EventType.PASTE} events when text is
|
|
* pasted in the {@code element}. Uses heuristics to detect paste events in FF2.
|
|
* See more details of the heuristic on {@link #handleEvent_}.
|
|
*
|
|
* @param {Element} element The textarea element we are listening on.
|
|
* @constructor
|
|
* @extends {goog.events.EventTarget}
|
|
*/
|
|
goog.events.PasteHandler = function(element) {
|
|
goog.events.EventTarget.call(this);
|
|
|
|
/**
|
|
* The element that you want to listen for paste events on.
|
|
* @type {Element}
|
|
* @private
|
|
*/
|
|
this.element_ = element;
|
|
|
|
/**
|
|
* The last known value of the element. Kept to check if things changed. See
|
|
* more details on {@link #handleEvent_}.
|
|
* @type {string}
|
|
* @private
|
|
*/
|
|
this.oldValue_ = this.element_.value;
|
|
|
|
/**
|
|
* Handler for events.
|
|
* @type {goog.events.EventHandler}
|
|
* @private
|
|
*/
|
|
this.eventHandler_ = new goog.events.EventHandler(this);
|
|
|
|
/**
|
|
* The last time an event occurred on the element. Kept to check whether the
|
|
* last event was generated by two input events or by multiple fast key events
|
|
* that got swallowed. See more details on {@link #handleEvent_}.
|
|
* @type {number}
|
|
* @private
|
|
*/
|
|
this.lastTime_ = goog.now();
|
|
|
|
if (goog.userAgent.WEBKIT ||
|
|
goog.userAgent.IE ||
|
|
goog.userAgent.GECKO && goog.userAgent.isVersionOrHigher('1.9')) {
|
|
// Most modern browsers support the paste event.
|
|
this.eventHandler_.listen(element, goog.events.EventType.PASTE,
|
|
this.dispatch_);
|
|
} else {
|
|
// But FF2 and Opera doesn't. we listen for a series of events to try to
|
|
// find out if a paste occurred. We enumerate and cover all known ways to
|
|
// paste text on textareas. See more details on {@link #handleEvent_}.
|
|
var events = [
|
|
goog.events.EventType.KEYDOWN,
|
|
goog.events.EventType.BLUR,
|
|
goog.events.EventType.FOCUS,
|
|
goog.events.EventType.MOUSEOVER,
|
|
'input'
|
|
];
|
|
this.eventHandler_.listen(element, events, this.handleEvent_);
|
|
}
|
|
|
|
/**
|
|
* ConditionalDelay used to poll for changes in the text element once users
|
|
* paste text. Browsers fire paste events BEFORE the text is actually present
|
|
* in the element.value property.
|
|
* @type {goog.async.ConditionalDelay}
|
|
* @private
|
|
*/
|
|
this.delay_ = new goog.async.ConditionalDelay(
|
|
goog.bind(this.checkUpdatedText_, this));
|
|
|
|
};
|
|
goog.inherits(goog.events.PasteHandler, goog.events.EventTarget);
|
|
|
|
|
|
/**
|
|
* The types of events fired by this class.
|
|
* @enum {string}
|
|
*/
|
|
goog.events.PasteHandler.EventType = {
|
|
/**
|
|
* Dispatched as soon as the paste event is detected, but before the pasted
|
|
* text has been added to the text element we're listening to.
|
|
*/
|
|
PASTE: 'paste',
|
|
|
|
/**
|
|
* Dispatched after detecting a change to the value of text element
|
|
* (within 200msec of receiving the PASTE event).
|
|
*/
|
|
AFTER_PASTE: 'after_paste'
|
|
};
|
|
|
|
|
|
/**
|
|
* The mandatory delay we expect between two {@code input} events, used to
|
|
* differentiated between non key paste events and key events.
|
|
* @type {number}
|
|
*/
|
|
goog.events.PasteHandler.MANDATORY_MS_BETWEEN_INPUT_EVENTS_TIE_BREAKER =
|
|
400;
|
|
|
|
|
|
/**
|
|
* The period between each time we check whether the pasted text appears in the
|
|
* text element or not.
|
|
* @type {number}
|
|
* @private
|
|
*/
|
|
goog.events.PasteHandler.PASTE_POLLING_PERIOD_MS_ = 50;
|
|
|
|
|
|
/**
|
|
* The maximum amount of time we want to poll for changes.
|
|
* @type {number}
|
|
* @private
|
|
*/
|
|
goog.events.PasteHandler.PASTE_POLLING_TIMEOUT_MS_ = 200;
|
|
|
|
|
|
/**
|
|
* The states that this class can be found, on the paste detection algorithm.
|
|
* @enum {string}
|
|
*/
|
|
goog.events.PasteHandler.State = {
|
|
INIT: 'init',
|
|
FOCUSED: 'focused',
|
|
TYPING: 'typing'
|
|
};
|
|
|
|
|
|
/**
|
|
* The initial state of the paste detection algorithm.
|
|
* @type {goog.events.PasteHandler.State}
|
|
* @private
|
|
*/
|
|
goog.events.PasteHandler.prototype.state_ =
|
|
goog.events.PasteHandler.State.INIT;
|
|
|
|
|
|
/**
|
|
* The previous event that caused us to be on the current state.
|
|
* @type {?string}
|
|
* @private
|
|
*/
|
|
goog.events.PasteHandler.prototype.previousEvent_;
|
|
|
|
|
|
/**
|
|
* A logger, used to help us debug the algorithm.
|
|
* @type {goog.log.Logger}
|
|
* @private
|
|
*/
|
|
goog.events.PasteHandler.prototype.logger_ =
|
|
goog.log.getLogger('goog.events.PasteHandler');
|
|
|
|
|
|
/** @override */
|
|
goog.events.PasteHandler.prototype.disposeInternal = function() {
|
|
goog.events.PasteHandler.superClass_.disposeInternal.call(this);
|
|
this.eventHandler_.dispose();
|
|
this.eventHandler_ = null;
|
|
this.delay_.dispose();
|
|
this.delay_ = null;
|
|
};
|
|
|
|
|
|
/**
|
|
* Returns the current state of the paste detection algorithm. Used mostly for
|
|
* testing.
|
|
* @return {goog.events.PasteHandler.State} The current state of the class.
|
|
*/
|
|
goog.events.PasteHandler.prototype.getState = function() {
|
|
return this.state_;
|
|
};
|
|
|
|
|
|
/**
|
|
* Returns the event handler.
|
|
* @return {goog.events.EventHandler} The event handler.
|
|
* @protected
|
|
*/
|
|
goog.events.PasteHandler.prototype.getEventHandler = function() {
|
|
return this.eventHandler_;
|
|
};
|
|
|
|
|
|
/**
|
|
* Checks whether the element.value property was updated, and if so, dispatches
|
|
* the event that let clients know that the text is available.
|
|
* @return {boolean} Whether the polling should stop or not, based on whether
|
|
* we found a text change or not.
|
|
* @private
|
|
*/
|
|
goog.events.PasteHandler.prototype.checkUpdatedText_ = function() {
|
|
if (this.oldValue_ == this.element_.value) {
|
|
return false;
|
|
}
|
|
goog.log.info(this.logger_, 'detected textchange after paste');
|
|
this.dispatchEvent(goog.events.PasteHandler.EventType.AFTER_PASTE);
|
|
return true;
|
|
};
|
|
|
|
|
|
/**
|
|
* Dispatches the paste event.
|
|
* @param {goog.events.BrowserEvent} e The underlying browser event.
|
|
* @private
|
|
*/
|
|
goog.events.PasteHandler.prototype.dispatch_ = function(e) {
|
|
var event = new goog.events.BrowserEvent(e.getBrowserEvent());
|
|
event.type = goog.events.PasteHandler.EventType.PASTE;
|
|
this.dispatchEvent(event);
|
|
|
|
// Starts polling for updates in the element.value property so we can tell
|
|
// when do dispatch the AFTER_PASTE event. (We do an initial check after an
|
|
// async delay of 0 msec since some browsers update the text right away and
|
|
// our poller will always wait one period before checking).
|
|
goog.Timer.callOnce(function() {
|
|
if (!this.checkUpdatedText_()) {
|
|
this.delay_.start(
|
|
goog.events.PasteHandler.PASTE_POLLING_PERIOD_MS_,
|
|
goog.events.PasteHandler.PASTE_POLLING_TIMEOUT_MS_);
|
|
}
|
|
}, 0, this);
|
|
};
|
|
|
|
|
|
/**
|
|
* The main event handler which implements a state machine.
|
|
*
|
|
* To handle FF2, we enumerate and cover all the known ways a user can paste:
|
|
*
|
|
* 1) ctrl+v, shift+insert, cmd+v
|
|
* 2) right click -> paste
|
|
* 3) edit menu -> paste
|
|
* 4) drag and drop
|
|
* 5) middle click
|
|
*
|
|
* (1) is easy and can be detected by listening for key events and finding out
|
|
* which keys are pressed. (2), (3), (4) and (5) do not generate a key event,
|
|
* so we need to listen for more than that. (2-5) all generate 'input' events,
|
|
* but so does key events. So we need to have some sort of 'how did the input
|
|
* event was generated' history algorithm.
|
|
*
|
|
* (2) is an interesting case in Opera on a Mac: since Macs does not have two
|
|
* buttons, right clicking involves pressing the CTRL key. Even more interesting
|
|
* is the fact that opera does NOT set the e.ctrlKey bit. Instead, it sets
|
|
* e.keyCode = 0.
|
|
* {@link http://www.quirksmode.org/js/keys.html}
|
|
*
|
|
* (1) is also an interesting case in Opera on a Mac: Opera is the only browser
|
|
* covered by this class that can detect the cmd key (FF2 can't apparently). And
|
|
* it fires e.keyCode = 17, which is the CTRL key code.
|
|
* {@link http://www.quirksmode.org/js/keys.html}
|
|
*
|
|
* NOTE(user, pbarry): There is an interesting thing about (5): on Linux, (5)
|
|
* pastes the last thing that you highlighted, not the last thing that you
|
|
* ctrl+c'ed. This code will still generate a {@code PASTE} event though.
|
|
*
|
|
* We enumerate all the possible steps a user can take to paste text and we
|
|
* implemented the transition between the steps in a state machine. The
|
|
* following is the design of the state machine:
|
|
*
|
|
* matching paths:
|
|
*
|
|
* (1) happens on INIT -> FOCUSED -> TYPING -> [e.ctrlKey & e.keyCode = 'v']
|
|
* (2-3) happens on INIT -> FOCUSED -> [input event happened]
|
|
* (4) happens on INIT -> [mouseover && text changed]
|
|
*
|
|
* non matching paths:
|
|
*
|
|
* user is typing normally
|
|
* INIT -> FOCUS -> TYPING -> INPUT -> INIT
|
|
*
|
|
* @param {goog.events.BrowserEvent} e The underlying browser event.
|
|
* @private
|
|
*/
|
|
goog.events.PasteHandler.prototype.handleEvent_ = function(e) {
|
|
// transition between states happen at each browser event, and depend on the
|
|
// current state, the event that led to this state, and the event input.
|
|
switch (this.state_) {
|
|
case goog.events.PasteHandler.State.INIT: {
|
|
this.handleUnderInit_(e);
|
|
break;
|
|
}
|
|
case goog.events.PasteHandler.State.FOCUSED: {
|
|
this.handleUnderFocused_(e);
|
|
break;
|
|
}
|
|
case goog.events.PasteHandler.State.TYPING: {
|
|
this.handleUnderTyping_(e);
|
|
break;
|
|
}
|
|
default: {
|
|
goog.log.error(this.logger_, 'invalid ' + this.state_ + ' state');
|
|
}
|
|
}
|
|
this.lastTime_ = goog.now();
|
|
this.oldValue_ = this.element_.value;
|
|
goog.log.info(this.logger_, e.type + ' -> ' + this.state_);
|
|
this.previousEvent_ = e.type;
|
|
};
|
|
|
|
|
|
/**
|
|
* {@code goog.events.PasteHandler.EventType.INIT} is the first initial state
|
|
* the textarea is found. You can only leave this state by setting focus on the
|
|
* textarea, which is how users will input text. You can also paste things using
|
|
* drag and drop, which will not generate a {@code goog.events.EventType.FOCUS}
|
|
* event, but will generate a {@code goog.events.EventType.MOUSEOVER}.
|
|
*
|
|
* For browsers that support the 'paste' event, we match it and stay on the same
|
|
* state.
|
|
*
|
|
* @param {goog.events.BrowserEvent} e The underlying browser event.
|
|
* @private
|
|
*/
|
|
goog.events.PasteHandler.prototype.handleUnderInit_ = function(e) {
|
|
switch (e.type) {
|
|
case goog.events.EventType.BLUR: {
|
|
this.state_ = goog.events.PasteHandler.State.INIT;
|
|
break;
|
|
}
|
|
case goog.events.EventType.FOCUS: {
|
|
this.state_ = goog.events.PasteHandler.State.FOCUSED;
|
|
break;
|
|
}
|
|
case goog.events.EventType.MOUSEOVER: {
|
|
this.state_ = goog.events.PasteHandler.State.INIT;
|
|
if (this.element_.value != this.oldValue_) {
|
|
goog.log.info(this.logger_, 'paste by dragdrop while on init!');
|
|
this.dispatch_(e);
|
|
}
|
|
break;
|
|
}
|
|
default: {
|
|
goog.log.error(this.logger_,
|
|
'unexpected event ' + e.type + 'during init');
|
|
}
|
|
}
|
|
};
|
|
|
|
|
|
/**
|
|
* {@code goog.events.PasteHandler.EventType.FOCUSED} is typically the second
|
|
* state the textarea will be, which is followed by the {@code INIT} state. On
|
|
* this state, users can paste in three different ways: edit -> paste,
|
|
* right click -> paste and drag and drop.
|
|
*
|
|
* The latter will generate a {@code goog.events.EventType.MOUSEOVER} event,
|
|
* which we match by making sure the textarea text changed. The first two will
|
|
* generate an 'input', which we match by making sure it was NOT generated by a
|
|
* key event (which also generates an 'input' event).
|
|
*
|
|
* Unfortunately, in Firefox, if you type fast, some KEYDOWN events are
|
|
* swallowed but an INPUT event may still happen. That means we need to
|
|
* differentiate between two consecutive INPUT events being generated either by
|
|
* swallowed key events OR by a valid edit -> paste -> edit -> paste action. We
|
|
* do this by checking a minimum time between the two events. This heuristic
|
|
* seems to work well, but it is obviously a heuristic :).
|
|
*
|
|
* @param {goog.events.BrowserEvent} e The underlying browser event.
|
|
* @private
|
|
*/
|
|
goog.events.PasteHandler.prototype.handleUnderFocused_ = function(e) {
|
|
switch (e.type) {
|
|
case 'input' : {
|
|
// there are two different events that happen in practice that involves
|
|
// consecutive 'input' events. we use a heuristic to differentiate
|
|
// between the one that generates a valid paste action and the one that
|
|
// doesn't.
|
|
// @see testTypingReallyFastDispatchesTwoInputEventsBeforeTheKEYDOWNEvent
|
|
// and
|
|
// @see testRightClickRightClickAlsoDispatchesTwoConsecutiveInputEvents
|
|
// Notice that an 'input' event may be also triggered by a 'middle click'
|
|
// paste event, which is described in
|
|
// @see testMiddleClickWithoutFocusTriggersPasteEvent
|
|
var minimumMilisecondsBetweenInputEvents = this.lastTime_ +
|
|
goog.events.PasteHandler.
|
|
MANDATORY_MS_BETWEEN_INPUT_EVENTS_TIE_BREAKER;
|
|
if (goog.now() > minimumMilisecondsBetweenInputEvents ||
|
|
this.previousEvent_ == goog.events.EventType.FOCUS) {
|
|
goog.log.info(this.logger_, 'paste by textchange while focused!');
|
|
this.dispatch_(e);
|
|
}
|
|
break;
|
|
}
|
|
case goog.events.EventType.BLUR: {
|
|
this.state_ = goog.events.PasteHandler.State.INIT;
|
|
break;
|
|
}
|
|
case goog.events.EventType.KEYDOWN: {
|
|
goog.log.info(this.logger_, 'key down ... looking for ctrl+v');
|
|
// Opera + MAC does not set e.ctrlKey. Instead, it gives me e.keyCode = 0.
|
|
// http://www.quirksmode.org/js/keys.html
|
|
if (goog.userAgent.MAC && goog.userAgent.OPERA && e.keyCode == 0 ||
|
|
goog.userAgent.MAC && goog.userAgent.OPERA && e.keyCode == 17) {
|
|
break;
|
|
}
|
|
this.state_ = goog.events.PasteHandler.State.TYPING;
|
|
break;
|
|
}
|
|
case goog.events.EventType.MOUSEOVER: {
|
|
if (this.element_.value != this.oldValue_) {
|
|
goog.log.info(this.logger_, 'paste by dragdrop while focused!');
|
|
this.dispatch_(e);
|
|
}
|
|
break;
|
|
}
|
|
default: {
|
|
goog.log.error(this.logger_,
|
|
'unexpected event ' + e.type + ' during focused');
|
|
}
|
|
}
|
|
};
|
|
|
|
|
|
/**
|
|
* {@code goog.events.PasteHandler.EventType.TYPING} is the third state
|
|
* this class can be. It exists because each KEYPRESS event will ALSO generate
|
|
* an INPUT event (because the textarea value changes), and we need to
|
|
* differentiate between an INPUT event generated by a key event and an INPUT
|
|
* event generated by edit -> paste actions.
|
|
*
|
|
* This is the state that we match the ctrl+v pattern.
|
|
*
|
|
* @param {goog.events.BrowserEvent} e The underlying browser event.
|
|
* @private
|
|
*/
|
|
goog.events.PasteHandler.prototype.handleUnderTyping_ = function(e) {
|
|
switch (e.type) {
|
|
case 'input' : {
|
|
this.state_ = goog.events.PasteHandler.State.FOCUSED;
|
|
break;
|
|
}
|
|
case goog.events.EventType.BLUR: {
|
|
this.state_ = goog.events.PasteHandler.State.INIT;
|
|
break;
|
|
}
|
|
case goog.events.EventType.KEYDOWN: {
|
|
if (e.ctrlKey && e.keyCode == goog.events.KeyCodes.V ||
|
|
e.shiftKey && e.keyCode == goog.events.KeyCodes.INSERT ||
|
|
e.metaKey && e.keyCode == goog.events.KeyCodes.V) {
|
|
goog.log.info(this.logger_, 'paste by ctrl+v while keypressed!');
|
|
this.dispatch_(e);
|
|
}
|
|
break;
|
|
}
|
|
case goog.events.EventType.MOUSEOVER: {
|
|
if (this.element_.value != this.oldValue_) {
|
|
goog.log.info(this.logger_, 'paste by dragdrop while keypressed!');
|
|
this.dispatch_(e);
|
|
}
|
|
break;
|
|
}
|
|
default: {
|
|
goog.log.error(this.logger_,
|
|
'unexpected event ' + e.type + ' during keypressed');
|
|
}
|
|
}
|
|
};
|