]> CyberLeo.Net >> Repos - Github/sugarcrm.git/blob - include/javascript/tiny_mce/plugins/lists/editor_plugin_src.js
Release 6.5.0
[Github/sugarcrm.git] / include / javascript / 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                                         return Event.cancel(e);
333                                 } else if (state == LIST_EMPTY_ITEM) {
334                                         var li = getLi();
335                                         var shouldOutdent =  ed.settings.list_outdent_on_enter === true || e.shiftKey;
336                                         ed.execCommand(shouldOutdent ? 'Outdent' : 'Indent', true, null);
337                                         if (tinymce.isIE) {
338                                                 setCursorPositionToOriginalLi(li);
339                                         }
340                                         return Event.cancel(e);
341                                 } else if (state == LIST_ESCAPE) {
342                                         if (tinymce.isIE8) {
343                                                 // append a zero sized nbsp so that caret is positioned correctly in IE8 after escaping and applying formatting.
344                                                 // if there is no text then applying formatting for e.g a H1 to the P tag immediately following list after
345                                                 // escaping from it will cause the caret to be positioned on the last li instead of staying the in P tag.
346                                                 var n = ed.getDoc().createTextNode('\uFEFF');
347                                                 ed.selection.getNode().appendChild(n);
348                                         } else if (tinymce.isIE9) {
349                                                 // IE9 does not escape the list so we use outdent to do this and cancel the default behaviour
350                                                 ed.execCommand('Outdent');
351                                                 return Event.cancel(e);
352                                         }
353                                 }
354                         });
355                         ed.onKeyDown.add(function(_, e) { state = getListKeyState(e); });
356                         ed.onKeyDown.add(cancelEnterAndTab);
357                         ed.onKeyDown.add(imageJoiningListItem);
358                         ed.onKeyPress.add(cancelEnterAndTab);
359                 },
360
361                 applyList: function(targetListType, oppositeListType) {
362                         var t = this, ed = t.ed, dom = ed.dom, applied = [], hasSameType = false, hasOppositeType = false, hasNonList = false, actions,
363                                         selectedBlocks = ed.selection.getSelectedBlocks();
364
365                         function cleanupBr(e) {
366                                 if (e && e.tagName === 'BR') {
367                                         dom.remove(e);
368                                 }
369                         }
370
371                         function makeList(element) {
372                                 var list = dom.create(targetListType), li;
373
374                                 function adjustIndentForNewList(element) {
375                                         // If there's a margin-left, outdent one level to account for the extra list margin.
376                                         if (element.style.marginLeft || element.style.paddingLeft) {
377                                                 t.adjustPaddingFunction(false)(element);
378                                         }
379                                 }
380
381                                 if (element.tagName === 'LI') {
382                                         // No change required.
383                                 } else if (element.tagName === 'P' || element.tagName === 'DIV' || element.tagName === 'BODY') {
384                                         processBrs(element, function(startSection, br, previousBR) {
385                                                 doWrapList(startSection, br, element.tagName === 'BODY' ? null : startSection.parentNode);
386                                                 li = startSection.parentNode;
387                                                 adjustIndentForNewList(li);
388                                                 cleanupBr(br);
389                                         });
390                                         if (element.tagName === 'P' || selectedBlocks.length > 1) {
391                                                 dom.split(li.parentNode.parentNode, li.parentNode);
392                                         }
393                                         attemptMergeWithAdjacent(li.parentNode, true);
394                                         return;
395                                 } else {
396                                         // Put the list around the element.
397                                         li = dom.create('li');
398                                         dom.insertAfter(li, element);
399                                         li.appendChild(element);
400                                         adjustIndentForNewList(element);
401                                         element = li;
402                                 }
403                                 dom.insertAfter(list, element);
404                                 list.appendChild(element);
405                                 attemptMergeWithAdjacent(list, true);
406                                 applied.push(element);
407                         }
408
409                         function doWrapList(start, end, template) {
410                                 var li, n = start, tmp, i;
411                                 while (!dom.isBlock(start.parentNode) && start.parentNode !== dom.getRoot()) {
412                                         start = dom.split(start.parentNode, start.previousSibling);
413                                         start = start.nextSibling;
414                                         n = start;
415                                 }
416                                 if (template) {
417                                         li = template.cloneNode(true);
418                                         start.parentNode.insertBefore(li, start);
419                                         while (li.firstChild) dom.remove(li.firstChild);
420                                         li = dom.rename(li, 'li');
421                                 } else {
422                                         li = dom.create('li');
423                                         start.parentNode.insertBefore(li, start);
424                                 }
425                                 while (n && n != end) {
426                                         tmp = n.nextSibling;
427                                         li.appendChild(n);
428                                         n = tmp;
429                                 }
430                                 if (li.childNodes.length === 0) {
431                                         li.innerHTML = '<br _mce_bogus="1" />';
432                                 }
433                                 makeList(li);
434                         }
435
436                         function processBrs(element, callback) {
437                                 var startSection, previousBR, END_TO_START = 3, START_TO_END = 1,
438                                                 breakElements = 'br,ul,ol,p,div,h1,h2,h3,h4,h5,h6,table,blockquote,address,pre,form,center,dl';
439
440                                 function isAnyPartSelected(start, end) {
441                                         var r = dom.createRng(), sel;
442                                         bookmark.keep = true;
443                                         ed.selection.moveToBookmark(bookmark);
444                                         bookmark.keep = false;
445                                         sel = ed.selection.getRng(true);
446                                         if (!end) {
447                                                 end = start.parentNode.lastChild;
448                                         }
449                                         r.setStartBefore(start);
450                                         r.setEndAfter(end);
451                                         return !(r.compareBoundaryPoints(END_TO_START, sel) > 0 || r.compareBoundaryPoints(START_TO_END, sel) <= 0);
452                                 }
453
454                                 function nextLeaf(br) {
455                                         if (br.nextSibling)
456                                                 return br.nextSibling;
457                                         if (!dom.isBlock(br.parentNode) && br.parentNode !== dom.getRoot())
458                                                 return nextLeaf(br.parentNode);
459                                 }
460
461                                 // Split on BRs within the range and process those.
462                                 startSection = element.firstChild;
463                                 // First mark the BRs that have any part of the previous section selected.
464                                 var trailingContentSelected = false;
465                                 each(dom.select(breakElements, element), function(br) {
466                                         var b;
467                                         if (br.hasAttribute && br.hasAttribute('_mce_bogus')) {
468                                                 return true; // Skip the bogus Brs that are put in to appease Firefox and Safari.
469                                         }
470                                         if (isAnyPartSelected(startSection, br)) {
471                                                 dom.addClass(br, '_mce_tagged_br');
472                                                 startSection = nextLeaf(br);
473                                         }
474                                 });
475                                 trailingContentSelected = (startSection && isAnyPartSelected(startSection, undefined));
476                                 startSection = element.firstChild;
477                                 each(dom.select(breakElements, element), function(br) {
478                                         // Got a section from start to br.
479                                         var tmp = nextLeaf(br);
480                                         if (br.hasAttribute && br.hasAttribute('_mce_bogus')) {
481                                                 return true; // Skip the bogus Brs that are put in to appease Firefox and Safari.
482                                         }
483                                         if (dom.hasClass(br, '_mce_tagged_br')) {
484                                                 callback(startSection, br, previousBR);
485                                                 previousBR = null;
486                                         } else {
487                                                 previousBR = br;
488                                         }
489                                         startSection = tmp;
490                                 });
491                                 if (trailingContentSelected) {
492                                         callback(startSection, undefined, previousBR);
493                                 }
494                         }
495
496                         function wrapList(element) {
497                                 processBrs(element, function(startSection, br, previousBR) {
498                                         // Need to indent this part
499                                         doWrapList(startSection, br);
500                                         cleanupBr(br);
501                                         cleanupBr(previousBR);
502                                 });
503                         }
504
505                         function changeList(element) {
506                                 if (tinymce.inArray(applied, element) !== -1) {
507                                         return;
508                                 }
509                                 if (element.parentNode.tagName === oppositeListType) {
510                                         dom.split(element.parentNode, element);
511                                         makeList(element);
512                                         attemptMergeWithNext(element.parentNode, false);
513                                 }
514                                 applied.push(element);
515                         }
516
517                         function convertListItemToParagraph(element) {
518                                 var child, nextChild, mergedElement, splitLast;
519                                 if (tinymce.inArray(applied, element) !== -1) {
520                                         return;
521                                 }
522                                 element = splitNestedLists(element, dom);
523                                 while (dom.is(element.parentNode, 'ol,ul,li')) {
524                                         dom.split(element.parentNode, element);
525                                 }
526                                 // Push the original element we have from the selection, not the renamed one.
527                                 applied.push(element);
528                                 element = dom.rename(element, 'p');
529                                 mergedElement = attemptMergeWithAdjacent(element, false, ed.settings.force_br_newlines);
530                                 if (mergedElement === element) {
531                                         // Now split out any block elements that can't be contained within a P.
532                                         // Manually iterate to ensure we handle modifications correctly (doesn't work with tinymce.each)
533                                         child = element.firstChild;
534                                         while (child) {
535                                                 if (dom.isBlock(child)) {
536                                                         child = dom.split(child.parentNode, child);
537                                                         splitLast = true;
538                                                         nextChild = child.nextSibling && child.nextSibling.firstChild;
539                                                 } else {
540                                                         nextChild = child.nextSibling;
541                                                         if (splitLast && child.tagName === 'BR') {
542                                                                 dom.remove(child);
543                                                         }
544                                                         splitLast = false;
545                                                 }
546                                                 child = nextChild;
547                                         }
548                                 }
549                         }
550
551                         each(selectedBlocks, function(e) {
552                                 e = findItemToOperateOn(e, dom);
553                                 if (e.tagName === oppositeListType || (e.tagName === 'LI' && e.parentNode.tagName === oppositeListType)) {
554                                         hasOppositeType = true;
555                                 } else if (e.tagName === targetListType || (e.tagName === 'LI' && e.parentNode.tagName === targetListType)) {
556                                         hasSameType = true;
557                                 } else {
558                                         hasNonList = true;
559                                 }
560                         });
561
562                         if (hasNonList || hasOppositeType || selectedBlocks.length === 0) {
563                                 actions = {
564                                         'LI': changeList,
565                                         'H1': makeList,
566                                         'H2': makeList,
567                                         'H3': makeList,
568                                         'H4': makeList,
569                                         'H5': makeList,
570                                         'H6': makeList,
571                                         'P': makeList,
572                                         'BODY': makeList,
573                                         'DIV': selectedBlocks.length > 1 ? makeList : wrapList,
574                                         defaultAction: wrapList
575                                 };
576                         } else {
577                                 actions = {
578                                         defaultAction: convertListItemToParagraph
579                                 };
580                         }
581                         this.process(actions);
582                 },
583
584                 indent: function() {
585                         var ed = this.ed, dom = ed.dom, indented = [];
586
587                         function createWrapItem(element) {
588                                 var wrapItem = dom.create('li', { style: 'list-style-type: none;'});
589                                 dom.insertAfter(wrapItem, element);
590                                 return wrapItem;
591                         }
592
593                         function createWrapList(element) {
594                                 var wrapItem = createWrapItem(element),
595                                                 list = dom.getParent(element, 'ol,ul'),
596                                                 listType = list.tagName,
597                                                 listStyle = dom.getStyle(list, 'list-style-type'),
598                                                 attrs = {},
599                                                 wrapList;
600                                 if (listStyle !== '') {
601                                         attrs.style = 'list-style-type: ' + listStyle + ';';
602                                 }
603                                 wrapList = dom.create(listType, attrs);
604                                 wrapItem.appendChild(wrapList);
605                                 return wrapList;
606                         }
607
608                         function indentLI(element) {
609                                 if (!hasParentInList(ed, element, indented)) {
610                                         element = splitNestedLists(element, dom);
611                                         var wrapList = createWrapList(element);
612                                         wrapList.appendChild(element);
613                                         attemptMergeWithAdjacent(wrapList.parentNode, false);
614                                         attemptMergeWithAdjacent(wrapList, false);
615                                         indented.push(element);
616                                 }
617                         }
618
619                         this.process({
620                                 'LI': indentLI,
621                                 defaultAction: this.adjustPaddingFunction(true)
622                         });
623
624                 },
625
626                 outdent: function() {
627                         var t = this, ed = t.ed, dom = ed.dom, outdented = [];
628
629                         function outdentLI(element) {
630                                 var listElement, targetParent, align;
631                                 if (!hasParentInList(ed, element, outdented)) {
632                                         if (dom.getStyle(element, 'margin-left') !== '' || dom.getStyle(element, 'padding-left') !== '') {
633                                                 return t.adjustPaddingFunction(false)(element);
634                                         }
635                                         align = dom.getStyle(element, 'text-align', true);
636                                         if (align === 'center' || align === 'right') {
637                                                 dom.setStyle(element, 'text-align', 'left');
638                                                 return;
639                                         }
640                                         element = splitNestedLists(element, dom);
641                                         listElement = element.parentNode;
642                                         targetParent = element.parentNode.parentNode;
643                                         if (targetParent.tagName === 'P') {
644                                                 dom.split(targetParent, element.parentNode);
645                                         } else {
646                                                 dom.split(listElement, element);
647                                                 if (targetParent.tagName === 'LI') {
648                                                         // Nested list, need to split the LI and go back out to the OL/UL element.
649                                                         dom.split(targetParent, element);
650                                                 } else if (!dom.is(targetParent, 'ol,ul')) {
651                                                         dom.rename(element, 'p');
652                                                 }
653                                         }
654                                         outdented.push(element);
655                                 }
656                         }
657
658                         this.process({
659                                 'LI': outdentLI,
660                                 defaultAction: this.adjustPaddingFunction(false)
661                         });
662
663                         each(outdented, attemptMergeWithAdjacent);
664                 },
665
666                 process: function(actions) {
667                         var t = this, sel = t.ed.selection, dom = t.ed.dom, selectedBlocks, r;
668
669                         function processElement(element) {
670                                 dom.removeClass(element, '_mce_act_on');
671                                 if (!element || element.nodeType !== 1) {
672                                         return;
673                                 }
674                                 element = findItemToOperateOn(element, dom);
675                                 var action = actions[element.tagName];
676                                 if (!action) {
677                                         action = actions.defaultAction;
678                                 }
679                                 action(element);
680                         }
681
682                         function recurse(element) {
683                                 t.splitSafeEach(element.childNodes, processElement);
684                         }
685
686                         function brAtEdgeOfSelection(container, offset) {
687                                 return offset >= 0 && container.hasChildNodes() && offset < container.childNodes.length &&
688                                                 container.childNodes[offset].tagName === 'BR';
689                         }
690
691                         selectedBlocks = sel.getSelectedBlocks();
692                         if (selectedBlocks.length === 0) {
693                                 selectedBlocks = [ dom.getRoot() ];
694                         }
695
696                         r = sel.getRng(true);
697                         if (!r.collapsed) {
698                                 if (brAtEdgeOfSelection(r.endContainer, r.endOffset - 1)) {
699                                         r.setEnd(r.endContainer, r.endOffset - 1);
700                                         sel.setRng(r);
701                                 }
702                                 if (brAtEdgeOfSelection(r.startContainer, r.startOffset)) {
703                                         r.setStart(r.startContainer, r.startOffset + 1);
704                                         sel.setRng(r);
705                                 }
706                         }
707                         bookmark = sel.getBookmark();
708                         actions.OL = actions.UL = recurse;
709                         t.splitSafeEach(selectedBlocks, processElement);
710                         sel.moveToBookmark(bookmark);
711                         bookmark = null;
712                         // Avoids table or image handles being left behind in Firefox.
713                         t.ed.execCommand('mceRepaint');
714                 },
715
716                 splitSafeEach: function(elements, f) {
717                         if (tinymce.isGecko && (/Firefox\/[12]\.[0-9]/.test(navigator.userAgent) ||
718                                         /Firefox\/3\.[0-4]/.test(navigator.userAgent))) {
719                                 this.classBasedEach(elements, f);
720                         } else {
721                                 each(elements, f);
722                         }
723                 },
724
725                 classBasedEach: function(elements, f) {
726                         var dom = this.ed.dom, nodes, element;
727                         // Mark nodes
728                         each(elements, function(element) {
729                                 dom.addClass(element, '_mce_act_on');
730                         });
731                         nodes = dom.select('._mce_act_on');
732                         while (nodes.length > 0) {
733                                 element = nodes.shift();
734                                 dom.removeClass(element, '_mce_act_on');
735                                 f(element);
736                                 nodes = dom.select('._mce_act_on');
737                         }
738                 },
739
740                 adjustPaddingFunction: function(isIndent) {
741                         var indentAmount, indentUnits, ed = this.ed;
742                         indentAmount = ed.settings.indentation;
743                         indentUnits = /[a-z%]+/i.exec(indentAmount);
744                         indentAmount = parseInt(indentAmount, 10);
745                         return function(element) {
746                                 var currentIndent, newIndentAmount;
747                                 currentIndent = parseInt(ed.dom.getStyle(element, 'margin-left') || 0, 10) + parseInt(ed.dom.getStyle(element, 'padding-left') || 0, 10);
748                                 if (isIndent) {
749                                         newIndentAmount = currentIndent + indentAmount;
750                                 } else {
751                                         newIndentAmount = currentIndent - indentAmount;
752                                 }
753                                 ed.dom.setStyle(element, 'padding-left', '');
754                                 ed.dom.setStyle(element, 'margin-left', newIndentAmount > 0 ? newIndentAmount + indentUnits : '');
755                         };
756                 },
757
758                 getInfo: function() {
759                         return {
760                                 longname : 'Lists',
761                                 author : 'Moxiecode Systems AB',
762                                 authorurl : 'http://tinymce.moxiecode.com',
763                                 infourl : 'http://wiki.moxiecode.com/index.php/TinyMCE:Plugins/lists',
764                                 version : tinymce.majorVersion + "." + tinymce.minorVersion
765                         };
766                 }
767         });
768         tinymce.PluginManager.add("lists", tinymce.plugins.Lists);
769 }());