Upgrade TinyMCE to v3.4.5
[citadel.git] / webcit / tiny_mce / plugins / lists / editor_plugin_src.js
1 /**
2  * editor_plugin_src.js
3  *
4  * Copyright 2011, Moxiecode Systems AB
5  * Released under LGPL License.
6  *
7  * License: http://tinymce.moxiecode.com/license
8  * Contributing: http://tinymce.moxiecode.com/contributing
9  */
10
11 (function() {
12         var each = tinymce.each, Event = tinymce.dom.Event, bookmark;
13
14         // Skips text nodes that only contain whitespace since they aren't semantically important.
15         function skipWhitespaceNodes(e, next) {
16                 while (e && (e.nodeType === 8 || (e.nodeType === 3 && /^[ \t\n\r]*$/.test(e.nodeValue)))) {
17                         e = next(e);
18                 }
19                 return e;
20         }
21
22         function skipWhitespaceNodesBackwards(e) {
23                 return skipWhitespaceNodes(e, function(e) {
24                         return e.previousSibling;
25                 });
26         }
27
28         function skipWhitespaceNodesForwards(e) {
29                 return skipWhitespaceNodes(e, function(e) {
30                         return e.nextSibling;
31                 });
32         }
33
34         function hasParentInList(ed, e, list) {
35                 return ed.dom.getParent(e, function(p) {
36                         return tinymce.inArray(list, p) !== -1;
37                 });
38         }
39
40         function isList(e) {
41                 return e && (e.tagName === 'OL' || e.tagName === 'UL');
42         }
43
44         function splitNestedLists(element, dom) {
45                 var tmp, nested, wrapItem;
46                 tmp = skipWhitespaceNodesBackwards(element.lastChild);
47                 while (isList(tmp)) {
48                         nested = tmp;
49                         tmp = skipWhitespaceNodesBackwards(nested.previousSibling);
50                 }
51                 if (nested) {
52                         wrapItem = dom.create('li', { style: 'list-style-type: none;'});
53                         dom.split(element, nested);
54                         dom.insertAfter(wrapItem, nested);
55                         wrapItem.appendChild(nested);
56                         wrapItem.appendChild(nested);
57                         element = wrapItem.previousSibling;
58                 }
59                 return element;
60         }
61
62         function attemptMergeWithAdjacent(e, allowDifferentListStyles, mergeParagraphs) {
63                 e = attemptMergeWithPrevious(e, allowDifferentListStyles, mergeParagraphs);
64                 return attemptMergeWithNext(e, allowDifferentListStyles, mergeParagraphs);
65         }
66
67         function attemptMergeWithPrevious(e, allowDifferentListStyles, mergeParagraphs) {
68                 var prev = skipWhitespaceNodesBackwards(e.previousSibling);
69                 if (prev) {
70                         return attemptMerge(prev, e, allowDifferentListStyles ? prev : false, mergeParagraphs);
71                 } else {
72                         return e;
73                 }
74         }
75
76         function attemptMergeWithNext(e, allowDifferentListStyles, mergeParagraphs) {
77                 var next = skipWhitespaceNodesForwards(e.nextSibling);
78                 if (next) {
79                         return attemptMerge(e, next, allowDifferentListStyles ? next : false, mergeParagraphs);
80                 } else {
81                         return e;
82                 }
83         }
84
85         function attemptMerge(e1, e2, differentStylesMasterElement, mergeParagraphs) {
86                 if (canMerge(e1, e2, !!differentStylesMasterElement, mergeParagraphs)) {
87                         return merge(e1, e2, differentStylesMasterElement);
88                 } else if (e1 && e1.tagName === 'LI' && isList(e2)) {
89                         // Fix invalidly nested lists.
90                         e1.appendChild(e2);
91                 }
92                 return e2;
93         }
94
95         function canMerge(e1, e2, allowDifferentListStyles, mergeParagraphs) {
96                 if (!e1 || !e2) {
97                         return false;
98                 } else if (e1.tagName === 'LI' && e2.tagName === 'LI') {
99                         return e2.style.listStyleType === 'none' || containsOnlyAList(e2);
100                 } else if (isList(e1)) {
101                         return (e1.tagName === e2.tagName && (allowDifferentListStyles || e1.style.listStyleType === e2.style.listStyleType)) || isListForIndent(e2);
102                 } else if (mergeParagraphs && e1.tagName === 'P' && e2.tagName === 'P') {
103                         return true;
104                 } else {
105                         return false;
106                 }
107         }
108
109         function isListForIndent(e) {
110                 var firstLI = skipWhitespaceNodesForwards(e.firstChild), lastLI = skipWhitespaceNodesBackwards(e.lastChild);
111                 return firstLI && lastLI && isList(e) && firstLI === lastLI && (isList(firstLI) || firstLI.style.listStyleType === 'none' || containsOnlyAList(firstLI));
112         }
113
114         function containsOnlyAList(e) {
115                 var firstChild = skipWhitespaceNodesForwards(e.firstChild), lastChild = skipWhitespaceNodesBackwards(e.lastChild);
116                 return firstChild && lastChild && firstChild === lastChild && isList(firstChild);
117         }
118
119         function merge(e1, e2, masterElement) {
120                 var lastOriginal = skipWhitespaceNodesBackwards(e1.lastChild), firstNew = skipWhitespaceNodesForwards(e2.firstChild);
121                 if (e1.tagName === 'P') {
122                         e1.appendChild(e1.ownerDocument.createElement('br'));
123                 }
124                 while (e2.firstChild) {
125                         e1.appendChild(e2.firstChild);
126                 }
127                 if (masterElement) {
128                         e1.style.listStyleType = masterElement.style.listStyleType;
129                 }
130                 e2.parentNode.removeChild(e2);
131                 attemptMerge(lastOriginal, firstNew, false);
132                 return e1;
133         }
134
135         function findItemToOperateOn(e, dom) {
136                 var item;
137                 if (!dom.is(e, 'li,ol,ul')) {
138                         item = dom.getParent(e, 'li');
139                         if (item) {
140                                 e = item;
141                         }
142                 }
143                 return e;
144         }
145
146         tinymce.create('tinymce.plugins.Lists', {
147                 init: function(ed, url) {
148                         var LIST_TABBING = 0;
149                         var LIST_EMPTY_ITEM = 1;
150                         var LIST_ESCAPE = 2;
151                         var LIST_UNKNOWN = 3;
152                         var state = LIST_UNKNOWN;
153
154                         function isTabInList(e) {
155                                 return e.keyCode === 9 && (ed.queryCommandState('InsertUnorderedList') || ed.queryCommandState('InsertOrderedList'));
156                         }
157
158                         function isOnLastListItem() {
159                                 var li = getLi();
160                                 var grandParent = li.parentNode.parentNode;
161                                 var isLastItem = li.parentNode.lastChild === li;
162                                 return isLastItem && !isNestedList(grandParent) && isEmptyListItem(li);
163                         }
164
165                         function isNestedList(grandParent) {
166                                 if (isList(grandParent)) {
167                                         return grandParent.parentNode && grandParent.parentNode.tagName === 'LI';
168                                 } else {
169                                         return  grandParent.tagName === 'LI';
170                                 }
171                         }
172
173                         function isInEmptyListItem() {
174                                 return ed.selection.isCollapsed() && isEmptyListItem(getLi());
175                         }
176
177                         function getLi() {
178                                 var n = ed.selection.getStart();
179                                 // Get start will return BR if the LI only contains a BR or an empty element as we use these to fix caret position
180                                 return ((n.tagName == 'BR' || n.tagName == '') && n.parentNode.tagName == 'LI') ? n.parentNode : n;
181                         }
182
183                         function isEmptyListItem(li) {
184                                 var numChildren = li.childNodes.length;
185                                 if (li.tagName === 'LI') {
186                                         return numChildren == 0 ? true : numChildren == 1 && (li.firstChild.tagName == '' || isEmptyWebKitLi(li) || isEmptyIE9Li(li));
187                                 }
188                                 return false;
189                         }
190
191                         function isEmptyWebKitLi(li) {
192                                 // Check for empty LI or a LI with just a child that is a BR since Gecko and WebKit uses BR elements to place the caret
193                                 return tinymce.isWebKit && li.firstChild.nodeName == 'BR';
194                         }
195
196                         function isEmptyIE9Li(li) {
197                                 // only consider this to be last item if there is no list item content or that content is nbsp or space since IE9 creates these
198                                 var lis = tinymce.grep(li.parentNode.childNodes, function(n) {return n.nodeName == 'LI'});
199                                 var isLastLi = li == lis[lis.length - 1];
200                                 var child = li.firstChild;
201                                 return tinymce.isIE9 && isLastLi && (child.nodeValue == String.fromCharCode(160) || child.nodeValue == String.fromCharCode(32));
202                         }
203
204                         function isEnter(e) {
205                                 return e.keyCode === 13;
206                         }
207
208                         function getListKeyState(e) {
209                                 if (isTabInList(e)) {
210                                         return LIST_TABBING;
211                                 } else if (isEnter(e) && isOnLastListItem()) {
212                                         return LIST_ESCAPE;
213                                 } else if (isEnter(e) && isInEmptyListItem()) {
214                                         return LIST_EMPTY_ITEM;
215                                 } else {
216                                         return LIST_UNKNOWN;
217                                 }
218                         }
219
220                         function cancelEnterAndTab(_, e) {
221                                 if (state == LIST_TABBING || state == LIST_EMPTY_ITEM) {
222                                         return Event.cancel(e);
223                                 }
224                         }
225
226                         function imageJoiningListItem(ed, e) {
227                                 var prevSibling;
228
229                                 if (!tinymce.isGecko)
230                                         return;
231
232                                 var n = ed.selection.getStart();
233                                 if (e.keyCode != 8 || n.tagName !== 'IMG')
234                                         return;
235
236                                 function lastLI(node) {
237                                         var child = node.firstChild;
238                                         var li = null;
239                                         do {
240                                                 if (!child)
241                                                         break;
242
243                                                 if (child.tagName === 'LI')
244                                                         li = child;
245                                         } while (child = child.nextSibling);
246
247                                         return li;
248                                 }
249
250                                 function addChildren(parentNode, destination) {
251                                         while (parentNode.childNodes.length > 0)
252                                                 destination.appendChild(parentNode.childNodes[0]);
253                                 }
254
255                                 // Check if there is a previous sibling
256                                 prevSibling = n.parentNode.previousSibling;
257                                 if (!prevSibling)
258                                         return;
259
260                                 var ul;
261                                 if (prevSibling.tagName === 'UL' || prevSibling.tagName === 'OL')
262                                         ul = prevSibling;
263                                 else if (prevSibling.previousSibling && (prevSibling.previousSibling.tagName === 'UL' || prevSibling.previousSibling.tagName === 'OL'))
264                                         ul = prevSibling.previousSibling;
265                                 else
266                                         return;
267
268                                 var li = lastLI(ul);
269
270                                 // move the caret to the end of the list item
271                                 var rng = ed.dom.createRng();
272                                 rng.setStart(li, 1);
273                                 rng.setEnd(li, 1);
274                                 ed.selection.setRng(rng);
275                                 ed.selection.collapse(true);
276
277                                 // save a bookmark at the end of the list item
278                                 var bookmark = ed.selection.getBookmark();
279
280                                 // copy the image an its text to the list item
281                                 var clone = n.parentNode.cloneNode(true);
282                                 if (clone.tagName === 'P' || clone.tagName === 'DIV')
283                                         addChildren(clone, li);
284                                 else
285                                         li.appendChild(clone);
286
287                                 // remove the old copy of the image
288                                 n.parentNode.parentNode.removeChild(n.parentNode);
289
290                                 // move the caret where we saved the bookmark
291                                 ed.selection.moveToBookmark(bookmark);
292                         }
293
294                         // fix the cursor position to ensure it is correct in IE
295                         function setCursorPositionToOriginalLi(li) {
296                                 var list = ed.dom.getParent(li, 'ol,ul');
297                                 if (list != null) {
298                                         var lastLi = list.lastChild;
299                                         lastLi.appendChild(ed.getDoc().createElement(''));
300                                         ed.selection.setCursorLocation(lastLi, 0);
301                                 }
302                         }
303
304                         this.ed = ed;
305                         ed.addCommand('Indent', this.indent, this);
306                         ed.addCommand('Outdent', this.outdent, this);
307                         ed.addCommand('InsertUnorderedList', function() {
308                                 this.applyList('UL', 'OL');
309                         }, this);
310                         ed.addCommand('InsertOrderedList', function() {
311                                 this.applyList('OL', 'UL');
312                         }, this);
313
314                         ed.onInit.add(function() {
315                                 ed.editorCommands.addCommands({
316                                         'outdent': function() {
317                                                 var sel = ed.selection, dom = ed.dom;
318
319                                                 function hasStyleIndent(n) {
320                                                         n = dom.getParent(n, dom.isBlock);
321                                                         return n && (parseInt(ed.dom.getStyle(n, 'margin-left') || 0, 10) + parseInt(ed.dom.getStyle(n, 'padding-left') || 0, 10)) > 0;
322                                                 }
323
324                                                 return hasStyleIndent(sel.getStart()) || hasStyleIndent(sel.getEnd()) || ed.queryCommandState('InsertOrderedList') || ed.queryCommandState('InsertUnorderedList');
325                                         }
326                                 }, 'state');
327                         });
328
329                         ed.onKeyUp.add(function(ed, e) {
330                                 if (state == LIST_TABBING) {
331                                         ed.execCommand(e.shiftKey ? 'Outdent' : 'Indent', true, null);
332                                         state = LIST_UNKNOWN;
333                                         return Event.cancel(e);
334                                 } else if (state == LIST_EMPTY_ITEM) {
335                                         var li = getLi();
336                                         var shouldOutdent =  ed.settings.list_outdent_on_enter === true || e.shiftKey;
337                                         ed.execCommand(shouldOutdent ? 'Outdent' : 'Indent', true, null);
338                                         if (tinymce.isIE) {
339                                                 setCursorPositionToOriginalLi(li);
340                                         }
341                                         return Event.cancel(e);
342                                 } else if (state == LIST_ESCAPE) {
343                                         if (tinymce.isIE8) {
344                                                 // append a zero sized nbsp so that caret is positioned correctly in IE8 after escaping and applying formatting.
345                                                 // if there is no text then applying formatting for e.g a H1 to the P tag immediately following list after
346                                                 // escaping from it will cause the caret to be positioned on the last li instead of staying the in P tag.
347                                                 var n = ed.getDoc().createTextNode('\uFEFF');
348                                                 ed.selection.getNode().appendChild(n);
349                                         } else if (tinymce.isIE9) {
350                                                 // IE9 does not escape the list so we use outdent to do this and cancel the default behaviour
351                                                 ed.execCommand('Outdent');
352                                                 return Event.cancel(e);
353                                         }
354                                 }
355                         });
356                         ed.onKeyDown.add(function(_, e) { state = getListKeyState(e); });
357                         ed.onKeyDown.add(cancelEnterAndTab);
358                         ed.onKeyDown.add(imageJoiningListItem);
359                         ed.onKeyPress.add(cancelEnterAndTab);
360                 },
361
362                 applyList: function(targetListType, oppositeListType) {
363                         var t = this, ed = t.ed, dom = ed.dom, applied = [], hasSameType = false, hasOppositeType = false, hasNonList = false, actions,
364                                         selectedBlocks = ed.selection.getSelectedBlocks();
365
366                         function cleanupBr(e) {
367                                 if (e && e.tagName === 'BR') {
368                                         dom.remove(e);
369                                 }
370                         }
371
372                         function makeList(element) {
373                                 var list = dom.create(targetListType), li;
374
375                                 function adjustIndentForNewList(element) {
376                                         // If there's a margin-left, outdent one level to account for the extra list margin.
377                                         if (element.style.marginLeft || element.style.paddingLeft) {
378                                                 t.adjustPaddingFunction(false)(element);
379                                         }
380                                 }
381
382                                 if (element.tagName === 'LI') {
383                                         // No change required.
384                                 } else if (element.tagName === 'P' || element.tagName === 'DIV' || element.tagName === 'BODY') {
385                                         processBrs(element, function(startSection, br, previousBR) {
386                                                 doWrapList(startSection, br, element.tagName === 'BODY' ? null : startSection.parentNode);
387                                                 li = startSection.parentNode;
388                                                 adjustIndentForNewList(li);
389                                                 cleanupBr(br);
390                                         });
391                                         if (element.tagName === 'P' || selectedBlocks.length > 1) {
392                                                 dom.split(li.parentNode.parentNode, li.parentNode);
393                                         }
394                                         attemptMergeWithAdjacent(li.parentNode, true);
395                                         return;
396                                 } else {
397                                         // Put the list around the element.
398                                         li = dom.create('li');
399                                         dom.insertAfter(li, element);
400                                         li.appendChild(element);
401                                         adjustIndentForNewList(element);
402                                         element = li;
403                                 }
404                                 dom.insertAfter(list, element);
405                                 list.appendChild(element);
406                                 attemptMergeWithAdjacent(list, true);
407                                 applied.push(element);
408                         }
409
410                         function doWrapList(start, end, template) {
411                                 var li, n = start, tmp, i;
412                                 while (!dom.isBlock(start.parentNode) && start.parentNode !== dom.getRoot()) {
413                                         start = dom.split(start.parentNode, start.previousSibling);
414                                         start = start.nextSibling;
415                                         n = start;
416                                 }
417                                 if (template) {
418                                         li = template.cloneNode(true);
419                                         start.parentNode.insertBefore(li, start);
420                                         while (li.firstChild) dom.remove(li.firstChild);
421                                         li = dom.rename(li, 'li');
422                                 } else {
423                                         li = dom.create('li');
424                                         start.parentNode.insertBefore(li, start);
425                                 }
426                                 while (n && n != end) {
427                                         tmp = n.nextSibling;
428                                         li.appendChild(n);
429                                         n = tmp;
430                                 }
431                                 if (li.childNodes.length === 0) {
432                                         li.innerHTML = '<br _mce_bogus="1" />';
433                                 }
434                                 makeList(li);
435                         }
436
437                         function processBrs(element, callback) {
438                                 var startSection, previousBR, END_TO_START = 3, START_TO_END = 1,
439                                                 breakElements = 'br,ul,ol,p,div,h1,h2,h3,h4,h5,h6,table,blockquote,address,pre,form,center,dl';
440
441                                 function isAnyPartSelected(start, end) {
442                                         var r = dom.createRng(), sel;
443                                         bookmark.keep = true;
444                                         ed.selection.moveToBookmark(bookmark);
445                                         bookmark.keep = false;
446                                         sel = ed.selection.getRng(true);
447                                         if (!end) {
448                                                 end = start.parentNode.lastChild;
449                                         }
450                                         r.setStartBefore(start);
451                                         r.setEndAfter(end);
452                                         return !(r.compareBoundaryPoints(END_TO_START, sel) > 0 || r.compareBoundaryPoints(START_TO_END, sel) <= 0);
453                                 }
454
455                                 function nextLeaf(br) {
456                                         if (br.nextSibling)
457                                                 return br.nextSibling;
458                                         if (!dom.isBlock(br.parentNode) && br.parentNode !== dom.getRoot())
459                                                 return nextLeaf(br.parentNode);
460                                 }
461
462                                 // Split on BRs within the range and process those.
463                                 startSection = element.firstChild;
464                                 // First mark the BRs that have any part of the previous section selected.
465                                 var trailingContentSelected = false;
466                                 each(dom.select(breakElements, element), function(br) {
467                                         var b;
468                                         if (br.hasAttribute && br.hasAttribute('_mce_bogus')) {
469                                                 return true; // Skip the bogus Brs that are put in to appease Firefox and Safari.
470                                         }
471                                         if (isAnyPartSelected(startSection, br)) {
472                                                 dom.addClass(br, '_mce_tagged_br');
473                                                 startSection = nextLeaf(br);
474                                         }
475                                 });
476                                 trailingContentSelected = (startSection && isAnyPartSelected(startSection, undefined));
477                                 startSection = element.firstChild;
478                                 each(dom.select(breakElements, element), function(br) {
479                                         // Got a section from start to br.
480                                         var tmp = nextLeaf(br);
481                                         if (br.hasAttribute && br.hasAttribute('_mce_bogus')) {
482                                                 return true; // Skip the bogus Brs that are put in to appease Firefox and Safari.
483                                         }
484                                         if (dom.hasClass(br, '_mce_tagged_br')) {
485                                                 callback(startSection, br, previousBR);
486                                                 previousBR = null;
487                                         } else {
488                                                 previousBR = br;
489                                         }
490                                         startSection = tmp;
491                                 });
492                                 if (trailingContentSelected) {
493                                         callback(startSection, undefined, previousBR);
494                                 }
495                         }
496
497                         function wrapList(element) {
498                                 processBrs(element, function(startSection, br, previousBR) {
499                                         // Need to indent this part
500                                         doWrapList(startSection, br);
501                                         cleanupBr(br);
502                                         cleanupBr(previousBR);
503                                 });
504                         }
505
506                         function changeList(element) {
507                                 if (tinymce.inArray(applied, element) !== -1) {
508                                         return;
509                                 }
510                                 if (element.parentNode.tagName === oppositeListType) {
511                                         dom.split(element.parentNode, element);
512                                         makeList(element);
513                                         attemptMergeWithNext(element.parentNode, false);
514                                 }
515                                 applied.push(element);
516                         }
517
518                         function convertListItemToParagraph(element) {
519                                 var child, nextChild, mergedElement, splitLast;
520                                 if (tinymce.inArray(applied, element) !== -1) {
521                                         return;
522                                 }
523                                 element = splitNestedLists(element, dom);
524                                 while (dom.is(element.parentNode, 'ol,ul,li')) {
525                                         dom.split(element.parentNode, element);
526                                 }
527                                 // Push the original element we have from the selection, not the renamed one.
528                                 applied.push(element);
529                                 element = dom.rename(element, 'p');
530                                 mergedElement = attemptMergeWithAdjacent(element, false, ed.settings.force_br_newlines);
531                                 if (mergedElement === element) {
532                                         // Now split out any block elements that can't be contained within a P.
533                                         // Manually iterate to ensure we handle modifications correctly (doesn't work with tinymce.each)
534                                         child = element.firstChild;
535                                         while (child) {
536                                                 if (dom.isBlock(child)) {
537                                                         child = dom.split(child.parentNode, child);
538                                                         splitLast = true;
539                                                         nextChild = child.nextSibling && child.nextSibling.firstChild;
540                                                 } else {
541                                                         nextChild = child.nextSibling;
542                                                         if (splitLast && child.tagName === 'BR') {
543                                                                 dom.remove(child);
544                                                         }
545                                                         splitLast = false;
546                                                 }
547                                                 child = nextChild;
548                                         }
549                                 }
550                         }
551
552                         each(selectedBlocks, function(e) {
553                                 e = findItemToOperateOn(e, dom);
554                                 if (e.tagName === oppositeListType || (e.tagName === 'LI' && e.parentNode.tagName === oppositeListType)) {
555                                         hasOppositeType = true;
556                                 } else if (e.tagName === targetListType || (e.tagName === 'LI' && e.parentNode.tagName === targetListType)) {
557                                         hasSameType = true;
558                                 } else {
559                                         hasNonList = true;
560                                 }
561                         });
562
563                         if (hasNonList || hasOppositeType || selectedBlocks.length === 0) {
564                                 actions = {
565                                         'LI': changeList,
566                                         'H1': makeList,
567                                         'H2': makeList,
568                                         'H3': makeList,
569                                         'H4': makeList,
570                                         'H5': makeList,
571                                         'H6': makeList,
572                                         'P': makeList,
573                                         'BODY': makeList,
574                                         'DIV': selectedBlocks.length > 1 ? makeList : wrapList,
575                                         defaultAction: wrapList
576                                 };
577                         } else {
578                                 actions = {
579                                         defaultAction: convertListItemToParagraph
580                                 };
581                         }
582                         this.process(actions);
583                 },
584
585                 indent: function() {
586                         var ed = this.ed, dom = ed.dom, indented = [];
587
588                         function createWrapItem(element) {
589                                 var wrapItem = dom.create('li', { style: 'list-style-type: none;'});
590                                 dom.insertAfter(wrapItem, element);
591                                 return wrapItem;
592                         }
593
594                         function createWrapList(element) {
595                                 var wrapItem = createWrapItem(element),
596                                                 list = dom.getParent(element, 'ol,ul'),
597                                                 listType = list.tagName,
598                                                 listStyle = dom.getStyle(list, 'list-style-type'),
599                                                 attrs = {},
600                                                 wrapList;
601                                 if (listStyle !== '') {
602                                         attrs.style = 'list-style-type: ' + listStyle + ';';
603                                 }
604                                 wrapList = dom.create(listType, attrs);
605                                 wrapItem.appendChild(wrapList);
606                                 return wrapList;
607                         }
608
609                         function indentLI(element) {
610                                 if (!hasParentInList(ed, element, indented)) {
611                                         element = splitNestedLists(element, dom);
612                                         var wrapList = createWrapList(element);
613                                         wrapList.appendChild(element);
614                                         attemptMergeWithAdjacent(wrapList.parentNode, false);
615                                         attemptMergeWithAdjacent(wrapList, false);
616                                         indented.push(element);
617                                 }
618                         }
619
620                         this.process({
621                                 'LI': indentLI,
622                                 defaultAction: this.adjustPaddingFunction(true)
623                         });
624
625                 },
626
627                 outdent: function() {
628                         var t = this, ed = t.ed, dom = ed.dom, outdented = [];
629
630                         function outdentLI(element) {
631                                 var listElement, targetParent, align;
632                                 if (!hasParentInList(ed, element, outdented)) {
633                                         if (dom.getStyle(element, 'margin-left') !== '' || dom.getStyle(element, 'padding-left') !== '') {
634                                                 return t.adjustPaddingFunction(false)(element);
635                                         }
636                                         align = dom.getStyle(element, 'text-align', true);
637                                         if (align === 'center' || align === 'right') {
638                                                 dom.setStyle(element, 'text-align', 'left');
639                                                 return;
640                                         }
641                                         element = splitNestedLists(element, dom);
642                                         listElement = element.parentNode;
643                                         targetParent = element.parentNode.parentNode;
644                                         if (targetParent.tagName === 'P') {
645                                                 dom.split(targetParent, element.parentNode);
646                                         } else {
647                                                 dom.split(listElement, element);
648                                                 if (targetParent.tagName === 'LI') {
649                                                         // Nested list, need to split the LI and go back out to the OL/UL element.
650                                                         dom.split(targetParent, element);
651                                                 } else if (!dom.is(targetParent, 'ol,ul')) {
652                                                         dom.rename(element, 'p');
653                                                 }
654                                         }
655                                         outdented.push(element);
656                                 }
657                         }
658
659                         this.process({
660                                 'LI': outdentLI,
661                                 defaultAction: this.adjustPaddingFunction(false)
662                         });
663
664                         each(outdented, attemptMergeWithAdjacent);
665                 },
666
667                 process: function(actions) {
668                         var t = this, sel = t.ed.selection, dom = t.ed.dom, selectedBlocks, r;
669
670                         function processElement(element) {
671                                 dom.removeClass(element, '_mce_act_on');
672                                 if (!element || element.nodeType !== 1) {
673                                         return;
674                                 }
675                                 element = findItemToOperateOn(element, dom);
676                                 var action = actions[element.tagName];
677                                 if (!action) {
678                                         action = actions.defaultAction;
679                                 }
680                                 action(element);
681                         }
682
683                         function recurse(element) {
684                                 t.splitSafeEach(element.childNodes, processElement);
685                         }
686
687                         function brAtEdgeOfSelection(container, offset) {
688                                 return offset >= 0 && container.hasChildNodes() && offset < container.childNodes.length &&
689                                                 container.childNodes[offset].tagName === 'BR';
690                         }
691
692                         selectedBlocks = sel.getSelectedBlocks();
693                         if (selectedBlocks.length === 0) {
694                                 selectedBlocks = [ dom.getRoot() ];
695                         }
696
697                         r = sel.getRng(true);
698                         if (!r.collapsed) {
699                                 if (brAtEdgeOfSelection(r.endContainer, r.endOffset - 1)) {
700                                         r.setEnd(r.endContainer, r.endOffset - 1);
701                                         sel.setRng(r);
702                                 }
703                                 if (brAtEdgeOfSelection(r.startContainer, r.startOffset)) {
704                                         r.setStart(r.startContainer, r.startOffset + 1);
705                                         sel.setRng(r);
706                                 }
707                         }
708
709
710                         if (tinymce.isIE8) {
711                                 // append a zero sized nbsp so that caret is restored correctly using bookmark
712                                 var s = t.ed.selection.getNode();
713                                 if (s.tagName === 'LI' && !(s.parentNode.lastChild === s)) {
714                                         var i = t.ed.getDoc().createTextNode('\uFEFF');
715                                         s.appendChild(i);
716                                 }
717                         }
718
719                         bookmark = sel.getBookmark();
720                         actions.OL = actions.UL = recurse;
721                         t.splitSafeEach(selectedBlocks, processElement);
722                         sel.moveToBookmark(bookmark);
723                         bookmark = null;
724                         // Avoids table or image handles being left behind in Firefox.
725                         t.ed.execCommand('mceRepaint');
726                 },
727
728                 splitSafeEach: function(elements, f) {
729                         if (tinymce.isGecko && (/Firefox\/[12]\.[0-9]/.test(navigator.userAgent) ||
730                                         /Firefox\/3\.[0-4]/.test(navigator.userAgent))) {
731                                 this.classBasedEach(elements, f);
732                         } else {
733                                 each(elements, f);
734                         }
735                 },
736
737                 classBasedEach: function(elements, f) {
738                         var dom = this.ed.dom, nodes, element;
739                         // Mark nodes
740                         each(elements, function(element) {
741                                 dom.addClass(element, '_mce_act_on');
742                         });
743                         nodes = dom.select('._mce_act_on');
744                         while (nodes.length > 0) {
745                                 element = nodes.shift();
746                                 dom.removeClass(element, '_mce_act_on');
747                                 f(element);
748                                 nodes = dom.select('._mce_act_on');
749                         }
750                 },
751
752                 adjustPaddingFunction: function(isIndent) {
753                         var indentAmount, indentUnits, ed = this.ed;
754                         indentAmount = ed.settings.indentation;
755                         indentUnits = /[a-z%]+/i.exec(indentAmount);
756                         indentAmount = parseInt(indentAmount, 10);
757                         return function(element) {
758                                 var currentIndent, newIndentAmount;
759                                 currentIndent = parseInt(ed.dom.getStyle(element, 'margin-left') || 0, 10) + parseInt(ed.dom.getStyle(element, 'padding-left') || 0, 10);
760                                 if (isIndent) {
761                                         newIndentAmount = currentIndent + indentAmount;
762                                 } else {
763                                         newIndentAmount = currentIndent - indentAmount;
764                                 }
765                                 ed.dom.setStyle(element, 'padding-left', '');
766                                 ed.dom.setStyle(element, 'margin-left', newIndentAmount > 0 ? newIndentAmount + indentUnits : '');
767                         };
768                 },
769
770                 getInfo: function() {
771                         return {
772                                 longname : 'Lists',
773                                 author : 'Moxiecode Systems AB',
774                                 authorurl : 'http://tinymce.moxiecode.com',
775                                 infourl : 'http://wiki.moxiecode.com/index.php/TinyMCE:Plugins/lists',
776                                 version : tinymce.majorVersion + "." + tinymce.minorVersion
777                         };
778                 }
779         });
780         tinymce.PluginManager.add("lists", tinymce.plugins.Lists);
781 }());