removeformatting.js 30 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818
  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. // All Rights Reserved.
  15. /**
  16. * @fileoverview Plugin to handle Remove Formatting.
  17. *
  18. */
  19. goog.provide('goog.editor.plugins.RemoveFormatting');
  20. goog.require('goog.dom');
  21. goog.require('goog.dom.NodeType');
  22. goog.require('goog.dom.Range');
  23. goog.require('goog.dom.TagName');
  24. goog.require('goog.editor.BrowserFeature');
  25. goog.require('goog.editor.Plugin');
  26. goog.require('goog.editor.node');
  27. goog.require('goog.editor.range');
  28. goog.require('goog.string');
  29. goog.require('goog.userAgent');
  30. /**
  31. * A plugin to handle removing formatting from selected text.
  32. * @constructor
  33. * @extends {goog.editor.Plugin}
  34. * @final
  35. */
  36. goog.editor.plugins.RemoveFormatting = function() {
  37. goog.editor.Plugin.call(this);
  38. /**
  39. * Optional function to perform remove formatting in place of the
  40. * provided removeFormattingWorker_.
  41. * @type {?function(string): string}
  42. * @private
  43. */
  44. this.optRemoveFormattingFunc_ = null;
  45. /**
  46. * The key that this plugin triggers on when pressed with the platform
  47. * modifier key. Can be set by calling {@link #setKeyboardShortcutKey}.
  48. * @type {string}
  49. * @private
  50. */
  51. this.keyboardShortcutKey_ = ' ';
  52. };
  53. goog.inherits(goog.editor.plugins.RemoveFormatting, goog.editor.Plugin);
  54. /**
  55. * The editor command this plugin in handling.
  56. * @type {string}
  57. */
  58. goog.editor.plugins.RemoveFormatting.REMOVE_FORMATTING_COMMAND =
  59. '+removeFormat';
  60. /**
  61. * Regular expression that matches a block tag name.
  62. * @type {RegExp}
  63. * @private
  64. */
  65. goog.editor.plugins.RemoveFormatting.BLOCK_RE_ =
  66. /^(DIV|TR|LI|BLOCKQUOTE|H\d|PRE|XMP)/;
  67. /**
  68. * Appends a new line to a string buffer.
  69. * @param {Array<string>} sb The string buffer to add to.
  70. * @private
  71. */
  72. goog.editor.plugins.RemoveFormatting.appendNewline_ = function(sb) {
  73. sb.push('<br>');
  74. };
  75. /**
  76. * Create a new range delimited by the start point of the first range and
  77. * the end point of the second range.
  78. * @param {goog.dom.AbstractRange} startRange Use the start point of this
  79. * range as the beginning of the new range.
  80. * @param {goog.dom.AbstractRange} endRange Use the end point of this
  81. * range as the end of the new range.
  82. * @return {!goog.dom.AbstractRange} The new range.
  83. * @private
  84. */
  85. goog.editor.plugins.RemoveFormatting.createRangeDelimitedByRanges_ = function(
  86. startRange, endRange) {
  87. return goog.dom.Range.createFromNodes(
  88. startRange.getStartNode(), startRange.getStartOffset(),
  89. endRange.getEndNode(), endRange.getEndOffset());
  90. };
  91. /** @override */
  92. goog.editor.plugins.RemoveFormatting.prototype.getTrogClassId = function() {
  93. return 'RemoveFormatting';
  94. };
  95. /** @override */
  96. goog.editor.plugins.RemoveFormatting.prototype.isSupportedCommand = function(
  97. command) {
  98. return command ==
  99. goog.editor.plugins.RemoveFormatting.REMOVE_FORMATTING_COMMAND;
  100. };
  101. /** @override */
  102. goog.editor.plugins.RemoveFormatting.prototype.execCommandInternal = function(
  103. command, var_args) {
  104. if (command ==
  105. goog.editor.plugins.RemoveFormatting.REMOVE_FORMATTING_COMMAND) {
  106. this.removeFormatting_();
  107. }
  108. };
  109. /** @override */
  110. goog.editor.plugins.RemoveFormatting.prototype.handleKeyboardShortcut =
  111. function(e, key, isModifierPressed) {
  112. if (!isModifierPressed) {
  113. return false;
  114. }
  115. // Disregard the shortcut if more than one modifier key is pressed
  116. // because the user may have intended a different shortcut (for example OSX
  117. // uses ctrlKey + metaKey + space to open the emoji picker).
  118. if (e.metaKey && e.ctrlKey) {
  119. return false;
  120. }
  121. // Disregard the shortcut if the shift key is also pressed because the user
  122. // may have intended a different shortcut (for example Chrome OS uses shiftKey
  123. // + ctrlKey + space to toggle input languages.
  124. if (e.shiftKey) {
  125. return false;
  126. }
  127. if (key == this.keyboardShortcutKey_) {
  128. this.getFieldObject().execCommand(
  129. goog.editor.plugins.RemoveFormatting.REMOVE_FORMATTING_COMMAND);
  130. return true;
  131. }
  132. return false;
  133. };
  134. /**
  135. * @param {string} key
  136. */
  137. goog.editor.plugins.RemoveFormatting.prototype.setKeyboardShortcutKey =
  138. function(key) {
  139. this.keyboardShortcutKey_ = key;
  140. };
  141. /**
  142. * Removes formatting from the current selection. Removes basic formatting
  143. * (B/I/U) using the browser's execCommand. Then extracts the html from the
  144. * selection to convert, calls either a client's specified removeFormattingFunc
  145. * callback or trogedit's general built-in removeFormattingWorker_,
  146. * and then replaces the current selection with the converted text.
  147. * @private
  148. */
  149. goog.editor.plugins.RemoveFormatting.prototype.removeFormatting_ = function() {
  150. var range = this.getFieldObject().getRange();
  151. if (range.isCollapsed()) {
  152. return;
  153. }
  154. // Get the html to format and send it off for formatting. Built in
  155. // removeFormat only strips some inline elements and some inline CSS styles
  156. var convFunc = this.optRemoveFormattingFunc_ ||
  157. goog.bind(this.removeFormattingWorker_, this);
  158. this.convertSelectedHtmlText_(convFunc);
  159. // Do the execCommand last as it needs block elements removed to work
  160. // properly on background/fontColor in FF. There are, unfortunately, still
  161. // cases where background/fontColor are not removed here.
  162. var doc = this.getFieldDomHelper().getDocument();
  163. doc.execCommand('RemoveFormat', false, undefined);
  164. if (goog.editor.BrowserFeature.ADDS_NBSPS_IN_REMOVE_FORMAT) {
  165. // WebKit converts spaces to non-breaking spaces when doing a RemoveFormat.
  166. // See: https://bugs.webkit.org/show_bug.cgi?id=14062
  167. this.convertSelectedHtmlText_(function(text) {
  168. // This loses anything that might have legitimately been a non-breaking
  169. // space, but that's better than the alternative of only having non-
  170. // breaking spaces.
  171. // Old versions of WebKit (Safari 3, Chrome 1) incorrectly match /u00A0
  172. // and newer versions properly match &nbsp;.
  173. var nbspRegExp =
  174. goog.userAgent.isVersionOrHigher('528') ? /&nbsp;/g : /\u00A0/g;
  175. return text.replace(nbspRegExp, ' ');
  176. });
  177. }
  178. };
  179. /**
  180. * Finds the nearest ancestor of the node that is a table.
  181. * @param {Node} nodeToCheck Node to search from.
  182. * @return {Node} The table, or null if one was not found.
  183. * @private
  184. */
  185. goog.editor.plugins.RemoveFormatting.prototype.getTableAncestor_ = function(
  186. nodeToCheck) {
  187. var fieldElement = this.getFieldObject().getElement();
  188. while (nodeToCheck && nodeToCheck != fieldElement) {
  189. if (nodeToCheck.tagName == goog.dom.TagName.TABLE) {
  190. return nodeToCheck;
  191. }
  192. nodeToCheck = nodeToCheck.parentNode;
  193. }
  194. return null;
  195. };
  196. /**
  197. * Replaces the contents of the selection with html. Does its best to maintain
  198. * the original selection. Also does its best to result in a valid DOM.
  199. *
  200. * TODO(user): See if there's any way to make this work on Ranges, and then
  201. * move it into goog.editor.range. The Firefox implementation uses execCommand
  202. * on the document, so must work on the actual selection.
  203. *
  204. * @param {string} html The html string to insert into the range.
  205. * @private
  206. */
  207. goog.editor.plugins.RemoveFormatting.prototype.pasteHtml_ = function(html) {
  208. var range = this.getFieldObject().getRange();
  209. var dh = this.getFieldDomHelper();
  210. // Use markers to set the extent of the selection so that we can reselect it
  211. // afterwards. This works better than builtin range manipulation in FF and IE
  212. // because their implementations are so self-inconsistent and buggy.
  213. var startSpanId = goog.string.createUniqueString();
  214. var endSpanId = goog.string.createUniqueString();
  215. html = '<span id="' + startSpanId + '"></span>' + html + '<span id="' +
  216. endSpanId + '"></span>';
  217. var dummyNodeId = goog.string.createUniqueString();
  218. var dummySpanText = '<span id="' + dummyNodeId + '"></span>';
  219. if (goog.editor.BrowserFeature.HAS_IE_RANGES) {
  220. // IE's selection often doesn't include the outermost tags.
  221. // We want to use pasteHTML to replace the range contents with the newly
  222. // unformatted text, so we have to check to make sure we aren't just
  223. // pasting into some stray tags. To do this, we first clear out the
  224. // contents of the range and then delete all empty nodes parenting the now
  225. // empty range. This way, the pasted contents are never re-embedded into
  226. // formated nodes. Pasting purely empty html does not work, since IE moves
  227. // the selection inside the next node, so we insert a dummy span.
  228. var textRange = range.getTextRange(0).getBrowserRangeObject();
  229. textRange.pasteHTML(dummySpanText);
  230. var parent;
  231. while ((parent = textRange.parentElement()) &&
  232. goog.editor.node.isEmpty(parent) &&
  233. !goog.editor.node.isEditableContainer(parent)) {
  234. var tag = parent.nodeName;
  235. // We can't remove these table tags as it will invalidate the table dom.
  236. if (tag == goog.dom.TagName.TD || tag == goog.dom.TagName.TR ||
  237. tag == goog.dom.TagName.TH) {
  238. break;
  239. }
  240. goog.dom.removeNode(parent);
  241. }
  242. textRange.pasteHTML(html);
  243. var dummySpan = dh.getElement(dummyNodeId);
  244. // If we entered the while loop above, the node has already been removed
  245. // since it was a child of parent and parent was removed.
  246. if (dummySpan) {
  247. goog.dom.removeNode(dummySpan);
  248. }
  249. } else if (goog.editor.BrowserFeature.HAS_W3C_RANGES) {
  250. // insertHtml and range.insertNode don't merge blocks correctly.
  251. // (e.g. if your selection spans two paragraphs)
  252. dh.getDocument().execCommand('insertImage', false, dummyNodeId);
  253. var dummyImageNodePattern = new RegExp('<[^<]*' + dummyNodeId + '[^>]*>');
  254. var parent = this.getFieldObject().getRange().getContainerElement();
  255. if (parent.nodeType == goog.dom.NodeType.TEXT) {
  256. // Opera sometimes returns a text node here.
  257. // TODO(user): perhaps we should modify getParentContainer?
  258. parent = parent.parentNode;
  259. }
  260. // We have to search up the DOM because in some cases, notably when
  261. // selecting li's within a list, execCommand('insertImage') actually splits
  262. // tags in such a way that parent that used to contain the selection does
  263. // not contain inserted image.
  264. while (!dummyImageNodePattern.test(parent.innerHTML)) {
  265. parent = parent.parentNode;
  266. }
  267. // Like the IE case above, sometimes the selection does not include the
  268. // outermost tags. For Gecko, we have already expanded the range so that
  269. // it does, so we can just replace the dummy image with the final html.
  270. // For WebKit, we use the same approach as we do with IE - we
  271. // inject a dummy span where we will eventually place the contents, and
  272. // remove parentNodes of the span while they are empty.
  273. if (goog.userAgent.GECKO) {
  274. // Escape dollars passed in second argument of String.proto.replace.
  275. // And since we're using that to replace, we need to escape those as well,
  276. // hence the 2*2 dollar signs.
  277. goog.editor.node.replaceInnerHtml(
  278. parent, parent.innerHTML.replace(
  279. dummyImageNodePattern, html.replace(/\$/g, '$$$$')));
  280. } else {
  281. goog.editor.node.replaceInnerHtml(
  282. parent,
  283. parent.innerHTML.replace(dummyImageNodePattern, dummySpanText));
  284. var dummySpan = dh.getElement(dummyNodeId);
  285. parent = dummySpan;
  286. while ((parent = dummySpan.parentNode) &&
  287. goog.editor.node.isEmpty(parent) &&
  288. !goog.editor.node.isEditableContainer(parent)) {
  289. var tag = parent.nodeName;
  290. // We can't remove these table tags as it will invalidate the table dom.
  291. if (tag == goog.dom.TagName.TD || tag == goog.dom.TagName.TR ||
  292. tag == goog.dom.TagName.TH) {
  293. break;
  294. }
  295. // We can't just remove parent since dummySpan is inside it, and we need
  296. // to keep dummy span around for the replacement. So we move the
  297. // dummySpan up as we go.
  298. goog.dom.insertSiblingAfter(dummySpan, parent);
  299. goog.dom.removeNode(parent);
  300. }
  301. goog.editor.node.replaceInnerHtml(
  302. parent,
  303. // Escape dollars passed in second argument of String.proto.replace
  304. parent.innerHTML.replace(
  305. new RegExp(dummySpanText, 'i'), html.replace(/\$/g, '$$$$')));
  306. }
  307. }
  308. var startSpan = dh.getElement(startSpanId);
  309. var endSpan = dh.getElement(endSpanId);
  310. goog.dom.Range
  311. .createFromNodes(startSpan, 0, endSpan, endSpan.childNodes.length)
  312. .select();
  313. goog.dom.removeNode(startSpan);
  314. goog.dom.removeNode(endSpan);
  315. };
  316. /**
  317. * Gets the html inside the selection to send off for further processing.
  318. *
  319. * TODO(user): Make this general so that it can be moved into
  320. * goog.editor.range. The main reason it can't be moved is because we need to
  321. * get the range before we do the execCommand and continue to operate on that
  322. * same range (reasons are documented above).
  323. *
  324. * @param {goog.dom.AbstractRange} range The selection.
  325. * @return {string} The html string to format.
  326. * @private
  327. */
  328. goog.editor.plugins.RemoveFormatting.prototype.getHtmlText_ = function(range) {
  329. var div = this.getFieldDomHelper().createDom(goog.dom.TagName.DIV);
  330. var textRange = range.getBrowserRangeObject();
  331. if (goog.editor.BrowserFeature.HAS_W3C_RANGES) {
  332. // Get the text to convert.
  333. div.appendChild(textRange.cloneContents());
  334. } else if (goog.editor.BrowserFeature.HAS_IE_RANGES) {
  335. // Trim the whitespace on the ends of the range, so that it the container
  336. // will be the container of only the text content that we are changing.
  337. // This gets around issues in IE where the spaces are included in the
  338. // selection, but ignored sometimes by execCommand, and left orphaned.
  339. var rngText = range.getText();
  340. // BRs get reported as \r\n, but only count as one character for moves.
  341. // Adjust the string so our move counter is correct.
  342. rngText = rngText.replace(/\r\n/g, '\r');
  343. var rngTextLength = rngText.length;
  344. var left = rngTextLength - goog.string.trimLeft(rngText).length;
  345. var right = rngTextLength - goog.string.trimRight(rngText).length;
  346. textRange.moveStart('character', left);
  347. textRange.moveEnd('character', -right);
  348. var htmlText = textRange.htmlText;
  349. // Check if in pretag and fix up formatting so that new lines are preserved.
  350. if (textRange.queryCommandValue('formatBlock') == 'Formatted') {
  351. htmlText = goog.string.newLineToBr(textRange.htmlText);
  352. }
  353. div.innerHTML = htmlText;
  354. }
  355. // Get the innerHTML of the node instead of just returning the text above
  356. // so that its properly html escaped.
  357. return div.innerHTML;
  358. };
  359. /**
  360. * Move the range so that it doesn't include any partially selected tables.
  361. * @param {goog.dom.AbstractRange} range The range to adjust.
  362. * @param {Node} startInTable Table node that the range starts in.
  363. * @param {Node} endInTable Table node that the range ends in.
  364. * @return {!goog.dom.SavedCaretRange} Range to use to restore the
  365. * selection after we run our custom remove formatting.
  366. * @private
  367. */
  368. goog.editor.plugins.RemoveFormatting.prototype.adjustRangeForTables_ = function(
  369. range, startInTable, endInTable) {
  370. // Create placeholders for the current selection so we can restore it
  371. // later.
  372. var savedCaretRange = goog.editor.range.saveUsingNormalizedCarets(range);
  373. var startNode = range.getStartNode();
  374. var startOffset = range.getStartOffset();
  375. var endNode = range.getEndNode();
  376. var endOffset = range.getEndOffset();
  377. var dh = this.getFieldDomHelper();
  378. // Move start after the table.
  379. if (startInTable) {
  380. var textNode = dh.createTextNode('');
  381. goog.dom.insertSiblingAfter(textNode, startInTable);
  382. startNode = textNode;
  383. startOffset = 0;
  384. }
  385. // Move end before the table.
  386. if (endInTable) {
  387. var textNode = dh.createTextNode('');
  388. goog.dom.insertSiblingBefore(textNode, endInTable);
  389. endNode = textNode;
  390. endOffset = 0;
  391. }
  392. goog.dom.Range.createFromNodes(startNode, startOffset, endNode, endOffset)
  393. .select();
  394. return savedCaretRange;
  395. };
  396. /**
  397. * Remove a caret from the dom and hide it in a safe place, so it can
  398. * be restored later via restoreCaretsFromCave.
  399. * @param {goog.dom.SavedCaretRange} caretRange The caret range to
  400. * get the carets from.
  401. * @param {boolean} isStart Whether this is the start or end caret.
  402. * @private
  403. */
  404. goog.editor.plugins.RemoveFormatting.prototype.putCaretInCave_ = function(
  405. caretRange, isStart) {
  406. var cavedCaret = goog.dom.removeNode(caretRange.getCaret(isStart));
  407. if (isStart) {
  408. this.startCaretInCave_ = cavedCaret;
  409. } else {
  410. this.endCaretInCave_ = cavedCaret;
  411. }
  412. };
  413. /**
  414. * Restore carets that were hidden away by adding them back into the dom.
  415. * Note: this does not restore to the original dom location, as that
  416. * will likely have been modified with remove formatting. The only
  417. * guarantees here are that start will still be before end, and that
  418. * they will be in the editable region. This should only be used when
  419. * you don't actually intend to USE the caret again.
  420. * @private
  421. */
  422. goog.editor.plugins.RemoveFormatting.prototype.restoreCaretsFromCave_ =
  423. function() {
  424. // To keep start before end, we put the end caret at the bottom of the field
  425. // and the start caret at the start of the field.
  426. var field = this.getFieldObject().getElement();
  427. if (this.startCaretInCave_) {
  428. field.insertBefore(this.startCaretInCave_, field.firstChild);
  429. this.startCaretInCave_ = null;
  430. }
  431. if (this.endCaretInCave_) {
  432. field.appendChild(this.endCaretInCave_);
  433. this.endCaretInCave_ = null;
  434. }
  435. };
  436. /**
  437. * Gets the html inside the current selection, passes it through the given
  438. * conversion function, and puts it back into the selection.
  439. *
  440. * @param {function(string): string} convertFunc A conversion function that
  441. * transforms an html string to new html string.
  442. * @private
  443. */
  444. goog.editor.plugins.RemoveFormatting.prototype.convertSelectedHtmlText_ =
  445. function(convertFunc) {
  446. var range = this.getFieldObject().getRange();
  447. // For multiple ranges, it is really hard to do our custom remove formatting
  448. // without invalidating other ranges. So instead of always losing the
  449. // content, this solution at least lets the browser do its own remove
  450. // formatting which works correctly most of the time.
  451. if (range.getTextRangeCount() > 1) {
  452. return;
  453. }
  454. if (goog.userAgent.GECKO || goog.userAgent.EDGE) {
  455. // Determine if we need to handle tables, since they are special cases.
  456. // If the selection is entirely within a table, there is no extra
  457. // formatting removal we can do. If a table is fully selected, we will
  458. // just blow it away. If a table is only partially selected, we can
  459. // perform custom remove formatting only on the non table parts, since we
  460. // we can't just remove the parts and paste back into it (eg. we can't
  461. // inject html where a TR used to be).
  462. // If the selection contains the table and more, this is automatically
  463. // handled, but if just the table is selected, it can be tricky to figure
  464. // this case out, because of the numerous ways selections can be formed -
  465. // ex. if a table has a single tr with a single td with a single text node
  466. // in it, and the selection is (textNode: 0), (textNode: nextNode.length)
  467. // then the entire table is selected, even though the start and end aren't
  468. // the table itself. We are truly inside a table if the expanded endpoints
  469. // are still inside the table.
  470. // Expand the selection to include any outermost tags that weren't included
  471. // in the selection, but have the same visible selection. Stop expanding
  472. // if we reach the top level field.
  473. var expandedRange =
  474. goog.editor.range.expand(range, this.getFieldObject().getElement());
  475. var startInTable = this.getTableAncestor_(expandedRange.getStartNode());
  476. var endInTable = this.getTableAncestor_(expandedRange.getEndNode());
  477. if (startInTable || endInTable) {
  478. if (startInTable == endInTable) {
  479. // We are fully contained in the same table, there is no extra
  480. // remove formatting that we can do, just return and run browser
  481. // formatting only.
  482. return;
  483. }
  484. // Adjust the range to not contain any partially selected tables, since
  485. // we don't want to run our custom remove formatting on them.
  486. var savedCaretRange =
  487. this.adjustRangeForTables_(range, startInTable, endInTable);
  488. // Hack alert!!
  489. // If start is not in a table, then the saved caret will get sent out
  490. // for uber remove formatting, and it will get blown away. This is
  491. // fine, except that we need to be able to re-create a range from the
  492. // savedCaretRange later on. So, we just remove it from the dom, and
  493. // put it back later so we can create a range later (not exactly in the
  494. // same spot, but don't worry we don't actually try to use it later)
  495. // and then it will be removed when we dispose the range.
  496. if (!startInTable) {
  497. this.putCaretInCave_(savedCaretRange, true);
  498. }
  499. if (!endInTable) {
  500. this.putCaretInCave_(savedCaretRange, false);
  501. }
  502. // Re-fetch the range, and re-expand it, since we just modified it.
  503. range = this.getFieldObject().getRange();
  504. expandedRange =
  505. goog.editor.range.expand(range, this.getFieldObject().getElement());
  506. }
  507. expandedRange.select();
  508. range = expandedRange;
  509. }
  510. // Convert the selected text to the format-less version, paste back into
  511. // the selection.
  512. var text = this.getHtmlText_(range);
  513. this.pasteHtml_(convertFunc(text));
  514. if ((goog.userAgent.GECKO || goog.userAgent.EDGE) && savedCaretRange) {
  515. // If we moved the selection, move it back so the user can't tell we did
  516. // anything crazy and so the browser removeFormat that we call next
  517. // will operate on the entire originally selected range.
  518. range = this.getFieldObject().getRange();
  519. this.restoreCaretsFromCave_();
  520. var realSavedCaretRange = savedCaretRange.toAbstractRange();
  521. var startRange = startInTable ? realSavedCaretRange : range;
  522. var endRange = endInTable ? realSavedCaretRange : range;
  523. var restoredRange =
  524. goog.editor.plugins.RemoveFormatting.createRangeDelimitedByRanges_(
  525. startRange, endRange);
  526. restoredRange.select();
  527. savedCaretRange.dispose();
  528. }
  529. };
  530. /**
  531. * Does a best-effort attempt at clobbering all formatting that the
  532. * browser's execCommand couldn't clobber without being totally inefficient.
  533. * Attempts to convert visual line breaks to BRs. Leaves anchors that contain an
  534. * href and images.
  535. * Adapted from Gmail's MessageUtil's htmlToPlainText. http://go/messageutil.js
  536. * @param {string} html The original html of the message.
  537. * @return {string} The unformatted html, which is just text, br's, anchors and
  538. * images.
  539. * @private
  540. */
  541. goog.editor.plugins.RemoveFormatting.prototype.removeFormattingWorker_ =
  542. function(html) {
  543. var el = goog.dom.createElement(goog.dom.TagName.DIV);
  544. el.innerHTML = html;
  545. // Put everything into a string buffer to avoid lots of expensive string
  546. // concatenation along the way.
  547. var sb = [];
  548. var stack = [el.childNodes, 0];
  549. // Keep separate stacks for places where we need to keep track of
  550. // how deeply embedded we are. These are analogous to the general stack.
  551. var preTagStack = [];
  552. var preTagLevel = 0; // Length of the prestack.
  553. var tableStack = [];
  554. var tableLevel = 0;
  555. // sp = stack pointer, pointing to the stack array.
  556. // decrement by 2 since the stack alternates node lists and
  557. // processed node counts
  558. for (var sp = 0; sp >= 0; sp -= 2) {
  559. // Check if we should pop the table level.
  560. var changedLevel = false;
  561. while (tableLevel > 0 && sp <= tableStack[tableLevel - 1]) {
  562. tableLevel--;
  563. changedLevel = true;
  564. }
  565. if (changedLevel) {
  566. goog.editor.plugins.RemoveFormatting.appendNewline_(sb);
  567. }
  568. // Check if we should pop the <pre>/<xmp> level.
  569. changedLevel = false;
  570. while (preTagLevel > 0 && sp <= preTagStack[preTagLevel - 1]) {
  571. preTagLevel--;
  572. changedLevel = true;
  573. }
  574. if (changedLevel) {
  575. goog.editor.plugins.RemoveFormatting.appendNewline_(sb);
  576. }
  577. // The list of of nodes to process at the current stack level.
  578. var nodeList = stack[sp];
  579. // The number of nodes processed so far, stored in the stack immediately
  580. // following the node list for that stack level.
  581. var numNodesProcessed = stack[sp + 1];
  582. while (numNodesProcessed < nodeList.length) {
  583. var node = nodeList[numNodesProcessed++];
  584. var nodeName = node.nodeName;
  585. var formatted = this.getValueForNode(node);
  586. if (goog.isDefAndNotNull(formatted)) {
  587. sb.push(formatted);
  588. continue;
  589. }
  590. // TODO(user): Handle case 'EMBED' and case 'OBJECT'.
  591. switch (nodeName) {
  592. case '#text':
  593. // Note that IE does not preserve whitespace in the dom
  594. // values, even in a pre tag, so this is useless for IE.
  595. var nodeValue = preTagLevel > 0 ?
  596. node.nodeValue :
  597. goog.string.stripNewlines(node.nodeValue);
  598. nodeValue = goog.string.htmlEscape(nodeValue);
  599. sb.push(nodeValue);
  600. continue;
  601. case String(goog.dom.TagName.P):
  602. goog.editor.plugins.RemoveFormatting.appendNewline_(sb);
  603. goog.editor.plugins.RemoveFormatting.appendNewline_(sb);
  604. break; // break (not continue) so that child nodes are processed.
  605. case String(goog.dom.TagName.BR):
  606. goog.editor.plugins.RemoveFormatting.appendNewline_(sb);
  607. continue;
  608. case String(goog.dom.TagName.TABLE):
  609. goog.editor.plugins.RemoveFormatting.appendNewline_(sb);
  610. tableStack[tableLevel++] = sp;
  611. break;
  612. case String(goog.dom.TagName.PRE):
  613. case 'XMP':
  614. // This doesn't fully handle xmp, since
  615. // it doesn't actually ignore tags within the xmp tag.
  616. preTagStack[preTagLevel++] = sp;
  617. break;
  618. case String(goog.dom.TagName.STYLE):
  619. case String(goog.dom.TagName.SCRIPT):
  620. case String(goog.dom.TagName.SELECT):
  621. continue;
  622. case String(goog.dom.TagName.A):
  623. if (node.href && node.href != '') {
  624. sb.push("<a href='");
  625. sb.push(node.href);
  626. sb.push("'>");
  627. sb.push(this.removeFormattingWorker_(node.innerHTML));
  628. sb.push('</a>');
  629. continue; // Children taken care of.
  630. } else {
  631. break; // Take care of the children.
  632. }
  633. case String(goog.dom.TagName.IMG):
  634. sb.push("<img src='");
  635. sb.push(node.src);
  636. sb.push("'");
  637. // border=0 is a common way to not show a blue border around an image
  638. // that is wrapped by a link. If we remove that, the blue border will
  639. // show up, which to the user looks like adding format, not removing.
  640. if (node.border == '0') {
  641. sb.push(" border='0'");
  642. }
  643. sb.push('>');
  644. continue;
  645. case String(goog.dom.TagName.TD):
  646. // Don't add a space for the first TD, we only want spaces to
  647. // separate td's.
  648. if (node.previousSibling) {
  649. sb.push(' ');
  650. }
  651. break;
  652. case String(goog.dom.TagName.TR):
  653. // Don't add a newline for the first TR.
  654. if (node.previousSibling) {
  655. goog.editor.plugins.RemoveFormatting.appendNewline_(sb);
  656. }
  657. break;
  658. case String(goog.dom.TagName.DIV):
  659. var parent = node.parentNode;
  660. if (parent.firstChild == node &&
  661. goog.editor.plugins.RemoveFormatting.BLOCK_RE_.test(
  662. parent.tagName)) {
  663. // If a DIV is the first child of another element that itself is a
  664. // block element, the DIV does not add a new line.
  665. break;
  666. }
  667. // Otherwise, the DIV does add a new line. Fall through.
  668. default:
  669. if (goog.editor.plugins.RemoveFormatting.BLOCK_RE_.test(nodeName)) {
  670. goog.editor.plugins.RemoveFormatting.appendNewline_(sb);
  671. }
  672. }
  673. // Recurse down the node.
  674. var children = node.childNodes;
  675. if (children.length > 0) {
  676. // Push the current state on the stack.
  677. stack[sp++] = nodeList;
  678. stack[sp++] = numNodesProcessed;
  679. // Iterate through the children nodes.
  680. nodeList = children;
  681. numNodesProcessed = 0;
  682. }
  683. }
  684. }
  685. // Replace &nbsp; with white space.
  686. return goog.string.normalizeSpaces(sb.join(''));
  687. };
  688. /**
  689. * Handle per node special processing if necessary. If this function returns
  690. * null then standard cleanup is applied. Otherwise this node and all children
  691. * are assumed to be cleaned.
  692. * NOTE(user): If an alternate RemoveFormatting processor is provided
  693. * (setRemoveFormattingFunc()), this will no longer work.
  694. * @param {Element} node The node to clean.
  695. * @return {?string} The HTML strig representation of the cleaned data.
  696. */
  697. goog.editor.plugins.RemoveFormatting.prototype.getValueForNode = function(
  698. node) {
  699. return null;
  700. };
  701. /**
  702. * Sets a function to be used for remove formatting.
  703. * @param {function(string): string} removeFormattingFunc - A function that
  704. * takes a string of html and returns a string of html that does any other
  705. * formatting changes desired. Use this only if trogedit's behavior doesn't
  706. * meet your needs.
  707. */
  708. goog.editor.plugins.RemoveFormatting.prototype.setRemoveFormattingFunc =
  709. function(removeFormattingFunc) {
  710. this.optRemoveFormattingFunc_ = removeFormattingFunc;
  711. };