| // Copyright 2008 The Closure Library Authors. All Rights Reserved. |
| // |
| // Licensed under the Apache License, Version 2.0 (the "License"); |
| // you may not use this file except in compliance with the License. |
| // You may obtain a copy of the License at |
| // |
| // http://www.apache.org/licenses/LICENSE-2.0 |
| // |
| // Unless required by applicable law or agreed to in writing, software |
| // distributed under the License is distributed on an "AS-IS" BASIS, |
| // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| // See the License for the specific language governing permissions and |
| // limitations under the License. |
| // All Rights Reserved. |
| |
| /** |
| * @fileoverview Plugin to handle Remove Formatting. |
| * |
| */ |
| |
| goog.provide('goog.editor.plugins.RemoveFormatting'); |
| |
| goog.require('goog.dom'); |
| goog.require('goog.dom.NodeType'); |
| goog.require('goog.dom.Range'); |
| goog.require('goog.dom.TagName'); |
| goog.require('goog.editor.BrowserFeature'); |
| goog.require('goog.editor.Plugin'); |
| goog.require('goog.editor.node'); |
| goog.require('goog.editor.range'); |
| goog.require('goog.string'); |
| goog.require('goog.userAgent'); |
| |
| |
| |
| /** |
| * A plugin to handle removing formatting from selected text. |
| * @constructor |
| * @extends {goog.editor.Plugin} |
| * @final |
| */ |
| goog.editor.plugins.RemoveFormatting = function() { |
| goog.editor.Plugin.call(this); |
| |
| /** |
| * Optional function to perform remove formatting in place of the |
| * provided removeFormattingWorker_. |
| * @type {?function(string): string} |
| * @private |
| */ |
| this.optRemoveFormattingFunc_ = null; |
| }; |
| goog.inherits(goog.editor.plugins.RemoveFormatting, goog.editor.Plugin); |
| |
| |
| /** |
| * The editor command this plugin in handling. |
| * @type {string} |
| */ |
| goog.editor.plugins.RemoveFormatting.REMOVE_FORMATTING_COMMAND = |
| '+removeFormat'; |
| |
| |
| /** |
| * Regular expression that matches a block tag name. |
| * @type {RegExp} |
| * @private |
| */ |
| goog.editor.plugins.RemoveFormatting.BLOCK_RE_ = |
| /^(DIV|TR|LI|BLOCKQUOTE|H\d|PRE|XMP)/; |
| |
| |
| /** |
| * Appends a new line to a string buffer. |
| * @param {Array<string>} sb The string buffer to add to. |
| * @private |
| */ |
| goog.editor.plugins.RemoveFormatting.appendNewline_ = function(sb) { |
| sb.push('<br>'); |
| }; |
| |
| |
| /** |
| * Create a new range delimited by the start point of the first range and |
| * the end point of the second range. |
| * @param {goog.dom.AbstractRange} startRange Use the start point of this |
| * range as the beginning of the new range. |
| * @param {goog.dom.AbstractRange} endRange Use the end point of this |
| * range as the end of the new range. |
| * @return {!goog.dom.AbstractRange} The new range. |
| * @private |
| */ |
| goog.editor.plugins.RemoveFormatting.createRangeDelimitedByRanges_ = function( |
| startRange, endRange) { |
| return goog.dom.Range.createFromNodes( |
| startRange.getStartNode(), startRange.getStartOffset(), |
| endRange.getEndNode(), endRange.getEndOffset()); |
| }; |
| |
| |
| /** @override */ |
| goog.editor.plugins.RemoveFormatting.prototype.getTrogClassId = function() { |
| return 'RemoveFormatting'; |
| }; |
| |
| |
| /** @override */ |
| goog.editor.plugins.RemoveFormatting.prototype.isSupportedCommand = function( |
| command) { |
| return command == |
| goog.editor.plugins.RemoveFormatting.REMOVE_FORMATTING_COMMAND; |
| }; |
| |
| |
| /** @override */ |
| goog.editor.plugins.RemoveFormatting.prototype.execCommandInternal = |
| function(command, var_args) { |
| if (command == |
| goog.editor.plugins.RemoveFormatting.REMOVE_FORMATTING_COMMAND) { |
| this.removeFormatting_(); |
| } |
| }; |
| |
| |
| /** @override */ |
| goog.editor.plugins.RemoveFormatting.prototype.handleKeyboardShortcut = |
| function(e, key, isModifierPressed) { |
| if (!isModifierPressed) { |
| return false; |
| } |
| |
| if (key == ' ') { |
| this.getFieldObject().execCommand( |
| goog.editor.plugins.RemoveFormatting.REMOVE_FORMATTING_COMMAND); |
| return true; |
| } |
| |
| return false; |
| }; |
| |
| |
| /** |
| * Removes formatting from the current selection. Removes basic formatting |
| * (B/I/U) using the browser's execCommand. Then extracts the html from the |
| * selection to convert, calls either a client's specified removeFormattingFunc |
| * callback or trogedit's general built-in removeFormattingWorker_, |
| * and then replaces the current selection with the converted text. |
| * @private |
| */ |
| goog.editor.plugins.RemoveFormatting.prototype.removeFormatting_ = function() { |
| var range = this.getFieldObject().getRange(); |
| if (range.isCollapsed()) { |
| return; |
| } |
| |
| // Get the html to format and send it off for formatting. Built in |
| // removeFormat only strips some inline elements and some inline CSS styles |
| var convFunc = this.optRemoveFormattingFunc_ || |
| goog.bind(this.removeFormattingWorker_, this); |
| this.convertSelectedHtmlText_(convFunc); |
| |
| // Do the execCommand last as it needs block elements removed to work |
| // properly on background/fontColor in FF. There are, unfortunately, still |
| // cases where background/fontColor are not removed here. |
| var doc = this.getFieldDomHelper().getDocument(); |
| doc.execCommand('RemoveFormat', false, undefined); |
| |
| if (goog.editor.BrowserFeature.ADDS_NBSPS_IN_REMOVE_FORMAT) { |
| // WebKit converts spaces to non-breaking spaces when doing a RemoveFormat. |
| // See: https://bugs.webkit.org/show_bug.cgi?id=14062 |
| this.convertSelectedHtmlText_(function(text) { |
| // This loses anything that might have legitimately been a non-breaking |
| // space, but that's better than the alternative of only having non- |
| // breaking spaces. |
| // Old versions of WebKit (Safari 3, Chrome 1) incorrectly match /u00A0 |
| // and newer versions properly match . |
| var nbspRegExp = |
| goog.userAgent.isVersionOrHigher('528') ? / /g : /\u00A0/g; |
| return text.replace(nbspRegExp, ' '); |
| }); |
| } |
| }; |
| |
| |
| /** |
| * Finds the nearest ancestor of the node that is a table. |
| * @param {Node} nodeToCheck Node to search from. |
| * @return {Node} The table, or null if one was not found. |
| * @private |
| */ |
| goog.editor.plugins.RemoveFormatting.prototype.getTableAncestor_ = function( |
| nodeToCheck) { |
| var fieldElement = this.getFieldObject().getElement(); |
| while (nodeToCheck && nodeToCheck != fieldElement) { |
| if (nodeToCheck.tagName == goog.dom.TagName.TABLE) { |
| return nodeToCheck; |
| } |
| nodeToCheck = nodeToCheck.parentNode; |
| } |
| return null; |
| }; |
| |
| |
| /** |
| * Replaces the contents of the selection with html. Does its best to maintain |
| * the original selection. Also does its best to result in a valid DOM. |
| * |
| * TODO(user): See if there's any way to make this work on Ranges, and then |
| * move it into goog.editor.range. The Firefox implementation uses execCommand |
| * on the document, so must work on the actual selection. |
| * |
| * @param {string} html The html string to insert into the range. |
| * @private |
| */ |
| goog.editor.plugins.RemoveFormatting.prototype.pasteHtml_ = function(html) { |
| var range = this.getFieldObject().getRange(); |
| |
| var dh = this.getFieldDomHelper(); |
| // Use markers to set the extent of the selection so that we can reselect it |
| // afterwards. This works better than builtin range manipulation in FF and IE |
| // because their implementations are so self-inconsistent and buggy. |
| var startSpanId = goog.string.createUniqueString(); |
| var endSpanId = goog.string.createUniqueString(); |
| html = '<span id="' + startSpanId + '"></span>' + html + |
| '<span id="' + endSpanId + '"></span>'; |
| var dummyNodeId = goog.string.createUniqueString(); |
| var dummySpanText = '<span id="' + dummyNodeId + '"></span>'; |
| |
| if (goog.editor.BrowserFeature.HAS_IE_RANGES) { |
| // IE's selection often doesn't include the outermost tags. |
| // We want to use pasteHTML to replace the range contents with the newly |
| // unformatted text, so we have to check to make sure we aren't just |
| // pasting into some stray tags. To do this, we first clear out the |
| // contents of the range and then delete all empty nodes parenting the now |
| // empty range. This way, the pasted contents are never re-embedded into |
| // formated nodes. Pasting purely empty html does not work, since IE moves |
| // the selection inside the next node, so we insert a dummy span. |
| var textRange = range.getTextRange(0).getBrowserRangeObject(); |
| textRange.pasteHTML(dummySpanText); |
| var parent; |
| while ((parent = textRange.parentElement()) && |
| goog.editor.node.isEmpty(parent) && |
| !goog.editor.node.isEditableContainer(parent)) { |
| var tag = parent.nodeName; |
| // We can't remove these table tags as it will invalidate the table dom. |
| if (tag == goog.dom.TagName.TD || |
| tag == goog.dom.TagName.TR || |
| tag == goog.dom.TagName.TH) { |
| break; |
| } |
| |
| goog.dom.removeNode(parent); |
| } |
| textRange.pasteHTML(html); |
| var dummySpan = dh.getElement(dummyNodeId); |
| // If we entered the while loop above, the node has already been removed |
| // since it was a child of parent and parent was removed. |
| if (dummySpan) { |
| goog.dom.removeNode(dummySpan); |
| } |
| } else if (goog.editor.BrowserFeature.HAS_W3C_RANGES) { |
| // insertHtml and range.insertNode don't merge blocks correctly. |
| // (e.g. if your selection spans two paragraphs) |
| dh.getDocument().execCommand('insertImage', false, dummyNodeId); |
| var dummyImageNodePattern = new RegExp('<[^<]*' + dummyNodeId + '[^>]*>'); |
| var parent = this.getFieldObject().getRange().getContainerElement(); |
| if (parent.nodeType == goog.dom.NodeType.TEXT) { |
| // Opera sometimes returns a text node here. |
| // TODO(user): perhaps we should modify getParentContainer? |
| parent = parent.parentNode; |
| } |
| |
| // We have to search up the DOM because in some cases, notably when |
| // selecting li's within a list, execCommand('insertImage') actually splits |
| // tags in such a way that parent that used to contain the selection does |
| // not contain inserted image. |
| while (!dummyImageNodePattern.test(parent.innerHTML)) { |
| parent = parent.parentNode; |
| } |
| |
| // Like the IE case above, sometimes the selection does not include the |
| // outermost tags. For Gecko, we have already expanded the range so that |
| // it does, so we can just replace the dummy image with the final html. |
| // For WebKit, we use the same approach as we do with IE - we |
| // inject a dummy span where we will eventually place the contents, and |
| // remove parentNodes of the span while they are empty. |
| |
| if (goog.userAgent.GECKO) { |
| goog.editor.node.replaceInnerHtml(parent, |
| parent.innerHTML.replace(dummyImageNodePattern, html)); |
| } else { |
| goog.editor.node.replaceInnerHtml(parent, |
| parent.innerHTML.replace(dummyImageNodePattern, dummySpanText)); |
| var dummySpan = dh.getElement(dummyNodeId); |
| parent = dummySpan; |
| while ((parent = dummySpan.parentNode) && |
| goog.editor.node.isEmpty(parent) && |
| !goog.editor.node.isEditableContainer(parent)) { |
| var tag = parent.nodeName; |
| // We can't remove these table tags as it will invalidate the table dom. |
| if (tag == goog.dom.TagName.TD || |
| tag == goog.dom.TagName.TR || |
| tag == goog.dom.TagName.TH) { |
| break; |
| } |
| |
| // We can't just remove parent since dummySpan is inside it, and we need |
| // to keep dummy span around for the replacement. So we move the |
| // dummySpan up as we go. |
| goog.dom.insertSiblingAfter(dummySpan, parent); |
| goog.dom.removeNode(parent); |
| } |
| goog.editor.node.replaceInnerHtml(parent, |
| parent.innerHTML.replace(new RegExp(dummySpanText, 'i'), html)); |
| } |
| } |
| |
| var startSpan = dh.getElement(startSpanId); |
| var endSpan = dh.getElement(endSpanId); |
| goog.dom.Range.createFromNodes(startSpan, 0, endSpan, |
| endSpan.childNodes.length).select(); |
| goog.dom.removeNode(startSpan); |
| goog.dom.removeNode(endSpan); |
| }; |
| |
| |
| /** |
| * Gets the html inside the selection to send off for further processing. |
| * |
| * TODO(user): Make this general so that it can be moved into |
| * goog.editor.range. The main reason it can't be moved is becuase we need to |
| * get the range before we do the execCommand and continue to operate on that |
| * same range (reasons are documented above). |
| * |
| * @param {goog.dom.AbstractRange} range The selection. |
| * @return {string} The html string to format. |
| * @private |
| */ |
| goog.editor.plugins.RemoveFormatting.prototype.getHtmlText_ = function(range) { |
| var div = this.getFieldDomHelper().createDom('div'); |
| var textRange = range.getBrowserRangeObject(); |
| |
| if (goog.editor.BrowserFeature.HAS_W3C_RANGES) { |
| // Get the text to convert. |
| div.appendChild(textRange.cloneContents()); |
| } else if (goog.editor.BrowserFeature.HAS_IE_RANGES) { |
| // Trim the whitespace on the ends of the range, so that it the container |
| // will be the container of only the text content that we are changing. |
| // This gets around issues in IE where the spaces are included in the |
| // selection, but ignored sometimes by execCommand, and left orphaned. |
| var rngText = range.getText(); |
| |
| // BRs get reported as \r\n, but only count as one character for moves. |
| // Adjust the string so our move counter is correct. |
| rngText = rngText.replace(/\r\n/g, '\r'); |
| |
| var rngTextLength = rngText.length; |
| var left = rngTextLength - goog.string.trimLeft(rngText).length; |
| var right = rngTextLength - goog.string.trimRight(rngText).length; |
| |
| textRange.moveStart('character', left); |
| textRange.moveEnd('character', -right); |
| |
| var htmlText = textRange.htmlText; |
| // Check if in pretag and fix up formatting so that new lines are preserved. |
| if (textRange.queryCommandValue('formatBlock') == 'Formatted') { |
| htmlText = goog.string.newLineToBr(textRange.htmlText); |
| } |
| div.innerHTML = htmlText; |
| } |
| |
| // Get the innerHTML of the node instead of just returning the text above |
| // so that its properly html escaped. |
| return div.innerHTML; |
| }; |
| |
| |
| /** |
| * Move the range so that it doesn't include any partially selected tables. |
| * @param {goog.dom.AbstractRange} range The range to adjust. |
| * @param {Node} startInTable Table node that the range starts in. |
| * @param {Node} endInTable Table node that the range ends in. |
| * @return {!goog.dom.SavedCaretRange} Range to use to restore the |
| * selection after we run our custom remove formatting. |
| * @private |
| */ |
| goog.editor.plugins.RemoveFormatting.prototype.adjustRangeForTables_ = |
| function(range, startInTable, endInTable) { |
| // Create placeholders for the current selection so we can restore it |
| // later. |
| var savedCaretRange = goog.editor.range.saveUsingNormalizedCarets(range); |
| |
| var startNode = range.getStartNode(); |
| var startOffset = range.getStartOffset(); |
| var endNode = range.getEndNode(); |
| var endOffset = range.getEndOffset(); |
| var dh = this.getFieldDomHelper(); |
| |
| // Move start after the table. |
| if (startInTable) { |
| var textNode = dh.createTextNode(''); |
| goog.dom.insertSiblingAfter(textNode, startInTable); |
| startNode = textNode; |
| startOffset = 0; |
| } |
| // Move end before the table. |
| if (endInTable) { |
| var textNode = dh.createTextNode(''); |
| goog.dom.insertSiblingBefore(textNode, endInTable); |
| endNode = textNode; |
| endOffset = 0; |
| } |
| |
| goog.dom.Range.createFromNodes(startNode, startOffset, |
| endNode, endOffset).select(); |
| |
| return savedCaretRange; |
| }; |
| |
| |
| /** |
| * Remove a caret from the dom and hide it in a safe place, so it can |
| * be restored later via restoreCaretsFromCave. |
| * @param {goog.dom.SavedCaretRange} caretRange The caret range to |
| * get the carets from. |
| * @param {boolean} isStart Whether this is the start or end caret. |
| * @private |
| */ |
| goog.editor.plugins.RemoveFormatting.prototype.putCaretInCave_ = function( |
| caretRange, isStart) { |
| var cavedCaret = goog.dom.removeNode(caretRange.getCaret(isStart)); |
| if (isStart) { |
| this.startCaretInCave_ = cavedCaret; |
| } else { |
| this.endCaretInCave_ = cavedCaret; |
| } |
| }; |
| |
| |
| /** |
| * Restore carets that were hidden away by adding them back into the dom. |
| * Note: this does not restore to the original dom location, as that |
| * will likely have been modified with remove formatting. The only |
| * guarentees here are that start will still be before end, and that |
| * they will be in the editable region. This should only be used when |
| * you don't actually intend to USE the caret again. |
| * @private |
| */ |
| goog.editor.plugins.RemoveFormatting.prototype.restoreCaretsFromCave_ = |
| function() { |
| // To keep start before end, we put the end caret at the bottom of the field |
| // and the start caret at the start of the field. |
| var field = this.getFieldObject().getElement(); |
| if (this.startCaretInCave_) { |
| field.insertBefore(this.startCaretInCave_, field.firstChild); |
| this.startCaretInCave_ = null; |
| } |
| if (this.endCaretInCave_) { |
| field.appendChild(this.endCaretInCave_); |
| this.endCaretInCave_ = null; |
| } |
| }; |
| |
| |
| /** |
| * Gets the html inside the current selection, passes it through the given |
| * conversion function, and puts it back into the selection. |
| * |
| * @param {function(string): string} convertFunc A conversion function that |
| * transforms an html string to new html string. |
| * @private |
| */ |
| goog.editor.plugins.RemoveFormatting.prototype.convertSelectedHtmlText_ = |
| function(convertFunc) { |
| var range = this.getFieldObject().getRange(); |
| |
| // For multiple ranges, it is really hard to do our custom remove formatting |
| // without invalidating other ranges. So instead of always losing the |
| // content, this solution at least lets the browser do its own remove |
| // formatting which works correctly most of the time. |
| if (range.getTextRangeCount() > 1) { |
| return; |
| } |
| |
| if (goog.userAgent.GECKO) { |
| // Determine if we need to handle tables, since they are special cases. |
| // If the selection is entirely within a table, there is no extra |
| // formatting removal we can do. If a table is fully selected, we will |
| // just blow it away. If a table is only partially selected, we can |
| // perform custom remove formatting only on the non table parts, since we |
| // we can't just remove the parts and paste back into it (eg. we can't |
| // inject html where a TR used to be). |
| // If the selection contains the table and more, this is automatically |
| // handled, but if just the table is selected, it can be tricky to figure |
| // this case out, because of the numerous ways selections can be formed - |
| // ex. if a table has a single tr with a single td with a single text node |
| // in it, and the selection is (textNode: 0), (textNode: nextNode.length) |
| // then the entire table is selected, even though the start and end aren't |
| // the table itself. We are truly inside a table if the expanded endpoints |
| // are still inside the table. |
| |
| // Expand the selection to include any outermost tags that weren't included |
| // in the selection, but have the same visible selection. Stop expanding |
| // if we reach the top level field. |
| var expandedRange = goog.editor.range.expand(range, |
| this.getFieldObject().getElement()); |
| |
| var startInTable = this.getTableAncestor_(expandedRange.getStartNode()); |
| var endInTable = this.getTableAncestor_(expandedRange.getEndNode()); |
| |
| if (startInTable || endInTable) { |
| if (startInTable == endInTable) { |
| // We are fully contained in the same table, there is no extra |
| // remove formatting that we can do, just return and run browser |
| // formatting only. |
| return; |
| } |
| |
| // Adjust the range to not contain any partially selected tables, since |
| // we don't want to run our custom remove formatting on them. |
| var savedCaretRange = this.adjustRangeForTables_(range, |
| startInTable, endInTable); |
| |
| // Hack alert!! |
| // If start is not in a table, then the saved caret will get sent out |
| // for uber remove formatting, and it will get blown away. This is |
| // fine, except that we need to be able to re-create a range from the |
| // savedCaretRange later on. So, we just remove it from the dom, and |
| // put it back later so we can create a range later (not exactly in the |
| // same spot, but don't worry we don't actually try to use it later) |
| // and then it will be removed when we dispose the range. |
| if (!startInTable) { |
| this.putCaretInCave_(savedCaretRange, true); |
| } |
| if (!endInTable) { |
| this.putCaretInCave_(savedCaretRange, false); |
| } |
| |
| // Re-fetch the range, and re-expand it, since we just modified it. |
| range = this.getFieldObject().getRange(); |
| expandedRange = goog.editor.range.expand(range, |
| this.getFieldObject().getElement()); |
| } |
| |
| expandedRange.select(); |
| range = expandedRange; |
| } |
| |
| // Convert the selected text to the format-less version, paste back into |
| // the selection. |
| var text = this.getHtmlText_(range); |
| this.pasteHtml_(convertFunc(text)); |
| |
| if (goog.userAgent.GECKO && savedCaretRange) { |
| // If we moved the selection, move it back so the user can't tell we did |
| // anything crazy and so the browser removeFormat that we call next |
| // will operate on the entire originally selected range. |
| range = this.getFieldObject().getRange(); |
| this.restoreCaretsFromCave_(); |
| var realSavedCaretRange = savedCaretRange.toAbstractRange(); |
| var startRange = startInTable ? realSavedCaretRange : range; |
| var endRange = endInTable ? realSavedCaretRange : range; |
| var restoredRange = |
| goog.editor.plugins.RemoveFormatting.createRangeDelimitedByRanges_( |
| startRange, endRange); |
| restoredRange.select(); |
| savedCaretRange.dispose(); |
| } |
| }; |
| |
| |
| /** |
| * Does a best-effort attempt at clobbering all formatting that the |
| * browser's execCommand couldn't clobber without being totally inefficient. |
| * Attempts to convert visual line breaks to BRs. Leaves anchors that contain an |
| * href and images. |
| * Adapted from Gmail's MessageUtil's htmlToPlainText. http://go/messageutil.js |
| * @param {string} html The original html of the message. |
| * @return {string} The unformatted html, which is just text, br's, anchors and |
| * images. |
| * @private |
| */ |
| goog.editor.plugins.RemoveFormatting.prototype.removeFormattingWorker_ = |
| function(html) { |
| var el = goog.dom.createElement('div'); |
| el.innerHTML = html; |
| |
| // Put everything into a string buffer to avoid lots of expensive string |
| // concatenation along the way. |
| var sb = []; |
| var stack = [el.childNodes, 0]; |
| |
| // Keep separate stacks for places where we need to keep track of |
| // how deeply embedded we are. These are analogous to the general stack. |
| var preTagStack = []; |
| var preTagLevel = 0; // Length of the prestack. |
| var tableStack = []; |
| var tableLevel = 0; |
| |
| // sp = stack pointer, pointing to the stack array. |
| // decrement by 2 since the stack alternates node lists and |
| // processed node counts |
| for (var sp = 0; sp >= 0; sp -= 2) { |
| // Check if we should pop the table level. |
| var changedLevel = false; |
| while (tableLevel > 0 && sp <= tableStack[tableLevel - 1]) { |
| tableLevel--; |
| changedLevel = true; |
| } |
| if (changedLevel) { |
| goog.editor.plugins.RemoveFormatting.appendNewline_(sb); |
| } |
| |
| |
| // Check if we should pop the <pre>/<xmp> level. |
| changedLevel = false; |
| while (preTagLevel > 0 && sp <= preTagStack[preTagLevel - 1]) { |
| preTagLevel--; |
| changedLevel = true; |
| } |
| if (changedLevel) { |
| goog.editor.plugins.RemoveFormatting.appendNewline_(sb); |
| } |
| |
| // The list of of nodes to process at the current stack level. |
| var nodeList = stack[sp]; |
| // The number of nodes processed so far, stored in the stack immediately |
| // following the node list for that stack level. |
| var numNodesProcessed = stack[sp + 1]; |
| |
| while (numNodesProcessed < nodeList.length) { |
| var node = nodeList[numNodesProcessed++]; |
| var nodeName = node.nodeName; |
| |
| var formatted = this.getValueForNode(node); |
| if (goog.isDefAndNotNull(formatted)) { |
| sb.push(formatted); |
| continue; |
| } |
| |
| // TODO(user): Handle case 'EMBED' and case 'OBJECT'. |
| switch (nodeName) { |
| case '#text': |
| // Note that IE does not preserve whitespace in the dom |
| // values, even in a pre tag, so this is useless for IE. |
| var nodeValue = preTagLevel > 0 ? |
| node.nodeValue : |
| goog.string.stripNewlines(node.nodeValue); |
| nodeValue = goog.string.htmlEscape(nodeValue); |
| sb.push(nodeValue); |
| continue; |
| |
| case goog.dom.TagName.P: |
| goog.editor.plugins.RemoveFormatting.appendNewline_(sb); |
| goog.editor.plugins.RemoveFormatting.appendNewline_(sb); |
| break; // break (not continue) so that child nodes are processed. |
| |
| case goog.dom.TagName.BR: |
| goog.editor.plugins.RemoveFormatting.appendNewline_(sb); |
| continue; |
| |
| case goog.dom.TagName.TABLE: |
| goog.editor.plugins.RemoveFormatting.appendNewline_(sb); |
| tableStack[tableLevel++] = sp; |
| break; |
| |
| case goog.dom.TagName.PRE: |
| case 'XMP': |
| // This doesn't fully handle xmp, since |
| // it doesn't actually ignore tags within the xmp tag. |
| preTagStack[preTagLevel++] = sp; |
| break; |
| |
| case goog.dom.TagName.STYLE: |
| case goog.dom.TagName.SCRIPT: |
| case goog.dom.TagName.SELECT: |
| continue; |
| |
| case goog.dom.TagName.A: |
| if (node.href && node.href != '') { |
| sb.push("<a href='"); |
| sb.push(node.href); |
| sb.push("'>"); |
| sb.push(this.removeFormattingWorker_(node.innerHTML)); |
| sb.push('</a>'); |
| continue; // Children taken care of. |
| } else { |
| break; // Take care of the children. |
| } |
| |
| case goog.dom.TagName.IMG: |
| sb.push("<img src='"); |
| sb.push(node.src); |
| sb.push("'"); |
| // border=0 is a common way to not show a blue border around an image |
| // that is wrapped by a link. If we remove that, the blue border will |
| // show up, which to the user looks like adding format, not removing. |
| if (node.border == '0') { |
| sb.push(" border='0'"); |
| } |
| sb.push('>'); |
| continue; |
| |
| case goog.dom.TagName.TD: |
| // Don't add a space for the first TD, we only want spaces to |
| // separate td's. |
| if (node.previousSibling) { |
| sb.push(' '); |
| } |
| break; |
| |
| case goog.dom.TagName.TR: |
| // Don't add a newline for the first TR. |
| if (node.previousSibling) { |
| goog.editor.plugins.RemoveFormatting.appendNewline_(sb); |
| } |
| break; |
| |
| case goog.dom.TagName.DIV: |
| var parent = node.parentNode; |
| if (parent.firstChild == node && |
| goog.editor.plugins.RemoveFormatting.BLOCK_RE_.test( |
| parent.tagName)) { |
| // If a DIV is the first child of another element that itself is a |
| // block element, the DIV does not add a new line. |
| break; |
| } |
| // Otherwise, the DIV does add a new line. Fall through. |
| |
| default: |
| if (goog.editor.plugins.RemoveFormatting.BLOCK_RE_.test(nodeName)) { |
| goog.editor.plugins.RemoveFormatting.appendNewline_(sb); |
| } |
| } |
| |
| // Recurse down the node. |
| var children = node.childNodes; |
| if (children.length > 0) { |
| // Push the current state on the stack. |
| stack[sp++] = nodeList; |
| stack[sp++] = numNodesProcessed; |
| |
| // Iterate through the children nodes. |
| nodeList = children; |
| numNodesProcessed = 0; |
| } |
| } |
| } |
| |
| // Replace with white space. |
| return goog.string.normalizeSpaces(sb.join('')); |
| }; |
| |
| |
| /** |
| * Handle per node special processing if neccessary. If this function returns |
| * null then standard cleanup is applied. Otherwise this node and all children |
| * are assumed to be cleaned. |
| * NOTE(user): If an alternate RemoveFormatting processor is provided |
| * (setRemoveFormattingFunc()), this will no longer work. |
| * @param {Element} node The node to clean. |
| * @return {?string} The HTML strig representation of the cleaned data. |
| */ |
| goog.editor.plugins.RemoveFormatting.prototype.getValueForNode = function( |
| node) { |
| return null; |
| }; |
| |
| |
| /** |
| * Sets a function to be used for remove formatting. |
| * @param {function(string): string} removeFormattingFunc - A function that |
| * takes a string of html and returns a string of html that does any other |
| * formatting changes desired. Use this only if trogedit's behavior doesn't |
| * meet your needs. |
| */ |
| goog.editor.plugins.RemoveFormatting.prototype.setRemoveFormattingFunc = |
| function(removeFormattingFunc) { |
| this.optRemoveFormattingFunc_ = removeFormattingFunc; |
| }; |