enterhandler.js 28 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773
  1. // Copyright 2008 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 Plugin to handle enter keys.
  16. *
  17. * @author robbyw@google.com (Robby Walker)
  18. */
  19. goog.provide('goog.editor.plugins.EnterHandler');
  20. goog.require('goog.dom');
  21. goog.require('goog.dom.NodeOffset');
  22. goog.require('goog.dom.NodeType');
  23. goog.require('goog.dom.Range');
  24. goog.require('goog.dom.TagName');
  25. goog.require('goog.editor.BrowserFeature');
  26. goog.require('goog.editor.Plugin');
  27. goog.require('goog.editor.node');
  28. goog.require('goog.editor.plugins.Blockquote');
  29. goog.require('goog.editor.range');
  30. goog.require('goog.editor.style');
  31. goog.require('goog.events.KeyCodes');
  32. goog.require('goog.functions');
  33. goog.require('goog.object');
  34. goog.require('goog.string');
  35. goog.require('goog.userAgent');
  36. /**
  37. * Plugin to handle enter keys. This does all the crazy to normalize (as much as
  38. * is reasonable) what happens when you hit enter. This also handles the
  39. * special casing of hitting enter in a blockquote.
  40. *
  41. * In IE, Webkit, and Opera, the resulting HTML uses one DIV tag per line. In
  42. * Firefox, the resulting HTML uses BR tags at the end of each line.
  43. *
  44. * @constructor
  45. * @extends {goog.editor.Plugin}
  46. */
  47. goog.editor.plugins.EnterHandler = function() {
  48. goog.editor.Plugin.call(this);
  49. };
  50. goog.inherits(goog.editor.plugins.EnterHandler, goog.editor.Plugin);
  51. /**
  52. * The type of block level tag to add on enter, for browsers that support
  53. * specifying the default block-level tag. Can be overriden by subclasses; must
  54. * be either DIV or P.
  55. * @type {!goog.dom.TagName}
  56. * @protected
  57. */
  58. goog.editor.plugins.EnterHandler.prototype.tag = goog.dom.TagName.DIV;
  59. /** @override */
  60. goog.editor.plugins.EnterHandler.prototype.getTrogClassId = function() {
  61. return 'EnterHandler';
  62. };
  63. /** @override */
  64. goog.editor.plugins.EnterHandler.prototype.enable = function(fieldObject) {
  65. goog.editor.plugins.EnterHandler.base(this, 'enable', fieldObject);
  66. if (goog.editor.BrowserFeature.SUPPORTS_OPERA_DEFAULTBLOCK_COMMAND &&
  67. (this.tag == goog.dom.TagName.P || this.tag == goog.dom.TagName.DIV)) {
  68. var doc = this.getFieldDomHelper().getDocument();
  69. doc.execCommand('opera-defaultBlock', false, this.tag);
  70. }
  71. };
  72. /**
  73. * If the contents are empty, return the 'default' html for the field.
  74. * The 'default' contents depend on the enter handling mode, so it
  75. * makes the most sense in this plugin.
  76. * @param {string} html The html to prepare.
  77. * @return {string} The original HTML, or default contents if that
  78. * html is empty.
  79. * @override
  80. */
  81. goog.editor.plugins.EnterHandler.prototype.prepareContentsHtml = function(
  82. html) {
  83. if (!html || goog.string.isBreakingWhitespace(html)) {
  84. return goog.editor.BrowserFeature.COLLAPSES_EMPTY_NODES ?
  85. this.getNonCollapsingBlankHtml() :
  86. '';
  87. }
  88. return html;
  89. };
  90. /**
  91. * Gets HTML with no contents that won't collapse, for browsers that
  92. * collapse the empty string.
  93. * @return {string} Blank html.
  94. * @protected
  95. */
  96. goog.editor.plugins.EnterHandler.prototype.getNonCollapsingBlankHtml =
  97. goog.functions.constant('<br>');
  98. /**
  99. * Internal backspace handler.
  100. * @param {goog.events.Event} e The keypress event.
  101. * @param {goog.dom.AbstractRange} range The closure range object.
  102. * @protected
  103. */
  104. goog.editor.plugins.EnterHandler.prototype.handleBackspaceInternal = function(
  105. e, range) {
  106. var field = this.getFieldObject().getElement();
  107. var container = range && range.getStartNode();
  108. if (field.firstChild == container && goog.editor.node.isEmpty(container)) {
  109. e.preventDefault();
  110. // TODO(user): I think we probably don't need to stopPropagation here
  111. e.stopPropagation();
  112. }
  113. };
  114. /**
  115. * Fix paragraphs to be the correct type of node.
  116. * @param {goog.events.Event} e The `<enter>` key event.
  117. * @param {boolean} split Whether we already split up a blockquote by
  118. * manually inserting elements.
  119. * @protected
  120. */
  121. goog.editor.plugins.EnterHandler.prototype.processParagraphTagsInternal =
  122. function(e, split) {
  123. // Force IE to turn the node we are leaving into a DIV. If we do turn
  124. // it into a DIV, the node IE creates in response to ENTER will also be
  125. // a DIV. If we don't, it will be a P. We handle that case
  126. // in handleKeyUpIE_
  127. if (goog.userAgent.IE || goog.userAgent.OPERA) {
  128. this.ensureBlockIeOpera(goog.dom.TagName.DIV);
  129. } else if (!split && goog.userAgent.WEBKIT) {
  130. // WebKit duplicates a blockquote when the user hits enter. Let's cancel
  131. // this and insert a BR instead, to make it more consistent with the other
  132. // browsers.
  133. var range = this.getFieldObject().getRange();
  134. if (!range ||
  135. !goog.editor.plugins.EnterHandler.isDirectlyInBlockquote(
  136. range.getContainerElement())) {
  137. return;
  138. }
  139. var dh = this.getFieldDomHelper();
  140. var br = dh.createElement(goog.dom.TagName.BR);
  141. range.insertNode(br, true);
  142. // If the BR is at the end of a block element, Safari still thinks there is
  143. // only one line instead of two, so we need to add another BR in that case.
  144. if (goog.editor.node.isBlockTag(br.parentNode) &&
  145. !goog.editor.node.skipEmptyTextNodes(br.nextSibling)) {
  146. goog.dom.insertSiblingBefore(dh.createElement(goog.dom.TagName.BR), br);
  147. }
  148. goog.editor.range.placeCursorNextTo(br, false);
  149. e.preventDefault();
  150. }
  151. };
  152. /**
  153. * Determines whether the lowest containing block node is a blockquote.
  154. * @param {Node} n The node.
  155. * @return {boolean} Whether the deepest block ancestor of n is a blockquote.
  156. */
  157. goog.editor.plugins.EnterHandler.isDirectlyInBlockquote = function(n) {
  158. for (var current = n; current; current = current.parentNode) {
  159. if (goog.editor.node.isBlockTag(current)) {
  160. return /** @type {!Element} */ (current).tagName ==
  161. goog.dom.TagName.BLOCKQUOTE;
  162. }
  163. }
  164. return false;
  165. };
  166. /**
  167. * Internal delete key handler.
  168. * @param {goog.events.Event} e The keypress event.
  169. * @protected
  170. */
  171. goog.editor.plugins.EnterHandler.prototype.handleDeleteGecko = function(e) {
  172. this.deleteBrGecko(e);
  173. };
  174. /**
  175. * Deletes the element at the cursor if it is a BR node, and if it does, calls
  176. * e.preventDefault to stop the browser from deleting. Only necessary in Gecko
  177. * as a workaround for mozilla bug 205350 where deleting a BR that is followed
  178. * by a block element doesn't work (the BR gets immediately replaced). We also
  179. * need to account for an ill-formed cursor which occurs from us trying to
  180. * stop the browser from deleting.
  181. *
  182. * @param {goog.events.Event} e The DELETE keypress event.
  183. * @protected
  184. */
  185. goog.editor.plugins.EnterHandler.prototype.deleteBrGecko = function(e) {
  186. var range = this.getFieldObject().getRange();
  187. if (range.isCollapsed()) {
  188. var container = range.getEndNode();
  189. if (container.nodeType == goog.dom.NodeType.ELEMENT) {
  190. var nextNode = container.childNodes[range.getEndOffset()];
  191. if (nextNode && nextNode.tagName == goog.dom.TagName.BR) {
  192. // We want to retrieve the first non-whitespace previous sibling
  193. // as we could have added an empty text node below and want to
  194. // properly handle deleting a sequence of BR's.
  195. var previousSibling = goog.editor.node.getPreviousSibling(nextNode);
  196. var nextSibling = nextNode.nextSibling;
  197. container.removeChild(nextNode);
  198. e.preventDefault();
  199. // When we delete a BR followed by a block level element, the cursor
  200. // has a line-height which spans the height of the block level element.
  201. // e.g. If we delete a BR followed by a UL, the resulting HTML will
  202. // appear to the end user like:-
  203. //
  204. // | * one
  205. // | * two
  206. // | * three
  207. //
  208. // There are a couple of cases that we have to account for in order to
  209. // properly conform to what the user expects when DELETE is pressed.
  210. //
  211. // 1. If the BR has a previous sibling and the previous sibling is
  212. // not a block level element or a BR, we place the cursor at the
  213. // end of that.
  214. // 2. If the BR doesn't have a previous sibling or the previous sibling
  215. // is a block level element or a BR, we place the cursor at the
  216. // beginning of the leftmost leaf of its next sibling.
  217. if (nextSibling && goog.editor.node.isBlockTag(nextSibling)) {
  218. if (previousSibling &&
  219. !(previousSibling.tagName == goog.dom.TagName.BR ||
  220. goog.editor.node.isBlockTag(previousSibling))) {
  221. goog.dom.Range
  222. .createCaret(
  223. previousSibling,
  224. goog.editor.node.getLength(previousSibling))
  225. .select();
  226. } else {
  227. var leftMostLeaf = goog.editor.node.getLeftMostLeaf(nextSibling);
  228. goog.dom.Range.createCaret(leftMostLeaf, 0).select();
  229. }
  230. }
  231. }
  232. }
  233. }
  234. };
  235. /** @override */
  236. goog.editor.plugins.EnterHandler.prototype.handleKeyPress = function(e) {
  237. // If a dialog doesn't have selectable field, Gecko grabs the event and
  238. // performs actions in editor window. This solves that problem and allows
  239. // the event to be passed on to proper handlers.
  240. if (goog.userAgent.GECKO && this.getFieldObject().inModalMode()) {
  241. return false;
  242. }
  243. // Firefox will allow the first node in an iframe to be deleted
  244. // on a backspace. Disallow it if the node is empty.
  245. if (e.keyCode == goog.events.KeyCodes.BACKSPACE) {
  246. this.handleBackspaceInternal(e, this.getFieldObject().getRange());
  247. } else if (e.keyCode == goog.events.KeyCodes.ENTER) {
  248. if (goog.userAgent.GECKO) {
  249. if (!e.shiftKey) {
  250. // Behave similarly to IE's content editable return carriage:
  251. // If the shift key is down or specified by the application, insert a
  252. // BR, otherwise split paragraphs
  253. this.handleEnterGecko_(e);
  254. }
  255. } else {
  256. // In Gecko-based browsers, this is handled in the handleEnterGecko_
  257. // method.
  258. this.getFieldObject().dispatchBeforeChange();
  259. var cursorPosition = this.deleteCursorSelection_();
  260. var split = !!this.getFieldObject().execCommand(
  261. goog.editor.plugins.Blockquote.SPLIT_COMMAND, cursorPosition);
  262. if (split) {
  263. // TODO(user): I think we probably don't need to stopPropagation here
  264. e.preventDefault();
  265. e.stopPropagation();
  266. }
  267. this.releasePositionObject_(cursorPosition);
  268. if (goog.userAgent.WEBKIT) {
  269. this.handleEnterWebkitInternal(e);
  270. }
  271. this.processParagraphTagsInternal(e, split);
  272. this.getFieldObject().dispatchChange();
  273. }
  274. } else if (goog.userAgent.GECKO && e.keyCode == goog.events.KeyCodes.DELETE) {
  275. this.handleDeleteGecko(e);
  276. }
  277. return false;
  278. };
  279. /** @override */
  280. goog.editor.plugins.EnterHandler.prototype.handleKeyUp = function(e) {
  281. // If a dialog doesn't have selectable field, Gecko grabs the event and
  282. // performs actions in editor window. This solves that problem and allows
  283. // the event to be passed on to proper handlers.
  284. if (goog.userAgent.GECKO && this.getFieldObject().inModalMode()) {
  285. return false;
  286. }
  287. this.handleKeyUpInternal(e);
  288. return false;
  289. };
  290. /**
  291. * Internal handler for keyup events.
  292. * @param {goog.events.Event} e The key event.
  293. * @protected
  294. */
  295. goog.editor.plugins.EnterHandler.prototype.handleKeyUpInternal = function(e) {
  296. if ((goog.userAgent.IE || goog.userAgent.OPERA) &&
  297. e.keyCode == goog.events.KeyCodes.ENTER) {
  298. this.ensureBlockIeOpera(goog.dom.TagName.DIV, true);
  299. }
  300. };
  301. /**
  302. * Handles an enter keypress event on fields in Gecko.
  303. * @param {goog.events.BrowserEvent} e The key event.
  304. * @private
  305. */
  306. goog.editor.plugins.EnterHandler.prototype.handleEnterGecko_ = function(e) {
  307. // Retrieve whether the selection is collapsed before we delete it.
  308. var range = this.getFieldObject().getRange();
  309. var wasCollapsed = !range || range.isCollapsed();
  310. var cursorPosition = this.deleteCursorSelection_();
  311. var handled = this.getFieldObject().execCommand(
  312. goog.editor.plugins.Blockquote.SPLIT_COMMAND, cursorPosition);
  313. if (handled) {
  314. // TODO(user): I think we probably don't need to stopPropagation here
  315. e.preventDefault();
  316. e.stopPropagation();
  317. }
  318. this.releasePositionObject_(cursorPosition);
  319. if (!handled) {
  320. this.handleEnterAtCursorGeckoInternal(e, wasCollapsed, range);
  321. }
  322. };
  323. /**
  324. * Handle an enter key press in WebKit.
  325. * @param {goog.events.BrowserEvent} e The key press event.
  326. * @protected
  327. */
  328. goog.editor.plugins.EnterHandler.prototype.handleEnterWebkitInternal =
  329. goog.nullFunction;
  330. /**
  331. * Handle an enter key press on collapsed selection. handleEnterGecko_ ensures
  332. * the selection is collapsed by deleting its contents if it is not. The
  333. * default implementation does nothing.
  334. * @param {goog.events.BrowserEvent} e The key press event.
  335. * @param {boolean} wasCollapsed Whether the selection was collapsed before
  336. * the key press. If it was not, code before this function has already
  337. * cleared the contents of the selection.
  338. * @param {goog.dom.AbstractRange} range Object representing the selection.
  339. * @protected
  340. */
  341. goog.editor.plugins.EnterHandler.prototype.handleEnterAtCursorGeckoInternal =
  342. goog.nullFunction;
  343. /**
  344. * Names of all the nodes that we don't want to turn into block nodes in IE when
  345. * the user hits enter.
  346. * @type {Object}
  347. * @private
  348. */
  349. goog.editor.plugins.EnterHandler.DO_NOT_ENSURE_BLOCK_NODES_ =
  350. goog.object.createSet(
  351. goog.dom.TagName.LI, goog.dom.TagName.DIV, goog.dom.TagName.H1,
  352. goog.dom.TagName.H2, goog.dom.TagName.H3, goog.dom.TagName.H4,
  353. goog.dom.TagName.H5, goog.dom.TagName.H6);
  354. /**
  355. * Whether this is a node that contains a single BR tag and non-nbsp
  356. * whitespace.
  357. * @param {Node} node Node to check.
  358. * @return {boolean} Whether this is an element that only contains a BR.
  359. * @protected
  360. */
  361. goog.editor.plugins.EnterHandler.isBrElem = function(node) {
  362. return goog.editor.node.isEmpty(node) &&
  363. goog.dom.getElementsByTagName(
  364. goog.dom.TagName.BR, /** @type {!Element} */ (node)).length == 1;
  365. };
  366. /**
  367. * Ensures all text in IE and Opera to be in the given tag in order to control
  368. * Enter spacing. Call this when Enter is pressed if desired.
  369. *
  370. * We want to make sure the user is always inside of a block (or other nodes
  371. * listed in goog.editor.plugins.EnterHandler.IGNORE_ENSURE_BLOCK_NODES_). We
  372. * listen to keypress to force nodes that the user is leaving to turn into
  373. * blocks, but we also need to listen to keyup to force nodes that the user is
  374. * entering to turn into blocks.
  375. * Example: html is: `<h2>foo[cursor]</h2>`, and the user hits enter. We
  376. * don't want to format the h2, but we do want to format the P that is
  377. * created on enter. The P node is not available until keyup.
  378. * @param {!goog.dom.TagName} tag The tag name to convert to.
  379. * @param {boolean=} opt_keyUp Whether the function is being called on key up.
  380. * When called on key up, the cursor is in the newly created node, so the
  381. * semantics for when to change it to a block are different. Specifically,
  382. * if the resulting node contains only a BR, it is converted to `<tag>`.
  383. * @protected
  384. */
  385. goog.editor.plugins.EnterHandler.prototype.ensureBlockIeOpera = function(
  386. tag, opt_keyUp) {
  387. var range = this.getFieldObject().getRange();
  388. var container = range.getContainer();
  389. var field = this.getFieldObject().getElement();
  390. var paragraph;
  391. while (container && container != field) {
  392. // We don't need to ensure a block if we are already in the same block, or
  393. // in another block level node that we don't want to change the format of
  394. // (unless we're handling keyUp and that block node just contains a BR).
  395. var nodeName = container.nodeName;
  396. // Due to @bug 2455389, the call to isBrElem needs to be inlined in the if
  397. // instead of done before and saved in a variable, so that it can be
  398. // short-circuited and avoid a weird IE edge case.
  399. if (nodeName == tag ||
  400. (goog.editor.plugins.EnterHandler
  401. .DO_NOT_ENSURE_BLOCK_NODES_[nodeName] &&
  402. !(opt_keyUp &&
  403. goog.editor.plugins.EnterHandler.isBrElem(container)))) {
  404. // Opera can create a <p> inside of a <div> in some situations,
  405. // such as when breaking out of a list that is contained in a <div>.
  406. if (goog.userAgent.OPERA && paragraph) {
  407. if (nodeName == tag && paragraph == container.lastChild &&
  408. goog.editor.node.isEmpty(paragraph)) {
  409. goog.dom.insertSiblingAfter(paragraph, container);
  410. goog.dom.Range.createFromNodeContents(paragraph).select();
  411. }
  412. break;
  413. }
  414. return;
  415. }
  416. if (goog.userAgent.OPERA && opt_keyUp && nodeName == goog.dom.TagName.P &&
  417. nodeName != tag) {
  418. paragraph = container;
  419. }
  420. container = container.parentNode;
  421. }
  422. if (goog.userAgent.IE && !goog.userAgent.isVersionOrHigher(9)) {
  423. // IE (before IE9) has a bug where if the cursor is directly before a block
  424. // node (e.g., the content is "foo[cursor]<blockquote>bar</blockquote>"),
  425. // the FormatBlock command actually formats the "bar" instead of the "foo".
  426. // This is just wrong. To work-around this, we want to move the
  427. // selection back one character, and then restore it to its prior position.
  428. // NOTE: We use the following "range math" to detect this situation because
  429. // using Closure ranges here triggers a bug in IE that causes a crash.
  430. // parent2 != parent3 ensures moving the cursor forward one character
  431. // crosses at least 1 element boundary, and therefore tests if the cursor is
  432. // at such a boundary. The second check, parent3 != range.parentElement()
  433. // weeds out some cases where the elements are siblings instead of cousins.
  434. var needsHelp = false;
  435. range = range.getBrowserRangeObject();
  436. var range2 = range.duplicate();
  437. range2.moveEnd('character', 1);
  438. // In whitebox mode, when the cursor is at the end of the field, trying to
  439. // move the end of the range will do nothing, and hence the range's text
  440. // will be empty. In this case, the cursor clearly isn't sitting just
  441. // before a block node, since it isn't before anything.
  442. if (range2.text.length) {
  443. var parent2 = range2.parentElement();
  444. var range3 = range2.duplicate();
  445. range3.collapse(false);
  446. var parent3 = range3.parentElement();
  447. if ((needsHelp =
  448. parent2 != parent3 && parent3 != range.parentElement())) {
  449. range.move('character', -1);
  450. range.select();
  451. }
  452. }
  453. }
  454. this.getFieldObject().getEditableDomHelper().getDocument().execCommand(
  455. 'FormatBlock', false, '<' + tag + '>');
  456. if (needsHelp) {
  457. range.move('character', 1);
  458. range.select();
  459. }
  460. };
  461. /**
  462. * Deletes the content at the current cursor position.
  463. * @return {!Node|!Object} Something representing the current cursor position.
  464. * See deleteCursorSelectionIE_ and deleteCursorSelectionW3C_ for details.
  465. * Should be passed to releasePositionObject_ when no longer in use.
  466. * @private
  467. */
  468. goog.editor.plugins.EnterHandler.prototype.deleteCursorSelection_ = function() {
  469. return goog.editor.BrowserFeature.HAS_W3C_RANGES ?
  470. this.deleteCursorSelectionW3C_() :
  471. this.deleteCursorSelectionIE_();
  472. };
  473. /**
  474. * Releases the object returned by deleteCursorSelection_.
  475. * @param {Node|Object} position The object returned by deleteCursorSelection_.
  476. * @private
  477. */
  478. goog.editor.plugins.EnterHandler.prototype.releasePositionObject_ = function(
  479. position) {
  480. if (!goog.editor.BrowserFeature.HAS_W3C_RANGES) {
  481. (/** @type {Node} */ (position)).removeNode(true);
  482. }
  483. };
  484. /**
  485. * Delete the selection at the current cursor position, then returns a temporary
  486. * node at the current position.
  487. * @return {!Node} A temporary node marking the current cursor position. This
  488. * node should eventually be removed from the DOM.
  489. * @private
  490. */
  491. goog.editor.plugins.EnterHandler.prototype.deleteCursorSelectionIE_ =
  492. function() {
  493. var doc = this.getFieldDomHelper().getDocument();
  494. var range = doc.selection.createRange();
  495. var id = goog.string.createUniqueString();
  496. range.pasteHTML('<span id="' + id + '"></span>');
  497. var splitNode = doc.getElementById(id);
  498. splitNode.id = '';
  499. return splitNode;
  500. };
  501. /**
  502. * Delete the selection at the current cursor position, then returns the node
  503. * at the current position.
  504. * @return {!goog.editor.range.Point} The current cursor position. Note that
  505. * unlike simulateEnterIE_, this should not be removed from the DOM.
  506. * @private
  507. */
  508. goog.editor.plugins.EnterHandler.prototype.deleteCursorSelectionW3C_ =
  509. function() {
  510. var range = this.getFieldObject().getRange();
  511. // Delete the current selection if it's is non-collapsed.
  512. // Although this is redundant in FF, it's necessary for Safari
  513. if (range && !range.isCollapsed()) {
  514. var shouldDelete = true;
  515. // Opera selects the <br> in an empty block if there is no text node
  516. // preceding it. To preserve inline formatting when pressing [enter] inside
  517. // an empty block, don't delete the selection if it only selects a <br> at
  518. // the end of the block.
  519. // TODO(user): Move this into goog.dom.Range. It should detect this state
  520. // when creating a range from the window selection and fix it in the created
  521. // range.
  522. if (goog.userAgent.OPERA) {
  523. var startNode = range.getStartNode();
  524. var startOffset = range.getStartOffset();
  525. if (startNode == range.getEndNode() &&
  526. // This weeds out cases where startNode is a text node.
  527. startNode.lastChild &&
  528. /** @type {!Element} */ (startNode.lastChild).tagName ==
  529. goog.dom.TagName.BR &&
  530. // If this check is true, then endOffset is implied to be
  531. // startOffset + 1, because the selection is not collapsed and
  532. // it starts and ends within the same element.
  533. startOffset == startNode.childNodes.length - 1) {
  534. shouldDelete = false;
  535. }
  536. }
  537. if (shouldDelete) {
  538. goog.editor.plugins.EnterHandler.deleteW3cRange_(range);
  539. }
  540. }
  541. return goog.editor.range.getDeepEndPoint(range, true);
  542. };
  543. /**
  544. * Deletes the contents of the selection from the DOM.
  545. * @param {goog.dom.AbstractRange} range The range to remove contents from.
  546. * @return {goog.dom.AbstractRange} The resulting range. Used for testing.
  547. * @private
  548. */
  549. goog.editor.plugins.EnterHandler.deleteW3cRange_ = function(range) {
  550. if (range && !range.isCollapsed()) {
  551. var reselect = true;
  552. var baseNode = range.getContainerElement();
  553. var nodeOffset = new goog.dom.NodeOffset(range.getStartNode(), baseNode);
  554. var rangeOffset = range.getStartOffset();
  555. // Whether the selection crosses no container boundaries.
  556. var isInOneContainer =
  557. goog.editor.plugins.EnterHandler.isInOneContainerW3c_(range);
  558. // Whether the selection ends in a container it doesn't fully select.
  559. var isPartialEnd = !isInOneContainer &&
  560. goog.editor.plugins.EnterHandler.isPartialEndW3c_(range);
  561. // Remove The range contents, and ensure the correct content stays selected.
  562. range.removeContents();
  563. var node = nodeOffset.findTargetNode(baseNode);
  564. if (node) {
  565. range = goog.dom.Range.createCaret(node, rangeOffset);
  566. } else {
  567. // This occurs when the node that would have been referenced has now been
  568. // deleted and there are no other nodes in the baseNode. Thus need to
  569. // set the caret to the end of the base node.
  570. range = goog.dom.Range.createCaret(baseNode, baseNode.childNodes.length);
  571. reselect = false;
  572. }
  573. range.select();
  574. // If we just deleted everything from the container, add an nbsp
  575. // to the container, and leave the cursor inside of it
  576. if (isInOneContainer) {
  577. var container = goog.editor.style.getContainer(range.getStartNode());
  578. if (goog.editor.node.isEmpty(container, true)) {
  579. var html = '&nbsp;';
  580. if (goog.userAgent.OPERA && container.tagName == goog.dom.TagName.LI) {
  581. // Don't break Opera's native break-out-of-lists behavior.
  582. html = '<br>';
  583. }
  584. goog.editor.node.replaceInnerHtml(container, html);
  585. goog.editor.range.selectNodeStart(container.firstChild);
  586. reselect = false;
  587. }
  588. }
  589. if (isPartialEnd) {
  590. /*
  591. This code handles the following, where | is the cursor:
  592. <div>a|b</div><div>c|d</div>
  593. After removeContents, the remaining HTML is
  594. <div>a</div><div>d</div>
  595. which means the line break between the two divs remains. This block
  596. moves children of the second div in to the first div to get the correct
  597. result:
  598. <div>ad</div>
  599. TODO(robbyw): Should we wrap the second div's contents in a span if they
  600. have inline style?
  601. */
  602. var rangeStart = goog.editor.style.getContainer(range.getStartNode());
  603. var redundantContainer = goog.editor.node.getNextSibling(rangeStart);
  604. if (rangeStart && redundantContainer) {
  605. goog.dom.append(rangeStart, redundantContainer.childNodes);
  606. goog.dom.removeNode(redundantContainer);
  607. }
  608. }
  609. if (reselect) {
  610. // The contents of the original range are gone, so restore the cursor
  611. // position at the start of where the range once was.
  612. range = goog.dom.Range.createCaret(
  613. nodeOffset.findTargetNode(baseNode), rangeOffset);
  614. range.select();
  615. }
  616. }
  617. return range;
  618. };
  619. /**
  620. * Checks whether the whole range is in a single block-level element.
  621. * @param {goog.dom.AbstractRange} range The range to check.
  622. * @return {boolean} Whether the whole range is in a single block-level element.
  623. * @private
  624. */
  625. goog.editor.plugins.EnterHandler.isInOneContainerW3c_ = function(range) {
  626. // Find the block element containing the start of the selection.
  627. var startContainer = range.getStartNode();
  628. if (goog.editor.style.isContainer(startContainer)) {
  629. startContainer =
  630. startContainer.childNodes[range.getStartOffset()] || startContainer;
  631. }
  632. startContainer = goog.editor.style.getContainer(startContainer);
  633. // Find the block element containing the end of the selection.
  634. var endContainer = range.getEndNode();
  635. if (goog.editor.style.isContainer(endContainer)) {
  636. endContainer =
  637. endContainer.childNodes[range.getEndOffset()] || endContainer;
  638. }
  639. endContainer = goog.editor.style.getContainer(endContainer);
  640. // Compare the two.
  641. return startContainer == endContainer;
  642. };
  643. /**
  644. * Checks whether the end of the range is not at the end of a block-level
  645. * element.
  646. * @param {goog.dom.AbstractRange} range The range to check.
  647. * @return {boolean} Whether the end of the range is not at the end of a
  648. * block-level element.
  649. * @private
  650. */
  651. goog.editor.plugins.EnterHandler.isPartialEndW3c_ = function(range) {
  652. var endContainer = range.getEndNode();
  653. var endOffset = range.getEndOffset();
  654. var node = endContainer;
  655. if (goog.editor.style.isContainer(node)) {
  656. var child = node.childNodes[endOffset];
  657. // Child is null when end offset is >= length, which indicates the entire
  658. // container is selected. Otherwise, we also know the entire container
  659. // is selected if the selection ends at a new container.
  660. if (!child ||
  661. child.nodeType == goog.dom.NodeType.ELEMENT &&
  662. goog.editor.style.isContainer(child)) {
  663. return false;
  664. }
  665. }
  666. var container = goog.editor.style.getContainer(node);
  667. while (container != node) {
  668. if (goog.editor.node.getNextSibling(node)) {
  669. return true;
  670. }
  671. node = node.parentNode;
  672. }
  673. return endOffset != goog.editor.node.getLength(endContainer);
  674. };