]> git.sur5r.net Git - bacula/bacula/blob - gui/baculum/debian/missing-sources/framework/Web/Javascripts/source/tinymce-405/classes/Formatter.js
baculum: Add missing-sources directory in debian metadata structure
[bacula/bacula] / gui / baculum / debian / missing-sources / framework / Web / Javascripts / source / tinymce-405 / classes / Formatter.js
1 /**
2  * Formatter.js
3  *
4  * Copyright, Moxiecode Systems AB
5  * Released under LGPL License.
6  *
7  * License: http://www.tinymce.com/license
8  * Contributing: http://www.tinymce.com/contributing
9  */
10
11 /**
12  * Text formatter engine class. This class is used to apply formats like bold, italic, font size
13  * etc to the current selection or specific nodes. This engine was build to replace the browsers
14  * default formatting logic for execCommand due to it's inconsistent and buggy behavior.
15  *
16  * @class tinymce.Formatter
17  * @example
18  *  tinymce.activeEditor.formatter.register('mycustomformat', {
19  *    inline: 'span',
20  *    styles: {color: '#ff0000'}
21  *  });
22  *
23  *  tinymce.activeEditor.formatter.apply('mycustomformat');
24  */
25 define("tinymce/Formatter", [
26         "tinymce/dom/TreeWalker",
27         "tinymce/dom/RangeUtils",
28         "tinymce/util/Tools"
29 ], function(TreeWalker, RangeUtils, Tools) {
30         /**
31          * Constructs a new formatter instance.
32          *
33          * @constructor Formatter
34          * @param {tinymce.Editor} ed Editor instance to construct the formatter engine to.
35          */
36         return function(ed) {
37                 var formats = {},
38                         dom = ed.dom,
39                         selection = ed.selection,
40                         rangeUtils = new RangeUtils(dom),
41                         isValid = ed.schema.isValidChild,
42                         isBlock = dom.isBlock,
43                         forcedRootBlock = ed.settings.forced_root_block,
44                         nodeIndex = dom.nodeIndex,
45                         INVISIBLE_CHAR = '\uFEFF',
46                         MCE_ATTR_RE = /^(src|href|style)$/,
47                         FALSE = false,
48                         TRUE = true,
49                         formatChangeData,
50                         undef,
51                         getContentEditable = dom.getContentEditable,
52                         disableCaretContainer,
53                         markCaretContainersBogus;
54
55                 var each = Tools.each,
56                         grep = Tools.grep,
57                         walk = Tools.walk,
58                         extend = Tools.extend;
59
60                 function isTextBlock(name) {
61                         if (name.nodeType) {
62                                 name = name.nodeName;
63                         }
64
65                         return !!ed.schema.getTextBlockElements()[name.toLowerCase()];
66                 }
67
68                 function getParents(node, selector) {
69                         return dom.getParents(node, selector, dom.getRoot());
70                 }
71
72                 function isCaretNode(node) {
73                         return node.nodeType === 1 && node.id === '_mce_caret';
74                 }
75
76                 function defaultFormats() {
77                         register({
78                                 alignleft: [
79                                         {selector: 'figure,p,h1,h2,h3,h4,h5,h6,td,th,tr,div,ul,ol,li', styles: {textAlign: 'left'}, defaultBlock: 'div'},
80                                         {selector: 'img,table', collapsed: false, styles: {'float': 'left'}}
81                                 ],
82
83                                 aligncenter: [
84                                         {selector: 'figure,p,h1,h2,h3,h4,h5,h6,td,th,tr,div,ul,ol,li', styles: {textAlign: 'center'}, defaultBlock: 'div'},
85                                         {selector: 'img', collapsed: false, styles: {display: 'block', marginLeft: 'auto', marginRight: 'auto'}},
86                                         {selector: 'table', collapsed: false, styles: {marginLeft: 'auto', marginRight: 'auto'}}
87                                 ],
88
89                                 alignright: [
90                                         {selector: 'figure,p,h1,h2,h3,h4,h5,h6,td,th,tr,div,ul,ol,li', styles: {textAlign: 'right'}, defaultBlock: 'div'},
91                                         {selector: 'img,table', collapsed: false, styles: {'float': 'right'}}
92                                 ],
93
94                                 alignjustify: [
95                                         {selector: 'figure,p,h1,h2,h3,h4,h5,h6,td,th,tr,div,ul,ol,li', styles: {textAlign: 'justify'}, defaultBlock: 'div'}
96                                 ],
97
98                                 bold: [
99                                         {inline: 'strong', remove: 'all'},
100                                         {inline: 'span', styles: {fontWeight: 'bold'}},
101                                         {inline: 'b', remove: 'all'}
102                                 ],
103
104                                 italic: [
105                                         {inline: 'em', remove: 'all'},
106                                         {inline: 'span', styles: {fontStyle: 'italic'}},
107                                         {inline: 'i', remove: 'all'}
108                                 ],
109
110                                 underline: [
111                                         {inline: 'span', styles: {textDecoration: 'underline'}, exact: true},
112                                         {inline: 'u', remove: 'all'}
113                                 ],
114
115                                 strikethrough: [
116                                         {inline: 'span', styles: {textDecoration: 'line-through'}, exact: true},
117                                         {inline: 'strike', remove: 'all'}
118                                 ],
119
120                                 forecolor: {inline: 'span', styles: {color: '%value'}, wrap_links: false},
121                                 hilitecolor: {inline: 'span', styles: {backgroundColor: '%value'}, wrap_links: false},
122                                 fontname: {inline: 'span', styles: {fontFamily: '%value'}},
123                                 fontsize: {inline: 'span', styles: {fontSize: '%value'}},
124                                 fontsize_class: {inline: 'span', attributes: {'class': '%value'}},
125                                 blockquote: {block: 'blockquote', wrapper: 1, remove: 'all'},
126                                 subscript: {inline: 'sub'},
127                                 superscript: {inline: 'sup'},
128                                 code: {inline: 'code'},
129
130                                 link: {inline: 'a', selector: 'a', remove: 'all', split: true, deep: true,
131                                         onmatch: function() {
132                                                 return true;
133                                         },
134
135                                         onformat: function(elm, fmt, vars) {
136                                                 each(vars, function(value, key) {
137                                                         dom.setAttrib(elm, key, value);
138                                                 });
139                                         }
140                                 },
141
142                                 removeformat: [
143                                         {
144                                                 selector: 'b,strong,em,i,font,u,strike,sub,sup',
145                                                 remove: 'all',
146                                                 split: true,
147                                                 expand: false,
148                                                 block_expand: true,
149                                                 deep: true
150                                         },
151                                         {selector: 'span', attributes: ['style', 'class'], remove: 'empty', split: true, expand: false, deep: true},
152                                         {selector: '*', attributes: ['style', 'class'], split: false, expand: false, deep: true}
153                                 ]
154                         });
155
156                         // Register default block formats
157                         each('p h1 h2 h3 h4 h5 h6 div address pre div dt dd samp'.split(/\s/), function(name) {
158                                 register(name, {block: name, remove: 'all'});
159                         });
160
161                         // Register user defined formats
162                         register(ed.settings.formats);
163                 }
164
165                 function addKeyboardShortcuts() {
166                         // Add some inline shortcuts
167                         ed.addShortcut('ctrl+b', 'bold_desc', 'Bold');
168                         ed.addShortcut('ctrl+i', 'italic_desc', 'Italic');
169                         ed.addShortcut('ctrl+u', 'underline_desc', 'Underline');
170
171                         // BlockFormat shortcuts keys
172                         for (var i = 1; i <= 6; i++) {
173                                 ed.addShortcut('ctrl+' + i, '', ['FormatBlock', false, 'h' + i]);
174                         }
175
176                         ed.addShortcut('ctrl+7', '', ['FormatBlock', false, 'p']);
177                         ed.addShortcut('ctrl+8', '', ['FormatBlock', false, 'div']);
178                         ed.addShortcut('ctrl+9', '', ['FormatBlock', false, 'address']);
179                 }
180
181                 // Public functions
182
183                 /**
184                  * Returns the format by name or all formats if no name is specified.
185                  *
186                  * @method get
187                  * @param {String} name Optional name to retrive by.
188                  * @return {Array/Object} Array/Object with all registred formats or a specific format.
189                  */
190                 function get(name) {
191                         return name ? formats[name] : formats;
192                 }
193
194                 /**
195                  * Registers a specific format by name.
196                  *
197                  * @method register
198                  * @param {Object/String} name Name of the format for example "bold".
199                  * @param {Object/Array} format Optional format object or array of format variants
200                  * can only be omitted if the first arg is an object.
201                  */
202                 function register(name, format) {
203                         if (name) {
204                                 if (typeof(name) !== 'string') {
205                                         each(name, function(format, name) {
206                                                 register(name, format);
207                                         });
208                                 } else {
209                                         // Force format into array and add it to internal collection
210                                         format = format.length ? format : [format];
211
212                                         each(format, function(format) {
213                                                 // Set deep to false by default on selector formats this to avoid removing
214                                                 // alignment on images inside paragraphs when alignment is changed on paragraphs
215                                                 if (format.deep === undef) {
216                                                         format.deep = !format.selector;
217                                                 }
218
219                                                 // Default to true
220                                                 if (format.split === undef) {
221                                                         format.split = !format.selector || format.inline;
222                                                 }
223
224                                                 // Default to true
225                                                 if (format.remove === undef && format.selector && !format.inline) {
226                                                         format.remove = 'none';
227                                                 }
228
229                                                 // Mark format as a mixed format inline + block level
230                                                 if (format.selector && format.inline) {
231                                                         format.mixed = true;
232                                                         format.block_expand = true;
233                                                 }
234
235                                                 // Split classes if needed
236                                                 if (typeof(format.classes) === 'string') {
237                                                         format.classes = format.classes.split(/\s+/);
238                                                 }
239                                         });
240
241                                         formats[name] = format;
242                                 }
243                         }
244                 }
245
246                 function getTextDecoration(node) {
247                         var decoration;
248
249                         ed.dom.getParent(node, function(n) {
250                                 decoration = ed.dom.getStyle(n, 'text-decoration');
251                                 return decoration && decoration !== 'none';
252                         });
253
254                         return decoration;
255                 }
256
257                 function processUnderlineAndColor(node) {
258                         var textDecoration;
259                         if (node.nodeType === 1 && node.parentNode && node.parentNode.nodeType === 1) {
260                                 textDecoration = getTextDecoration(node.parentNode);
261                                 if (ed.dom.getStyle(node, 'color') && textDecoration) {
262                                         ed.dom.setStyle(node, 'text-decoration', textDecoration);
263                                 } else if (ed.dom.getStyle(node, 'textdecoration') === textDecoration) {
264                                         ed.dom.setStyle(node, 'text-decoration', null);
265                                 }
266                         }
267                 }
268
269                 /**
270                  * Applies the specified format to the current selection or specified node.
271                  *
272                  * @method apply
273                  * @param {String} name Name of format to apply.
274                  * @param {Object} vars Optional list of variables to replace within format before applying it.
275                  * @param {Node} node Optional node to apply the format to defaults to current selection.
276                  */
277                 function apply(name, vars, node) {
278                         var formatList = get(name), format = formatList[0], bookmark, rng, isCollapsed = !node && selection.isCollapsed();
279
280                         function setElementFormat(elm, fmt) {
281                                 fmt = fmt || format;
282
283                                 if (elm) {
284                                         if (fmt.onformat) {
285                                                 fmt.onformat(elm, fmt, vars, node);
286                                         }
287
288                                         each(fmt.styles, function(value, name) {
289                                                 dom.setStyle(elm, name, replaceVars(value, vars));
290                                         });
291
292                                         each(fmt.attributes, function(value, name) {
293                                                 dom.setAttrib(elm, name, replaceVars(value, vars));
294                                         });
295
296                                         each(fmt.classes, function(value) {
297                                                 value = replaceVars(value, vars);
298
299                                                 if (!dom.hasClass(elm, value)) {
300                                                         dom.addClass(elm, value);
301                                                 }
302                                         });
303                                 }
304                         }
305
306                         function adjustSelectionToVisibleSelection() {
307                                 function findSelectionEnd(start, end) {
308                                         var walker = new TreeWalker(end);
309                                         for (node = walker.current(); node; node = walker.prev()) {
310                                                 if (node.childNodes.length > 1 || node == start || node.tagName == 'BR') {
311                                                         return node;
312                                                 }
313                                         }
314                                 }
315
316                                 // Adjust selection so that a end container with a end offset of zero is not included in the selection
317                                 // as this isn't visible to the user.
318                                 var rng = ed.selection.getRng();
319                                 var start = rng.startContainer;
320                                 var end = rng.endContainer;
321
322                                 if (start != end && rng.endOffset === 0) {
323                                         var newEnd = findSelectionEnd(start, end);
324                                         var endOffset = newEnd.nodeType == 3 ? newEnd.length : newEnd.childNodes.length;
325
326                                         rng.setEnd(newEnd, endOffset);
327                                 }
328
329                                 return rng;
330                         }
331
332                         function applyStyleToList(node, bookmark, wrapElm, newWrappers, process){
333                                 var nodes = [], listIndex = -1, list, startIndex = -1, endIndex = -1, currentWrapElm;
334
335                                 // find the index of the first child list.
336                                 each(node.childNodes, function(n, index) {
337                                         if (n.nodeName === "UL" || n.nodeName === "OL") {
338                                                 listIndex = index;
339                                                 list = n;
340                                                 return false;
341                                         }
342                                 });
343
344                                 // get the index of the bookmarks
345                                 each(node.childNodes, function(n, index) {
346                                         if (n.nodeName === "SPAN" && dom.getAttrib(n, "data-mce-type") == "bookmark") {
347                                                 if (n.id == bookmark.id + "_start") {
348                                                         startIndex = index;
349                                                 } else if (n.id == bookmark.id + "_end") {
350                                                         endIndex = index;
351                                                 }
352                                         }
353                                 });
354
355                                 // if the selection spans across an embedded list, or there isn't an embedded list - handle processing normally
356                                 if (listIndex <= 0 || (startIndex < listIndex && endIndex > listIndex)) {
357                                         each(grep(node.childNodes), process);
358                                         return 0;
359                                 } else {
360                                         currentWrapElm = dom.clone(wrapElm, FALSE);
361
362                                         // create a list of the nodes on the same side of the list as the selection
363                                         each(grep(node.childNodes), function(n, index) {
364                                                 if ((startIndex < listIndex && index < listIndex) || (startIndex > listIndex && index > listIndex)) {
365                                                         nodes.push(n);
366                                                         n.parentNode.removeChild(n);
367                                                 }
368                                         });
369
370                                         // insert the wrapping element either before or after the list.
371                                         if (startIndex < listIndex) {
372                                                 node.insertBefore(currentWrapElm, list);
373                                         } else if (startIndex > listIndex) {
374                                                 node.insertBefore(currentWrapElm, list.nextSibling);
375                                         }
376
377                                         // add the new nodes to the list.
378                                         newWrappers.push(currentWrapElm);
379
380                                         each(nodes, function(node) {
381                                                 currentWrapElm.appendChild(node);
382                                         });
383
384                                         return currentWrapElm;
385                                 }
386                         }
387
388                         function applyRngStyle(rng, bookmark, node_specific) {
389                                 var newWrappers = [], wrapName, wrapElm, contentEditable = true;
390
391                                 // Setup wrapper element
392                                 wrapName = format.inline || format.block;
393                                 wrapElm = dom.create(wrapName);
394                                 setElementFormat(wrapElm);
395
396                                 rangeUtils.walk(rng, function(nodes) {
397                                         var currentWrapElm;
398
399                                         /**
400                                          * Process a list of nodes wrap them.
401                                          */
402                                         function process(node) {
403                                                 var nodeName, parentName, found, hasContentEditableState, lastContentEditable;
404
405                                                 lastContentEditable = contentEditable;
406                                                 nodeName = node.nodeName.toLowerCase();
407                                                 parentName = node.parentNode.nodeName.toLowerCase();
408
409                                                 // Node has a contentEditable value
410                                                 if (node.nodeType === 1 && getContentEditable(node)) {
411                                                         lastContentEditable = contentEditable;
412                                                         contentEditable = getContentEditable(node) === "true";
413                                                         hasContentEditableState = true; // We don't want to wrap the container only it's children
414                                                 }
415
416                                                 // Stop wrapping on br elements
417                                                 if (isEq(nodeName, 'br')) {
418                                                         currentWrapElm = 0;
419
420                                                         // Remove any br elements when we wrap things
421                                                         if (format.block) {
422                                                                 dom.remove(node);
423                                                         }
424
425                                                         return;
426                                                 }
427
428                                                 // If node is wrapper type
429                                                 if (format.wrapper && matchNode(node, name, vars)) {
430                                                         currentWrapElm = 0;
431                                                         return;
432                                                 }
433
434                                                 // Can we rename the block
435                                                 // TODO: Break this if up, too complex
436                                                 if (contentEditable && !hasContentEditableState && format.block &&
437                                                         !format.wrapper && isTextBlock(nodeName) && isValid(parentName, wrapName)) {
438                                                         node = dom.rename(node, wrapName);
439                                                         setElementFormat(node);
440                                                         newWrappers.push(node);
441                                                         currentWrapElm = 0;
442                                                         return;
443                                                 }
444
445                                                 // Handle selector patterns
446                                                 if (format.selector) {
447                                                         // Look for matching formats
448                                                         each(formatList, function(format) {
449                                                                 // Check collapsed state if it exists
450                                                                 if ('collapsed' in format && format.collapsed !== isCollapsed) {
451                                                                         return;
452                                                                 }
453
454                                                                 if (dom.is(node, format.selector) && !isCaretNode(node)) {
455                                                                         setElementFormat(node, format);
456                                                                         found = true;
457                                                                 }
458                                                         });
459
460                                                         // Continue processing if a selector match wasn't found and a inline element is defined
461                                                         if (!format.inline || found) {
462                                                                 currentWrapElm = 0;
463                                                                 return;
464                                                         }
465                                                 }
466
467                                                 // Is it valid to wrap this item
468                                                 // TODO: Break this if up, too complex
469                                                 if (contentEditable && !hasContentEditableState && isValid(wrapName, nodeName) && isValid(parentName, wrapName) &&
470                                                                 !(!node_specific && node.nodeType === 3 &&
471                                                                 node.nodeValue.length === 1 &&
472                                                                 node.nodeValue.charCodeAt(0) === 65279) &&
473                                                                 !isCaretNode(node) &&
474                                                                 (!format.inline || !isBlock(node))) {
475                                                         // Start wrapping
476                                                         if (!currentWrapElm) {
477                                                                 // Wrap the node
478                                                                 currentWrapElm = dom.clone(wrapElm, FALSE);
479                                                                 node.parentNode.insertBefore(currentWrapElm, node);
480                                                                 newWrappers.push(currentWrapElm);
481                                                         }
482
483                                                         currentWrapElm.appendChild(node);
484                                                 } else if (nodeName == 'li' && bookmark) {
485                                                         // Start wrapping - if we are in a list node and have a bookmark, then
486                                                         // we will always begin by wrapping in a new element.
487                                                         currentWrapElm = applyStyleToList(node, bookmark, wrapElm, newWrappers, process);
488                                                 } else {
489                                                         // Start a new wrapper for possible children
490                                                         currentWrapElm = 0;
491
492                                                         each(grep(node.childNodes), process);
493
494                                                         if (hasContentEditableState) {
495                                                                 contentEditable = lastContentEditable; // Restore last contentEditable state from stack
496                                                         }
497
498                                                         // End the last wrapper
499                                                         currentWrapElm = 0;
500                                                 }
501                                         }
502
503                                         // Process siblings from range
504                                         each(nodes, process);
505                                 });
506
507                                 // Wrap links inside as well, for example color inside a link when the wrapper is around the link
508                                 if (format.wrap_links === false) {
509                                         each(newWrappers, function(node) {
510                                                 function process(node) {
511                                                         var i, currentWrapElm, children;
512
513                                                         if (node.nodeName === 'A') {
514                                                                 currentWrapElm = dom.clone(wrapElm, FALSE);
515                                                                 newWrappers.push(currentWrapElm);
516
517                                                                 children = grep(node.childNodes);
518                                                                 for (i = 0; i < children.length; i++) {
519                                                                         currentWrapElm.appendChild(children[i]);
520                                                                 }
521
522                                                                 node.appendChild(currentWrapElm);
523                                                         }
524
525                                                         each(grep(node.childNodes), process);
526                                                 }
527
528                                                 process(node);
529                                         });
530                                 }
531
532                                 // Cleanup
533                                 each(newWrappers, function(node) {
534                                         var childCount;
535
536                                         function getChildCount(node) {
537                                                 var count = 0;
538
539                                                 each(node.childNodes, function(node) {
540                                                         if (!isWhiteSpaceNode(node) && !isBookmarkNode(node)) {
541                                                                 count++;
542                                                         }
543                                                 });
544
545                                                 return count;
546                                         }
547
548                                         function mergeStyles(node) {
549                                                 var child, clone;
550
551                                                 each(node.childNodes, function(node) {
552                                                         if (node.nodeType == 1 && !isBookmarkNode(node) && !isCaretNode(node)) {
553                                                                 child = node;
554                                                                 return FALSE; // break loop
555                                                         }
556                                                 });
557
558                                                 // If child was found and of the same type as the current node
559                                                 if (child && matchName(child, format)) {
560                                                         clone = dom.clone(child, FALSE);
561                                                         setElementFormat(clone);
562
563                                                         dom.replace(clone, node, TRUE);
564                                                         dom.remove(child, 1);
565                                                 }
566
567                                                 return clone || node;
568                                         }
569
570                                         childCount = getChildCount(node);
571
572                                         // Remove empty nodes but only if there is multiple wrappers and they are not block
573                                         // elements so never remove single <h1></h1> since that would remove the
574                                         // currrent empty block element where the caret is at
575                                         if ((newWrappers.length > 1 || !isBlock(node)) && childCount === 0) {
576                                                 dom.remove(node, 1);
577                                                 return;
578                                         }
579
580                                         if (format.inline || format.wrapper) {
581                                                 // Merges the current node with it's children of similar type to reduce the number of elements
582                                                 if (!format.exact && childCount === 1) {
583                                                         node = mergeStyles(node);
584                                                 }
585
586                                                 // Remove/merge children
587                                                 each(formatList, function(format) {
588                                                         // Merge all children of similar type will move styles from child to parent
589                                                         // this: <span style="color:red"><b><span style="color:red; font-size:10px">text</span></b></span>
590                                                         // will become: <span style="color:red"><b><span style="font-size:10px">text</span></b></span>
591                                                         each(dom.select(format.inline, node), function(child) {
592                                                                 var parent;
593
594                                                                 // When wrap_links is set to false we don't want
595                                                                 // to remove the format on children within links
596                                                                 if (format.wrap_links === false) {
597                                                                         parent = child.parentNode;
598
599                                                                         do {
600                                                                                 if (parent.nodeName === 'A') {
601                                                                                         return;
602                                                                                 }
603                                                                         } while ((parent = parent.parentNode));
604                                                                 }
605
606                                                                 removeFormat(format, vars, child, format.exact ? child : null);
607                                                         });
608                                                 });
609
610                                                 // Remove child if direct parent is of same type
611                                                 if (matchNode(node.parentNode, name, vars)) {
612                                                         dom.remove(node, 1);
613                                                         node = 0;
614                                                         return TRUE;
615                                                 }
616
617                                                 // Look for parent with similar style format
618                                                 if (format.merge_with_parents) {
619                                                         dom.getParent(node.parentNode, function(parent) {
620                                                                 if (matchNode(parent, name, vars)) {
621                                                                         dom.remove(node, 1);
622                                                                         node = 0;
623                                                                         return TRUE;
624                                                                 }
625                                                         });
626                                                 }
627
628                                                 // Merge next and previous siblings if they are similar <b>text</b><b>text</b> becomes <b>texttext</b>
629                                                 if (node && format.merge_siblings !== false) {
630                                                         node = mergeSiblings(getNonWhiteSpaceSibling(node), node);
631                                                         node = mergeSiblings(node, getNonWhiteSpaceSibling(node, TRUE));
632                                                 }
633                                         }
634                                 });
635                         }
636
637                         if (format) {
638                                 if (node) {
639                                         if (node.nodeType) {
640                                                 rng = dom.createRng();
641                                                 rng.setStartBefore(node);
642                                                 rng.setEndAfter(node);
643                                                 applyRngStyle(expandRng(rng, formatList), null, true);
644                                         } else {
645                                                 applyRngStyle(node, null, true);
646                                         }
647                                 } else {
648                                         if (!isCollapsed || !format.inline || dom.select('td.mce-item-selected,th.mce-item-selected').length) {
649                                                 // Obtain selection node before selection is unselected by applyRngStyle()
650                                                 var curSelNode = ed.selection.getNode();
651
652                                                 // If the formats have a default block and we can't find a parent block then
653                                                 // start wrapping it with a DIV this is for forced_root_blocks: false
654                                                 // It's kind of a hack but people should be using the default block type P since all desktop editors work that way
655                                                 if (!forcedRootBlock && formatList[0].defaultBlock && !dom.getParent(curSelNode, dom.isBlock)) {
656                                                         apply(formatList[0].defaultBlock);
657                                                 }
658
659                                                 // Apply formatting to selection
660                                                 ed.selection.setRng(adjustSelectionToVisibleSelection());
661                                                 bookmark = selection.getBookmark();
662                                                 applyRngStyle(expandRng(selection.getRng(TRUE), formatList), bookmark);
663
664                                                 // Colored nodes should be underlined so that the color of the underline matches the text color.
665                                                 if (format.styles && (format.styles.color || format.styles.textDecoration)) {
666                                                         walk(curSelNode, processUnderlineAndColor, 'childNodes');
667                                                         processUnderlineAndColor(curSelNode);
668                                                 }
669
670                                                 selection.moveToBookmark(bookmark);
671                                                 moveStart(selection.getRng(TRUE));
672                                                 ed.nodeChanged();
673                                         } else {
674                                                 performCaretAction('apply', name, vars);
675                                         }
676                                 }
677                         }
678                 }
679
680                 /**
681                  * Removes the specified format from the current selection or specified node.
682                  *
683                  * @method remove
684                  * @param {String} name Name of format to remove.
685                  * @param {Object} vars Optional list of variables to replace within format before removing it.
686                  * @param {Node/Range} node Optional node or DOM range to remove the format from defaults to current selection.
687                  */
688                 function remove(name, vars, node) {
689                         var formatList = get(name), format = formatList[0], bookmark, rng, contentEditable = true;
690
691                         // Merges the styles for each node
692                         function process(node) {
693                                 var children, i, l, lastContentEditable, hasContentEditableState;
694
695                                 // Node has a contentEditable value
696                                 if (node.nodeType === 1 && getContentEditable(node)) {
697                                         lastContentEditable = contentEditable;
698                                         contentEditable = getContentEditable(node) === "true";
699                                         hasContentEditableState = true; // We don't want to wrap the container only it's children
700                                 }
701
702                                 // Grab the children first since the nodelist might be changed
703                                 children = grep(node.childNodes);
704
705                                 // Process current node
706                                 if (contentEditable && !hasContentEditableState) {
707                                         for (i = 0, l = formatList.length; i < l; i++) {
708                                                 if (removeFormat(formatList[i], vars, node, node)) {
709                                                         break;
710                                                 }
711                                         }
712                                 }
713
714                                 // Process the children
715                                 if (format.deep) {
716                                         if (children.length) {
717                                                 for (i = 0, l = children.length; i < l; i++) {
718                                                         process(children[i]);
719                                                 }
720
721                                                 if (hasContentEditableState) {
722                                                         contentEditable = lastContentEditable; // Restore last contentEditable state from stack
723                                                 }
724                                         }
725                                 }
726                         }
727
728                         function findFormatRoot(container) {
729                                 var formatRoot;
730
731                                 // Find format root
732                                 each(getParents(container.parentNode).reverse(), function(parent) {
733                                         var format;
734
735                                         // Find format root element
736                                         if (!formatRoot && parent.id != '_start' && parent.id != '_end') {
737                                                 // Is the node matching the format we are looking for
738                                                 format = matchNode(parent, name, vars);
739                                                 if (format && format.split !== false) {
740                                                         formatRoot = parent;
741                                                 }
742                                         }
743                                 });
744
745                                 return formatRoot;
746                         }
747
748                         function wrapAndSplit(format_root, container, target, split) {
749                                 var parent, clone, lastClone, firstClone, i, formatRootParent;
750
751                                 // Format root found then clone formats and split it
752                                 if (format_root) {
753                                         formatRootParent = format_root.parentNode;
754
755                                         for (parent = container.parentNode; parent && parent != formatRootParent; parent = parent.parentNode) {
756                                                 clone = dom.clone(parent, FALSE);
757
758                                                 for (i = 0; i < formatList.length; i++) {
759                                                         if (removeFormat(formatList[i], vars, clone, clone)) {
760                                                                 clone = 0;
761                                                                 break;
762                                                         }
763                                                 }
764
765                                                 // Build wrapper node
766                                                 if (clone) {
767                                                         if (lastClone) {
768                                                                 clone.appendChild(lastClone);
769                                                         }
770
771                                                         if (!firstClone) {
772                                                                 firstClone = clone;
773                                                         }
774
775                                                         lastClone = clone;
776                                                 }
777                                         }
778
779                                         // Never split block elements if the format is mixed
780                                         if (split && (!format.mixed || !isBlock(format_root))) {
781                                                 container = dom.split(format_root, container);
782                                         }
783
784                                         // Wrap container in cloned formats
785                                         if (lastClone) {
786                                                 target.parentNode.insertBefore(lastClone, target);
787                                                 firstClone.appendChild(target);
788                                         }
789                                 }
790
791                                 return container;
792                         }
793
794                         function splitToFormatRoot(container) {
795                                 return wrapAndSplit(findFormatRoot(container), container, container, true);
796                         }
797
798                         function unwrap(start) {
799                                 var node = dom.get(start ? '_start' : '_end'),
800                                         out = node[start ? 'firstChild' : 'lastChild'];
801
802                                 // If the end is placed within the start the result will be removed
803                                 // So this checks if the out node is a bookmark node if it is it
804                                 // checks for another more suitable node
805                                 if (isBookmarkNode(out)) {
806                                         out = out[start ? 'firstChild' : 'lastChild'];
807                                 }
808
809                                 dom.remove(node, true);
810
811                                 return out;
812                         }
813
814                         function removeRngStyle(rng) {
815                                 var startContainer, endContainer;
816
817                                 rng = expandRng(rng, formatList, TRUE);
818
819                                 if (format.split) {
820                                         startContainer = getContainer(rng, TRUE);
821                                         endContainer = getContainer(rng);
822
823                                         if (startContainer != endContainer) {
824                                                 // WebKit will render the table incorrectly if we wrap a TD in a SPAN
825                                                 // so lets see if the can use the first child instead
826                                                 // This will happen if you tripple click a table cell and use remove formatting
827                                                 if (/^(TR|TD)$/.test(startContainer.nodeName) && startContainer.firstChild) {
828                                                         if (startContainer.nodeName == "TD") {
829                                                                 startContainer = startContainer.firstChild || startContainer;
830                                                         } else {
831                                                                 startContainer = startContainer.firstChild.firstChild || startContainer;
832                                                         }
833                                                 }
834
835                                                 // Wrap start/end nodes in span element since these might be cloned/moved
836                                                 startContainer = wrap(startContainer, 'span', {id: '_start', 'data-mce-type': 'bookmark'});
837                                                 endContainer = wrap(endContainer, 'span', {id: '_end', 'data-mce-type': 'bookmark'});
838
839                                                 // Split start/end
840                                                 splitToFormatRoot(startContainer);
841                                                 splitToFormatRoot(endContainer);
842
843                                                 // Unwrap start/end to get real elements again
844                                                 startContainer = unwrap(TRUE);
845                                                 endContainer = unwrap();
846                                         } else {
847                                                 startContainer = endContainer = splitToFormatRoot(startContainer);
848                                         }
849
850                                         // Update range positions since they might have changed after the split operations
851                                         rng.startContainer = startContainer.parentNode;
852                                         rng.startOffset = nodeIndex(startContainer);
853                                         rng.endContainer = endContainer.parentNode;
854                                         rng.endOffset = nodeIndex(endContainer) + 1;
855                                 }
856
857                                 // Remove items between start/end
858                                 rangeUtils.walk(rng, function(nodes) {
859                                         each(nodes, function(node) {
860                                                 process(node);
861
862                                                 // Remove parent span if it only contains text-decoration: underline, yet a parent node is also underlined.
863                                                 if (node.nodeType === 1 && ed.dom.getStyle(node, 'text-decoration') === 'underline' &&
864                                                         node.parentNode && getTextDecoration(node.parentNode) === 'underline') {
865                                                         removeFormat({
866                                                                 'deep': false,
867                                                                 'exact': true,
868                                                                 'inline': 'span',
869                                                                 'styles': {
870                                                                         'textDecoration': 'underline'
871                                                                 }
872                                                         }, null, node);
873                                                 }
874                                         });
875                                 });
876                         }
877
878                         // Handle node
879                         if (node) {
880                                 if (node.nodeType) {
881                                         rng = dom.createRng();
882                                         rng.setStartBefore(node);
883                                         rng.setEndAfter(node);
884                                         removeRngStyle(rng);
885                                 } else {
886                                         removeRngStyle(node);
887                                 }
888
889                                 return;
890                         }
891
892                         if (!selection.isCollapsed() || !format.inline || dom.select('td.mce-item-selected,th.mce-item-selected').length) {
893                                 bookmark = selection.getBookmark();
894                                 removeRngStyle(selection.getRng(TRUE));
895                                 selection.moveToBookmark(bookmark);
896
897                                 // Check if start element still has formatting then we are at: "<b>text|</b>text"
898                                 // and need to move the start into the next text node
899                                 if (format.inline && match(name, vars, selection.getStart())) {
900                                         moveStart(selection.getRng(true));
901                                 }
902
903                                 ed.nodeChanged();
904                         } else {
905                                 performCaretAction('remove', name, vars);
906                         }
907                 }
908
909                 /**
910                  * Toggles the specified format on/off.
911                  *
912                  * @method toggle
913                  * @param {String} name Name of format to apply/remove.
914                  * @param {Object} vars Optional list of variables to replace within format before applying/removing it.
915                  * @param {Node} node Optional node to apply the format to or remove from. Defaults to current selection.
916                  */
917                 function toggle(name, vars, node) {
918                         var fmt = get(name);
919
920                         if (match(name, vars, node) && (!('toggle' in fmt[0]) || fmt[0].toggle)) {
921                                 remove(name, vars, node);
922                         } else {
923                                 apply(name, vars, node);
924                         }
925                 }
926
927                 /**
928                  * Return true/false if the specified node has the specified format.
929                  *
930                  * @method matchNode
931                  * @param {Node} node Node to check the format on.
932                  * @param {String} name Format name to check.
933                  * @param {Object} vars Optional list of variables to replace before checking it.
934                  * @param {Boolean} similar Match format that has similar properties.
935                  * @return {Object} Returns the format object it matches or undefined if it doesn't match.
936                  */
937                 function matchNode(node, name, vars, similar) {
938                         var formatList = get(name), format, i, classes;
939
940                         function matchItems(node, format, item_name) {
941                                 var key, value, items = format[item_name], i;
942
943                                 // Custom match
944                                 if (format.onmatch) {
945                                         return format.onmatch(node, format, item_name);
946                                 }
947
948                                 // Check all items
949                                 if (items) {
950                                         // Non indexed object
951                                         if (items.length === undef) {
952                                                 for (key in items) {
953                                                         if (items.hasOwnProperty(key)) {
954                                                                 if (item_name === 'attributes') {
955                                                                         value = dom.getAttrib(node, key);
956                                                                 } else {
957                                                                         value = getStyle(node, key);
958                                                                 }
959
960                                                                 if (similar && !value && !format.exact) {
961                                                                         return;
962                                                                 }
963
964                                                                 if ((!similar || format.exact) && !isEq(value, normalizeStyleValue(replaceVars(items[key], vars), key))) {
965                                                                         return;
966                                                                 }
967                                                         }
968                                                 }
969                                         } else {
970                                                 // Only one match needed for indexed arrays
971                                                 for (i = 0; i < items.length; i++) {
972                                                         if (item_name === 'attributes' ? dom.getAttrib(node, items[i]) : getStyle(node, items[i])) {
973                                                                 return format;
974                                                         }
975                                                 }
976                                         }
977                                 }
978
979                                 return format;
980                         }
981
982                         if (formatList && node) {
983                                 // Check each format in list
984                                 for (i = 0; i < formatList.length; i++) {
985                                         format = formatList[i];
986
987                                         // Name name, attributes, styles and classes
988                                         if (matchName(node, format) && matchItems(node, format, 'attributes') && matchItems(node, format, 'styles')) {
989                                                 // Match classes
990                                                 if ((classes = format.classes)) {
991                                                         for (i = 0; i < classes.length; i++) {
992                                                                 if (!dom.hasClass(node, classes[i])) {
993                                                                         return;
994                                                                 }
995                                                         }
996                                                 }
997
998                                                 return format;
999                                         }
1000                                 }
1001                         }
1002                 }
1003
1004                 /**
1005                  * Matches the current selection or specified node against the specified format name.
1006                  *
1007                  * @method match
1008                  * @param {String} name Name of format to match.
1009                  * @param {Object} vars Optional list of variables to replace before checking it.
1010                  * @param {Node} node Optional node to check.
1011                  * @return {boolean} true/false if the specified selection/node matches the format.
1012                  */
1013                 function match(name, vars, node) {
1014                         var startNode;
1015
1016                         function matchParents(node) {
1017                                 var root = dom.getRoot();
1018
1019                                 // Find first node with similar format settings
1020                                 node = dom.getParent(node, function(node) {
1021                                         return node.parentNode === root || !!matchNode(node, name, vars, true);
1022                                 });
1023
1024                                 // Do an exact check on the similar format element
1025                                 return matchNode(node, name, vars);
1026                         }
1027
1028                         // Check specified node
1029                         if (node) {
1030                                 return matchParents(node);
1031                         }
1032
1033                         // Check selected node
1034                         node = selection.getNode();
1035                         if (matchParents(node)) {
1036                                 return TRUE;
1037                         }
1038
1039                         // Check start node if it's different
1040                         startNode = selection.getStart();
1041                         if (startNode != node) {
1042                                 if (matchParents(startNode)) {
1043                                         return TRUE;
1044                                 }
1045                         }
1046
1047                         return FALSE;
1048                 }
1049
1050                 /**
1051                  * Matches the current selection against the array of formats and returns a new array with matching formats.
1052                  *
1053                  * @method matchAll
1054                  * @param {Array} names Name of format to match.
1055                  * @param {Object} vars Optional list of variables to replace before checking it.
1056                  * @return {Array} Array with matched formats.
1057                  */
1058                 function matchAll(names, vars) {
1059                         var startElement, matchedFormatNames = [], checkedMap = {};
1060
1061                         // Check start of selection for formats
1062                         startElement = selection.getStart();
1063                         dom.getParent(startElement, function(node) {
1064                                 var i, name;
1065
1066                                 for (i = 0; i < names.length; i++) {
1067                                         name = names[i];
1068
1069                                         if (!checkedMap[name] && matchNode(node, name, vars)) {
1070                                                 checkedMap[name] = true;
1071                                                 matchedFormatNames.push(name);
1072                                         }
1073                                 }
1074                         }, dom.getRoot());
1075
1076                         return matchedFormatNames;
1077                 }
1078
1079                 /**
1080                  * Returns true/false if the specified format can be applied to the current selection or not. It
1081                  * will currently only check the state for selector formats, it returns true on all other format types.
1082                  *
1083                  * @method canApply
1084                  * @param {String} name Name of format to check.
1085                  * @return {boolean} true/false if the specified format can be applied to the current selection/node.
1086                  */
1087                 function canApply(name) {
1088                         var formatList = get(name), startNode, parents, i, x, selector;
1089
1090                         if (formatList) {
1091                                 startNode = selection.getStart();
1092                                 parents = getParents(startNode);
1093
1094                                 for (x = formatList.length - 1; x >= 0; x--) {
1095                                         selector = formatList[x].selector;
1096
1097                                         // Format is not selector based then always return TRUE
1098                                         // Is it has a defaultBlock then it's likely it can be applied for example align on a non block element line
1099                                         if (!selector || formatList[x].defaultBlock) {
1100                                                 return TRUE;
1101                                         }
1102
1103                                         for (i = parents.length - 1; i >= 0; i--) {
1104                                                 if (dom.is(parents[i], selector)) {
1105                                                         return TRUE;
1106                                                 }
1107                                         }
1108                                 }
1109                         }
1110
1111                         return FALSE;
1112                 }
1113
1114                 /**
1115                  * Executes the specified callback when the current selection matches the formats or not.
1116                  *
1117                  * @method formatChanged
1118                  * @param {String} formats Comma separated list of formats to check for.
1119                  * @param {function} callback Callback with state and args when the format is changed/toggled on/off.
1120                  * @param {Boolean} similar True/false state if the match should handle similar or exact formats.
1121                  */
1122                 function formatChanged(formats, callback, similar) {
1123                         var currentFormats;
1124
1125                         // Setup format node change logic
1126                         if (!formatChangeData) {
1127                                 formatChangeData = {};
1128                                 currentFormats = {};
1129
1130                                 ed.on('NodeChange', function(e) {
1131                                         var parents = getParents(e.element), matchedFormats = {};
1132
1133                                         // Check for new formats
1134                                         each(formatChangeData, function(callbacks, format) {
1135                                                 each(parents, function(node) {
1136                                                         if (matchNode(node, format, {}, callbacks.similar)) {
1137                                                                 if (!currentFormats[format]) {
1138                                                                         // Execute callbacks
1139                                                                         each(callbacks, function(callback) {
1140                                                                                 callback(true, {node: node, format: format, parents: parents});
1141                                                                         });
1142
1143                                                                         currentFormats[format] = callbacks;
1144                                                                 }
1145
1146                                                                 matchedFormats[format] = callbacks;
1147                                                                 return false;
1148                                                         }
1149                                                 });
1150                                         });
1151
1152                                         // Check if current formats still match
1153                                         each(currentFormats, function(callbacks, format) {
1154                                                 if (!matchedFormats[format]) {
1155                                                         delete currentFormats[format];
1156
1157                                                         each(callbacks, function(callback) {
1158                                                                 callback(false, {node: e.element, format: format, parents: parents});
1159                                                         });
1160                                                 }
1161                                         });
1162                                 });
1163                         }
1164
1165                         // Add format listeners
1166                         each(formats.split(','), function(format) {
1167                                 if (!formatChangeData[format]) {
1168                                         formatChangeData[format] = [];
1169                                         formatChangeData[format].similar = similar;
1170                                 }
1171
1172                                 formatChangeData[format].push(callback);
1173                         });
1174
1175                         return this;
1176                 }
1177
1178                 // Expose to public
1179                 extend(this, {
1180                         get: get,
1181                         register: register,
1182                         apply: apply,
1183                         remove: remove,
1184                         toggle: toggle,
1185                         match: match,
1186                         matchAll: matchAll,
1187                         matchNode: matchNode,
1188                         canApply: canApply,
1189                         formatChanged: formatChanged
1190                 });
1191
1192                 // Initialize
1193                 defaultFormats();
1194                 addKeyboardShortcuts();
1195                 ed.on('BeforeGetContent', function() {
1196                         if (markCaretContainersBogus) {
1197                                 markCaretContainersBogus();
1198                         }
1199                 });
1200                 ed.on('mouseup keydown', function(e) {
1201                         if (disableCaretContainer) {
1202                                 disableCaretContainer(e);
1203                         }
1204                 });
1205
1206                 // Private functions
1207
1208                 /**
1209                  * Checks if the specified nodes name matches the format inline/block or selector.
1210                  *
1211                  * @private
1212                  * @param {Node} node Node to match against the specified format.
1213                  * @param {Object} format Format object o match with.
1214                  * @return {boolean} true/false if the format matches.
1215                  */
1216                 function matchName(node, format) {
1217                         // Check for inline match
1218                         if (isEq(node, format.inline)) {
1219                                 return TRUE;
1220                         }
1221
1222                         // Check for block match
1223                         if (isEq(node, format.block)) {
1224                                 return TRUE;
1225                         }
1226
1227                         // Check for selector match
1228                         if (format.selector) {
1229                                 return node.nodeType == 1 && dom.is(node, format.selector);
1230                         }
1231                 }
1232
1233                 /**
1234                  * Compares two string/nodes regardless of their case.
1235                  *
1236                  * @private
1237                  * @param {String/Node} Node or string to compare.
1238                  * @param {String/Node} Node or string to compare.
1239                  * @return {boolean} True/false if they match.
1240                  */
1241                 function isEq(str1, str2) {
1242                         str1 = str1 || '';
1243                         str2 = str2 || '';
1244
1245                         str1 = '' + (str1.nodeName || str1);
1246                         str2 = '' + (str2.nodeName || str2);
1247
1248                         return str1.toLowerCase() == str2.toLowerCase();
1249                 }
1250
1251                 /**
1252                  * Returns the style by name on the specified node. This method modifies the style
1253                  * contents to make it more easy to match. This will resolve a few browser issues.
1254                  *
1255                  * @private
1256                  * @param {Node} node to get style from.
1257                  * @param {String} name Style name to get.
1258                  * @return {String} Style item value.
1259                  */
1260                 function getStyle(node, name) {
1261                         return normalizeStyleValue(dom.getStyle(node, name), name);
1262                 }
1263
1264                 /**
1265                  * Normalize style value by name. This method modifies the style contents
1266                  * to make it more easy to match. This will resolve a few browser issues.
1267                  *
1268                  * @private
1269                  * @param {Node} node to get style from.
1270                  * @param {String} name Style name to get.
1271                  * @return {String} Style item value.
1272                  */
1273                 function normalizeStyleValue(value, name) {
1274                         // Force the format to hex
1275                         if (name == 'color' || name == 'backgroundColor') {
1276                                 value = dom.toHex(value);
1277                         }
1278
1279                         // Opera will return bold as 700
1280                         if (name == 'fontWeight' && value == 700) {
1281                                 value = 'bold';
1282                         }
1283
1284                         // Normalize fontFamily so "'Font name', Font" becomes: "Font name,Font"
1285                         if (name == 'fontFamily') {
1286                                 value = value.replace(/[\'\"]/g, '').replace(/,\s+/g, ',');
1287                         }
1288
1289                         return '' + value;
1290                 }
1291
1292                 /**
1293                  * Replaces variables in the value. The variable format is %var.
1294                  *
1295                  * @private
1296                  * @param {String} value Value to replace variables in.
1297                  * @param {Object} vars Name/value array with variables to replace.
1298                  * @return {String} New value with replaced variables.
1299                  */
1300                 function replaceVars(value, vars) {
1301                         if (typeof(value) != "string") {
1302                                 value = value(vars);
1303                         } else if (vars) {
1304                                 value = value.replace(/%(\w+)/g, function(str, name) {
1305                                         return vars[name] || str;
1306                                 });
1307                         }
1308
1309                         return value;
1310                 }
1311
1312                 function isWhiteSpaceNode(node) {
1313                         return node && node.nodeType === 3 && /^([\t \r\n]+|)$/.test(node.nodeValue);
1314                 }
1315
1316                 function wrap(node, name, attrs) {
1317                         var wrapper = dom.create(name, attrs);
1318
1319                         node.parentNode.insertBefore(wrapper, node);
1320                         wrapper.appendChild(node);
1321
1322                         return wrapper;
1323                 }
1324
1325                 /**
1326                  * Expands the specified range like object to depending on format.
1327                  *
1328                  * For example on block formats it will move the start/end position
1329                  * to the beginning of the current block.
1330                  *
1331                  * @private
1332                  * @param {Object} rng Range like object.
1333                  * @param {Array} formats Array with formats to expand by.
1334                  * @return {Object} Expanded range like object.
1335                  */
1336                 function expandRng(rng, format, remove) {
1337                         var lastIdx, leaf, endPoint,
1338                                 startContainer = rng.startContainer,
1339                                 startOffset = rng.startOffset,
1340                                 endContainer = rng.endContainer,
1341                                 endOffset = rng.endOffset;
1342
1343                         // This function walks up the tree if there is no siblings before/after the node
1344                         function findParentContainer(start) {
1345                                 var container, parent, sibling, siblingName, root;
1346
1347                                 container = parent = start ? startContainer : endContainer;
1348                                 siblingName = start ? 'previousSibling' : 'nextSibling';
1349                                 root = dom.getRoot();
1350
1351                                 function isBogusBr(node) {
1352                                         return node.nodeName == "BR" && node.getAttribute('data-mce-bogus') && !node.nextSibling;
1353                                 }
1354
1355                                 // If it's a text node and the offset is inside the text
1356                                 if (container.nodeType == 3 && !isWhiteSpaceNode(container)) {
1357                                         if (start ? startOffset > 0 : endOffset < container.nodeValue.length) {
1358                                                 return container;
1359                                         }
1360                                 }
1361
1362                                 for (;;) {
1363                                         // Stop expanding on block elements
1364                                         if (!format[0].block_expand && isBlock(parent)) {
1365                                                 return parent;
1366                                         }
1367
1368                                         // Walk left/right
1369                                         for (sibling = parent[siblingName]; sibling; sibling = sibling[siblingName]) {
1370                                                 if (!isBookmarkNode(sibling) && !isWhiteSpaceNode(sibling) && !isBogusBr(sibling)) {
1371                                                         return parent;
1372                                                 }
1373                                         }
1374
1375                                         // Check if we can move up are we at root level or body level
1376                                         if (parent.parentNode == root) {
1377                                                 container = parent;
1378                                                 break;
1379                                         }
1380
1381                                         parent = parent.parentNode;
1382                                 }
1383
1384                                 return container;
1385                         }
1386
1387                         // This function walks down the tree to find the leaf at the selection.
1388                         // The offset is also returned as if node initially a leaf, the offset may be in the middle of the text node.
1389                         function findLeaf(node, offset) {
1390                                 if (offset === undef) {
1391                                         offset = node.nodeType === 3 ? node.length : node.childNodes.length;
1392                                 }
1393
1394                                 while (node && node.hasChildNodes()) {
1395                                         node = node.childNodes[offset];
1396                                         if (node) {
1397                                                 offset = node.nodeType === 3 ? node.length : node.childNodes.length;
1398                                         }
1399                                 }
1400                                 return { node: node, offset: offset };
1401                         }
1402
1403                         // If index based start position then resolve it
1404                         if (startContainer.nodeType == 1 && startContainer.hasChildNodes()) {
1405                                 lastIdx = startContainer.childNodes.length - 1;
1406                                 startContainer = startContainer.childNodes[startOffset > lastIdx ? lastIdx : startOffset];
1407
1408                                 if (startContainer.nodeType == 3) {
1409                                         startOffset = 0;
1410                                 }
1411                         }
1412
1413                         // If index based end position then resolve it
1414                         if (endContainer.nodeType == 1 && endContainer.hasChildNodes()) {
1415                                 lastIdx = endContainer.childNodes.length - 1;
1416                                 endContainer = endContainer.childNodes[endOffset > lastIdx ? lastIdx : endOffset - 1];
1417
1418                                 if (endContainer.nodeType == 3) {
1419                                         endOffset = endContainer.nodeValue.length;
1420                                 }
1421                         }
1422
1423                         // Expands the node to the closes contentEditable false element if it exists
1424                         function findParentContentEditable(node) {
1425                                 var parent = node;
1426
1427                                 while (parent) {
1428                                         if (parent.nodeType === 1 && getContentEditable(parent)) {
1429                                                 return getContentEditable(parent) === "false" ? parent : node;
1430                                         }
1431
1432                                         parent = parent.parentNode;
1433                                 }
1434
1435                                 return node;
1436                         }
1437
1438                         function findWordEndPoint(container, offset, start) {
1439                                 var walker, node, pos, lastTextNode;
1440
1441                                 function findSpace(node, offset) {
1442                                         var pos, pos2, str = node.nodeValue;
1443
1444                                         if (typeof(offset) == "undefined") {
1445                                                 offset = start ? str.length : 0;
1446                                         }
1447
1448                                         if (start) {
1449                                                 pos = str.lastIndexOf(' ', offset);
1450                                                 pos2 = str.lastIndexOf('\u00a0', offset);
1451                                                 pos = pos > pos2 ? pos : pos2;
1452
1453                                                 // Include the space on remove to avoid tag soup
1454                                                 if (pos !== -1 && !remove) {
1455                                                         pos++;
1456                                                 }
1457                                         } else {
1458                                                 pos = str.indexOf(' ', offset);
1459                                                 pos2 = str.indexOf('\u00a0', offset);
1460                                                 pos = pos !== -1 && (pos2 === -1 || pos < pos2) ? pos : pos2;
1461                                         }
1462
1463                                         return pos;
1464                                 }
1465
1466                                 if (container.nodeType === 3) {
1467                                         pos = findSpace(container, offset);
1468
1469                                         if (pos !== -1) {
1470                                                 return {container: container, offset: pos};
1471                                         }
1472
1473                                         lastTextNode = container;
1474                                 }
1475
1476                                 // Walk the nodes inside the block
1477                                 walker = new TreeWalker(container, dom.getParent(container, isBlock) || ed.getBody());
1478                                 while ((node = walker[start ? 'prev' : 'next']())) {
1479                                         if (node.nodeType === 3) {
1480                                                 lastTextNode = node;
1481                                                 pos = findSpace(node);
1482
1483                                                 if (pos !== -1) {
1484                                                         return {container: node, offset: pos};
1485                                                 }
1486                                         } else if (isBlock(node)) {
1487                                                 break;
1488                                         }
1489                                 }
1490
1491                                 if (lastTextNode) {
1492                                         if (start) {
1493                                                 offset = 0;
1494                                         } else {
1495                                                 offset = lastTextNode.length;
1496                                         }
1497
1498                                         return {container: lastTextNode, offset: offset};
1499                                 }
1500                         }
1501
1502                         function findSelectorEndPoint(container, sibling_name) {
1503                                 var parents, i, y, curFormat;
1504
1505                                 if (container.nodeType == 3 && container.nodeValue.length === 0 && container[sibling_name]) {
1506                                         container = container[sibling_name];
1507                                 }
1508
1509                                 parents = getParents(container);
1510                                 for (i = 0; i < parents.length; i++) {
1511                                         for (y = 0; y < format.length; y++) {
1512                                                 curFormat = format[y];
1513
1514                                                 // If collapsed state is set then skip formats that doesn't match that
1515                                                 if ("collapsed" in curFormat && curFormat.collapsed !== rng.collapsed) {
1516                                                         continue;
1517                                                 }
1518
1519                                                 if (dom.is(parents[i], curFormat.selector)) {
1520                                                         return parents[i];
1521                                                 }
1522                                         }
1523                                 }
1524
1525                                 return container;
1526                         }
1527
1528                         function findBlockEndPoint(container, sibling_name) {
1529                                 var node, root = dom.getRoot();
1530
1531                                 // Expand to block of similar type
1532                                 if (!format[0].wrapper) {
1533                                         node = dom.getParent(container, format[0].block);
1534                                 }
1535
1536                                 // Expand to first wrappable block element or any block element
1537                                 if (!node) {
1538                                         node = dom.getParent(container.nodeType == 3 ? container.parentNode : container, function(node) {
1539                                                 // Fixes #6183 where it would expand to editable parent element in inline mode
1540                                                 return node != root && isTextBlock(node);
1541                                         });
1542                                 }
1543
1544                                 // Exclude inner lists from wrapping
1545                                 if (node && format[0].wrapper) {
1546                                         node = getParents(node, 'ul,ol').reverse()[0] || node;
1547                                 }
1548
1549                                 // Didn't find a block element look for first/last wrappable element
1550                                 if (!node) {
1551                                         node = container;
1552
1553                                         while (node[sibling_name] && !isBlock(node[sibling_name])) {
1554                                                 node = node[sibling_name];
1555
1556                                                 // Break on BR but include it will be removed later on
1557                                                 // we can't remove it now since we need to check if it can be wrapped
1558                                                 if (isEq(node, 'br')) {
1559                                                         break;
1560                                                 }
1561                                         }
1562                                 }
1563
1564                                 return node || container;
1565                         }
1566
1567                         // Expand to closest contentEditable element
1568                         startContainer = findParentContentEditable(startContainer);
1569                         endContainer = findParentContentEditable(endContainer);
1570
1571                         // Exclude bookmark nodes if possible
1572                         if (isBookmarkNode(startContainer.parentNode) || isBookmarkNode(startContainer)) {
1573                                 startContainer = isBookmarkNode(startContainer) ? startContainer : startContainer.parentNode;
1574                                 startContainer = startContainer.nextSibling || startContainer;
1575
1576                                 if (startContainer.nodeType == 3) {
1577                                         startOffset = 0;
1578                                 }
1579                         }
1580
1581                         if (isBookmarkNode(endContainer.parentNode) || isBookmarkNode(endContainer)) {
1582                                 endContainer = isBookmarkNode(endContainer) ? endContainer : endContainer.parentNode;
1583                                 endContainer = endContainer.previousSibling || endContainer;
1584
1585                                 if (endContainer.nodeType == 3) {
1586                                         endOffset = endContainer.length;
1587                                 }
1588                         }
1589
1590                         if (format[0].inline) {
1591                                 if (rng.collapsed) {
1592                                         // Expand left to closest word boundary
1593                                         endPoint = findWordEndPoint(startContainer, startOffset, true);
1594                                         if (endPoint) {
1595                                                 startContainer = endPoint.container;
1596                                                 startOffset = endPoint.offset;
1597                                         }
1598
1599                                         // Expand right to closest word boundary
1600                                         endPoint = findWordEndPoint(endContainer, endOffset);
1601                                         if (endPoint) {
1602                                                 endContainer = endPoint.container;
1603                                                 endOffset = endPoint.offset;
1604                                         }
1605                                 }
1606
1607                                 // Avoid applying formatting to a trailing space.
1608                                 leaf = findLeaf(endContainer, endOffset);
1609                                 if (leaf.node) {
1610                                         while (leaf.node && leaf.offset === 0 && leaf.node.previousSibling) {
1611                                                 leaf = findLeaf(leaf.node.previousSibling);
1612                                         }
1613
1614                                         if (leaf.node && leaf.offset > 0 && leaf.node.nodeType === 3 &&
1615                                                         leaf.node.nodeValue.charAt(leaf.offset - 1) === ' ') {
1616
1617                                                 if (leaf.offset > 1) {
1618                                                         endContainer = leaf.node;
1619                                                         endContainer.splitText(leaf.offset - 1);
1620                                                 }
1621                                         }
1622                                 }
1623                         }
1624
1625                         // Move start/end point up the tree if the leaves are sharp and if we are in different containers
1626                         // Example * becomes !: !<p><b><i>*text</i><i>text*</i></b></p>!
1627                         // This will reduce the number of wrapper elements that needs to be created
1628                         // Move start point up the tree
1629                         if (format[0].inline || format[0].block_expand) {
1630                                 if (!format[0].inline || (startContainer.nodeType != 3 || startOffset === 0)) {
1631                                         startContainer = findParentContainer(true);
1632                                 }
1633
1634                                 if (!format[0].inline || (endContainer.nodeType != 3 || endOffset === endContainer.nodeValue.length)) {
1635                                         endContainer = findParentContainer();
1636                                 }
1637                         }
1638
1639                         // Expand start/end container to matching selector
1640                         if (format[0].selector && format[0].expand !== FALSE && !format[0].inline) {
1641                                 // Find new startContainer/endContainer if there is better one
1642                                 startContainer = findSelectorEndPoint(startContainer, 'previousSibling');
1643                                 endContainer = findSelectorEndPoint(endContainer, 'nextSibling');
1644                         }
1645
1646                         // Expand start/end container to matching block element or text node
1647                         if (format[0].block || format[0].selector) {
1648                                 // Find new startContainer/endContainer if there is better one
1649                                 startContainer = findBlockEndPoint(startContainer, 'previousSibling');
1650                                 endContainer = findBlockEndPoint(endContainer, 'nextSibling');
1651
1652                                 // Non block element then try to expand up the leaf
1653                                 if (format[0].block) {
1654                                         if (!isBlock(startContainer)) {
1655                                                 startContainer = findParentContainer(true);
1656                                         }
1657
1658                                         if (!isBlock(endContainer)) {
1659                                                 endContainer = findParentContainer();
1660                                         }
1661                                 }
1662                         }
1663
1664                         // Setup index for startContainer
1665                         if (startContainer.nodeType == 1) {
1666                                 startOffset = nodeIndex(startContainer);
1667                                 startContainer = startContainer.parentNode;
1668                         }
1669
1670                         // Setup index for endContainer
1671                         if (endContainer.nodeType == 1) {
1672                                 endOffset = nodeIndex(endContainer) + 1;
1673                                 endContainer = endContainer.parentNode;
1674                         }
1675
1676                         // Return new range like object
1677                         return {
1678                                 startContainer: startContainer,
1679                                 startOffset: startOffset,
1680                                 endContainer: endContainer,
1681                                 endOffset: endOffset
1682                         };
1683                 }
1684
1685                 /**
1686                  * Removes the specified format for the specified node. It will also remove the node if it doesn't have
1687                  * any attributes if the format specifies it to do so.
1688                  *
1689                  * @private
1690                  * @param {Object} format Format object with items to remove from node.
1691                  * @param {Object} vars Name/value object with variables to apply to format.
1692                  * @param {Node} node Node to remove the format styles on.
1693                  * @param {Node} compare_node Optional compare node, if specified the styles will be compared to that node.
1694                  * @return {Boolean} True/false if the node was removed or not.
1695                  */
1696                 function removeFormat(format, vars, node, compare_node) {
1697                         var i, attrs, stylesModified;
1698
1699                         // Check if node matches format
1700                         if (!matchName(node, format)) {
1701                                 return FALSE;
1702                         }
1703
1704                         // Should we compare with format attribs and styles
1705                         if (format.remove != 'all') {
1706                                 // Remove styles
1707                                 each(format.styles, function(value, name) {
1708                                         value = normalizeStyleValue(replaceVars(value, vars), name);
1709
1710                                         // Indexed array
1711                                         if (typeof(name) === 'number') {
1712                                                 name = value;
1713                                                 compare_node = 0;
1714                                         }
1715
1716                                         if (!compare_node || isEq(getStyle(compare_node, name), value)) {
1717                                                 dom.setStyle(node, name, '');
1718                                         }
1719
1720                                         stylesModified = 1;
1721                                 });
1722
1723                                 // Remove style attribute if it's empty
1724                                 if (stylesModified && dom.getAttrib(node, 'style') === '') {
1725                                         node.removeAttribute('style');
1726                                         node.removeAttribute('data-mce-style');
1727                                 }
1728
1729                                 // Remove attributes
1730                                 each(format.attributes, function(value, name) {
1731                                         var valueOut;
1732
1733                                         value = replaceVars(value, vars);
1734
1735                                         // Indexed array
1736                                         if (typeof(name) === 'number') {
1737                                                 name = value;
1738                                                 compare_node = 0;
1739                                         }
1740
1741                                         if (!compare_node || isEq(dom.getAttrib(compare_node, name), value)) {
1742                                                 // Keep internal classes
1743                                                 if (name == 'class') {
1744                                                         value = dom.getAttrib(node, name);
1745                                                         if (value) {
1746                                                                 // Build new class value where everything is removed except the internal prefixed classes
1747                                                                 valueOut = '';
1748                                                                 each(value.split(/\s+/), function(cls) {
1749                                                                         if (/mce\w+/.test(cls)) {
1750                                                                                 valueOut += (valueOut ? ' ' : '') + cls;
1751                                                                         }
1752                                                                 });
1753
1754                                                                 // We got some internal classes left
1755                                                                 if (valueOut) {
1756                                                                         dom.setAttrib(node, name, valueOut);
1757                                                                         return;
1758                                                                 }
1759                                                         }
1760                                                 }
1761
1762                                                 // IE6 has a bug where the attribute doesn't get removed correctly
1763                                                 if (name == "class") {
1764                                                         node.removeAttribute('className');
1765                                                 }
1766
1767                                                 // Remove mce prefixed attributes
1768                                                 if (MCE_ATTR_RE.test(name)) {
1769                                                         node.removeAttribute('data-mce-' + name);
1770                                                 }
1771
1772                                                 node.removeAttribute(name);
1773                                         }
1774                                 });
1775
1776                                 // Remove classes
1777                                 each(format.classes, function(value) {
1778                                         value = replaceVars(value, vars);
1779
1780                                         if (!compare_node || dom.hasClass(compare_node, value)) {
1781                                                 dom.removeClass(node, value);
1782                                         }
1783                                 });
1784
1785                                 // Check for non internal attributes
1786                                 attrs = dom.getAttribs(node);
1787                                 for (i = 0; i < attrs.length; i++) {
1788                                         if (attrs[i].nodeName.indexOf('_') !== 0) {
1789                                                 return FALSE;
1790                                         }
1791                                 }
1792                         }
1793
1794                         // Remove the inline child if it's empty for example <b> or <span>
1795                         if (format.remove != 'none') {
1796                                 removeNode(node, format);
1797                                 return TRUE;
1798                         }
1799                 }
1800
1801                 /**
1802                  * Removes the node and wrap it's children in paragraphs before doing so or
1803                  * appends BR elements to the beginning/end of the block element if forcedRootBlocks is disabled.
1804                  *
1805                  * If the div in the node below gets removed:
1806                  *  text<div>text</div>text
1807                  *
1808                  * Output becomes:
1809                  *  text<div><br />text<br /></div>text
1810                  *
1811                  * So when the div is removed the result is:
1812                  *  text<br />text<br />text
1813                  *
1814                  * @private
1815                  * @param {Node} node Node to remove + apply BR/P elements to.
1816                  * @param {Object} format Format rule.
1817                  * @return {Node} Input node.
1818                  */
1819                 function removeNode(node, format) {
1820                         var parentNode = node.parentNode, rootBlockElm;
1821
1822                         function find(node, next, inc) {
1823                                 node = getNonWhiteSpaceSibling(node, next, inc);
1824
1825                                 return !node || (node.nodeName == 'BR' || isBlock(node));
1826                         }
1827
1828                         if (format.block) {
1829                                 if (!forcedRootBlock) {
1830                                         // Append BR elements if needed before we remove the block
1831                                         if (isBlock(node) && !isBlock(parentNode)) {
1832                                                 if (!find(node, FALSE) && !find(node.firstChild, TRUE, 1)) {
1833                                                         node.insertBefore(dom.create('br'), node.firstChild);
1834                                                 }
1835
1836                                                 if (!find(node, TRUE) && !find(node.lastChild, FALSE, 1)) {
1837                                                         node.appendChild(dom.create('br'));
1838                                                 }
1839                                         }
1840                                 } else {
1841                                         // Wrap the block in a forcedRootBlock if we are at the root of document
1842                                         if (parentNode == dom.getRoot()) {
1843                                                 if (!format.list_block || !isEq(node, format.list_block)) {
1844                                                         each(grep(node.childNodes), function(node) {
1845                                                                 if (isValid(forcedRootBlock, node.nodeName.toLowerCase())) {
1846                                                                         if (!rootBlockElm) {
1847                                                                                 rootBlockElm = wrap(node, forcedRootBlock);
1848                                                                         } else {
1849                                                                                 rootBlockElm.appendChild(node);
1850                                                                         }
1851                                                                 } else {
1852                                                                         rootBlockElm = 0;
1853                                                                 }
1854                                                         });
1855                                                 }
1856                                         }
1857                                 }
1858                         }
1859
1860                         // Never remove nodes that isn't the specified inline element if a selector is specified too
1861                         if (format.selector && format.inline && !isEq(format.inline, node)) {
1862                                 return;
1863                         }
1864
1865                         dom.remove(node, 1);
1866                 }
1867
1868                 /**
1869                  * Returns the next/previous non whitespace node.
1870                  *
1871                  * @private
1872                  * @param {Node} node Node to start at.
1873                  * @param {boolean} next (Optional) Include next or previous node defaults to previous.
1874                  * @param {boolean} inc (Optional) Include the current node in checking. Defaults to false.
1875                  * @return {Node} Next or previous node or undefined if it wasn't found.
1876                  */
1877                 function getNonWhiteSpaceSibling(node, next, inc) {
1878                         if (node) {
1879                                 next = next ? 'nextSibling' : 'previousSibling';
1880
1881                                 for (node = inc ? node : node[next]; node; node = node[next]) {
1882                                         if (node.nodeType == 1 || !isWhiteSpaceNode(node)) {
1883                                                 return node;
1884                                         }
1885                                 }
1886                         }
1887                 }
1888
1889                 /**
1890                  * Checks if the specified node is a bookmark node or not.
1891                  *
1892                  * @private
1893                  * @param {Node} node Node to check if it's a bookmark node or not.
1894                  * @return {Boolean} true/false if the node is a bookmark node.
1895                  */
1896                 function isBookmarkNode(node) {
1897                         return node && node.nodeType == 1 && node.getAttribute('data-mce-type') == 'bookmark';
1898                 }
1899
1900                 /**
1901                  * Merges the next/previous sibling element if they match.
1902                  *
1903                  * @private
1904                  * @param {Node} prev Previous node to compare/merge.
1905                  * @param {Node} next Next node to compare/merge.
1906                  * @return {Node} Next node if we didn't merge and prev node if we did.
1907                  */
1908                 function mergeSiblings(prev, next) {
1909                         var sibling, tmpSibling;
1910
1911                         /**
1912                          * Compares two nodes and checks if it's attributes and styles matches.
1913                          * This doesn't compare classes as items since their order is significant.
1914                          *
1915                          * @private
1916                          * @param {Node} node1 First node to compare with.
1917                          * @param {Node} node2 Second node to compare with.
1918                          * @return {boolean} True/false if the nodes are the same or not.
1919                          */
1920                         function compareElements(node1, node2) {
1921                                 // Not the same name
1922                                 if (node1.nodeName != node2.nodeName) {
1923                                         return FALSE;
1924                                 }
1925
1926                                 /**
1927                                  * Returns all the nodes attributes excluding internal ones, styles and classes.
1928                                  *
1929                                  * @private
1930                                  * @param {Node} node Node to get attributes from.
1931                                  * @return {Object} Name/value object with attributes and attribute values.
1932                                  */
1933                                 function getAttribs(node) {
1934                                         var attribs = {};
1935
1936                                         each(dom.getAttribs(node), function(attr) {
1937                                                 var name = attr.nodeName.toLowerCase();
1938
1939                                                 // Don't compare internal attributes or style
1940                                                 if (name.indexOf('_') !== 0 && name !== 'style') {
1941                                                         attribs[name] = dom.getAttrib(node, name);
1942                                                 }
1943                                         });
1944
1945                                         return attribs;
1946                                 }
1947
1948                                 /**
1949                                  * Compares two objects checks if it's key + value exists in the other one.
1950                                  *
1951                                  * @private
1952                                  * @param {Object} obj1 First object to compare.
1953                                  * @param {Object} obj2 Second object to compare.
1954                                  * @return {boolean} True/false if the objects matches or not.
1955                                  */
1956                                 function compareObjects(obj1, obj2) {
1957                                         var value, name;
1958
1959                                         for (name in obj1) {
1960                                                 // Obj1 has item obj2 doesn't have
1961                                                 if (obj1.hasOwnProperty(name)) {
1962                                                         value = obj2[name];
1963
1964                                                         // Obj2 doesn't have obj1 item
1965                                                         if (value === undef) {
1966                                                                 return FALSE;
1967                                                         }
1968
1969                                                         // Obj2 item has a different value
1970                                                         if (obj1[name] != value) {
1971                                                                 return FALSE;
1972                                                         }
1973
1974                                                         // Delete similar value
1975                                                         delete obj2[name];
1976                                                 }
1977                                         }
1978
1979                                         // Check if obj 2 has something obj 1 doesn't have
1980                                         for (name in obj2) {
1981                                                 // Obj2 has item obj1 doesn't have
1982                                                 if (obj2.hasOwnProperty(name)) {
1983                                                         return FALSE;
1984                                                 }
1985                                         }
1986
1987                                         return TRUE;
1988                                 }
1989
1990                                 // Attribs are not the same
1991                                 if (!compareObjects(getAttribs(node1), getAttribs(node2))) {
1992                                         return FALSE;
1993                                 }
1994
1995                                 // Styles are not the same
1996                                 if (!compareObjects(dom.parseStyle(dom.getAttrib(node1, 'style')), dom.parseStyle(dom.getAttrib(node2, 'style')))) {
1997                                         return FALSE;
1998                                 }
1999
2000                                 return TRUE;
2001                         }
2002
2003                         function findElementSibling(node, sibling_name) {
2004                                 for (sibling = node; sibling; sibling = sibling[sibling_name]) {
2005                                         if (sibling.nodeType == 3 && sibling.nodeValue.length !== 0) {
2006                                                 return node;
2007                                         }
2008
2009                                         if (sibling.nodeType == 1 && !isBookmarkNode(sibling)) {
2010                                                 return sibling;
2011                                         }
2012                                 }
2013
2014                                 return node;
2015                         }
2016
2017                         // Check if next/prev exists and that they are elements
2018                         if (prev && next) {
2019                                 // If previous sibling is empty then jump over it
2020                                 prev = findElementSibling(prev, 'previousSibling');
2021                                 next = findElementSibling(next, 'nextSibling');
2022
2023                                 // Compare next and previous nodes
2024                                 if (compareElements(prev, next)) {
2025                                         // Append nodes between
2026                                         for (sibling = prev.nextSibling; sibling && sibling != next;) {
2027                                                 tmpSibling = sibling;
2028                                                 sibling = sibling.nextSibling;
2029                                                 prev.appendChild(tmpSibling);
2030                                         }
2031
2032                                         // Remove next node
2033                                         dom.remove(next);
2034
2035                                         // Move children into prev node
2036                                         each(grep(next.childNodes), function(node) {
2037                                                 prev.appendChild(node);
2038                                         });
2039
2040                                         return prev;
2041                                 }
2042                         }
2043
2044                         return next;
2045                 }
2046
2047                 function getContainer(rng, start) {
2048                         var container, offset, lastIdx;
2049
2050                         container = rng[start ? 'startContainer' : 'endContainer'];
2051                         offset = rng[start ? 'startOffset' : 'endOffset'];
2052
2053                         if (container.nodeType == 1) {
2054                                 lastIdx = container.childNodes.length - 1;
2055
2056                                 if (!start && offset) {
2057                                         offset--;
2058                                 }
2059
2060                                 container = container.childNodes[offset > lastIdx ? lastIdx : offset];
2061                         }
2062
2063                         // If start text node is excluded then walk to the next node
2064                         if (container.nodeType === 3 && start && offset >= container.nodeValue.length) {
2065                                 container = new TreeWalker(container, ed.getBody()).next() || container;
2066                         }
2067
2068                         // If end text node is excluded then walk to the previous node
2069                         if (container.nodeType === 3 && !start && offset === 0) {
2070                                 container = new TreeWalker(container, ed.getBody()).prev() || container;
2071                         }
2072
2073                         return container;
2074                 }
2075
2076                 function performCaretAction(type, name, vars) {
2077                         var caretContainerId = '_mce_caret', debug = ed.settings.caret_debug;
2078
2079                         // Creates a caret container bogus element
2080                         function createCaretContainer(fill) {
2081                                 var caretContainer = dom.create('span', {id: caretContainerId, 'data-mce-bogus': true, style: debug ? 'color:red' : ''});
2082
2083                                 if (fill) {
2084                                         caretContainer.appendChild(ed.getDoc().createTextNode(INVISIBLE_CHAR));
2085                                 }
2086
2087                                 return caretContainer;
2088                         }
2089
2090                         function isCaretContainerEmpty(node, nodes) {
2091                                 while (node) {
2092                                         if ((node.nodeType === 3 && node.nodeValue !== INVISIBLE_CHAR) || node.childNodes.length > 1) {
2093                                                 return false;
2094                                         }
2095
2096                                         // Collect nodes
2097                                         if (nodes && node.nodeType === 1) {
2098                                                 nodes.push(node);
2099                                         }
2100
2101                                         node = node.firstChild;
2102                                 }
2103
2104                                 return true;
2105                         }
2106
2107                         // Returns any parent caret container element
2108                         function getParentCaretContainer(node) {
2109                                 while (node) {
2110                                         if (node.id === caretContainerId) {
2111                                                 return node;
2112                                         }
2113
2114                                         node = node.parentNode;
2115                                 }
2116                         }
2117
2118                         // Finds the first text node in the specified node
2119                         function findFirstTextNode(node) {
2120                                 var walker;
2121
2122                                 if (node) {
2123                                         walker = new TreeWalker(node, node);
2124
2125                                         for (node = walker.current(); node; node = walker.next()) {
2126                                                 if (node.nodeType === 3) {
2127                                                         return node;
2128                                                 }
2129                                         }
2130                                 }
2131                         }
2132
2133                         // Removes the caret container for the specified node or all on the current document
2134                         function removeCaretContainer(node, move_caret) {
2135                                 var child, rng;
2136
2137                                 if (!node) {
2138                                         node = getParentCaretContainer(selection.getStart());
2139
2140                                         if (!node) {
2141                                                 while ((node = dom.get(caretContainerId))) {
2142                                                         removeCaretContainer(node, false);
2143                                                 }
2144                                         }
2145                                 } else {
2146                                         rng = selection.getRng(true);
2147
2148                                         if (isCaretContainerEmpty(node)) {
2149                                                 if (move_caret !== false) {
2150                                                         rng.setStartBefore(node);
2151                                                         rng.setEndBefore(node);
2152                                                 }
2153
2154                                                 dom.remove(node);
2155                                         } else {
2156                                                 child = findFirstTextNode(node);
2157
2158                                                 if (child.nodeValue.charAt(0) === INVISIBLE_CHAR) {
2159                                                         child = child.deleteData(0, 1);
2160                                                 }
2161
2162                                                 dom.remove(node, 1);
2163                                         }
2164
2165                                         selection.setRng(rng);
2166                                 }
2167                         }
2168
2169                         // Applies formatting to the caret postion
2170                         function applyCaretFormat() {
2171                                 var rng, caretContainer, textNode, offset, bookmark, container, text;
2172
2173                                 rng = selection.getRng(true);
2174                                 offset = rng.startOffset;
2175                                 container = rng.startContainer;
2176                                 text = container.nodeValue;
2177
2178                                 caretContainer = getParentCaretContainer(selection.getStart());
2179                                 if (caretContainer) {
2180                                         textNode = findFirstTextNode(caretContainer);
2181                                 }
2182
2183                                 // Expand to word is caret is in the middle of a text node and the char before/after is a alpha numeric character
2184                                 if (text && offset > 0 && offset < text.length && /\w/.test(text.charAt(offset)) && /\w/.test(text.charAt(offset - 1))) {
2185                                         // Get bookmark of caret position
2186                                         bookmark = selection.getBookmark();
2187
2188                                         // Collapse bookmark range (WebKit)
2189                                         rng.collapse(true);
2190
2191                                         // Expand the range to the closest word and split it at those points
2192                                         rng = expandRng(rng, get(name));
2193                                         rng = rangeUtils.split(rng);
2194
2195                                         // Apply the format to the range
2196                                         apply(name, vars, rng);
2197
2198                                         // Move selection back to caret position
2199                                         selection.moveToBookmark(bookmark);
2200                                 } else {
2201                                         if (!caretContainer || textNode.nodeValue !== INVISIBLE_CHAR) {
2202                                                 caretContainer = createCaretContainer(true);
2203                                                 textNode = caretContainer.firstChild;
2204
2205                                                 rng.insertNode(caretContainer);
2206                                                 offset = 1;
2207
2208                                                 apply(name, vars, caretContainer);
2209                                         } else {
2210                                                 apply(name, vars, caretContainer);
2211                                         }
2212
2213                                         // Move selection to text node
2214                                         selection.setCursorLocation(textNode, offset);
2215                                 }
2216                         }
2217
2218                         function removeCaretFormat() {
2219                                 var rng = selection.getRng(true), container, offset, bookmark,
2220                                         hasContentAfter, node, formatNode, parents = [], i, caretContainer;
2221
2222                                 container = rng.startContainer;
2223                                 offset = rng.startOffset;
2224                                 node = container;
2225
2226                                 if (container.nodeType == 3) {
2227                                         if (offset != container.nodeValue.length || container.nodeValue === INVISIBLE_CHAR) {
2228                                                 hasContentAfter = true;
2229                                         }
2230
2231                                         node = node.parentNode;
2232                                 }
2233
2234                                 while (node) {
2235                                         if (matchNode(node, name, vars)) {
2236                                                 formatNode = node;
2237                                                 break;
2238                                         }
2239
2240                                         if (node.nextSibling) {
2241                                                 hasContentAfter = true;
2242                                         }
2243
2244                                         parents.push(node);
2245                                         node = node.parentNode;
2246                                 }
2247
2248                                 // Node doesn't have the specified format
2249                                 if (!formatNode) {
2250                                         return;
2251                                 }
2252
2253                                 // Is there contents after the caret then remove the format on the element
2254                                 if (hasContentAfter) {
2255                                         // Get bookmark of caret position
2256                                         bookmark = selection.getBookmark();
2257
2258                                         // Collapse bookmark range (WebKit)
2259                                         rng.collapse(true);
2260
2261                                         // Expand the range to the closest word and split it at those points
2262                                         rng = expandRng(rng, get(name), true);
2263                                         rng = rangeUtils.split(rng);
2264
2265                                         // Remove the format from the range
2266                                         remove(name, vars, rng);
2267
2268                                         // Move selection back to caret position
2269                                         selection.moveToBookmark(bookmark);
2270                                 } else {
2271                                         caretContainer = createCaretContainer();
2272
2273                                         node = caretContainer;
2274                                         for (i = parents.length - 1; i >= 0; i--) {
2275                                                 node.appendChild(dom.clone(parents[i], false));
2276                                                 node = node.firstChild;
2277                                         }
2278
2279                                         // Insert invisible character into inner most format element
2280                                         node.appendChild(dom.doc.createTextNode(INVISIBLE_CHAR));
2281                                         node = node.firstChild;
2282
2283                                         var block = dom.getParent(formatNode, isTextBlock);
2284
2285                                         if (block && dom.isEmpty(block)) {
2286                                                 // Replace formatNode with caretContainer when removing format from empty block like <p><b>|</b></p>
2287                                                 formatNode.parentNode.replaceChild(caretContainer, formatNode);
2288                                         } else {
2289                                                 // Insert caret container after the formated node
2290                                                 dom.insertAfter(caretContainer, formatNode);
2291                                         }
2292
2293                                         // Move selection to text node
2294                                         selection.setCursorLocation(node, 1);
2295                                         // If the formatNode is empty, we can remove it safely. 
2296                                         if(dom.isEmpty(formatNode)) {
2297                                                 dom.remove(formatNode);
2298                                         }
2299                                 }
2300                         }
2301
2302                         // Checks if the parent caret container node isn't empty if that is the case it
2303                         // will remove the bogus state on all children that isn't empty
2304                         function unmarkBogusCaretParents() {
2305                                 var caretContainer;
2306
2307                                 caretContainer = getParentCaretContainer(selection.getStart());
2308                                 if (caretContainer && !dom.isEmpty(caretContainer)) {
2309                                         walk(caretContainer, function(node) {
2310                                                 if (node.nodeType == 1 && node.id !== caretContainerId && !dom.isEmpty(node)) {
2311                                                         dom.setAttrib(node, 'data-mce-bogus', null);
2312                                                 }
2313                                         }, 'childNodes');
2314                                 }
2315                         }
2316
2317                         // Only bind the caret events once
2318                         if (!ed._hasCaretEvents) {
2319                                 // Mark current caret container elements as bogus when getting the contents so we don't end up with empty elements
2320                                 markCaretContainersBogus = function() {
2321                                         var nodes = [], i;
2322
2323                                         if (isCaretContainerEmpty(getParentCaretContainer(selection.getStart()), nodes)) {
2324                                                 // Mark children
2325                                                 i = nodes.length;
2326                                                 while (i--) {
2327                                                         dom.setAttrib(nodes[i], 'data-mce-bogus', '1');
2328                                                 }
2329                                         }
2330                                 };
2331
2332                                 disableCaretContainer = function(e) {
2333                                         var keyCode = e.keyCode;
2334
2335                                         removeCaretContainer();
2336
2337                                         // Remove caret container on keydown and it's a backspace, enter or left/right arrow keys
2338                                         if (keyCode == 8 || keyCode == 37 || keyCode == 39) {
2339                                                 removeCaretContainer(getParentCaretContainer(selection.getStart()));
2340                                         }
2341
2342                                         unmarkBogusCaretParents();
2343                                 };
2344
2345                                 // Remove bogus state if they got filled by contents using editor.selection.setContent
2346                                 ed.on('SetContent', function(e) {
2347                                         if (e.selection) {
2348                                                 unmarkBogusCaretParents();
2349                                         }
2350                                 });
2351                                 ed._hasCaretEvents = true;
2352                         }
2353
2354                         // Do apply or remove caret format
2355                         if (type == "apply") {
2356                                 applyCaretFormat();
2357                         } else {
2358                                 removeCaretFormat();
2359                         }
2360                 }
2361
2362                 /**
2363                  * Moves the start to the first suitable text node.
2364                  */
2365                 function moveStart(rng) {
2366                         var container = rng.startContainer,
2367                                         offset = rng.startOffset, isAtEndOfText,
2368                                         walker, node, nodes, tmpNode;
2369
2370                         // Convert text node into index if possible
2371                         if (container.nodeType == 3 && offset >= container.nodeValue.length) {
2372                                 // Get the parent container location and walk from there
2373                                 offset = nodeIndex(container);
2374                                 container = container.parentNode;
2375                                 isAtEndOfText = true;
2376                         }
2377
2378                         // Move startContainer/startOffset in to a suitable node
2379                         if (container.nodeType == 1) {
2380                                 nodes = container.childNodes;
2381                                 container = nodes[Math.min(offset, nodes.length - 1)];
2382                                 walker = new TreeWalker(container, dom.getParent(container, dom.isBlock));
2383
2384                                 // If offset is at end of the parent node walk to the next one
2385                                 if (offset > nodes.length - 1 || isAtEndOfText) {
2386                                         walker.next();
2387                                 }
2388
2389                                 for (node = walker.current(); node; node = walker.next()) {
2390                                         if (node.nodeType == 3 && !isWhiteSpaceNode(node)) {
2391                                                 // IE has a "neat" feature where it moves the start node into the closest element
2392                                                 // we can avoid this by inserting an element before it and then remove it after we set the selection
2393                                                 tmpNode = dom.create('a', null, INVISIBLE_CHAR);
2394                                                 node.parentNode.insertBefore(tmpNode, node);
2395
2396                                                 // Set selection and remove tmpNode
2397                                                 rng.setStart(node, 0);
2398                                                 selection.setRng(rng);
2399                                                 dom.remove(tmpNode);
2400
2401                                                 return;
2402                                         }
2403                                 }
2404                         }
2405                 }
2406         };
2407 });