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