pastehandler.js 17 KB

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