]> CyberLeo.Net >> Repos - Github/sugarcrm.git/blob - include/javascript/tiny_mce/classes/Formatter.js
Release 6.2.2
[Github/sugarcrm.git] / include / javascript / tiny_mce / classes / Formatter.js
1 /**
2  * Formatter.js
3  *
4  * Copyright 2009, Moxiecode Systems AB
5  * Released under LGPL License.
6  *
7  * License: http://tinymce.moxiecode.com/license
8  * Contributing: http://tinymce.moxiecode.com/contributing
9  */
10
11 (function(tinymce) {
12         /**
13          * Text formatter engine class. This class is used to apply formats like bold, italic, font size
14          * etc to the current selection or specific nodes. This engine was build to replace the browsers
15          * default formatting logic for execCommand due to it's inconsistant and buggy behavior.
16          *
17          * @class tinymce.Formatter
18          * @example
19          *  tinymce.activeEditor.formatter.register('mycustomformat', {
20          *    inline : 'span',
21          *    styles : {color : '#ff0000'}
22          *  });
23          *
24          *  tinymce.activeEditor.formatter.apply('mycustomformat');
25          */
26
27         /**
28          * Constructs a new formatter instance.
29          *
30          * @constructor Formatter
31          * @param {tinymce.Editor} ed Editor instance to construct the formatter engine to.
32          */
33         tinymce.Formatter = function(ed) {
34                 var formats = {},
35                         each = tinymce.each,
36                         dom = ed.dom,
37                         selection = ed.selection,
38                         TreeWalker = tinymce.dom.TreeWalker,
39                         rangeUtils = new tinymce.dom.RangeUtils(dom),
40                         isValid = ed.schema.isValidChild,
41                         isBlock = dom.isBlock,
42                         forcedRootBlock = ed.settings.forced_root_block,
43                         nodeIndex = dom.nodeIndex,
44                         INVISIBLE_CHAR = '\uFEFF',
45                         MCE_ATTR_RE = /^(src|href|style)$/,
46                         FALSE = false,
47                         TRUE = true,
48                         undefined,
49                         pendingFormats = {apply : [], remove : []};
50
51                 function isArray(obj) {
52                         return obj instanceof Array;
53                 };
54
55                 function getParents(node, selector) {
56                         return dom.getParents(node, selector, dom.getRoot());
57                 };
58
59                 function isCaretNode(node) {
60                         return node.nodeType === 1 && (node.face === 'mceinline' || node.style.fontFamily === 'mceinline');
61                 };
62
63                 // Public functions
64
65                 /**
66                  * Returns the format by name or all formats if no name is specified.
67                  *
68                  * @method get
69                  * @param {String} name Optional name to retrive by.
70                  * @return {Array/Object} Array/Object with all registred formats or a specific format.
71                  */
72                 function get(name) {
73                         return name ? formats[name] : formats;
74                 };
75
76                 /**
77                  * Registers a specific format by name.
78                  *
79                  * @method register
80                  * @param {Object/String} name Name of the format for example "bold".
81                  * @param {Object/Array} format Optional format object or array of format variants can only be omitted if the first arg is an object.
82                  */
83                 function register(name, format) {
84                         if (name) {
85                                 if (typeof(name) !== 'string') {
86                                         each(name, function(format, name) {
87                                                 register(name, format);
88                                         });
89                                 } else {
90                                         // Force format into array and add it to internal collection
91                                         format = format.length ? format : [format];
92
93                                         each(format, function(format) {
94                                                 // Set deep to false by default on selector formats this to avoid removing
95                                                 // alignment on images inside paragraphs when alignment is changed on paragraphs
96                                                 if (format.deep === undefined)
97                                                         format.deep = !format.selector;
98
99                                                 // Default to true
100                                                 if (format.split === undefined)
101                                                         format.split = !format.selector || format.inline;
102
103                                                 // Default to true
104                                                 if (format.remove === undefined && format.selector && !format.inline)
105                                                         format.remove = 'none';
106
107                                                 // Mark format as a mixed format inline + block level
108                                                 if (format.selector && format.inline) {
109                                                         format.mixed = true;
110                                                         format.block_expand = true;
111                                                 }
112
113                                                 // Split classes if needed
114                                                 if (typeof(format.classes) === 'string')
115                                                         format.classes = format.classes.split(/\s+/);
116                                         });
117
118                                         formats[name] = format;
119                                 }
120                         }
121                 };
122
123                 var getTextDecoration = function(node) {
124                         var decoration;
125
126                         ed.dom.getParent(node, function(n) {
127                                 decoration = ed.dom.getStyle(n, 'text-decoration');
128                                 return decoration && decoration !== 'none';
129                         });
130
131                         return decoration;
132                 };
133
134                 var processUnderlineAndColor = function(node) {
135                         var textDecoration;
136                         if (node.nodeType === 1 && node.parentNode && node.parentNode.nodeType === 1) {
137                                 textDecoration = getTextDecoration(node.parentNode);
138                                 if (ed.dom.getStyle(node, 'color') && textDecoration) {
139                                         ed.dom.setStyle(node, 'text-decoration', textDecoration);
140                                 } else if (ed.dom.getStyle(node, 'textdecoration') === textDecoration) {
141                                         ed.dom.setStyle(node, 'text-decoration', null);
142                                 }
143                         }
144                 };
145
146                 /**
147                  * Applies the specified format to the current selection or specified node.
148                  *
149                  * @method apply
150                  * @param {String} name Name of format to apply.
151                  * @param {Object} vars Optional list of variables to replace within format before applying it.
152                  * @param {Node} node Optional node to apply the format to defaults to current selection.
153                  */
154                 function apply(name, vars, node) {
155                         var formatList = get(name), format = formatList[0], bookmark, rng, i, isCollapsed = selection.isCollapsed();
156
157                         /**
158                          * Moves the start to the first suitable text node.
159                          */
160                         function moveStart(rng) {
161                                 var container = rng.startContainer,
162                                         offset = rng.startOffset,
163                                         walker, node;
164
165                                 // Move startContainer/startOffset in to a suitable node
166                                 if (container.nodeType == 1 || container.nodeValue === "") {
167                                         container = container.nodeType == 1 ? container.childNodes[offset] : container;
168
169                                         // Might fail if the offset is behind the last element in it's container
170                                         if (container) {
171                                                 walker = new TreeWalker(container, container.parentNode);
172                                                 for (node = walker.current(); node; node = walker.next()) {
173                                                         if (node.nodeType == 3 && !isWhiteSpaceNode(node)) {
174                                                                 rng.setStart(node, 0);
175                                                                 break;
176                                                         }
177                                                 }
178                                         }
179                                 }
180
181                                 return rng;
182                         };
183
184                         function setElementFormat(elm, fmt) {
185                                 fmt = fmt || format;
186
187                                 if (elm) {
188                                         each(fmt.styles, function(value, name) {
189                                                 dom.setStyle(elm, name, replaceVars(value, vars));
190                                         });
191
192                                         each(fmt.attributes, function(value, name) {
193                                                 dom.setAttrib(elm, name, replaceVars(value, vars));
194                                         });
195
196                                         each(fmt.classes, function(value) {
197                                                 value = replaceVars(value, vars);
198
199                                                 if (!dom.hasClass(elm, value))
200                                                         dom.addClass(elm, value);
201                                         });
202                                 }
203                         };
204
205                         function applyRngStyle(rng) {
206                                 var newWrappers = [], wrapName, wrapElm;
207
208                                 // Setup wrapper element
209                                 wrapName = format.inline || format.block;
210                                 wrapElm = dom.create(wrapName);
211                                 setElementFormat(wrapElm);
212
213                                 rangeUtils.walk(rng, function(nodes) {
214                                         var currentWrapElm;
215
216                                         /**
217                                          * Process a list of nodes wrap them.
218                                          */
219                                         function process(node) {
220                                                 var nodeName = node.nodeName.toLowerCase(), parentName = node.parentNode.nodeName.toLowerCase(), found;
221
222                                                 // Stop wrapping on br elements
223                                                 if (isEq(nodeName, 'br')) {
224                                                         currentWrapElm = 0;
225
226                                                         // Remove any br elements when we wrap things
227                                                         if (format.block)
228                                                                 dom.remove(node);
229
230                                                         return;
231                                                 }
232
233                                                 // If node is wrapper type
234                                                 if (format.wrapper && matchNode(node, name, vars)) {
235                                                         currentWrapElm = 0;
236                                                         return;
237                                                 }
238
239                                                 // Can we rename the block
240                                                 if (format.block && !format.wrapper && isTextBlock(nodeName)) {
241                                                         node = dom.rename(node, wrapName);
242                                                         setElementFormat(node);
243                                                         newWrappers.push(node);
244                                                         currentWrapElm = 0;
245                                                         return;
246                                                 }
247
248                                                 // Handle selector patterns
249                                                 if (format.selector) {
250                                                         // Look for matching formats
251                                                         each(formatList, function(format) {
252                                                                 // Check collapsed state if it exists
253                                                                 if ('collapsed' in format && format.collapsed !== isCollapsed) {
254                                                                         return;
255                                                                 }
256
257                                                                 if (dom.is(node, format.selector) && !isCaretNode(node)) {
258                                                                         setElementFormat(node, format);
259                                                                         found = true;
260                                                                 }
261                                                         });
262
263                                                         // Continue processing if a selector match wasn't found and a inline element is defined
264                                                         if (!format.inline || found) {
265                                                                 currentWrapElm = 0;
266                                                                 return;
267                                                         }
268                                                 }
269
270                                                 // Is it valid to wrap this item
271                                                 if (isValid(wrapName, nodeName) && isValid(parentName, wrapName) &&
272                                                                 !(node.nodeType === 3 && node.nodeValue.length === 1 && node.nodeValue.charCodeAt(0) === 65279)) {
273                                                         // Start wrapping
274                                                         if (!currentWrapElm) {
275                                                                 // Wrap the node
276                                                                 currentWrapElm = wrapElm.cloneNode(FALSE);
277                                                                 node.parentNode.insertBefore(currentWrapElm, node);
278                                                                 newWrappers.push(currentWrapElm);
279                                                         }
280
281                                                         currentWrapElm.appendChild(node);
282                                                 } else {
283                                                         // Start a new wrapper for possible children
284                                                         currentWrapElm = 0;
285
286                                                         each(tinymce.grep(node.childNodes), process);
287
288                                                         // End the last wrapper
289                                                         currentWrapElm = 0;
290                                                 }
291                                         };
292
293                                         // Process siblings from range
294                                         each(nodes, process);
295                                 });
296
297                                 // Wrap links inside as well, for example color inside a link when the wrapper is around the link
298                                 if (format.wrap_links === false) {
299                                         each(newWrappers, function(node) {
300                                                 function process(node) {
301                                                         var i, currentWrapElm, children;
302
303                                                         if (node.nodeName === 'A') {
304                                                                 currentWrapElm = wrapElm.cloneNode(FALSE);
305                                                                 newWrappers.push(currentWrapElm);
306
307                                                                 children = tinymce.grep(node.childNodes);
308                                                                 for (i = 0; i < children.length; i++)
309                                                                         currentWrapElm.appendChild(children[i]);
310
311                                                                 node.appendChild(currentWrapElm);
312                                                         }
313
314                                                         each(tinymce.grep(node.childNodes), process);
315                                                 };
316
317                                                 process(node);
318                                         });
319                                 }
320
321                                 // Cleanup
322                                 each(newWrappers, function(node) {
323                                         var childCount;
324
325                                         function getChildCount(node) {
326                                                 var count = 0;
327
328                                                 each(node.childNodes, function(node) {
329                                                         if (!isWhiteSpaceNode(node) && !isBookmarkNode(node))
330                                                                 count++;
331                                                 });
332
333                                                 return count;
334                                         };
335
336                                         function mergeStyles(node) {
337                                                 var child, clone;
338
339                                                 each(node.childNodes, function(node) {
340                                                         if (node.nodeType == 1 && !isBookmarkNode(node) && !isCaretNode(node)) {
341                                                                 child = node;
342                                                                 return FALSE; // break loop
343                                                         }
344                                                 });
345
346                                                 // If child was found and of the same type as the current node
347                                                 if (child && matchName(child, format)) {
348                                                         clone = child.cloneNode(FALSE);
349                                                         setElementFormat(clone);
350
351                                                         dom.replace(clone, node, TRUE);
352                                                         dom.remove(child, 1);
353                                                 }
354
355                                                 return clone || node;
356                                         };
357
358                                         childCount = getChildCount(node);
359
360                                         // Remove empty nodes but only if there is multiple wrappers and they are not block
361                                         // elements so never remove single <h1></h1> since that would remove the currrent empty block element where the caret is at
362                                         if ((newWrappers.length > 1 || !isBlock(node)) && childCount === 0) {
363                                                 dom.remove(node, 1);
364                                                 return;
365                                         }
366
367                                         if (format.inline || format.wrapper) {
368                                                 // Merges the current node with it's children of similar type to reduce the number of elements
369                                                 if (!format.exact && childCount === 1)
370                                                         node = mergeStyles(node);
371
372                                                 // Remove/merge children
373                                                 each(formatList, function(format) {
374                                                         // Merge all children of similar type will move styles from child to parent
375                                                         // this: <span style="color:red"><b><span style="color:red; font-size:10px">text</span></b></span>
376                                                         // will become: <span style="color:red"><b><span style="font-size:10px">text</span></b></span>
377                                                         each(dom.select(format.inline, node), function(child) {
378                                                                 var parent;
379
380                                                                 // When wrap_links is set to false we don't want
381                                                                 // to remove the format on children within links
382                                                                 if (format.wrap_links === false) {
383                                                                         parent = child.parentNode;
384
385                                                                         do {
386                                                                                 if (parent.nodeName === 'A')
387                                                                                         return;
388                                                                         } while (parent = parent.parentNode);
389                                                                 }
390
391                                                                 removeFormat(format, vars, child, format.exact ? child : null);
392                                                         });
393                                                 });
394
395                                                 // Remove child if direct parent is of same type
396                                                 if (matchNode(node.parentNode, name, vars)) {
397                                                         dom.remove(node, 1);
398                                                         node = 0;
399                                                         return TRUE;
400                                                 }
401
402                                                 // Look for parent with similar style format
403                                                 if (format.merge_with_parents) {
404                                                         dom.getParent(node.parentNode, function(parent) {
405                                                                 if (matchNode(parent, name, vars)) {
406                                                                         dom.remove(node, 1);
407                                                                         node = 0;
408                                                                         return TRUE;
409                                                                 }
410                                                         });
411                                                 }
412
413                                                 // Merge next and previous siblings if they are similar <b>text</b><b>text</b> becomes <b>texttext</b>
414                                                 if (node) {
415                                                         node = mergeSiblings(getNonWhiteSpaceSibling(node), node);
416                                                         node = mergeSiblings(node, getNonWhiteSpaceSibling(node, TRUE));
417                                                 }
418                                         }
419                                 });
420                         };
421
422                         if (format) {
423                                 if (node) {
424                                         rng = dom.createRng();
425
426                                         rng.setStartBefore(node);
427                                         rng.setEndAfter(node);
428
429                                         applyRngStyle(expandRng(rng, formatList));
430                                 } else {
431                                         if (!isCollapsed || !format.inline || dom.select('td.mceSelected,th.mceSelected').length) {
432                                                 // Obtain selection node before selection is unselected by applyRngStyle()
433                                                 var curSelNode = ed.selection.getNode();
434
435                                                 // Apply formatting to selection
436                                                 bookmark = selection.getBookmark();
437                                                 applyRngStyle(expandRng(selection.getRng(TRUE), formatList));
438
439                                                 // Colored nodes should be underlined so that the color of the underline matches the text color.
440                                                 if (format.styles && (format.styles.color || format.styles.textDecoration)) {
441                                                         tinymce.walk(curSelNode, processUnderlineAndColor, 'childNodes');
442                                                         processUnderlineAndColor(curSelNode);
443                                                 }
444
445                                                 selection.moveToBookmark(bookmark);
446                                                 selection.setRng(moveStart(selection.getRng(TRUE)));
447                                                 ed.nodeChanged();
448                                         } else
449                                                 performCaretAction('apply', name, vars);
450                                 }
451                         }
452                 };
453
454                 /**
455                  * Removes the specified format from the current selection or specified node.
456                  *
457                  * @method remove
458                  * @param {String} name Name of format to remove.
459                  * @param {Object} vars Optional list of variables to replace within format before removing it.
460                  * @param {Node} node Optional node to remove the format from defaults to current selection.
461                  */
462                 function remove(name, vars, node) {
463                         var formatList = get(name), format = formatList[0], bookmark, i, rng;
464
465                         /**
466                          * Moves the start to the first suitable text node.
467                          */
468                         function moveStart(rng) {
469                                 var container = rng.startContainer,
470                                         offset = rng.startOffset,
471                                         walker, node, nodes, tmpNode;
472
473                                 // Convert text node into index if possible
474                                 if (container.nodeType == 3 && offset >= container.nodeValue.length - 1) {
475                                         container = container.parentNode;
476                                         offset = nodeIndex(container) + 1;
477                                 }
478
479                                 // Move startContainer/startOffset in to a suitable node
480                                 if (container.nodeType == 1) {
481                                         nodes = container.childNodes;
482                                         container = nodes[Math.min(offset, nodes.length - 1)];
483                                         walker = new TreeWalker(container);
484
485                                         // If offset is at end of the parent node walk to the next one
486                                         if (offset > nodes.length - 1)
487                                                 walker.next();
488
489                                         for (node = walker.current(); node; node = walker.next()) {
490                                                 if (node.nodeType == 3 && !isWhiteSpaceNode(node)) {
491                                                         // IE has a "neat" feature where it moves the start node into the closest element
492                                                         // we can avoid this by inserting an element before it and then remove it after we set the selection
493                                                         tmpNode = dom.create('a', null, INVISIBLE_CHAR);
494                                                         node.parentNode.insertBefore(tmpNode, node);
495
496                                                         // Set selection and remove tmpNode
497                                                         rng.setStart(node, 0);
498                                                         selection.setRng(rng);
499                                                         dom.remove(tmpNode);
500
501                                                         return;
502                                                 }
503                                         }
504                                 }
505                         };
506
507                         // Merges the styles for each node
508                         function process(node) {
509                                 var children, i, l;
510
511                                 // Grab the children first since the nodelist might be changed
512                                 children = tinymce.grep(node.childNodes);
513
514                                 // Process current node
515                                 for (i = 0, l = formatList.length; i < l; i++) {
516                                         if (removeFormat(formatList[i], vars, node, node))
517                                                 break;
518                                 }
519
520                                 // Process the children
521                                 if (format.deep) {
522                                         for (i = 0, l = children.length; i < l; i++)
523                                                 process(children[i]);
524                                 }
525                         };
526
527                         function findFormatRoot(container) {
528                                 var formatRoot;
529
530                                 // Find format root
531                                 each(getParents(container.parentNode).reverse(), function(parent) {
532                                         var format;
533
534                                         // Find format root element
535                                         if (!formatRoot && parent.id != '_start' && parent.id != '_end') {
536                                                 // Is the node matching the format we are looking for
537                                                 format = matchNode(parent, name, vars);
538                                                 if (format && format.split !== false)
539                                                         formatRoot = parent;
540                                         }
541                                 });
542
543                                 return formatRoot;
544                         };
545
546                         function wrapAndSplit(format_root, container, target, split) {
547                                 var parent, clone, lastClone, firstClone, i, formatRootParent;
548
549                                 // Format root found then clone formats and split it
550                                 if (format_root) {
551                                         formatRootParent = format_root.parentNode;
552
553                                         for (parent = container.parentNode; parent && parent != formatRootParent; parent = parent.parentNode) {
554                                                 clone = parent.cloneNode(FALSE);
555
556                                                 for (i = 0; i < formatList.length; i++) {
557                                                         if (removeFormat(formatList[i], vars, clone, clone)) {
558                                                                 clone = 0;
559                                                                 break;
560                                                         }
561                                                 }
562
563                                                 // Build wrapper node
564                                                 if (clone) {
565                                                         if (lastClone)
566                                                                 clone.appendChild(lastClone);
567
568                                                         if (!firstClone)
569                                                                 firstClone = clone;
570
571                                                         lastClone = clone;
572                                                 }
573                                         }
574
575                                         // Never split block elements if the format is mixed
576                                         if (split && (!format.mixed || !isBlock(format_root)))
577                                                 container = dom.split(format_root, container);
578
579                                         // Wrap container in cloned formats
580                                         if (lastClone) {
581                                                 target.parentNode.insertBefore(lastClone, target);
582                                                 firstClone.appendChild(target);
583                                         }
584                                 }
585
586                                 return container;
587                         };
588
589                         function splitToFormatRoot(container) {
590                                 return wrapAndSplit(findFormatRoot(container), container, container, true);
591                         };
592
593                         function unwrap(start) {
594                                 var node = dom.get(start ? '_start' : '_end'),
595                                         out = node[start ? 'firstChild' : 'lastChild'];
596
597                                 // If the end is placed within the start the result will be removed
598                                 // So this checks if the out node is a bookmark node if it is it
599                                 // checks for another more suitable node
600                                 if (isBookmarkNode(out))
601                                         out = out[start ? 'firstChild' : 'lastChild'];
602
603                                 dom.remove(node, true);
604
605                                 return out;
606                         };
607
608                         function removeRngStyle(rng) {
609                                 var startContainer, endContainer;
610
611                                 rng = expandRng(rng, formatList, TRUE);
612
613                                 if (format.split) {
614                                         startContainer = getContainer(rng, TRUE);
615                                         endContainer = getContainer(rng);
616
617                                         if (startContainer != endContainer) {
618                                                 // Wrap start/end nodes in span element since these might be cloned/moved
619                                                 startContainer = wrap(startContainer, 'span', {id : '_start', 'data-mce-type' : 'bookmark'});
620                                                 endContainer = wrap(endContainer, 'span', {id : '_end', 'data-mce-type' : 'bookmark'});
621
622                                                 // Split start/end
623                                                 splitToFormatRoot(startContainer);
624                                                 splitToFormatRoot(endContainer);
625
626                                                 // Unwrap start/end to get real elements again
627                                                 startContainer = unwrap(TRUE);
628                                                 endContainer = unwrap();
629                                         } else
630                                                 startContainer = endContainer = splitToFormatRoot(startContainer);
631
632                                         // Update range positions since they might have changed after the split operations
633                                         rng.startContainer = startContainer.parentNode;
634                                         rng.startOffset = nodeIndex(startContainer);
635                                         rng.endContainer = endContainer.parentNode;
636                                         rng.endOffset = nodeIndex(endContainer) + 1;
637                                 }
638
639                                 // Remove items between start/end
640                                 rangeUtils.walk(rng, function(nodes) {
641                                         each(nodes, function(node) {
642                                                 process(node);
643
644                                                 // Remove parent span if it only contains text-decoration: underline, yet a parent node is also underlined.
645                                                 if (node.nodeType === 1 && ed.dom.getStyle(node, 'text-decoration') === 'underline' && node.parentNode && getTextDecoration(node.parentNode) === 'underline') {
646                                                         removeFormat({'deep': false, 'exact': true, 'inline': 'span', 'styles': {'textDecoration' : 'underline'}}, null, node);
647                                                 }
648                                         });
649                                 });
650                         };
651
652                         // Handle node
653                         if (node) {
654                                 rng = dom.createRng();
655                                 rng.setStartBefore(node);
656                                 rng.setEndAfter(node);
657                                 removeRngStyle(rng);
658                                 return;
659                         }
660
661                         if (!selection.isCollapsed() || !format.inline || dom.select('td.mceSelected,th.mceSelected').length) {
662                                 bookmark = selection.getBookmark();
663                                 removeRngStyle(selection.getRng(TRUE));
664                                 selection.moveToBookmark(bookmark);
665
666                                 // Check if start element still has formatting then we are at: "<b>text|</b>text" and need to move the start into the next text node
667                                 if (match(name, vars, selection.getStart())) {
668                                         moveStart(selection.getRng(true));
669                                 }
670
671                                 ed.nodeChanged();
672                         } else
673                                 performCaretAction('remove', name, vars);
674                 };
675
676                 /**
677                  * Toggles the specified format on/off.
678                  *
679                  * @method toggle
680                  * @param {String} name Name of format to apply/remove.
681                  * @param {Object} vars Optional list of variables to replace within format before applying/removing it.
682                  * @param {Node} node Optional node to apply the format to or remove from. Defaults to current selection.
683                  */
684                 function toggle(name, vars, node) {
685                         var fmt = get(name);
686
687                         if (match(name, vars, node) && (!('toggle' in fmt[0]) || fmt[0]['toggle']))
688                                 remove(name, vars, node);
689                         else
690                                 apply(name, vars, node);
691                 };
692
693                 /**
694                  * Return true/false if the specified node has the specified format.
695                  *
696                  * @method matchNode
697                  * @param {Node} node Node to check the format on.
698                  * @param {String} name Format name to check.
699                  * @param {Object} vars Optional list of variables to replace before checking it.
700                  * @param {Boolean} similar Match format that has similar properties.
701                  * @return {Object} Returns the format object it matches or undefined if it doesn't match.
702                  */
703                 function matchNode(node, name, vars, similar) {
704                         var formatList = get(name), format, i, classes;
705
706                         function matchItems(node, format, item_name) {
707                                 var key, value, items = format[item_name], i;
708
709                                 // Check all items
710                                 if (items) {
711                                         // Non indexed object
712                                         if (items.length === undefined) {
713                                                 for (key in items) {
714                                                         if (items.hasOwnProperty(key)) {
715                                                                 if (item_name === 'attributes')
716                                                                         value = dom.getAttrib(node, key);
717                                                                 else
718                                                                         value = getStyle(node, key);
719
720                                                                 if (similar && !value && !format.exact)
721                                                                         return;
722
723                                                                 if ((!similar || format.exact) && !isEq(value, replaceVars(items[key], vars)))
724                                                                         return;
725                                                         }
726                                                 }
727                                         } else {
728                                                 // Only one match needed for indexed arrays
729                                                 for (i = 0; i < items.length; i++) {
730                                                         if (item_name === 'attributes' ? dom.getAttrib(node, items[i]) : getStyle(node, items[i]))
731                                                                 return format;
732                                                 }
733                                         }
734                                 }
735
736                                 return format;
737                         };
738
739                         if (formatList && node) {
740                                 // Check each format in list
741                                 for (i = 0; i < formatList.length; i++) {
742                                         format = formatList[i];
743
744                                         // Name name, attributes, styles and classes
745                                         if (matchName(node, format) && matchItems(node, format, 'attributes') && matchItems(node, format, 'styles')) {
746                                                 // Match classes
747                                                 if (classes = format.classes) {
748                                                         for (i = 0; i < classes.length; i++) {
749                                                                 if (!dom.hasClass(node, classes[i]))
750                                                                         return;
751                                                         }
752                                                 }
753
754                                                 return format;
755                                         }
756                                 }
757                         }
758                 };
759
760                 /**
761                  * Matches the current selection or specified node against the specified format name.
762                  *
763                  * @method match
764                  * @param {String} name Name of format to match.
765                  * @param {Object} vars Optional list of variables to replace before checking it.
766                  * @param {Node} node Optional node to check.
767                  * @return {boolean} true/false if the specified selection/node matches the format.
768                  */
769                 function match(name, vars, node) {
770                         var startNode, i;
771
772                         function matchParents(node) {
773                                 // Find first node with similar format settings
774                                 node = dom.getParent(node, function(node) {
775                                         return !!matchNode(node, name, vars, true);
776                                 });
777
778                                 // Do an exact check on the similar format element
779                                 return matchNode(node, name, vars);
780                         };
781
782                         // Check specified node
783                         if (node)
784                                 return matchParents(node);
785
786                         // Check pending formats
787                         if (selection.isCollapsed()) {
788                                 for (i = pendingFormats.apply.length - 1; i >= 0; i--) {
789                                         if (pendingFormats.apply[i].name == name)
790                                                 return true;
791                                 }
792
793                                 for (i = pendingFormats.remove.length - 1; i >= 0; i--) {
794                                         if (pendingFormats.remove[i].name == name)
795                                                 return false;
796                                 }
797
798                                 return matchParents(selection.getNode());
799                         }
800
801                         // Check selected node
802                         node = selection.getNode();
803                         if (matchParents(node))
804                                 return TRUE;
805
806                         // Check start node if it's different
807                         startNode = selection.getStart();
808                         if (startNode != node) {
809                                 if (matchParents(startNode))
810                                         return TRUE;
811                         }
812
813                         return FALSE;
814                 };
815
816                 /**
817                  * Matches the current selection against the array of formats and returns a new array with matching formats.
818                  *
819                  * @method matchAll
820                  * @param {Array} names Name of format to match.
821                  * @param {Object} vars Optional list of variables to replace before checking it.
822                  * @return {Array} Array with matched formats.
823                  */
824                 function matchAll(names, vars) {
825                         var startElement, matchedFormatNames = [], checkedMap = {}, i, ni, name;
826
827                         // If the selection is collapsed then check pending formats
828                         if (selection.isCollapsed()) {
829                                 for (ni = 0; ni < names.length; ni++) {
830                                         // If the name is to be removed, then stop it from being added
831                                         for (i = pendingFormats.remove.length - 1; i >= 0; i--) {
832                                                 name = names[ni];
833
834                                                 if (pendingFormats.remove[i].name == name) {
835                                                         checkedMap[name] = true;
836                                                         break;
837                                                 }
838                                         }
839                                 }
840
841                                 // If the format is to be applied
842                                 for (i = pendingFormats.apply.length - 1; i >= 0; i--) {
843                                         for (ni = 0; ni < names.length; ni++) {
844                                                 name = names[ni];
845
846                                                 if (!checkedMap[name] && pendingFormats.apply[i].name == name) {
847                                                         checkedMap[name] = true;
848                                                         matchedFormatNames.push(name);
849                                                 }
850                                         }
851                                 }
852                         }
853
854                         // Check start of selection for formats
855                         startElement = selection.getStart();
856                         dom.getParent(startElement, function(node) {
857                                 var i, name;
858
859                                 for (i = 0; i < names.length; i++) {
860                                         name = names[i];
861
862                                         if (!checkedMap[name] && matchNode(node, name, vars)) {
863                                                 checkedMap[name] = true;
864                                                 matchedFormatNames.push(name);
865                                         }
866                                 }
867                         });
868
869                         return matchedFormatNames;
870                 };
871
872                 /**
873                  * 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.
874                  *
875                  * @method canApply
876                  * @param {String} name Name of format to check.
877                  * @return {boolean} true/false if the specified format can be applied to the current selection/node.
878                  */
879                 function canApply(name) {
880                         var formatList = get(name), startNode, parents, i, x, selector;
881
882                         if (formatList) {
883                                 startNode = selection.getStart();
884                                 parents = getParents(startNode);
885
886                                 for (x = formatList.length - 1; x >= 0; x--) {
887                                         selector = formatList[x].selector;
888
889                                         // Format is not selector based, then always return TRUE
890                                         if (!selector)
891                                                 return TRUE;
892
893                                         for (i = parents.length - 1; i >= 0; i--) {
894                                                 if (dom.is(parents[i], selector))
895                                                         return TRUE;
896                                         }
897                                 }
898                         }
899
900                         return FALSE;
901                 };
902
903                 // Expose to public
904                 tinymce.extend(this, {
905                         get : get,
906                         register : register,
907                         apply : apply,
908                         remove : remove,
909                         toggle : toggle,
910                         match : match,
911                         matchAll : matchAll,
912                         matchNode : matchNode,
913                         canApply : canApply
914                 });
915
916                 // Private functions
917
918                 /**
919                  * Checks if the specified nodes name matches the format inline/block or selector.
920                  *
921                  * @private
922                  * @param {Node} node Node to match against the specified format.
923                  * @param {Object} format Format object o match with.
924                  * @return {boolean} true/false if the format matches.
925                  */
926                 function matchName(node, format) {
927                         // Check for inline match
928                         if (isEq(node, format.inline))
929                                 return TRUE;
930
931                         // Check for block match
932                         if (isEq(node, format.block))
933                                 return TRUE;
934
935                         // Check for selector match
936                         if (format.selector)
937                                 return dom.is(node, format.selector);
938                 };
939
940                 /**
941                  * Compares two string/nodes regardless of their case.
942                  *
943                  * @private
944                  * @param {String/Node} Node or string to compare.
945                  * @param {String/Node} Node or string to compare.
946                  * @return {boolean} True/false if they match.
947                  */
948                 function isEq(str1, str2) {
949                         str1 = str1 || '';
950                         str2 = str2 || '';
951
952                         str1 = '' + (str1.nodeName || str1);
953                         str2 = '' + (str2.nodeName || str2);
954
955                         return str1.toLowerCase() == str2.toLowerCase();
956                 };
957
958                 /**
959                  * Returns the style by name on the specified node. This method modifies the style
960                  * contents to make it more easy to match. This will resolve a few browser issues.
961                  *
962                  * @private
963                  * @param {Node} node to get style from.
964                  * @param {String} name Style name to get.
965                  * @return {String} Style item value.
966                  */
967                 function getStyle(node, name) {
968                         var styleVal = dom.getStyle(node, name);
969
970                         // Force the format to hex
971                         if (name == 'color' || name == 'backgroundColor')
972                                 styleVal = dom.toHex(styleVal);
973
974                         // Opera will return bold as 700
975                         if (name == 'fontWeight' && styleVal == 700)
976                                 styleVal = 'bold';
977
978                         return '' + styleVal;
979                 };
980
981                 /**
982                  * Replaces variables in the value. The variable format is %var.
983                  *
984                  * @private
985                  * @param {String} value Value to replace variables in.
986                  * @param {Object} vars Name/value array with variables to replace.
987                  * @return {String} New value with replaced variables.
988                  */
989                 function replaceVars(value, vars) {
990                         if (typeof(value) != "string")
991                                 value = value(vars);
992                         else if (vars) {
993                                 value = value.replace(/%(\w+)/g, function(str, name) {
994                                         return vars[name] || str;
995                                 });
996                         }
997
998                         return value;
999                 };
1000
1001                 function isWhiteSpaceNode(node) {
1002                         return node && node.nodeType === 3 && /^([\s\r\n]+|)$/.test(node.nodeValue);
1003                 };
1004
1005                 function wrap(node, name, attrs) {
1006                         var wrapper = dom.create(name, attrs);
1007
1008                         node.parentNode.insertBefore(wrapper, node);
1009                         wrapper.appendChild(node);
1010
1011                         return wrapper;
1012                 };
1013
1014                 /**
1015                  * Expands the specified range like object to depending on format.
1016                  *
1017                  * For example on block formats it will move the start/end position
1018                  * to the beginning of the current block.
1019                  *
1020                  * @private
1021                  * @param {Object} rng Range like object.
1022                  * @param {Array} formats Array with formats to expand by.
1023                  * @return {Object} Expanded range like object.
1024                  */
1025                 function expandRng(rng, format, remove) {
1026                         var startContainer = rng.startContainer,
1027                                 startOffset = rng.startOffset,
1028                                 endContainer = rng.endContainer,
1029                                 endOffset = rng.endOffset, sibling, lastIdx, leaf;
1030
1031                         // This function walks up the tree if there is no siblings before/after the node
1032                         function findParentContainer(container, child_name, sibling_name, root) {
1033                                 var parent, child;
1034
1035                                 root = root || dom.getRoot();
1036
1037                                 for (;;) {
1038                                         // Check if we can move up are we at root level or body level
1039                                         parent = container.parentNode;
1040
1041                                         // Stop expanding on block elements or root depending on format
1042                                         if (parent == root || (!format[0].block_expand && isBlock(parent)))
1043                                                 return container;
1044
1045                                         for (sibling = parent[child_name]; sibling && sibling != container; sibling = sibling[sibling_name]) {
1046                                                 if (sibling.nodeType == 1 && !isBookmarkNode(sibling))
1047                                                         return container;
1048
1049                                                 if (sibling.nodeType == 3 && !isWhiteSpaceNode(sibling))
1050                                                         return container;
1051                                         }
1052
1053                                         container = container.parentNode;
1054                                 }
1055
1056                                 return container;
1057                         };
1058
1059                         // This function walks down the tree to find the leaf at the selection.
1060                         // The offset is also returned as if node initially a leaf, the offset may be in the middle of the text node.
1061                         function findLeaf(node, offset) {
1062                                 if (offset === undefined)
1063                                         offset = node.nodeType === 3 ? node.length : node.childNodes.length;
1064                                 while (node && node.hasChildNodes()) {
1065                                         node = node.childNodes[offset];
1066                                         if (node)
1067                                                 offset = node.nodeType === 3 ? node.length : node.childNodes.length;
1068                                 }
1069                                 return { node: node, offset: offset };
1070                         }
1071
1072                         // If index based start position then resolve it
1073                         if (startContainer.nodeType == 1 && startContainer.hasChildNodes()) {
1074                                 lastIdx = startContainer.childNodes.length - 1;
1075                                 startContainer = startContainer.childNodes[startOffset > lastIdx ? lastIdx : startOffset];
1076
1077                                 if (startContainer.nodeType == 3)
1078                                         startOffset = 0;
1079                         }
1080
1081                         // If index based end position then resolve it
1082                         if (endContainer.nodeType == 1 && endContainer.hasChildNodes()) {
1083                                 lastIdx = endContainer.childNodes.length - 1;
1084                                 endContainer = endContainer.childNodes[endOffset > lastIdx ? lastIdx : endOffset - 1];
1085
1086                                 if (endContainer.nodeType == 3)
1087                                         endOffset = endContainer.nodeValue.length;
1088                         }
1089
1090                         // Exclude bookmark nodes if possible
1091                         if (isBookmarkNode(startContainer.parentNode))
1092                                 startContainer = startContainer.parentNode;
1093
1094                         if (isBookmarkNode(startContainer))
1095                                 startContainer = startContainer.nextSibling || startContainer;
1096
1097                         if (isBookmarkNode(endContainer.parentNode)) {
1098                                 endOffset = dom.nodeIndex(endContainer);
1099                                 endContainer = endContainer.parentNode;
1100                         }
1101
1102                         if (isBookmarkNode(endContainer) && endContainer.previousSibling) {
1103                                 endContainer = endContainer.previousSibling;
1104                                 endOffset = endContainer.length;
1105                         }
1106
1107                         if (format[0].inline) {
1108                                 // Avoid applying formatting to a trailing space.
1109                                 leaf = findLeaf(endContainer, endOffset);
1110                                 if (leaf.node) {
1111                                         while (leaf.node && leaf.offset === 0 && leaf.node.previousSibling)
1112                                                 leaf = findLeaf(leaf.node.previousSibling);
1113
1114                                         if (leaf.node && leaf.offset > 0 && leaf.node.nodeType === 3 &&
1115                                                         leaf.node.nodeValue.charAt(leaf.offset - 1) === ' ') {
1116
1117                                                 if (leaf.offset > 1) {
1118                                                         endContainer = leaf.node;
1119                                                         endContainer.splitText(leaf.offset - 1);
1120                                                 } else if (leaf.node.previousSibling) {
1121                                                         endContainer = leaf.node.previousSibling;
1122                                                 }
1123                                         }
1124                                 }
1125                         }
1126                         
1127                         // Move start/end point up the tree if the leaves are sharp and if we are in different containers
1128                         // Example * becomes !: !<p><b><i>*text</i><i>text*</i></b></p>!
1129                         // This will reduce the number of wrapper elements that needs to be created
1130                         // Move start point up the tree
1131                         if (format[0].inline || format[0].block_expand) {
1132                                 startContainer = findParentContainer(startContainer, 'firstChild', 'nextSibling');
1133                                 endContainer = findParentContainer(endContainer, 'lastChild', 'previousSibling');
1134                         }
1135
1136                         // Expand start/end container to matching selector
1137                         if (format[0].selector && format[0].expand !== FALSE && !format[0].inline) {
1138                                 function findSelectorEndPoint(container, sibling_name) {
1139                                         var parents, i, y, curFormat;
1140
1141                                         if (container.nodeType == 3 && container.nodeValue.length == 0 && container[sibling_name])
1142                                                 container = container[sibling_name];
1143
1144                                         parents = getParents(container);
1145                                         for (i = 0; i < parents.length; i++) {
1146                                                 for (y = 0; y < format.length; y++) {
1147                                                         curFormat = format[y];
1148
1149                                                         // If collapsed state is set then skip formats that doesn't match that
1150                                                         if ("collapsed" in curFormat && curFormat.collapsed !== rng.collapsed)
1151                                                                 continue;
1152
1153                                                         if (dom.is(parents[i], curFormat.selector))
1154                                                                 return parents[i];
1155                                                 }
1156                                         }
1157
1158                                         return container;
1159                                 };
1160
1161                                 // Find new startContainer/endContainer if there is better one
1162                                 startContainer = findSelectorEndPoint(startContainer, 'previousSibling');
1163                                 endContainer = findSelectorEndPoint(endContainer, 'nextSibling');
1164                         }
1165
1166                         // Expand start/end container to matching block element or text node
1167                         if (format[0].block || format[0].selector) {
1168                                 function findBlockEndPoint(container, sibling_name, sibling_name2) {
1169                                         var node;
1170
1171                                         // Expand to block of similar type
1172                                         if (!format[0].wrapper)
1173                                                 node = dom.getParent(container, format[0].block);
1174
1175                                         // Expand to first wrappable block element or any block element
1176                                         if (!node)
1177                                                 node = dom.getParent(container.nodeType == 3 ? container.parentNode : container, isBlock);
1178
1179                                         // Exclude inner lists from wrapping
1180                                         if (node && format[0].wrapper)
1181                                                 node = getParents(node, 'ul,ol').reverse()[0] || node;
1182
1183                                         // Didn't find a block element look for first/last wrappable element
1184                                         if (!node) {
1185                                                 node = container;
1186
1187                                                 while (node[sibling_name] && !isBlock(node[sibling_name])) {
1188                                                         node = node[sibling_name];
1189
1190                                                         // Break on BR but include it will be removed later on
1191                                                         // we can't remove it now since we need to check if it can be wrapped
1192                                                         if (isEq(node, 'br'))
1193                                                                 break;
1194                                                 }
1195                                         }
1196
1197                                         return node || container;
1198                                 };
1199
1200                                 // Find new startContainer/endContainer if there is better one
1201                                 startContainer = findBlockEndPoint(startContainer, 'previousSibling');
1202                                 endContainer = findBlockEndPoint(endContainer, 'nextSibling');
1203
1204                                 // Non block element then try to expand up the leaf
1205                                 if (format[0].block) {
1206                                         if (!isBlock(startContainer))
1207                                                 startContainer = findParentContainer(startContainer, 'firstChild', 'nextSibling');
1208
1209                                         if (!isBlock(endContainer))
1210                                                 endContainer = findParentContainer(endContainer, 'lastChild', 'previousSibling');
1211                                 }
1212                         }
1213
1214                         // Setup index for startContainer
1215                         if (startContainer.nodeType == 1) {
1216                                 startOffset = nodeIndex(startContainer);
1217                                 startContainer = startContainer.parentNode;
1218                         }
1219
1220                         // Setup index for endContainer
1221                         if (endContainer.nodeType == 1) {
1222                                 endOffset = nodeIndex(endContainer) + 1;
1223                                 endContainer = endContainer.parentNode;
1224                         }
1225
1226                         // Return new range like object
1227                         return {
1228                                 startContainer : startContainer,
1229                                 startOffset : startOffset,
1230                                 endContainer : endContainer,
1231                                 endOffset : endOffset
1232                         };
1233                 }
1234
1235                 /**
1236                  * Removes the specified format for the specified node. It will also remove the node if it doesn't have
1237                  * any attributes if the format specifies it to do so.
1238                  *
1239                  * @private
1240                  * @param {Object} format Format object with items to remove from node.
1241                  * @param {Object} vars Name/value object with variables to apply to format.
1242                  * @param {Node} node Node to remove the format styles on.
1243                  * @param {Node} compare_node Optional compare node, if specified the styles will be compared to that node.
1244                  * @return {Boolean} True/false if the node was removed or not.
1245                  */
1246                 function removeFormat(format, vars, node, compare_node) {
1247                         var i, attrs, stylesModified;
1248
1249                         // Check if node matches format
1250                         if (!matchName(node, format))
1251                                 return FALSE;
1252
1253                         // Should we compare with format attribs and styles
1254                         if (format.remove != 'all') {
1255                                 // Remove styles
1256                                 each(format.styles, function(value, name) {
1257                                         value = replaceVars(value, vars);
1258
1259                                         // Indexed array
1260                                         if (typeof(name) === 'number') {
1261                                                 name = value;
1262                                                 compare_node = 0;
1263                                         }
1264
1265                                         if (!compare_node || isEq(getStyle(compare_node, name), value))
1266                                                 dom.setStyle(node, name, '');
1267
1268                                         stylesModified = 1;
1269                                 });
1270
1271                                 // Remove style attribute if it's empty
1272                                 if (stylesModified && dom.getAttrib(node, 'style') == '') {
1273                                         node.removeAttribute('style');
1274                                         node.removeAttribute('data-mce-style');
1275                                 }
1276
1277                                 // Remove attributes
1278                                 each(format.attributes, function(value, name) {
1279                                         var valueOut;
1280
1281                                         value = replaceVars(value, vars);
1282
1283                                         // Indexed array
1284                                         if (typeof(name) === 'number') {
1285                                                 name = value;
1286                                                 compare_node = 0;
1287                                         }
1288
1289                                         if (!compare_node || isEq(dom.getAttrib(compare_node, name), value)) {
1290                                                 // Keep internal classes
1291                                                 if (name == 'class') {
1292                                                         value = dom.getAttrib(node, name);
1293                                                         if (value) {
1294                                                                 // Build new class value where everything is removed except the internal prefixed classes
1295                                                                 valueOut = '';
1296                                                                 each(value.split(/\s+/), function(cls) {
1297                                                                         if (/mce\w+/.test(cls))
1298                                                                                 valueOut += (valueOut ? ' ' : '') + cls;
1299                                                                 });
1300
1301                                                                 // We got some internal classes left
1302                                                                 if (valueOut) {
1303                                                                         dom.setAttrib(node, name, valueOut);
1304                                                                         return;
1305                                                                 }
1306                                                         }
1307                                                 }
1308
1309                                                 // IE6 has a bug where the attribute doesn't get removed correctly
1310                                                 if (name == "class")
1311                                                         node.removeAttribute('className');
1312
1313                                                 // Remove mce prefixed attributes
1314                                                 if (MCE_ATTR_RE.test(name))
1315                                                         node.removeAttribute('data-mce-' + name);
1316
1317                                                 node.removeAttribute(name);
1318                                         }
1319                                 });
1320
1321                                 // Remove classes
1322                                 each(format.classes, function(value) {
1323                                         value = replaceVars(value, vars);
1324
1325                                         if (!compare_node || dom.hasClass(compare_node, value))
1326                                                 dom.removeClass(node, value);
1327                                 });
1328
1329                                 // Check for non internal attributes
1330                                 attrs = dom.getAttribs(node);
1331                                 for (i = 0; i < attrs.length; i++) {
1332                                         if (attrs[i].nodeName.indexOf('_') !== 0)
1333                                                 return FALSE;
1334                                 }
1335                         }
1336
1337                         // Remove the inline child if it's empty for example <b> or <span>
1338                         if (format.remove != 'none') {
1339                                 removeNode(node, format);
1340                                 return TRUE;
1341                         }
1342                 };
1343
1344                 /**
1345                  * Removes the node and wrap it's children in paragraphs before doing so or
1346                  * appends BR elements to the beginning/end of the block element if forcedRootBlocks is disabled.
1347                  *
1348                  * If the div in the node below gets removed:
1349                  *  text<div>text</div>text
1350                  *
1351                  * Output becomes:
1352                  *  text<div><br />text<br /></div>text
1353                  *
1354                  * So when the div is removed the result is:
1355                  *  text<br />text<br />text
1356                  *
1357                  * @private
1358                  * @param {Node} node Node to remove + apply BR/P elements to.
1359                  * @param {Object} format Format rule.
1360                  * @return {Node} Input node.
1361                  */
1362                 function removeNode(node, format) {
1363                         var parentNode = node.parentNode, rootBlockElm;
1364
1365                         if (format.block) {
1366                                 if (!forcedRootBlock) {
1367                                         function find(node, next, inc) {
1368                                                 node = getNonWhiteSpaceSibling(node, next, inc);
1369
1370                                                 return !node || (node.nodeName == 'BR' || isBlock(node));
1371                                         };
1372
1373                                         // Append BR elements if needed before we remove the block
1374                                         if (isBlock(node) && !isBlock(parentNode)) {
1375                                                 if (!find(node, FALSE) && !find(node.firstChild, TRUE, 1))
1376                                                         node.insertBefore(dom.create('br'), node.firstChild);
1377
1378                                                 if (!find(node, TRUE) && !find(node.lastChild, FALSE, 1))
1379                                                         node.appendChild(dom.create('br'));
1380                                         }
1381                                 } else {
1382                                         // Wrap the block in a forcedRootBlock if we are at the root of document
1383                                         if (parentNode == dom.getRoot()) {
1384                                                 if (!format.list_block || !isEq(node, format.list_block)) {
1385                                                         each(tinymce.grep(node.childNodes), function(node) {
1386                                                                 if (isValid(forcedRootBlock, node.nodeName.toLowerCase())) {
1387                                                                         if (!rootBlockElm)
1388                                                                                 rootBlockElm = wrap(node, forcedRootBlock);
1389                                                                         else
1390                                                                                 rootBlockElm.appendChild(node);
1391                                                                 } else
1392                                                                         rootBlockElm = 0;
1393                                                         });
1394                                                 }
1395                                         }
1396                                 }
1397                         }
1398
1399                         // Never remove nodes that isn't the specified inline element if a selector is specified too
1400                         if (format.selector && format.inline && !isEq(format.inline, node))
1401                                 return;
1402
1403                         dom.remove(node, 1);
1404                 };
1405
1406                 /**
1407                  * Returns the next/previous non whitespace node.
1408                  *
1409                  * @private
1410                  * @param {Node} node Node to start at.
1411                  * @param {boolean} next (Optional) Include next or previous node defaults to previous.
1412                  * @param {boolean} inc (Optional) Include the current node in checking. Defaults to false.
1413                  * @return {Node} Next or previous node or undefined if it wasn't found.
1414                  */
1415                 function getNonWhiteSpaceSibling(node, next, inc) {
1416                         if (node) {
1417                                 next = next ? 'nextSibling' : 'previousSibling';
1418
1419                                 for (node = inc ? node : node[next]; node; node = node[next]) {
1420                                         if (node.nodeType == 1 || !isWhiteSpaceNode(node))
1421                                                 return node;
1422                                 }
1423                         }
1424                 };
1425
1426                 /**
1427                  * Checks if the specified node is a bookmark node or not.
1428                  *
1429                  * @param {Node} node Node to check if it's a bookmark node or not.
1430                  * @return {Boolean} true/false if the node is a bookmark node.
1431                  */
1432                 function isBookmarkNode(node) {
1433                         return node && node.nodeType == 1 && node.getAttribute('data-mce-type') == 'bookmark';
1434                 };
1435
1436                 /**
1437                  * Merges the next/previous sibling element if they match.
1438                  *
1439                  * @private
1440                  * @param {Node} prev Previous node to compare/merge.
1441                  * @param {Node} next Next node to compare/merge.
1442                  * @return {Node} Next node if we didn't merge and prev node if we did.
1443                  */
1444                 function mergeSiblings(prev, next) {
1445                         var marker, sibling, tmpSibling;
1446
1447                         /**
1448                          * Compares two nodes and checks if it's attributes and styles matches.
1449                          * This doesn't compare classes as items since their order is significant.
1450                          *
1451                          * @private
1452                          * @param {Node} node1 First node to compare with.
1453                          * @param {Node} node2 Second node to compare with.
1454                          * @return {boolean} True/false if the nodes are the same or not.
1455                          */
1456                         function compareElements(node1, node2) {
1457                                 // Not the same name
1458                                 if (node1.nodeName != node2.nodeName)
1459                                         return FALSE;
1460
1461                                 /**
1462                                  * Returns all the nodes attributes excluding internal ones, styles and classes.
1463                                  *
1464                                  * @private
1465                                  * @param {Node} node Node to get attributes from.
1466                                  * @return {Object} Name/value object with attributes and attribute values.
1467                                  */
1468                                 function getAttribs(node) {
1469                                         var attribs = {};
1470
1471                                         each(dom.getAttribs(node), function(attr) {
1472                                                 var name = attr.nodeName.toLowerCase();
1473
1474                                                 // Don't compare internal attributes or style
1475                                                 if (name.indexOf('_') !== 0 && name !== 'style')
1476                                                         attribs[name] = dom.getAttrib(node, name);
1477                                         });
1478
1479                                         return attribs;
1480                                 };
1481
1482                                 /**
1483                                  * Compares two objects checks if it's key + value exists in the other one.
1484                                  *
1485                                  * @private
1486                                  * @param {Object} obj1 First object to compare.
1487                                  * @param {Object} obj2 Second object to compare.
1488                                  * @return {boolean} True/false if the objects matches or not.
1489                                  */
1490                                 function compareObjects(obj1, obj2) {
1491                                         var value, name;
1492
1493                                         for (name in obj1) {
1494                                                 // Obj1 has item obj2 doesn't have
1495                                                 if (obj1.hasOwnProperty(name)) {
1496                                                         value = obj2[name];
1497
1498                                                         // Obj2 doesn't have obj1 item
1499                                                         if (value === undefined)
1500                                                                 return FALSE;
1501
1502                                                         // Obj2 item has a different value
1503                                                         if (obj1[name] != value)
1504                                                                 return FALSE;
1505
1506                                                         // Delete similar value
1507                                                         delete obj2[name];
1508                                                 }
1509                                         }
1510
1511                                         // Check if obj 2 has something obj 1 doesn't have
1512                                         for (name in obj2) {
1513                                                 // Obj2 has item obj1 doesn't have
1514                                                 if (obj2.hasOwnProperty(name))
1515                                                         return FALSE;
1516                                         }
1517
1518                                         return TRUE;
1519                                 };
1520
1521                                 // Attribs are not the same
1522                                 if (!compareObjects(getAttribs(node1), getAttribs(node2)))
1523                                         return FALSE;
1524
1525                                 // Styles are not the same
1526                                 if (!compareObjects(dom.parseStyle(dom.getAttrib(node1, 'style')), dom.parseStyle(dom.getAttrib(node2, 'style'))))
1527                                         return FALSE;
1528
1529                                 return TRUE;
1530                         };
1531
1532                         // Check if next/prev exists and that they are elements
1533                         if (prev && next) {
1534                                 function findElementSibling(node, sibling_name) {
1535                                         for (sibling = node; sibling; sibling = sibling[sibling_name]) {
1536                                                 if (sibling.nodeType == 3 && sibling.nodeValue.length !== 0)
1537                                                         return node;
1538
1539                                                 if (sibling.nodeType == 1 && !isBookmarkNode(sibling))
1540                                                         return sibling;
1541                                         }
1542
1543                                         return node;
1544                                 };
1545
1546                                 // If previous sibling is empty then jump over it
1547                                 prev = findElementSibling(prev, 'previousSibling');
1548                                 next = findElementSibling(next, 'nextSibling');
1549
1550                                 // Compare next and previous nodes
1551                                 if (compareElements(prev, next)) {
1552                                         // Append nodes between
1553                                         for (sibling = prev.nextSibling; sibling && sibling != next;) {
1554                                                 tmpSibling = sibling;
1555                                                 sibling = sibling.nextSibling;
1556                                                 prev.appendChild(tmpSibling);
1557                                         }
1558
1559                                         // Remove next node
1560                                         dom.remove(next);
1561
1562                                         // Move children into prev node
1563                                         each(tinymce.grep(next.childNodes), function(node) {
1564                                                 prev.appendChild(node);
1565                                         });
1566
1567                                         return prev;
1568                                 }
1569                         }
1570
1571                         return next;
1572                 };
1573
1574                 /**
1575                  * Returns true/false if the specified node is a text block or not.
1576                  *
1577                  * @private
1578                  * @param {Node} node Node to check.
1579                  * @return {boolean} True/false if the node is a text block.
1580                  */
1581                 function isTextBlock(name) {
1582                         return /^(h[1-6]|p|div|pre|address|dl|dt|dd)$/.test(name);
1583                 };
1584
1585                 function getContainer(rng, start) {
1586                         var container, offset, lastIdx;
1587
1588                         container = rng[start ? 'startContainer' : 'endContainer'];
1589                         offset = rng[start ? 'startOffset' : 'endOffset'];
1590
1591                         if (container.nodeType == 1) {
1592                                 lastIdx = container.childNodes.length - 1;
1593
1594                                 if (!start && offset)
1595                                         offset--;
1596
1597                                 container = container.childNodes[offset > lastIdx ? lastIdx : offset];
1598                         }
1599
1600                         return container;
1601                 };
1602
1603                 function performCaretAction(type, name, vars) {
1604                         var i, currentPendingFormats = pendingFormats[type],
1605                                 otherPendingFormats = pendingFormats[type == 'apply' ? 'remove' : 'apply'];
1606
1607                         function hasPending() {
1608                                 return pendingFormats.apply.length || pendingFormats.remove.length;
1609                         };
1610
1611                         function resetPending() {
1612                                 pendingFormats.apply = [];
1613                                 pendingFormats.remove = [];
1614                         };
1615
1616                         function perform(caret_node) {
1617                                 // Apply pending formats
1618                                 each(pendingFormats.apply.reverse(), function(item) {
1619                                         apply(item.name, item.vars, caret_node);
1620
1621                                         // Colored nodes should be underlined so that the color of the underline matches the text color.
1622                                         if (item.name === 'forecolor' && item.vars.value)
1623                                                 processUnderlineAndColor(caret_node.parentNode);
1624                                 });
1625
1626                                 // Remove pending formats
1627                                 each(pendingFormats.remove.reverse(), function(item) {
1628                                         remove(item.name, item.vars, caret_node);
1629                                 });
1630
1631                                 dom.remove(caret_node, 1);
1632                                 resetPending();
1633                         };
1634
1635                         // Check if it already exists then ignore it
1636                         for (i = currentPendingFormats.length - 1; i >= 0; i--) {
1637                                 if (currentPendingFormats[i].name == name)
1638                                         return;
1639                         }
1640
1641                         currentPendingFormats.push({name : name, vars : vars});
1642
1643                         // Check if it's in the other type, then remove it
1644                         for (i = otherPendingFormats.length - 1; i >= 0; i--) {
1645                                 if (otherPendingFormats[i].name == name)
1646                                         otherPendingFormats.splice(i, 1);
1647                         }
1648
1649                         // Pending apply or remove formats
1650                         if (hasPending()) {
1651                                 ed.getDoc().execCommand('FontName', false, 'mceinline');
1652                                 pendingFormats.lastRng = selection.getRng();
1653
1654                                 // IE will convert the current word
1655                                 each(dom.select('font,span'), function(node) {
1656                                         var bookmark;
1657
1658                                         if (isCaretNode(node)) {
1659                                                 bookmark = selection.getBookmark();
1660                                                 perform(node);
1661                                                 selection.moveToBookmark(bookmark);
1662                                                 ed.nodeChanged();
1663                                         }
1664                                 });
1665
1666                                 // Only register listeners once if we need to
1667                                 if (!pendingFormats.isListening && hasPending()) {
1668                                         pendingFormats.isListening = true;
1669
1670                                         each('onKeyDown,onKeyUp,onKeyPress,onMouseUp'.split(','), function(event) {
1671                                                 ed[event].addToTop(function(ed, e) {
1672                                                         // Do we have pending formats and is the selection moved has moved
1673                                                         if (hasPending() && !tinymce.dom.RangeUtils.compareRanges(pendingFormats.lastRng, selection.getRng())) {
1674                                                                 each(dom.select('font,span'), function(node) {
1675                                                                         var textNode, rng;
1676
1677                                                                         // Look for marker
1678                                                                         if (isCaretNode(node)) {
1679                                                                                 textNode = node.firstChild;
1680
1681                                                                                 if (textNode) {
1682                                                                                         perform(node);
1683
1684                                                                                         rng = dom.createRng();
1685                                                                                         rng.setStart(textNode, textNode.nodeValue.length);
1686                                                                                         rng.setEnd(textNode, textNode.nodeValue.length);
1687                                                                                         selection.setRng(rng);
1688                                                                                         ed.nodeChanged();
1689                                                                                 } else
1690                                                                                         dom.remove(node);
1691                                                                         }
1692                                                                 });
1693
1694                                                                 // Always unbind and clear pending styles on keyup
1695                                                                 if (e.type == 'keyup' || e.type == 'mouseup')
1696                                                                         resetPending();
1697                                                         }
1698                                                 });
1699                                         });
1700                                 }
1701                         }
1702                 };
1703         };
1704 })(tinymce);