dragdropdetector.js 18 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652
  1. // Copyright 2007 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 Detects images dragged and dropped on to the window.
  16. *
  17. * @author robbyw@google.com (Robby Walker)
  18. */
  19. goog.provide('goog.ui.DragDropDetector');
  20. goog.provide('goog.ui.DragDropDetector.EventType');
  21. goog.provide('goog.ui.DragDropDetector.ImageDropEvent');
  22. goog.provide('goog.ui.DragDropDetector.LinkDropEvent');
  23. goog.require('goog.dom');
  24. goog.require('goog.dom.InputType');
  25. goog.require('goog.dom.TagName');
  26. goog.require('goog.events.Event');
  27. goog.require('goog.events.EventHandler');
  28. goog.require('goog.events.EventTarget');
  29. goog.require('goog.events.EventType');
  30. goog.require('goog.math.Coordinate');
  31. goog.require('goog.string');
  32. goog.require('goog.style');
  33. goog.require('goog.userAgent');
  34. /**
  35. * Creates a new drag and drop detector.
  36. * @param {string=} opt_filePath The URL of the page to use for the detector.
  37. * It should contain the same contents as dragdropdetector_target.html in
  38. * the demos directory.
  39. * @constructor
  40. * @extends {goog.events.EventTarget}
  41. * @final
  42. */
  43. goog.ui.DragDropDetector = function(opt_filePath) {
  44. goog.ui.DragDropDetector.base(this, 'constructor');
  45. var iframe = goog.dom.createDom(goog.dom.TagName.IFRAME, {'frameborder': 0});
  46. // In Firefox, we do all drop detection with an IFRAME. In IE, we only use
  47. // the IFRAME to capture copied, non-linked images. (When we don't need it,
  48. // we put a text INPUT before it and push it off screen.)
  49. iframe.className = goog.userAgent.IE ?
  50. goog.getCssName(
  51. goog.ui.DragDropDetector.BASE_CSS_NAME_, 'ie-editable-iframe') :
  52. goog.getCssName(
  53. goog.ui.DragDropDetector.BASE_CSS_NAME_, 'w3c-editable-iframe');
  54. iframe.src = opt_filePath || goog.ui.DragDropDetector.DEFAULT_FILE_PATH_;
  55. this.element_ = /** @type {!HTMLIFrameElement} */ (iframe);
  56. this.handler_ = new goog.events.EventHandler(this);
  57. this.handler_.listen(iframe, goog.events.EventType.LOAD, this.initIframe_);
  58. if (goog.userAgent.IE) {
  59. // In IE, we have to bounce between an INPUT for catching links and an
  60. // IFRAME for catching images.
  61. this.textInput_ = goog.dom.createDom(goog.dom.TagName.INPUT, {
  62. 'type': goog.dom.InputType.TEXT,
  63. 'className':
  64. goog.getCssName(goog.ui.DragDropDetector.BASE_CSS_NAME_, 'ie-input')
  65. });
  66. this.root_ = goog.dom.createDom(
  67. goog.dom.TagName.DIV,
  68. goog.getCssName(goog.ui.DragDropDetector.BASE_CSS_NAME_, 'ie-div'),
  69. this.textInput_, iframe);
  70. } else {
  71. this.root_ = iframe;
  72. }
  73. document.body.appendChild(this.root_);
  74. };
  75. goog.inherits(goog.ui.DragDropDetector, goog.events.EventTarget);
  76. /**
  77. * Drag and drop event types.
  78. * @enum {string}
  79. */
  80. goog.ui.DragDropDetector.EventType = {
  81. IMAGE_DROPPED: 'onimagedrop',
  82. LINK_DROPPED: 'onlinkdrop'
  83. };
  84. /**
  85. * Browser specific drop event type.
  86. * @type {string}
  87. * @private
  88. */
  89. goog.ui.DragDropDetector.DROP_EVENT_TYPE_ =
  90. goog.userAgent.IE ? goog.events.EventType.DROP : 'dragdrop';
  91. /**
  92. * Initial value for clientX and clientY indicating that the location has
  93. * never been updated.
  94. */
  95. goog.ui.DragDropDetector.INIT_POSITION = -10000;
  96. /**
  97. * Prefix for all CSS names.
  98. * @type {string}
  99. * @private
  100. */
  101. goog.ui.DragDropDetector.BASE_CSS_NAME_ = goog.getCssName('goog-dragdrop');
  102. /**
  103. * @desc Message shown to users to inform them that they can't drag and drop
  104. * local files.
  105. */
  106. goog.ui.DragDropDetector.MSG_DRAG_DROP_LOCAL_FILE_ERROR = goog.getMsg(
  107. 'It is not possible to drag ' +
  108. 'and drop image files at this time.\nPlease drag an image from your web ' +
  109. 'browser.');
  110. /**
  111. * @desc Message shown to users trying to drag and drop protected images from
  112. * Flickr, etc.
  113. */
  114. goog.ui.DragDropDetector.MSG_DRAG_DROP_PROTECTED_FILE_ERROR = goog.getMsg(
  115. 'The image you are ' +
  116. 'trying to drag has been blocked by the hosting site.');
  117. /**
  118. * A map of special case information for URLs that cannot be dropped. Each
  119. * entry is of the form:
  120. * regex: url regex
  121. * message: user visible message about this special case
  122. * @type {Array<{regex: RegExp, message: string}>}
  123. * @private
  124. */
  125. goog.ui.DragDropDetector.SPECIAL_CASE_URLS_ = [
  126. {
  127. regex: /^file:\/\/\//,
  128. message: goog.ui.DragDropDetector.MSG_DRAG_DROP_LOCAL_FILE_ERROR
  129. },
  130. {
  131. regex: /flickr(.*)spaceball.gif$/,
  132. message: goog.ui.DragDropDetector.MSG_DRAG_DROP_PROTECTED_FILE_ERROR
  133. }
  134. ];
  135. /**
  136. * Regex that matches anything that looks kind of like a URL. It matches
  137. * nonspacechars://nonspacechars
  138. * @type {RegExp}
  139. * @private
  140. */
  141. goog.ui.DragDropDetector.URL_LIKE_REGEX_ = /^\S+:\/\/\S*$/;
  142. /**
  143. * Path to the dragdrop.html file.
  144. * @type {string}
  145. * @private
  146. */
  147. goog.ui.DragDropDetector.DEFAULT_FILE_PATH_ = 'dragdropdetector_target.html';
  148. /**
  149. * Our event handler object.
  150. * @type {goog.events.EventHandler}
  151. * @private
  152. */
  153. goog.ui.DragDropDetector.prototype.handler_;
  154. /**
  155. * The root element (the IFRAME on most browsers, the DIV on IE).
  156. * @type {Element}
  157. * @private
  158. */
  159. goog.ui.DragDropDetector.prototype.root_;
  160. /**
  161. * The text INPUT element used to detect link drops on IE. null on Firefox.
  162. * @type {Element}
  163. * @private
  164. */
  165. goog.ui.DragDropDetector.prototype.textInput_;
  166. /**
  167. * The iframe element.
  168. * @type {HTMLIFrameElement}
  169. * @private
  170. */
  171. goog.ui.DragDropDetector.prototype.element_;
  172. /**
  173. * The iframe's window, null if the iframe hasn't loaded yet.
  174. * @type {Window}
  175. * @private
  176. */
  177. goog.ui.DragDropDetector.prototype.window_ = null;
  178. /**
  179. * The iframe's document, null if the iframe hasn't loaded yet.
  180. * @type {Document}
  181. * @private
  182. */
  183. goog.ui.DragDropDetector.prototype.document_ = null;
  184. /**
  185. * The iframe's body, null if the iframe hasn't loaded yet.
  186. * @type {HTMLBodyElement}
  187. * @private
  188. */
  189. goog.ui.DragDropDetector.prototype.body_ = null;
  190. /**
  191. * Whether we are in "screen cover" mode in which the iframe or div is
  192. * covering the entire screen.
  193. * @type {boolean}
  194. * @private
  195. */
  196. goog.ui.DragDropDetector.prototype.isCoveringScreen_ = false;
  197. /**
  198. * The last position of the mouse while dragging.
  199. * @type {goog.math.Coordinate}
  200. * @private
  201. */
  202. goog.ui.DragDropDetector.prototype.mousePosition_ = null;
  203. /**
  204. * Initialize the iframe after it has loaded.
  205. * @private
  206. */
  207. goog.ui.DragDropDetector.prototype.initIframe_ = function() {
  208. // Set up a holder for position data.
  209. this.mousePosition_ = new goog.math.Coordinate(
  210. goog.ui.DragDropDetector.INIT_POSITION,
  211. goog.ui.DragDropDetector.INIT_POSITION);
  212. // Set up pointers to the important parts of the IFrame.
  213. this.window_ = this.element_.contentWindow;
  214. this.document_ = this.window_.document;
  215. this.body_ = this.document_.body;
  216. if (goog.userAgent.GECKO) {
  217. this.document_.designMode = 'on';
  218. } else if (!goog.userAgent.IE) {
  219. // Bug 1667110
  220. // In IE, we only set the IFrame body as content-editable when we bring it
  221. // into view at the top of the page. Otherwise it may take focus when the
  222. // page is loaded, scrolling the user far offscreen.
  223. // Note that this isn't easily unit-testable, since it depends on a
  224. // browser-specific behavior with content-editable areas.
  225. this.body_.contentEditable = true;
  226. }
  227. this.handler_.listen(
  228. document.body, goog.events.EventType.DRAGENTER, this.coverScreen_);
  229. if (goog.userAgent.IE) {
  230. // IE only events.
  231. // Set up events on the IFrame.
  232. this.handler_
  233. .listen(
  234. this.body_,
  235. [goog.events.EventType.DRAGENTER, goog.events.EventType.DRAGOVER],
  236. goog.ui.DragDropDetector.enforceCopyEffect_)
  237. .listen(this.body_, goog.events.EventType.MOUSEOUT, this.switchToInput_)
  238. .listen(
  239. this.body_, goog.events.EventType.DRAGLEAVE, this.uncoverScreen_)
  240. .listen(
  241. this.body_, goog.ui.DragDropDetector.DROP_EVENT_TYPE_,
  242. function(e) {
  243. this.trackMouse_(e);
  244. // The drop event occurs before the content is added to the
  245. // iframe. We setTimeout so that handleNodeInserted_ is called
  246. // after the content is in the document.
  247. goog.global.setTimeout(
  248. goog.bind(this.handleNodeInserted_, this, e), 0);
  249. return true;
  250. })
  251. .
  252. // Set up events on the DIV.
  253. listen(
  254. this.root_,
  255. [goog.events.EventType.DRAGENTER, goog.events.EventType.DRAGOVER],
  256. this.handleNewDrag_)
  257. .listen(
  258. this.root_,
  259. [goog.events.EventType.MOUSEMOVE, goog.events.EventType.KEYPRESS],
  260. this.uncoverScreen_)
  261. .
  262. // Set up events on the text INPUT.
  263. listen(
  264. this.textInput_, goog.events.EventType.DRAGOVER,
  265. goog.events.Event.preventDefault)
  266. .listen(
  267. this.textInput_, goog.ui.DragDropDetector.DROP_EVENT_TYPE_,
  268. this.handleInputDrop_);
  269. } else {
  270. // W3C events.
  271. this.handler_
  272. .listen(
  273. this.body_, goog.ui.DragDropDetector.DROP_EVENT_TYPE_,
  274. function(e) {
  275. this.trackMouse_(e);
  276. this.uncoverScreen_();
  277. })
  278. .listen(
  279. this.body_,
  280. [goog.events.EventType.MOUSEMOVE, goog.events.EventType.KEYPRESS],
  281. this.uncoverScreen_)
  282. .
  283. // Detect content insertion.
  284. listen(this.document_, 'DOMNodeInserted', this.handleNodeInserted_);
  285. }
  286. };
  287. /**
  288. * Enforce that anything dragged over the IFRAME is copied in to it, rather
  289. * than making it navigate to a different URL.
  290. * @param {goog.events.BrowserEvent} e The event to enforce copying on.
  291. * @private
  292. */
  293. goog.ui.DragDropDetector.enforceCopyEffect_ = function(e) {
  294. var event = e.getBrowserEvent();
  295. // This function is only called on IE.
  296. if (event.dataTransfer.dropEffect.toLowerCase() != 'copy') {
  297. event.dataTransfer.dropEffect = 'copy';
  298. }
  299. };
  300. /**
  301. * Cover the screen with the iframe.
  302. * @param {goog.events.BrowserEvent} e The event that caused this function call.
  303. * @private
  304. */
  305. goog.ui.DragDropDetector.prototype.coverScreen_ = function(e) {
  306. // Don't do anything if the drop effect is 'none' and we are in IE.
  307. // It is set to 'none' in cases like dragging text inside a text area.
  308. if (goog.userAgent.IE &&
  309. e.getBrowserEvent().dataTransfer.dropEffect == 'none') {
  310. return;
  311. }
  312. if (!this.isCoveringScreen_) {
  313. this.isCoveringScreen_ = true;
  314. if (goog.userAgent.IE) {
  315. goog.style.setStyle(this.root_, 'top', '0');
  316. this.body_.contentEditable = true;
  317. this.switchToInput_(e);
  318. } else {
  319. goog.style.setStyle(this.root_, 'height', '5000px');
  320. }
  321. }
  322. };
  323. /**
  324. * Uncover the screen.
  325. * @private
  326. */
  327. goog.ui.DragDropDetector.prototype.uncoverScreen_ = function() {
  328. if (this.isCoveringScreen_) {
  329. this.isCoveringScreen_ = false;
  330. if (goog.userAgent.IE) {
  331. this.body_.contentEditable = false;
  332. goog.style.setStyle(this.root_, 'top', '-5000px');
  333. } else {
  334. goog.style.setStyle(this.root_, 'height', '10px');
  335. }
  336. }
  337. };
  338. /**
  339. * Re-insert the INPUT into the DIV. Does nothing when the DIV is off screen.
  340. * @param {goog.events.BrowserEvent} e The event that caused this function call.
  341. * @private
  342. */
  343. goog.ui.DragDropDetector.prototype.switchToInput_ = function(e) {
  344. // This is only called on IE.
  345. if (this.isCoveringScreen_) {
  346. goog.style.setElementShown(this.textInput_, true);
  347. }
  348. };
  349. /**
  350. * Remove the text INPUT so the IFRAME is showing. Does nothing when the DIV is
  351. * off screen.
  352. * @param {goog.events.BrowserEvent} e The event that caused this function call.
  353. * @private
  354. */
  355. goog.ui.DragDropDetector.prototype.switchToIframe_ = function(e) {
  356. // This is only called on IE.
  357. if (this.isCoveringScreen_) {
  358. goog.style.setElementShown(this.textInput_, false);
  359. }
  360. };
  361. /**
  362. * Handle a new drag event.
  363. * @param {goog.events.BrowserEvent} e The event object.
  364. * @return {boolean|undefined} Returns false in IE to cancel the event.
  365. * @private
  366. */
  367. goog.ui.DragDropDetector.prototype.handleNewDrag_ = function(e) {
  368. var event = e.getBrowserEvent();
  369. // This is only called on IE.
  370. if (event.dataTransfer.dropEffect == 'link') {
  371. this.switchToInput_(e);
  372. e.preventDefault();
  373. return false;
  374. }
  375. // Things that aren't links can be placed in the contentEditable iframe.
  376. this.switchToIframe_(e);
  377. // No need to return true since for events return true is the same as no
  378. // return.
  379. };
  380. /**
  381. * Handle mouse tracking.
  382. * @param {goog.events.BrowserEvent} e The event object.
  383. * @private
  384. */
  385. goog.ui.DragDropDetector.prototype.trackMouse_ = function(e) {
  386. this.mousePosition_.x = e.clientX;
  387. this.mousePosition_.y = e.clientY;
  388. // Check if the event is coming from within the iframe.
  389. if (goog.dom.getOwnerDocument(/** @type {Node} */ (e.target)) != document) {
  390. var iframePosition = goog.style.getClientPosition(this.element_);
  391. this.mousePosition_.x += iframePosition.x;
  392. this.mousePosition_.y += iframePosition.y;
  393. }
  394. };
  395. /**
  396. * Handle a drop on the IE text INPUT.
  397. * @param {goog.events.BrowserEvent} e The event object.
  398. * @private
  399. */
  400. goog.ui.DragDropDetector.prototype.handleInputDrop_ = function(e) {
  401. this.dispatchEvent(
  402. new goog.ui.DragDropDetector.LinkDropEvent(
  403. e.getBrowserEvent().dataTransfer.getData('Text')));
  404. this.uncoverScreen_();
  405. e.preventDefault();
  406. };
  407. /**
  408. * Clear the contents of the iframe.
  409. * @private
  410. */
  411. goog.ui.DragDropDetector.prototype.clearContents_ = function() {
  412. if (goog.userAgent.WEBKIT) {
  413. // Since this is called on a mutation event for the nodes we are going to
  414. // clear, calling this right away crashes some versions of WebKit. Wait
  415. // until the events are finished.
  416. goog.global.setTimeout(goog.bind(function() {
  417. goog.dom.setTextContent(this, '');
  418. }, this.body_), 0);
  419. } else {
  420. this.document_.execCommand('selectAll', false, null);
  421. this.document_.execCommand('delete', false, null);
  422. this.document_.execCommand('selectAll', false, null);
  423. }
  424. };
  425. /**
  426. * Event handler called when the content of the iframe changes.
  427. * @param {goog.events.BrowserEvent} e The event that caused this function call.
  428. * @private
  429. */
  430. goog.ui.DragDropDetector.prototype.handleNodeInserted_ = function(e) {
  431. var uri;
  432. if (this.body_.innerHTML.indexOf('<') == -1) {
  433. // If the document contains no tags (i.e. is just text), try it out.
  434. uri = goog.string.trim(goog.dom.getTextContent(this.body_));
  435. // See if it looks kind of like a url.
  436. if (!uri.match(goog.ui.DragDropDetector.URL_LIKE_REGEX_)) {
  437. uri = null;
  438. }
  439. }
  440. if (!uri) {
  441. var imgs = goog.dom.getElementsByTagName(goog.dom.TagName.IMG, this.body_);
  442. if (imgs && imgs.length) {
  443. // TODO(robbyw): Grab all the images, instead of just the first.
  444. var img = imgs[0];
  445. uri = img.src;
  446. }
  447. }
  448. if (uri) {
  449. var specialCases = goog.ui.DragDropDetector.SPECIAL_CASE_URLS_;
  450. var len = specialCases.length;
  451. for (var i = 0; i < len; i++) {
  452. var specialCase = specialCases[i];
  453. if (uri.match(specialCase.regex)) {
  454. alert(specialCase.message);
  455. break;
  456. }
  457. }
  458. // If no special cases matched, add the image.
  459. if (i == len) {
  460. this.dispatchEvent(
  461. new goog.ui.DragDropDetector.ImageDropEvent(
  462. uri, this.mousePosition_));
  463. return;
  464. }
  465. }
  466. var links = goog.dom.getElementsByTagName(goog.dom.TagName.A, this.body_);
  467. if (links) {
  468. for (i = 0, len = links.length; i < len; i++) {
  469. this.dispatchEvent(
  470. new goog.ui.DragDropDetector.LinkDropEvent(links[i].href));
  471. }
  472. }
  473. this.clearContents_();
  474. this.uncoverScreen_();
  475. };
  476. /** @override */
  477. goog.ui.DragDropDetector.prototype.disposeInternal = function() {
  478. goog.ui.DragDropDetector.base(this, 'disposeInternal');
  479. this.handler_.dispose();
  480. this.handler_ = null;
  481. };
  482. /**
  483. * Creates a new image drop event object.
  484. * @param {string} url The url of the dropped image.
  485. * @param {goog.math.Coordinate} position The screen position where the drop
  486. * occurred.
  487. * @constructor
  488. * @extends {goog.events.Event}
  489. * @final
  490. */
  491. goog.ui.DragDropDetector.ImageDropEvent = function(url, position) {
  492. goog.ui.DragDropDetector.ImageDropEvent.base(
  493. this, 'constructor', goog.ui.DragDropDetector.EventType.IMAGE_DROPPED);
  494. /**
  495. * The url of the image that was dropped.
  496. * @type {string}
  497. * @private
  498. */
  499. this.url_ = url;
  500. /**
  501. * The screen position where the drop occurred.
  502. * @type {goog.math.Coordinate}
  503. * @private
  504. */
  505. this.position_ = position;
  506. };
  507. goog.inherits(goog.ui.DragDropDetector.ImageDropEvent, goog.events.Event);
  508. /**
  509. * @return {string} The url of the image that was dropped.
  510. */
  511. goog.ui.DragDropDetector.ImageDropEvent.prototype.getUrl = function() {
  512. return this.url_;
  513. };
  514. /**
  515. * @return {goog.math.Coordinate} The screen position where the drop occurred.
  516. * This may be have x and y of goog.ui.DragDropDetector.INIT_POSITION,
  517. * indicating the drop position is unknown.
  518. */
  519. goog.ui.DragDropDetector.ImageDropEvent.prototype.getPosition = function() {
  520. return this.position_;
  521. };
  522. /**
  523. * Creates a new link drop event object.
  524. * @param {string} url The url of the dropped link.
  525. * @constructor
  526. * @extends {goog.events.Event}
  527. * @final
  528. */
  529. goog.ui.DragDropDetector.LinkDropEvent = function(url) {
  530. goog.ui.DragDropDetector.LinkDropEvent.base(
  531. this, 'constructor', goog.ui.DragDropDetector.EventType.LINK_DROPPED);
  532. /**
  533. * The url of the link that was dropped.
  534. * @type {string}
  535. * @private
  536. */
  537. this.url_ = url;
  538. };
  539. goog.inherits(goog.ui.DragDropDetector.LinkDropEvent, goog.events.Event);
  540. /**
  541. * @return {string} The url of the link that was dropped.
  542. */
  543. goog.ui.DragDropDetector.LinkDropEvent.prototype.getUrl = function() {
  544. return this.url_;
  545. };