clicktoeditwrapper.js 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421
  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 A wrapper around a goog.editor.Field
  16. * that listens to mouse events on the specified un-editable field, and makes
  17. * the field editable if the user clicks on it. Clients are still responsible
  18. * for determining when to make the field un-editable again.
  19. *
  20. * Clients can still determine when the field has loaded by listening to
  21. * field's load event.
  22. *
  23. * @author nicksantos@google.com (Nick Santos)
  24. */
  25. goog.provide('goog.editor.ClickToEditWrapper');
  26. goog.require('goog.Disposable');
  27. goog.require('goog.dom');
  28. goog.require('goog.dom.Range');
  29. goog.require('goog.dom.TagName');
  30. goog.require('goog.editor.BrowserFeature');
  31. goog.require('goog.editor.Command');
  32. goog.require('goog.editor.Field');
  33. goog.require('goog.editor.range');
  34. goog.require('goog.events.BrowserEvent');
  35. goog.require('goog.events.EventHandler');
  36. goog.require('goog.events.EventType');
  37. /**
  38. * Initialize the wrapper, and begin listening to mouse events immediately.
  39. * @param {goog.editor.Field} fieldObj The editable field being wrapped.
  40. * @constructor
  41. * @extends {goog.Disposable}
  42. */
  43. goog.editor.ClickToEditWrapper = function(fieldObj) {
  44. goog.Disposable.call(this);
  45. /**
  46. * The field this wrapper interacts with.
  47. * @type {goog.editor.Field}
  48. * @private
  49. */
  50. this.fieldObj_ = fieldObj;
  51. /**
  52. * DOM helper for the field's original element.
  53. * @type {goog.dom.DomHelper}
  54. * @private
  55. */
  56. this.originalDomHelper_ =
  57. goog.dom.getDomHelper(fieldObj.getOriginalElement());
  58. /**
  59. * @type {goog.dom.SavedCaretRange}
  60. * @private
  61. */
  62. this.savedCaretRange_ = null;
  63. /**
  64. * Event handler for field related events.
  65. * @type {!goog.events.EventHandler<!goog.editor.ClickToEditWrapper>}
  66. * @private
  67. */
  68. this.fieldEventHandler_ = new goog.events.EventHandler(this);
  69. /**
  70. * Bound version of the finishMouseUp method.
  71. * @type {Function}
  72. * @private
  73. */
  74. this.finishMouseUpBound_ = goog.bind(this.finishMouseUp_, this);
  75. /**
  76. * Event handler for mouse events.
  77. * @type {!goog.events.EventHandler<!goog.editor.ClickToEditWrapper>}
  78. * @private
  79. */
  80. this.mouseEventHandler_ = new goog.events.EventHandler(this);
  81. // Start listening to mouse events immediately if necessary.
  82. if (!this.fieldObj_.isLoaded()) {
  83. this.enterDocument();
  84. }
  85. this.fieldEventHandler_
  86. .
  87. // Whenever the field is made editable, we need to check if there
  88. // are any carets in it, and if so, use them to render the selection.
  89. listen(
  90. this.fieldObj_, goog.editor.Field.EventType.LOAD,
  91. this.renderSelection_)
  92. .
  93. // Whenever the field is made uneditable, we need to set up
  94. // the click-to-edit listeners.
  95. listen(
  96. this.fieldObj_, goog.editor.Field.EventType.UNLOAD,
  97. this.enterDocument);
  98. };
  99. goog.inherits(goog.editor.ClickToEditWrapper, goog.Disposable);
  100. /** @return {goog.editor.Field} The field. */
  101. goog.editor.ClickToEditWrapper.prototype.getFieldObject = function() {
  102. return this.fieldObj_;
  103. };
  104. /** @return {goog.dom.DomHelper} The dom helper of the uneditable element. */
  105. goog.editor.ClickToEditWrapper.prototype.getOriginalDomHelper = function() {
  106. return this.originalDomHelper_;
  107. };
  108. /** @override */
  109. goog.editor.ClickToEditWrapper.prototype.disposeInternal = function() {
  110. goog.editor.ClickToEditWrapper.base(this, 'disposeInternal');
  111. this.exitDocument();
  112. if (this.savedCaretRange_) {
  113. this.savedCaretRange_.dispose();
  114. }
  115. this.fieldEventHandler_.dispose();
  116. this.mouseEventHandler_.dispose();
  117. this.savedCaretRange_ = null;
  118. delete this.fieldEventHandler_;
  119. delete this.mouseEventHandler_;
  120. };
  121. /**
  122. * Initialize listeners when the uneditable field is added to the document.
  123. * Also sets up lorem ipsum text.
  124. */
  125. goog.editor.ClickToEditWrapper.prototype.enterDocument = function() {
  126. if (this.isInDocument_) {
  127. return;
  128. }
  129. this.isInDocument_ = true;
  130. this.mouseEventTriggeredLoad_ = false;
  131. var field = this.fieldObj_.getOriginalElement();
  132. // To do artificial selection preservation, we have to listen to mouseup,
  133. // get the current selection, and re-select the same text in the iframe.
  134. //
  135. // NOTE(nicksantos): Artificial selection preservation is needed in all cases
  136. // where we set the field contents by setting innerHTML. There are a few
  137. // rare cases where we don't need it. But these cases are highly
  138. // implementation-specific, and computationally hard to detect (bidi
  139. // and ig modules both set innerHTML), so we just do it in all cases.
  140. this.savedAnchorClicked_ = null;
  141. this.mouseEventHandler_
  142. .listen(field, goog.events.EventType.MOUSEUP, this.handleMouseUp_)
  143. .listen(field, goog.events.EventType.CLICK, this.handleClick_);
  144. // manage lorem ipsum text, if necessary
  145. this.fieldObj_.execCommand(goog.editor.Command.UPDATE_LOREM);
  146. };
  147. /**
  148. * Destroy listeners when the field is removed from the document.
  149. */
  150. goog.editor.ClickToEditWrapper.prototype.exitDocument = function() {
  151. this.mouseEventHandler_.removeAll();
  152. this.isInDocument_ = false;
  153. };
  154. /**
  155. * Returns the uneditable field element if the field is not yet editable
  156. * (equivalent to EditableField.getOriginalElement()), and the editable DOM
  157. * element if the field is currently editable (equivalent to
  158. * EditableField.getElement()).
  159. * @return {Element} The element containing the editable field contents.
  160. */
  161. goog.editor.ClickToEditWrapper.prototype.getElement = function() {
  162. return this.fieldObj_.isLoaded() ? this.fieldObj_.getElement() :
  163. this.fieldObj_.getOriginalElement();
  164. };
  165. /**
  166. * True if a mouse event should be handled, false if it should be ignored.
  167. * @param {goog.events.BrowserEvent} e The mouse event.
  168. * @return {boolean} Wether or not this mouse event should be handled.
  169. * @private
  170. */
  171. goog.editor.ClickToEditWrapper.prototype.shouldHandleMouseEvent_ = function(e) {
  172. return e.isButton(goog.events.BrowserEvent.MouseButton.LEFT) &&
  173. !(e.shiftKey || e.ctrlKey || e.altKey || e.metaKey);
  174. };
  175. /**
  176. * Handle mouse click events on the field.
  177. * @param {goog.events.BrowserEvent} e The click event.
  178. * @private
  179. */
  180. goog.editor.ClickToEditWrapper.prototype.handleClick_ = function(e) {
  181. // If the user clicked on a link in an uneditable field,
  182. // we want to cancel the click.
  183. var anchorAncestor = goog.dom.getAncestorByTagNameAndClass(
  184. /** @type {Node} */ (e.target), goog.dom.TagName.A);
  185. if (anchorAncestor) {
  186. e.preventDefault();
  187. if (!goog.editor.BrowserFeature.HAS_ACTIVE_ELEMENT) {
  188. this.savedAnchorClicked_ = anchorAncestor;
  189. }
  190. }
  191. };
  192. /**
  193. * Handle a mouse up event on the field.
  194. * @param {goog.events.BrowserEvent} e The mouseup event.
  195. * @private
  196. */
  197. goog.editor.ClickToEditWrapper.prototype.handleMouseUp_ = function(e) {
  198. // Only respond to the left mouse button.
  199. if (this.shouldHandleMouseEvent_(e)) {
  200. // We need to get the selection when the user mouses up, but the
  201. // selection doesn't actually change until after the mouseup event has
  202. // propagated. So we need to do this asynchronously.
  203. this.originalDomHelper_.getWindow().setTimeout(this.finishMouseUpBound_, 0);
  204. }
  205. };
  206. /**
  207. * A helper function for handleMouseUp_ -- does the actual work
  208. * when the event is finished propagating.
  209. * @private
  210. */
  211. goog.editor.ClickToEditWrapper.prototype.finishMouseUp_ = function() {
  212. // Make sure that the field is still not editable.
  213. if (!this.fieldObj_.isLoaded()) {
  214. if (this.savedCaretRange_) {
  215. this.savedCaretRange_.dispose();
  216. this.savedCaretRange_ = null;
  217. }
  218. if (!this.fieldObj_.queryCommandValue(goog.editor.Command.USING_LOREM)) {
  219. // We need carets (blank span nodes) to maintain the selection when
  220. // the html is copied into an iframe. However, because our code
  221. // clears the selection to make the behavior consistent, we need to do
  222. // this even when we're not using an iframe.
  223. this.insertCarets_();
  224. }
  225. this.ensureFieldEditable_();
  226. }
  227. this.exitDocument();
  228. this.savedAnchorClicked_ = null;
  229. };
  230. /**
  231. * Ensure that the field is editable. If the field is not editable,
  232. * make it so, and record the fact that it was done by a user mouse event.
  233. * @private
  234. */
  235. goog.editor.ClickToEditWrapper.prototype.ensureFieldEditable_ = function() {
  236. if (!this.fieldObj_.isLoaded()) {
  237. this.mouseEventTriggeredLoad_ = true;
  238. this.makeFieldEditable(this.fieldObj_);
  239. }
  240. };
  241. /**
  242. * Once the field has loaded in an iframe, re-create the selection
  243. * as marked by the carets.
  244. * @private
  245. */
  246. goog.editor.ClickToEditWrapper.prototype.renderSelection_ = function() {
  247. if (this.savedCaretRange_) {
  248. // Make sure that the restoration document is inside the iframe
  249. // if we're using one.
  250. this.savedCaretRange_.setRestorationDocument(
  251. this.fieldObj_.getEditableDomHelper().getDocument());
  252. var startCaret = this.savedCaretRange_.getCaret(true);
  253. var endCaret = this.savedCaretRange_.getCaret(false);
  254. var hasCarets = startCaret && endCaret;
  255. }
  256. // There are two reasons why we might want to focus the field:
  257. // 1) makeFieldEditable was triggered by the click-to-edit wrapper.
  258. // In this case, the mouse event should have triggered a focus, but
  259. // the editor might have taken the focus away to create lorem ipsum
  260. // text or create an iframe for the field. So we make sure the focus
  261. // is restored.
  262. // 2) somebody placed carets, and we need to select those carets. The field
  263. // needs focus to ensure that the selection appears.
  264. if (this.mouseEventTriggeredLoad_ || hasCarets) {
  265. this.focusOnFieldObj(this.fieldObj_);
  266. }
  267. if (hasCarets) {
  268. this.savedCaretRange_.restore();
  269. this.fieldObj_.dispatchSelectionChangeEvent();
  270. // NOTE(nicksantos): Bubbles aren't actually enabled until the end
  271. // if the load sequence, so if the user clicked on a link, the bubble
  272. // will not pop up.
  273. }
  274. if (this.savedCaretRange_) {
  275. this.savedCaretRange_.dispose();
  276. this.savedCaretRange_ = null;
  277. }
  278. this.mouseEventTriggeredLoad_ = false;
  279. };
  280. /**
  281. * Focus on the field object.
  282. * @param {goog.editor.Field} field The field to focus.
  283. * @protected
  284. */
  285. goog.editor.ClickToEditWrapper.prototype.focusOnFieldObj = function(field) {
  286. field.focusAndPlaceCursorAtStart();
  287. };
  288. /**
  289. * Make the field object editable.
  290. * @param {goog.editor.Field} field The field to make editable.
  291. * @protected
  292. */
  293. goog.editor.ClickToEditWrapper.prototype.makeFieldEditable = function(field) {
  294. field.makeEditable();
  295. };
  296. //================================================================
  297. // Caret-handling methods
  298. /**
  299. * Gets a saved caret range for the given range.
  300. * @param {goog.dom.AbstractRange} range A range wrapper.
  301. * @return {goog.dom.SavedCaretRange} The range, saved with carets, or null
  302. * if the range wrapper was null.
  303. * @private
  304. */
  305. goog.editor.ClickToEditWrapper.createCaretRange_ = function(range) {
  306. return range && goog.editor.range.saveUsingNormalizedCarets(range);
  307. };
  308. /**
  309. * Inserts the carets, given the current selection.
  310. *
  311. * Note that for all practical purposes, a cursor position is just
  312. * a selection with the start and end at the same point.
  313. * @private
  314. */
  315. goog.editor.ClickToEditWrapper.prototype.insertCarets_ = function() {
  316. var fieldElement = this.fieldObj_.getOriginalElement();
  317. this.savedCaretRange_ = null;
  318. var originalWindow = this.originalDomHelper_.getWindow();
  319. if (goog.dom.Range.hasSelection(originalWindow)) {
  320. var range = goog.dom.Range.createFromWindow(originalWindow);
  321. range = range && goog.editor.range.narrow(range, fieldElement);
  322. this.savedCaretRange_ =
  323. goog.editor.ClickToEditWrapper.createCaretRange_(range);
  324. }
  325. if (!this.savedCaretRange_) {
  326. // We couldn't figure out where to put the carets.
  327. // But in FF2/IE6+, this could mean that the user clicked on a
  328. // 'special' node, (e.g., a link or an unselectable item). So the
  329. // selection appears to be null or the full page, even though the user did
  330. // click on something. In IE, we can determine the real selection via
  331. // document.activeElement. In FF, we have to be more hacky.
  332. var specialNodeClicked;
  333. if (goog.editor.BrowserFeature.HAS_ACTIVE_ELEMENT) {
  334. specialNodeClicked =
  335. goog.dom.getActiveElement(this.originalDomHelper_.getDocument());
  336. } else {
  337. specialNodeClicked = this.savedAnchorClicked_;
  338. }
  339. var isFieldElement = function(node) { return node == fieldElement; };
  340. if (specialNodeClicked &&
  341. goog.dom.getAncestor(specialNodeClicked, isFieldElement, true)) {
  342. // Insert the cursor at the beginning of the active element to be
  343. // consistent with the behavior in FF1.5, where clicking on a
  344. // link makes the current selection equal to the cursor position
  345. // directly before that link.
  346. //
  347. // TODO(nicksantos): Is there a way to more accurately place the cursor?
  348. this.savedCaretRange_ = goog.editor.ClickToEditWrapper.createCaretRange_(
  349. goog.dom.Range.createFromNodes(
  350. specialNodeClicked, 0, specialNodeClicked, 0));
  351. }
  352. }
  353. };