/** * Formatter.js * * Copyright 2009, Moxiecode Systems AB * Released under LGPL License. * * License: http://tinymce.moxiecode.com/license * Contributing: http://tinymce.moxiecode.com/contributing */ (function(tinymce) { /** * Text formatter engine class. This class is used to apply formats like bold, italic, font size * etc to the current selection or specific nodes. This engine was build to replace the browsers * default formatting logic for execCommand due to it's inconsistant and buggy behavior. * * @class tinymce.Formatter * @example * tinymce.activeEditor.formatter.register('mycustomformat', { * inline : 'span', * styles : {color : '#ff0000'} * }); * * tinymce.activeEditor.formatter.apply('mycustomformat'); */ /** * Constructs a new formatter instance. * * @constructor Formatter * @param {tinymce.Editor} ed Editor instance to construct the formatter engine to. */ tinymce.Formatter = function(ed) { var formats = {}, each = tinymce.each, dom = ed.dom, selection = ed.selection, TreeWalker = tinymce.dom.TreeWalker, rangeUtils = new tinymce.dom.RangeUtils(dom), isValid = ed.schema.isValid, isBlock = dom.isBlock, forcedRootBlock = ed.settings.forced_root_block, nodeIndex = dom.nodeIndex, INVISIBLE_CHAR = '\uFEFF', MCE_ATTR_RE = /^(src|href|style)$/, FALSE = false, TRUE = true, undefined, pendingFormats = {apply : [], remove : []}; function isArray(obj) { return obj instanceof Array; }; function getParents(node, selector) { return dom.getParents(node, selector, dom.getRoot()); }; function isCaretNode(node) { return node.nodeType === 1 && (node.face === 'mceinline' || node.style.fontFamily === 'mceinline'); }; // Public functions /** * Returns the format by name or all formats if no name is specified. * * @method get * @param {String} name Optional name to retrive by. * @return {Array/Object} Array/Object with all registred formats or a specific format. */ function get(name) { return name ? formats[name] : formats; }; /** * Registers a specific format by name. * * @method register * @param {Object/String} name Name of the format for example "bold". * @param {Object/Array} format Optional format object or array of format variants can only be omitted if the first arg is an object. */ function register(name, format) { if (name) { if (typeof(name) !== 'string') { each(name, function(format, name) { register(name, format); }); } else { // Force format into array and add it to internal collection format = format.length ? format : [format]; each(format, function(format) { // Set deep to false by default on selector formats this to avoid removing // alignment on images inside paragraphs when alignment is changed on paragraphs if (format.deep === undefined) format.deep = !format.selector; // Default to true if (format.split === undefined) format.split = !format.selector || format.inline; // Default to true if (format.remove === undefined && format.selector && !format.inline) format.remove = 'none'; // Mark format as a mixed format inline + block level if (format.selector && format.inline) { format.mixed = true; format.block_expand = true; } // Split classes if needed if (typeof(format.classes) === 'string') format.classes = format.classes.split(/\s+/); }); formats[name] = format; } } }; /** * Applies the specified format to the current selection or specified node. * * @method apply * @param {String} name Name of format to apply. * @param {Object} vars Optional list of variables to replace within format before applying it. * @param {Node} node Optional node to apply the format to defaults to current selection. */ function apply(name, vars, node) { var formatList = get(name), format = formatList[0], bookmark, rng, i; /** * Moves the start to the first suitable text node. */ function moveStart(rng) { var container = rng.startContainer, offset = rng.startOffset, walker, node; // Move startContainer/startOffset in to a suitable node if (container.nodeType == 1 || container.nodeValue === "") { container = container.nodeType == 1 ? container.childNodes[offset] : container; // Might fail if the offset is behind the last element in it's container if (container) { walker = new TreeWalker(container, container.parentNode); for (node = walker.current(); node; node = walker.next()) { if (node.nodeType == 3 && !isWhiteSpaceNode(node)) { rng.setStart(node, 0); break; } } } } return rng; }; function setElementFormat(elm, fmt) { fmt = fmt || format; if (elm) { each(fmt.styles, function(value, name) { dom.setStyle(elm, name, replaceVars(value, vars)); }); each(fmt.attributes, function(value, name) { dom.setAttrib(elm, name, replaceVars(value, vars)); }); each(fmt.classes, function(value) { value = replaceVars(value, vars); if (!dom.hasClass(elm, value)) dom.addClass(elm, value); }); } }; function applyRngStyle(rng) { var newWrappers = [], wrapName, wrapElm; // Setup wrapper element wrapName = format.inline || format.block; wrapElm = dom.create(wrapName); setElementFormat(wrapElm); rangeUtils.walk(rng, function(nodes) { var currentWrapElm; /** * Process a list of nodes wrap them. */ function process(node) { var nodeName = node.nodeName.toLowerCase(), parentName = node.parentNode.nodeName.toLowerCase(), found; // Stop wrapping on br elements if (isEq(nodeName, 'br')) { currentWrapElm = 0; // Remove any br elements when we wrap things if (format.block) dom.remove(node); return; } // If node is wrapper type if (format.wrapper && matchNode(node, name, vars)) { currentWrapElm = 0; return; } // Can we rename the block if (format.block && !format.wrapper && isTextBlock(nodeName)) { node = dom.rename(node, wrapName); setElementFormat(node); newWrappers.push(node); currentWrapElm = 0; return; } // Handle selector patterns if (format.selector) { // Look for matching formats each(formatList, function(format) { if (dom.is(node, format.selector) && !isCaretNode(node)) { setElementFormat(node, format); found = true; } }); // Continue processing if a selector match wasn't found and a inline element is defined if (!format.inline || found) { currentWrapElm = 0; return; } } // Is it valid to wrap this item if (isValid(wrapName, nodeName) && isValid(parentName, wrapName)) { // Start wrapping if (!currentWrapElm) { // Wrap the node currentWrapElm = wrapElm.cloneNode(FALSE); node.parentNode.insertBefore(currentWrapElm, node); newWrappers.push(currentWrapElm); } currentWrapElm.appendChild(node); } else { // Start a new wrapper for possible children currentWrapElm = 0; each(tinymce.grep(node.childNodes), process); // End the last wrapper currentWrapElm = 0; } }; // Process siblings from range each(nodes, process); }); // Cleanup each(newWrappers, function(node) { var childCount; function getChildCount(node) { var count = 0; each(node.childNodes, function(node) { if (!isWhiteSpaceNode(node) && !isBookmarkNode(node)) count++; }); return count; }; function mergeStyles(node) { var child, clone; each(node.childNodes, function(node) { if (node.nodeType == 1 && !isBookmarkNode(node) && !isCaretNode(node)) { child = node; return FALSE; // break loop } }); // If child was found and of the same type as the current node if (child && matchName(child, format)) { clone = child.cloneNode(FALSE); setElementFormat(clone); dom.replace(clone, node, TRUE); dom.remove(child, 1); } return clone || node; }; childCount = getChildCount(node); // Remove empty nodes if (childCount === 0) { dom.remove(node, 1); return; } if (format.inline || format.wrapper) { // Merges the current node with it's children of similar type to reduce the number of elements if (!format.exact && childCount === 1) node = mergeStyles(node); // Remove/merge children each(formatList, function(format) { // Merge all children of similar type will move styles from child to parent // this: text // will become: text each(dom.select(format.inline, node), function(child) { removeFormat(format, vars, child, format.exact ? child : null); }); }); // Remove child if direct parent is of same type if (matchNode(node.parentNode, name, vars)) { dom.remove(node, 1); node = 0; return TRUE; } // Look for parent with similar style format if (format.merge_with_parents) { dom.getParent(node.parentNode, function(parent) { if (matchNode(parent, name, vars)) { dom.remove(node, 1); node = 0; return TRUE; } }); } // Merge next and previous siblings if they are similar texttext becomes texttext if (node) { node = mergeSiblings(getNonWhiteSpaceSibling(node), node); node = mergeSiblings(node, getNonWhiteSpaceSibling(node, TRUE)); } } }); }; if (format) { if (node) { rng = dom.createRng(); rng.setStartBefore(node); rng.setEndAfter(node); applyRngStyle(expandRng(rng, formatList)); } else { if (!selection.isCollapsed() || !format.inline) { // Apply formatting to selection bookmark = selection.getBookmark(); applyRngStyle(expandRng(selection.getRng(TRUE), formatList)); selection.moveToBookmark(bookmark); selection.setRng(moveStart(selection.getRng(TRUE))); ed.nodeChanged(); } else performCaretAction('apply', name, vars); } } }; /** * Removes the specified format from the current selection or specified node. * * @method remove * @param {String} name Name of format to remove. * @param {Object} vars Optional list of variables to replace within format before removing it. * @param {Node} node Optional node to remove the format from defaults to current selection. */ function remove(name, vars, node) { var formatList = get(name), format = formatList[0], bookmark, i, rng; // Merges the styles for each node function process(node) { var children, i, l; // Grab the children first since the nodelist might be changed children = tinymce.grep(node.childNodes); // Process current node for (i = 0, l = formatList.length; i < l; i++) { if (removeFormat(formatList[i], vars, node, node)) break; } // Process the children if (format.deep) { for (i = 0, l = children.length; i < l; i++) process(children[i]); } }; function findFormatRoot(container) { var formatRoot; // Find format root each(getParents(container.parentNode).reverse(), function(parent) { var format; // Find format root element if (!formatRoot && parent.id != '_start' && parent.id != '_end') { // Is the node matching the format we are looking for format = matchNode(parent, name, vars); if (format && format.split !== false) formatRoot = parent; } }); return formatRoot; }; function wrapAndSplit(format_root, container, target, split) { var parent, clone, lastClone, firstClone, i, formatRootParent; // Format root found then clone formats and split it if (format_root) { formatRootParent = format_root.parentNode; for (parent = container.parentNode; parent && parent != formatRootParent; parent = parent.parentNode) { clone = parent.cloneNode(FALSE); for (i = 0; i < formatList.length; i++) { if (removeFormat(formatList[i], vars, clone, clone)) { clone = 0; break; } } // Build wrapper node if (clone) { if (lastClone) clone.appendChild(lastClone); if (!firstClone) firstClone = clone; lastClone = clone; } } // Never split block elements if the format is mixed if (split && (!format.mixed || !isBlock(format_root))) container = dom.split(format_root, container); // Wrap container in cloned formats if (lastClone) { target.parentNode.insertBefore(lastClone, target); firstClone.appendChild(target); } } return container; }; function splitToFormatRoot(container) { return wrapAndSplit(findFormatRoot(container), container, container, true); }; function unwrap(start) { var node = dom.get(start ? '_start' : '_end'), out = node[start ? 'firstChild' : 'lastChild']; // If the end is placed within the start the result will be removed // So this checks if the out node is a bookmark node if it is it // checks for another more suitable node if (isBookmarkNode(out)) out = out[start ? 'firstChild' : 'lastChild']; dom.remove(node, true); return out; }; function removeRngStyle(rng) { var startContainer, endContainer; rng = expandRng(rng, formatList, TRUE); if (format.split) { startContainer = getContainer(rng, TRUE); endContainer = getContainer(rng); if (startContainer != endContainer) { // Wrap start/end nodes in span element since these might be cloned/moved startContainer = wrap(startContainer, 'span', {id : '_start', _mce_type : 'bookmark'}); endContainer = wrap(endContainer, 'span', {id : '_end', _mce_type : 'bookmark'}); // Split start/end splitToFormatRoot(startContainer); splitToFormatRoot(endContainer); // Unwrap start/end to get real elements again startContainer = unwrap(TRUE); endContainer = unwrap(); } else startContainer = endContainer = splitToFormatRoot(startContainer); // Update range positions since they might have changed after the split operations rng.startContainer = startContainer.parentNode; rng.startOffset = nodeIndex(startContainer); rng.endContainer = endContainer.parentNode; rng.endOffset = nodeIndex(endContainer) + 1; } // Remove items between start/end rangeUtils.walk(rng, function(nodes) { each(nodes, function(node) { process(node); }); }); }; // Handle node if (node) { rng = dom.createRng(); rng.setStartBefore(node); rng.setEndAfter(node); removeRngStyle(rng); return; } if (!selection.isCollapsed() || !format.inline) { bookmark = selection.getBookmark(); removeRngStyle(selection.getRng(TRUE)); selection.moveToBookmark(bookmark); ed.nodeChanged(); } else performCaretAction('remove', name, vars); }; /** * Toggles the specified format on/off. * * @method toggle * @param {String} name Name of format to apply/remove. * @param {Object} vars Optional list of variables to replace within format before applying/removing it. * @param {Node} node Optional node to apply the format to or remove from. Defaults to current selection. */ function toggle(name, vars, node) { if (match(name, vars, node)) remove(name, vars, node); else apply(name, vars, node); }; /** * Return true/false if the specified node has the specified format. * * @method matchNode * @param {Node} node Node to check the format on. * @param {String} name Format name to check. * @param {Object} vars Optional list of variables to replace before checking it. * @param {Boolean} similar Match format that has similar properties. * @return {Object} Returns the format object it matches or undefined if it doesn't match. */ function matchNode(node, name, vars, similar) { var formatList = get(name), format, i, classes; function matchItems(node, format, item_name) { var key, value, items = format[item_name], i; // Check all items if (items) { // Non indexed object if (items.length === undefined) { for (key in items) { if (items.hasOwnProperty(key)) { if (item_name === 'attributes') value = dom.getAttrib(node, key); else value = getStyle(node, key); if (similar && !value && !format.exact) return; if ((!similar || format.exact) && !isEq(value, replaceVars(items[key], vars))) return; } } } else { // Only one match needed for indexed arrays for (i = 0; i < items.length; i++) { if (item_name === 'attributes' ? dom.getAttrib(node, items[i]) : getStyle(node, items[i])) return format; } } } return format; }; if (formatList && node) { // Check each format in list for (i = 0; i < formatList.length; i++) { format = formatList[i]; // Name name, attributes, styles and classes if (matchName(node, format) && matchItems(node, format, 'attributes') && matchItems(node, format, 'styles')) { // Match classes if (classes = format.classes) { for (i = 0; i < classes.length; i++) { if (!dom.hasClass(node, classes[i])) return; } } return format; } } } }; /** * Matches the current selection or specified node against the specified format name. * * @method match * @param {String} name Name of format to match. * @param {Object} vars Optional list of variables to replace before checking it. * @param {Node} node Optional node to check. * @return {boolean} true/false if the specified selection/node matches the format. */ function match(name, vars, node) { var startNode, i; function matchParents(node) { // Find first node with similar format settings node = dom.getParent(node, function(node) { return !!matchNode(node, name, vars, true); }); // Do an exact check on the similar format element return matchNode(node, name, vars); }; // Check specified node if (node) return matchParents(node); // Check pending formats if (selection.isCollapsed()) { for (i = pendingFormats.apply.length - 1; i >= 0; i--) { if (pendingFormats.apply[i].name == name) return true; } for (i = pendingFormats.remove.length - 1; i >= 0; i--) { if (pendingFormats.remove[i].name == name) return false; } return matchParents(selection.getNode()); } // Check selected node node = selection.getNode(); if (matchParents(node)) return TRUE; // Check start node if it's different startNode = selection.getStart(); if (startNode != node) { if (matchParents(startNode)) return TRUE; } return FALSE; }; /** * Matches the current selection against the array of formats and returns a new array with matching formats. * * @method matchAll * @param {Array} names Name of format to match. * @param {Object} vars Optional list of variables to replace before checking it. * @return {Array} Array with matched formats. */ function matchAll(names, vars) { var startElement, matchedFormatNames = [], checkedMap = {}, i, ni, name; // If the selection is collapsed then check pending formats if (selection.isCollapsed()) { for (ni = 0; ni < names.length; ni++) { // If the name is to be removed, then stop it from being added for (i = pendingFormats.remove.length - 1; i >= 0; i--) { name = names[ni]; if (pendingFormats.remove[i].name == name) { checkedMap[name] = true; break; } } } // If the format is to be applied for (i = pendingFormats.apply.length - 1; i >= 0; i--) { for (ni = 0; ni < names.length; ni++) { name = names[ni]; if (!checkedMap[name] && pendingFormats.apply[i].name == name) { checkedMap[name] = true; matchedFormatNames.push(name); } } } } // Check start of selection for formats startElement = selection.getStart(); dom.getParent(startElement, function(node) { var i, name; for (i = 0; i < names.length; i++) { name = names[i]; if (!checkedMap[name] && matchNode(node, name, vars)) { checkedMap[name] = true; matchedFormatNames.push(name); } } }); return matchedFormatNames; }; /** * Returns true/false if the specified format can be applied to the current selection or not. It will currently only check the state for selector formats, it returns true on all other format types. * * @method canApply * @param {String} name Name of format to check. * @return {boolean} true/false if the specified format can be applied to the current selection/node. */ function canApply(name) { var formatList = get(name), startNode, parents, i, x, selector; if (formatList) { startNode = selection.getStart(); parents = getParents(startNode); for (x = formatList.length - 1; x >= 0; x--) { selector = formatList[x].selector; // Format is not selector based, then always return TRUE if (!selector) return TRUE; for (i = parents.length - 1; i >= 0; i--) { if (dom.is(parents[i], selector)) return TRUE; } } } return FALSE; }; // Expose to public tinymce.extend(this, { get : get, register : register, apply : apply, remove : remove, toggle : toggle, match : match, matchAll : matchAll, matchNode : matchNode, canApply : canApply }); // Private functions /** * Checks if the specified nodes name matches the format inline/block or selector. * * @private * @param {Node} node Node to match against the specified format. * @param {Object} format Format object o match with. * @return {boolean} true/false if the format matches. */ function matchName(node, format) { // Check for inline match if (isEq(node, format.inline)) return TRUE; // Check for block match if (isEq(node, format.block)) return TRUE; // Check for selector match if (format.selector) return dom.is(node, format.selector); }; /** * Compares two string/nodes regardless of their case. * * @private * @param {String/Node} Node or string to compare. * @param {String/Node} Node or string to compare. * @return {boolean} True/false if they match. */ function isEq(str1, str2) { str1 = str1 || ''; str2 = str2 || ''; str1 = '' + (str1.nodeName || str1); str2 = '' + (str2.nodeName || str2); return str1.toLowerCase() == str2.toLowerCase(); }; /** * Returns the style by name on the specified node. This method modifies the style * contents to make it more easy to match. This will resolve a few browser issues. * * @private * @param {Node} node to get style from. * @param {String} name Style name to get. * @return {String} Style item value. */ function getStyle(node, name) { var styleVal = dom.getStyle(node, name); // Force the format to hex if (name == 'color' || name == 'backgroundColor') styleVal = dom.toHex(styleVal); // Opera will return bold as 700 if (name == 'fontWeight' && styleVal == 700) styleVal = 'bold'; return '' + styleVal; }; /** * Replaces variables in the value. The variable format is %var. * * @private * @param {String} value Value to replace variables in. * @param {Object} vars Name/value array with variables to replace. * @return {String} New value with replaced variables. */ function replaceVars(value, vars) { if (typeof(value) != "string") value = value(vars); else if (vars) { value = value.replace(/%(\w+)/g, function(str, name) { return vars[name] || str; }); } return value; }; function isWhiteSpaceNode(node) { return node && node.nodeType === 3 && /^([\s\r\n]+|)$/.test(node.nodeValue); }; function wrap(node, name, attrs) { var wrapper = dom.create(name, attrs); node.parentNode.insertBefore(wrapper, node); wrapper.appendChild(node); return wrapper; }; /** * Expands the specified range like object to depending on format. * * For example on block formats it will move the start/end position * to the beginning of the current block. * * @private * @param {Object} rng Range like object. * @param {Array} formats Array with formats to expand by. * @return {Object} Expanded range like object. */ function expandRng(rng, format, remove) { var startContainer = rng.startContainer, startOffset = rng.startOffset, endContainer = rng.endContainer, endOffset = rng.endOffset, sibling, lastIdx; // This function walks up the tree if there is no siblings before/after the node function findParentContainer(container, child_name, sibling_name, root) { var parent, child; root = root || dom.getRoot(); for (;;) { // Check if we can move up are we at root level or body level parent = container.parentNode; // Stop expanding on block elements or root depending on format if (parent == root || (!format[0].block_expand && isBlock(parent))) return container; for (sibling = parent[child_name]; sibling && sibling != container; sibling = sibling[sibling_name]) { if (sibling.nodeType == 1 && !isBookmarkNode(sibling)) return container; if (sibling.nodeType == 3 && !isWhiteSpaceNode(sibling)) return container; } container = container.parentNode; } return container; }; // If index based start position then resolve it if (startContainer.nodeType == 1 && startContainer.hasChildNodes()) { lastIdx = startContainer.childNodes.length - 1; startContainer = startContainer.childNodes[startOffset > lastIdx ? lastIdx : startOffset]; if (startContainer.nodeType == 3) startOffset = 0; } // If index based end position then resolve it if (endContainer.nodeType == 1 && endContainer.hasChildNodes()) { lastIdx = endContainer.childNodes.length - 1; endContainer = endContainer.childNodes[endOffset > lastIdx ? lastIdx : endOffset - 1]; if (endContainer.nodeType == 3) endOffset = endContainer.nodeValue.length; } // Exclude bookmark nodes if possible if (isBookmarkNode(startContainer.parentNode)) startContainer = startContainer.parentNode; if (isBookmarkNode(startContainer)) startContainer = startContainer.nextSibling || startContainer; if (isBookmarkNode(endContainer.parentNode)) endContainer = endContainer.parentNode; if (isBookmarkNode(endContainer)) endContainer = endContainer.previousSibling || endContainer; // Move start/end point up the tree if the leaves are sharp and if we are in different containers // Example * becomes !: !
*texttext*
! // This will reduce the number of wrapper elements that needs to be created // Move start point up the tree if (format[0].inline || format[0].block_expand) { startContainer = findParentContainer(startContainer, 'firstChild', 'nextSibling'); endContainer = findParentContainer(endContainer, 'lastChild', 'previousSibling'); } // Expand start/end container to matching selector if (format[0].selector && format[0].expand !== FALSE && !format[0].inline) { function findSelectorEndPoint(container, sibling_name) { var parents, i, y; if (container.nodeType == 3 && container.nodeValue.length == 0 && container[sibling_name]) container = container[sibling_name]; parents = getParents(container); for (i = 0; i < parents.length; i++) { for (y = 0; y < format.length; y++) { if (dom.is(parents[i], format[y].selector)) return parents[i]; } } return container; }; // Find new startContainer/endContainer if there is better one startContainer = findSelectorEndPoint(startContainer, 'previousSibling'); endContainer = findSelectorEndPoint(endContainer, 'nextSibling'); } // Expand start/end container to matching block element or text node if (format[0].block || format[0].selector) { function findBlockEndPoint(container, sibling_name, sibling_name2) { var node; // Expand to block of similar type if (!format[0].wrapper) node = dom.getParent(container, format[0].block); // Expand to first wrappable block element or any block element if (!node) node = dom.getParent(container.nodeType == 3 ? container.parentNode : container, isBlock); // Exclude inner lists from wrapping if (node && format[0].wrapper) node = getParents(node, 'ul,ol').reverse()[0] || node; // Didn't find a block element look for first/last wrappable element if (!node) { node = container; while (node[sibling_name] && !isBlock(node[sibling_name])) { node = node[sibling_name]; // Break on BR but include it will be removed later on // we can't remove it now since we need to check if it can be wrapped if (isEq(node, 'br')) break; } } return node || container; }; // Find new startContainer/endContainer if there is better one startContainer = findBlockEndPoint(startContainer, 'previousSibling'); endContainer = findBlockEndPoint(endContainer, 'nextSibling'); // Non block element then try to expand up the leaf if (format[0].block) { if (!isBlock(startContainer)) startContainer = findParentContainer(startContainer, 'firstChild', 'nextSibling'); if (!isBlock(endContainer)) endContainer = findParentContainer(endContainer, 'lastChild', 'previousSibling'); } } // Setup index for startContainer if (startContainer.nodeType == 1) { startOffset = nodeIndex(startContainer); startContainer = startContainer.parentNode; } // Setup index for endContainer if (endContainer.nodeType == 1) { endOffset = nodeIndex(endContainer) + 1; endContainer = endContainer.parentNode; } // Return new range like object return { startContainer : startContainer, startOffset : startOffset, endContainer : endContainer, endOffset : endOffset }; } /** * Removes the specified format for the specified node. It will also remove the node if it doesn't have * any attributes if the format specifies it to do so. * * @private * @param {Object} format Format object with items to remove from node. * @param {Object} vars Name/value object with variables to apply to format. * @param {Node} node Node to remove the format styles on. * @param {Node} compare_node Optional compare node, if specified the styles will be compared to that node. * @return {Boolean} True/false if the node was removed or not. */ function removeFormat(format, vars, node, compare_node) { var i, attrs, stylesModified; // Check if node matches format if (!matchName(node, format)) return FALSE; // Should we compare with format attribs and styles if (format.remove != 'all') { // Remove styles each(format.styles, function(value, name) { value = replaceVars(value, vars); // Indexed array if (typeof(name) === 'number') { name = value; compare_node = 0; } if (!compare_node || isEq(getStyle(compare_node, name), value)) dom.setStyle(node, name, ''); stylesModified = 1; }); // Remove style attribute if it's empty if (stylesModified && dom.getAttrib(node, 'style') == '') { node.removeAttribute('style'); node.removeAttribute('_mce_style'); } // Remove attributes each(format.attributes, function(value, name) { var valueOut; value = replaceVars(value, vars); // Indexed array if (typeof(name) === 'number') { name = value; compare_node = 0; } if (!compare_node || isEq(dom.getAttrib(compare_node, name), value)) { // Keep internal classes if (name == 'class') { value = dom.getAttrib(node, name); if (value) { // Build new class value where everything is removed except the internal prefixed classes valueOut = ''; each(value.split(/\s+/), function(cls) { if (/mce\w+/.test(cls)) valueOut += (valueOut ? ' ' : '') + cls; }); // We got some internal classes left if (valueOut) { dom.setAttrib(node, name, valueOut); return; } } } // IE6 has a bug where the attribute doesn't get removed correctly if (name == "class") node.removeAttribute('className'); // Remove mce prefixed attributes if (MCE_ATTR_RE.test(name)) node.removeAttribute('_mce_' + name); node.removeAttribute(name); } }); // Remove classes each(format.classes, function(value) { value = replaceVars(value, vars); if (!compare_node || dom.hasClass(compare_node, value)) dom.removeClass(node, value); }); // Check for non internal attributes attrs = dom.getAttribs(node); for (i = 0; i < attrs.length; i++) { if (attrs[i].nodeName.indexOf('_') !== 0) return FALSE; } } // Remove the inline child if it's empty for example or if (format.remove != 'none') { removeNode(node, format); return TRUE; } }; /** * Removes the node and wrap it's children in paragraphs before doing so or * appends BR elements to the beginning/end of the block element if forcedRootBlocks is disabled. * * If the div in the node below gets removed: * text