Updated tiny-mce to most recent 3.4 version
[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 return mergeParagraphs && e1.tagName === 'P' && e2.tagName === 'P';
103         }
104
105         function isListForIndent(e) {
106                 var firstLI = skipWhitespaceNodesForwards(e.firstChild), lastLI = skipWhitespaceNodesBackwards(e.lastChild);
107                 return firstLI && lastLI && isList(e) && firstLI === lastLI && (isList(firstLI) || firstLI.style.listStyleType === 'none' || containsOnlyAList(firstLI));
108         }
109
110         function containsOnlyAList(e) {
111                 var firstChild = skipWhitespaceNodesForwards(e.firstChild), lastChild = skipWhitespaceNodesBackwards(e.lastChild);
112                 return firstChild && lastChild && firstChild === lastChild && isList(firstChild);
113         }
114
115         function merge(e1, e2, masterElement) {
116                 var lastOriginal = skipWhitespaceNodesBackwards(e1.lastChild), firstNew = skipWhitespaceNodesForwards(e2.firstChild);
117                 if (e1.tagName === 'P') {
118                         e1.appendChild(e1.ownerDocument.createElement('br'));
119                 }
120                 while (e2.firstChild) {
121                         e1.appendChild(e2.firstChild);
122                 }
123                 if (masterElement) {
124                         e1.style.listStyleType = masterElement.style.listStyleType;
125                 }
126                 e2.parentNode.removeChild(e2);
127                 attemptMerge(lastOriginal, firstNew, false);
128                 return e1;
129         }
130
131         function findItemToOperateOn(e, dom) {
132                 var item;
133                 if (!dom.is(e, 'li,ol,ul')) {
134                         item = dom.getParent(e, 'li');
135                         if (item) {
136                                 e = item;
137                         }
138                 }
139                 return e;
140         }
141
142         tinymce.create('tinymce.plugins.Lists', {
143                 init: function(ed) {
144                         var LIST_TABBING = 'TABBING';
145                         var LIST_EMPTY_ITEM = 'EMPTY';
146                         var LIST_ESCAPE = 'ESCAPE';
147                         var LIST_PARAGRAPH = 'PARAGRAPH';
148                         var LIST_UNKNOWN = 'UNKNOWN';
149                         var state = LIST_UNKNOWN;
150
151                         function isTabInList(e) {
152                                 // Don't indent on Ctrl+Tab or Alt+Tab
153                                 return e.keyCode === tinymce.VK.TAB && !(e.altKey || e.ctrlKey) &&
154                                         (ed.queryCommandState('InsertUnorderedList') || ed.queryCommandState('InsertOrderedList'));
155                         }
156
157                         function isOnLastListItem() {
158                                 var li = getLi();
159                                 var grandParent = li.parentNode.parentNode;
160                                 var isLastItem = li.parentNode.lastChild === li;
161                                 return isLastItem && !isNestedList(grandParent) && isEmptyListItem(li);
162                         }
163
164                         function isNestedList(grandParent) {
165                                 if (isList(grandParent)) {
166                                         return grandParent.parentNode && grandParent.parentNode.tagName === 'LI';
167                                 } else {
168                                         return  grandParent.tagName === 'LI';
169                                 }
170                         }
171
172                         function isInEmptyListItem() {
173                                 return ed.selection.isCollapsed() && isEmptyListItem(getLi());
174                         }
175
176                         function getLi() {
177                                 var n = ed.selection.getStart();
178                                 // Get start will return BR if the LI only contains a BR or an empty element as we use these to fix caret position
179                                 return ((n.tagName == 'BR' || n.tagName == '') && n.parentNode.tagName == 'LI') ? n.parentNode : n;
180                         }
181
182                         function isEmptyListItem(li) {
183                                 var numChildren = li.childNodes.length;
184                                 if (li.tagName === 'LI') {
185                                         return numChildren == 0 ? true : numChildren == 1 && (li.firstChild.tagName == '' || li.firstChild.tagName == 'BR' || isEmptyIE9Li(li));
186                                 }
187                                 return false;
188                         }
189
190                         function isEmptyIE9Li(li) {
191                                 // 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
192                                 var lis = tinymce.grep(li.parentNode.childNodes, function(n) {return n.tagName == 'LI'});
193                                 var isLastLi = li == lis[lis.length - 1];
194                                 var child = li.firstChild;
195                                 return tinymce.isIE9 && isLastLi && (child.nodeValue == String.fromCharCode(160) || child.nodeValue == String.fromCharCode(32));
196                         }
197
198                         function isEnter(e) {
199                                 return e.keyCode === tinymce.VK.ENTER;
200                         }
201
202                         function isEnterWithoutShift(e) {
203                                 return isEnter(e) && !e.shiftKey;
204                         }
205
206                         function getListKeyState(e) {
207                                 if (isTabInList(e)) {
208                                         return LIST_TABBING;
209                                 } else if (isEnterWithoutShift(e) && isOnLastListItem()) {
210                                         return LIST_ESCAPE;
211                                 } else if (isEnterWithoutShift(e) && isInEmptyListItem()) {
212                                         return LIST_EMPTY_ITEM;
213                                 } else {
214                                         return LIST_UNKNOWN;
215                                 }
216                         }
217
218                         function cancelDefaultEvents(ed, e) {
219                                 // list escape is done manually using outdent as it does not create paragraphs correctly in td's
220                                 if (state == LIST_TABBING || state == LIST_EMPTY_ITEM || tinymce.isGecko && state == LIST_ESCAPE) {
221                                         Event.cancel(e);
222                                 }
223                         }
224
225                         function isCursorAtEndOfContainer() {
226                                 var range = ed.selection.getRng(true);
227                                 var startContainer = range.startContainer;
228                                 if (startContainer.nodeType == 3) {
229                                         var value = startContainer.nodeValue;
230                                         if (tinymce.isIE9 && value.length > 1 && value.charCodeAt(value.length-1) == 32) {
231                                                 // IE9 places a space on the end of the text in some cases so ignore last char
232                                                 return (range.endOffset == value.length-1);
233                                         } else {
234                                                 return (range.endOffset == value.length);
235                                         }
236                                 } else if (startContainer.nodeType == 1) {
237                                         return range.endOffset == startContainer.childNodes.length;
238                                 }
239                                 return false;
240                         }
241
242                         /*
243                                 If we are at the end of a list item surrounded with an element, pressing enter should create a
244                                 new list item instead without splitting the element e.g. don't want to create new P or H1 tag
245                           */
246                         function isEndOfListItem() {
247                                 var node = ed.selection.getNode();
248                                 var validElements = 'h1,h2,h3,h4,h5,h6,p,div';
249                                 var isLastParagraphOfLi = ed.dom.is(node, validElements) && node.parentNode.tagName === 'LI' && node.parentNode.lastChild === node;
250                                 return ed.selection.isCollapsed() && isLastParagraphOfLi && isCursorAtEndOfContainer();
251                         }
252
253                         // Creates a new list item after the current selection's list item parent
254                         function createNewLi(ed, e) {
255                                 if (isEnterWithoutShift(e) && isEndOfListItem()) {
256                                         var node = ed.selection.getNode();
257                                         var li = ed.dom.create("li");
258                                         var parentLi = ed.dom.getParent(node, 'li');
259                                         ed.dom.insertAfter(li, parentLi);
260
261                                         // Move caret to new list element.
262                                         if (tinymce.isIE6 || tinymce.isIE7 || tinyMCE.isIE8) {
263                                                 li.appendChild(ed.dom.create(" ")); // IE needs an element within the bullet point
264                                                 ed.selection.setCursorLocation(li, 1);
265                                         } else if (tinyMCE.isGecko) {
266                                                 // This setTimeout is a hack as FF behaves badly if there is no content after the bullet point
267                                                 setTimeout(function () {
268                                                         var n = ed.getDoc().createTextNode('\uFEFF');
269                                                         li.appendChild(n);
270                                                         ed.selection.setCursorLocation(li, 0);
271                                                 }, 0);
272                                         } else {
273                                                 ed.selection.setCursorLocation(li, 0);
274                                         }
275                                         e.preventDefault();
276                                 }
277                         }
278
279                         function imageJoiningListItem(ed, e) {
280                                 var prevSibling;
281
282                                 if (!tinymce.isGecko)
283                                         return;
284
285                                 var n = ed.selection.getStart();
286                                 if (e.keyCode != tinymce.VK.BACKSPACE || n.tagName !== 'IMG')
287                                         return;
288
289                                 function lastLI(node) {
290                                         var child = node.firstChild;
291                                         var li = null;
292                                         do {
293                                                 if (!child)
294                                                         break;
295
296                                                 if (child.tagName === 'LI')
297                                                         li = child;
298                                         } while (child = child.nextSibling);
299
300                                         return li;
301                                 }
302
303                                 function addChildren(parentNode, destination) {
304                                         while (parentNode.childNodes.length > 0)
305                                                 destination.appendChild(parentNode.childNodes[0]);
306                                 }
307
308                                 // Check if there is a previous sibling
309                                 prevSibling = n.parentNode.previousSibling;
310                                 if (!prevSibling)
311                                         return;
312
313                                 var ul;
314                                 if (prevSibling.tagName === 'UL' || prevSibling.tagName === 'OL')
315                                         ul = prevSibling;
316                                 else if (prevSibling.previousSibling && (prevSibling.previousSibling.tagName === 'UL' || prevSibling.previousSibling.tagName === 'OL'))
317                                         ul = prevSibling.previousSibling;
318                                 else
319                                         return;
320
321                                 var li = lastLI(ul);
322
323                                 // move the caret to the end of the list item
324                                 var rng = ed.dom.createRng();
325                                 rng.setStart(li, 1);
326                                 rng.setEnd(li, 1);
327                                 ed.selection.setRng(rng);
328                                 ed.selection.collapse(true);
329
330                                 // save a bookmark at the end of the list item
331                                 var bookmark = ed.selection.getBookmark();
332
333                                 // copy the image an its text to the list item
334                                 var clone = n.parentNode.cloneNode(true);
335                                 if (clone.tagName === 'P' || clone.tagName === 'DIV')
336                                         addChildren(clone, li);
337                                 else
338                                         li.appendChild(clone);
339
340                                 // remove the old copy of the image
341                                 n.parentNode.parentNode.removeChild(n.parentNode);
342
343                                 // move the caret where we saved the bookmark
344                                 ed.selection.moveToBookmark(bookmark);
345                         }
346
347                         // fix the cursor position to ensure it is correct in IE
348                         function setCursorPositionToOriginalLi(li) {
349                                 var list = ed.dom.getParent(li, 'ol,ul');
350                                 if (list != null) {
351                                         var lastLi = list.lastChild;
352                                         lastLi.appendChild(ed.getDoc().createElement(''));
353                                         ed.selection.setCursorLocation(lastLi, 0);
354                                 }
355                         }
356
357                         this.ed = ed;
358                         ed.addCommand('Indent', this.indent, this);
359                         ed.addCommand('Outdent', this.outdent, this);
360                         ed.addCommand('InsertUnorderedList', function() {
361                                 this.applyList('UL', 'OL');
362                         }, this);
363                         ed.addCommand('InsertOrderedList', function() {
364                                 this.applyList('OL', 'UL');
365                         }, this);
366
367                         ed.onInit.add(function() {
368                                 ed.editorCommands.addCommands({
369                                         'outdent': function() {
370                                                 var sel = ed.selection, dom = ed.dom;
371
372                                                 function hasStyleIndent(n) {
373                                                         n = dom.getParent(n, dom.isBlock);
374                                                         return n && (parseInt(ed.dom.getStyle(n, 'margin-left') || 0, 10) + parseInt(ed.dom.getStyle(n, 'padding-left') || 0, 10)) > 0;
375                                                 }
376
377                                                 return hasStyleIndent(sel.getStart()) || hasStyleIndent(sel.getEnd()) || ed.queryCommandState('InsertOrderedList') || ed.queryCommandState('InsertUnorderedList');
378                                         }
379                                 }, 'state');
380                         });
381
382                         ed.onKeyUp.add(function(ed, e) {
383                                 if (state == LIST_TABBING) {
384                                         ed.execCommand(e.shiftKey ? 'Outdent' : 'Indent', true, null);
385                                         state = LIST_UNKNOWN;
386                                         return Event.cancel(e);
387                                 } else if (state == LIST_EMPTY_ITEM) {
388                                         var li = getLi();
389                                         var shouldOutdent =  ed.settings.list_outdent_on_enter === true || e.shiftKey;
390                                         ed.execCommand(shouldOutdent ? 'Outdent' : 'Indent', true, null);
391                                         if (tinymce.isIE) {
392                                                 setCursorPositionToOriginalLi(li);
393                                         }
394
395                                         return Event.cancel(e);
396                                 } else if (state == LIST_ESCAPE) {
397                                         if (tinymce.isIE6 || tinymce.isIE7 || tinymce.isIE8) {
398                                                 // append a zero sized nbsp so that caret is positioned correctly in IE after escaping and applying formatting.
399                                                 // if there is no text then applying formatting for e.g a H1 to the P tag immediately following list after
400                                                 // escaping from it will cause the caret to be positioned on the last li instead of staying the in P tag.
401                                                 var n = ed.getDoc().createTextNode('\uFEFF');
402                                                 ed.selection.getNode().appendChild(n);
403                                         } else if (tinymce.isIE9 || tinymce.isGecko) {
404                                                 // IE9 does not escape the list so we use outdent to do this and cancel the default behaviour
405                                                 // Gecko does not create a paragraph outdenting inside a TD so default behaviour is cancelled and we outdent ourselves
406                                                 ed.execCommand('Outdent');
407                                                 return Event.cancel(e);
408                                         }
409                                 }
410                         });
411
412                         function fixListItem(parent, reference) {
413                                 // a zero-sized non-breaking space is placed in the empty list item so that the nested list is
414                                 // displayed on the below line instead of next to it
415                                 var n = ed.getDoc().createTextNode('\uFEFF');
416                                 parent.insertBefore(n, reference);
417                                 ed.selection.setCursorLocation(n, 0);
418                                 // repaint to remove rendering artifact. only visible when creating new list
419                                 ed.execCommand('mceRepaint');
420                         }
421
422                         function fixIndentedListItemForGecko(ed, e) {
423                                 if (isEnter(e)) {
424                                         var li = getLi();
425                                         if (li) {
426                                                 var parent = li.parentNode;
427                                                 var grandParent = parent && parent.parentNode;
428                                                 if (grandParent && grandParent.nodeName == 'LI' && grandParent.firstChild == parent && li == parent.firstChild) {
429                                                         fixListItem(grandParent, parent);
430                                                 }
431                                         }
432                                 }
433                         }
434
435                         function fixIndentedListItemForIE8(ed, e) {
436                                 if (isEnter(e)) {
437                                         var li = getLi();
438                                         if (ed.dom.select('ul li', li).length === 1) {
439                                                 var list = li.firstChild;
440                                                 fixListItem(li, list);
441                                         }
442                                 }
443                         }
444
445                         function fixDeletingFirstCharOfList(ed, e) {
446                                 function listElements(list, li) {
447                                         var elements = [];
448                                         var walker = new tinymce.dom.TreeWalker(li, list);
449                                         for (var node = walker.current(); node; node = walker.next()) {
450                                                 if (ed.dom.is(node, 'ol,ul,li')) {
451                                                         elements.push(node);
452                                                 }
453                                         }
454                                         return elements;
455                                 }
456
457                                 if (e.keyCode == tinymce.VK.BACKSPACE) {
458                                         var li = getLi();
459                                         if (li) {
460                                                 var list = ed.dom.getParent(li, 'ol,ul');
461                                                 if (list && list.firstChild === li) {
462                                                         var elements = listElements(list, li);
463                                                         ed.execCommand("Outdent", false, elements);
464                                                         ed.undoManager.add();
465                                                         return Event.cancel(e);
466                                                 }
467                                         }
468                                 }
469                         }
470
471                         function fixDeletingEmptyLiInWebkit(ed, e) {
472                                 var li = getLi();
473                                 if (e.keyCode === tinymce.VK.BACKSPACE && ed.dom.is(li, 'li') && li.parentNode.firstChild!==li) {
474                                         if (ed.dom.select('ul,ol', li).length === 1) {
475                                                 var prevLi = li.previousSibling;
476                                                 ed.dom.remove(ed.dom.select('br', li));
477                                                 ed.dom.remove(li, true);
478                                                 var textNodes = tinymce.grep(prevLi.childNodes, function(n){ return n.nodeType === 3 });
479                                                 if (textNodes.length === 1) {
480                                                         var textNode = textNodes[0]
481                                                         ed.selection.setCursorLocation(textNode, textNode.length);
482                                                 }
483                                                 ed.undoManager.add();
484                                                 return Event.cancel(e);
485                                         }
486                                 }
487                         }
488
489                         ed.onKeyDown.add(function(_, e) { state = getListKeyState(e); });
490                         ed.onKeyDown.add(cancelDefaultEvents);
491                         ed.onKeyDown.add(imageJoiningListItem);
492                         ed.onKeyDown.add(createNewLi);
493
494                         if (tinymce.isGecko) {
495                                 ed.onKeyUp.add(fixIndentedListItemForGecko);
496                         }
497                         if (tinymce.isIE8) {
498                                 ed.onKeyUp.add(fixIndentedListItemForIE8);
499                         }
500                         if (tinymce.isGecko || tinymce.isWebKit) {
501                                 ed.onKeyDown.add(fixDeletingFirstCharOfList);
502                         }
503                         if (tinymce.isWebKit) {
504                                 ed.onKeyDown.add(fixDeletingEmptyLiInWebkit);
505                         }
506                 },
507
508                 applyList: function(targetListType, oppositeListType) {
509                         var t = this, ed = t.ed, dom = ed.dom, applied = [], hasSameType = false, hasOppositeType = false, hasNonList = false, actions,
510                                         selectedBlocks = ed.selection.getSelectedBlocks();
511
512                         function cleanupBr(e) {
513                                 if (e && e.tagName === 'BR') {
514                                         dom.remove(e);
515                                 }
516                         }
517
518                         function makeList(element) {
519                                 var list = dom.create(targetListType), li;
520
521                                 function adjustIndentForNewList(element) {
522                                         // If there's a margin-left, outdent one level to account for the extra list margin.
523                                         if (element.style.marginLeft || element.style.paddingLeft) {
524                                                 t.adjustPaddingFunction(false)(element);
525                                         }
526                                 }
527
528                                 if (element.tagName === 'LI') {
529                                         // No change required.
530                                 } else if (element.tagName === 'P' || element.tagName === 'DIV' || element.tagName === 'BODY') {
531                                         processBrs(element, function(startSection, br) {
532                                                 doWrapList(startSection, br, element.tagName === 'BODY' ? null : startSection.parentNode);
533                                                 li = startSection.parentNode;
534                                                 adjustIndentForNewList(li);
535                                                 cleanupBr(br);
536                                         });
537                                         if (li) {
538                                                 if (li.tagName === 'LI' && (element.tagName === 'P' || selectedBlocks.length > 1)) {
539                                                         dom.split(li.parentNode.parentNode, li.parentNode);
540                                                 }
541                                                 attemptMergeWithAdjacent(li.parentNode, true);
542                                         }
543                                         return;
544                                 } else {
545                                         // Put the list around the element.
546                                         li = dom.create('li');
547                                         dom.insertAfter(li, element);
548                                         li.appendChild(element);
549                                         adjustIndentForNewList(element);
550                                         element = li;
551                                 }
552                                 dom.insertAfter(list, element);
553                                 list.appendChild(element);
554                                 attemptMergeWithAdjacent(list, true);
555                                 applied.push(element);
556                         }
557
558                         function doWrapList(start, end, template) {
559                                 var li, n = start, tmp;
560                                 while (!dom.isBlock(start.parentNode) && start.parentNode !== dom.getRoot()) {
561                                         start = dom.split(start.parentNode, start.previousSibling);
562                                         start = start.nextSibling;
563                                         n = start;
564                                 }
565                                 if (template) {
566                                         li = template.cloneNode(true);
567                                         start.parentNode.insertBefore(li, start);
568                                         while (li.firstChild) dom.remove(li.firstChild);
569                                         li = dom.rename(li, 'li');
570                                 } else {
571                                         li = dom.create('li');
572                                         start.parentNode.insertBefore(li, start);
573                                 }
574                                 while (n && n != end) {
575                                         tmp = n.nextSibling;
576                                         li.appendChild(n);
577                                         n = tmp;
578                                 }
579                                 if (li.childNodes.length === 0) {
580                                         li.innerHTML = '<br _mce_bogus="1" />';
581                                 }
582                                 makeList(li);
583                         }
584
585                         function processBrs(element, callback) {
586                                 var startSection, previousBR, END_TO_START = 3, START_TO_END = 1,
587                                                 breakElements = 'br,ul,ol,p,div,h1,h2,h3,h4,h5,h6,table,blockquote,address,pre,form,center,dl';
588
589                                 function isAnyPartSelected(start, end) {
590                                         var r = dom.createRng(), sel;
591                                         bookmark.keep = true;
592                                         ed.selection.moveToBookmark(bookmark);
593                                         bookmark.keep = false;
594                                         sel = ed.selection.getRng(true);
595                                         if (!end) {
596                                                 end = start.parentNode.lastChild;
597                                         }
598                                         r.setStartBefore(start);
599                                         r.setEndAfter(end);
600                                         return !(r.compareBoundaryPoints(END_TO_START, sel) > 0 || r.compareBoundaryPoints(START_TO_END, sel) <= 0);
601                                 }
602
603                                 function nextLeaf(br) {
604                                         if (br.nextSibling)
605                                                 return br.nextSibling;
606                                         if (!dom.isBlock(br.parentNode) && br.parentNode !== dom.getRoot())
607                                                 return nextLeaf(br.parentNode);
608                                 }
609
610                                 // Split on BRs within the range and process those.
611                                 startSection = element.firstChild;
612                                 // First mark the BRs that have any part of the previous section selected.
613                                 var trailingContentSelected = false;
614                                 each(dom.select(breakElements, element), function(br) {
615                                         if (br.hasAttribute && br.hasAttribute('_mce_bogus')) {
616                                                 return true; // Skip the bogus Brs that are put in to appease Firefox and Safari.
617                                         }
618                                         if (isAnyPartSelected(startSection, br)) {
619                                                 dom.addClass(br, '_mce_tagged_br');
620                                                 startSection = nextLeaf(br);
621                                         }
622                                 });
623                                 trailingContentSelected = (startSection && isAnyPartSelected(startSection, undefined));
624                                 startSection = element.firstChild;
625                                 each(dom.select(breakElements, element), function(br) {
626                                         // Got a section from start to br.
627                                         var tmp = nextLeaf(br);
628                                         if (br.hasAttribute && br.hasAttribute('_mce_bogus')) {
629                                                 return true; // Skip the bogus Brs that are put in to appease Firefox and Safari.
630                                         }
631                                         if (dom.hasClass(br, '_mce_tagged_br')) {
632                                                 callback(startSection, br, previousBR);
633                                                 previousBR = null;
634                                         } else {
635                                                 previousBR = br;
636                                         }
637                                         startSection = tmp;
638                                 });
639                                 if (trailingContentSelected) {
640                                         callback(startSection, undefined, previousBR);
641                                 }
642                         }
643
644                         function wrapList(element) {
645                                 processBrs(element, function(startSection, br, previousBR) {
646                                         // Need to indent this part
647                                         doWrapList(startSection, br);
648                                         cleanupBr(br);
649                                         cleanupBr(previousBR);
650                                 });
651                         }
652
653                         function changeList(element) {
654                                 if (tinymce.inArray(applied, element) !== -1) {
655                                         return;
656                                 }
657                                 if (element.parentNode.tagName === oppositeListType) {
658                                         dom.split(element.parentNode, element);
659                                         makeList(element);
660                                         attemptMergeWithNext(element.parentNode, false);
661                                 }
662                                 applied.push(element);
663                         }
664
665                         function convertListItemToParagraph(element) {
666                                 var child, nextChild, mergedElement, splitLast;
667                                 if (tinymce.inArray(applied, element) !== -1) {
668                                         return;
669                                 }
670                                 element = splitNestedLists(element, dom);
671                                 while (dom.is(element.parentNode, 'ol,ul,li')) {
672                                         dom.split(element.parentNode, element);
673                                 }
674                                 // Push the original element we have from the selection, not the renamed one.
675                                 applied.push(element);
676                                 element = dom.rename(element, 'p');
677                                 mergedElement = attemptMergeWithAdjacent(element, false, ed.settings.force_br_newlines);
678                                 if (mergedElement === element) {
679                                         // Now split out any block elements that can't be contained within a P.
680                                         // Manually iterate to ensure we handle modifications correctly (doesn't work with tinymce.each)
681                                         child = element.firstChild;
682                                         while (child) {
683                                                 if (dom.isBlock(child)) {
684                                                         child = dom.split(child.parentNode, child);
685                                                         splitLast = true;
686                                                         nextChild = child.nextSibling && child.nextSibling.firstChild;
687                                                 } else {
688                                                         nextChild = child.nextSibling;
689                                                         if (splitLast && child.tagName === 'BR') {
690                                                                 dom.remove(child);
691                                                         }
692                                                         splitLast = false;
693                                                 }
694                                                 child = nextChild;
695                                         }
696                                 }
697                         }
698
699                         each(selectedBlocks, function(e) {
700                                 e = findItemToOperateOn(e, dom);
701                                 if (e.tagName === oppositeListType || (e.tagName === 'LI' && e.parentNode.tagName === oppositeListType)) {
702                                         hasOppositeType = true;
703                                 } else if (e.tagName === targetListType || (e.tagName === 'LI' && e.parentNode.tagName === targetListType)) {
704                                         hasSameType = true;
705                                 } else {
706                                         hasNonList = true;
707                                 }
708                         });
709
710                         if (hasNonList &&!hasSameType || hasOppositeType || selectedBlocks.length === 0) {
711                                 actions = {
712                                         'LI': changeList,
713                                         'H1': makeList,
714                                         'H2': makeList,
715                                         'H3': makeList,
716                                         'H4': makeList,
717                                         'H5': makeList,
718                                         'H6': makeList,
719                                         'P': makeList,
720                                         'BODY': makeList,
721                                         'DIV': selectedBlocks.length > 1 ? makeList : wrapList,
722                                         defaultAction: wrapList,
723                                         elements: this.selectedBlocks()
724                                 };
725                         } else {
726                                 actions = {
727                                         defaultAction: convertListItemToParagraph,
728                                         elements: this.selectedBlocks()
729                                 };
730                         }
731                         this.process(actions);
732                 },
733
734                 indent: function() {
735                         var ed = this.ed, dom = ed.dom, indented = [];
736
737                         function createWrapItem(element) {
738                                 var wrapItem = dom.create('li', { style: 'list-style-type: none;'});
739                                 dom.insertAfter(wrapItem, element);
740                                 return wrapItem;
741                         }
742
743                         function createWrapList(element) {
744                                 var wrapItem = createWrapItem(element),
745                                                 list = dom.getParent(element, 'ol,ul'),
746                                                 listType = list.tagName,
747                                                 listStyle = dom.getStyle(list, 'list-style-type'),
748                                                 attrs = {},
749                                                 wrapList;
750                                 if (listStyle !== '') {
751                                         attrs.style = 'list-style-type: ' + listStyle + ';';
752                                 }
753                                 wrapList = dom.create(listType, attrs);
754                                 wrapItem.appendChild(wrapList);
755                                 return wrapList;
756                         }
757
758                         function indentLI(element) {
759                                 if (!hasParentInList(ed, element, indented)) {
760                                         element = splitNestedLists(element, dom);
761                                         var wrapList = createWrapList(element);
762                                         wrapList.appendChild(element);
763                                         attemptMergeWithAdjacent(wrapList.parentNode, false);
764                                         attemptMergeWithAdjacent(wrapList, false);
765                                         indented.push(element);
766                                 }
767                         }
768
769                         this.process({
770                                 'LI': indentLI,
771                                 defaultAction: this.adjustPaddingFunction(true),
772                                 elements: this.selectedBlocks()
773                         });
774
775                 },
776
777                 outdent: function(ui, elements) {
778                         var t = this, ed = t.ed, dom = ed.dom, outdented = [];
779
780                         function outdentLI(element) {
781                                 var listElement, targetParent, align;
782                                 if (!hasParentInList(ed, element, outdented)) {
783                                         if (dom.getStyle(element, 'margin-left') !== '' || dom.getStyle(element, 'padding-left') !== '') {
784                                                 return t.adjustPaddingFunction(false)(element);
785                                         }
786                                         align = dom.getStyle(element, 'text-align', true);
787                                         if (align === 'center' || align === 'right') {
788                                                 dom.setStyle(element, 'text-align', 'left');
789                                                 return;
790                                         }
791                                         element = splitNestedLists(element, dom);
792                                         listElement = element.parentNode;
793                                         targetParent = element.parentNode.parentNode;
794                                         if (targetParent.tagName === 'P') {
795                                                 dom.split(targetParent, element.parentNode);
796                                         } else {
797                                                 dom.split(listElement, element);
798                                                 if (targetParent.tagName === 'LI') {
799                                                         // Nested list, need to split the LI and go back out to the OL/UL element.
800                                                         dom.split(targetParent, element);
801                                                 } else if (!dom.is(targetParent, 'ol,ul')) {
802                                                         dom.rename(element, 'p');
803                                                 }
804                                         }
805                                         outdented.push(element);
806                                 }
807                         }
808
809                         var listElements = elements && tinymce.is(elements, 'array') ? elements : this.selectedBlocks();
810                         this.process({
811                                 'LI': outdentLI,
812                                 defaultAction: this.adjustPaddingFunction(false),
813                                 elements: listElements
814                         });
815
816                         each(outdented, attemptMergeWithAdjacent);
817                 },
818
819                 process: function(actions) {
820                         var t = this, sel = t.ed.selection, dom = t.ed.dom, selectedBlocks, r;
821
822                         function isEmptyElement(element) {
823                                 var excludeBrsAndBookmarks = tinymce.grep(element.childNodes, function(n) {
824                                         return !(n.nodeName === 'BR' || n.nodeName === 'SPAN' && dom.getAttrib(n, 'data-mce-type') == 'bookmark'
825                                                         || n.nodeType == 3 && (n.nodeValue == String.fromCharCode(160) || n.nodeValue == ''));
826                                 });
827                                 return excludeBrsAndBookmarks.length === 0;
828                         }
829
830                         function processElement(element) {
831                                 dom.removeClass(element, '_mce_act_on');
832                                 if (!element || element.nodeType !== 1 || selectedBlocks.length > 1 && isEmptyElement(element)) {
833                                         return;
834                                 }
835                                 element = findItemToOperateOn(element, dom);
836                                 var action = actions[element.tagName];
837                                 if (!action) {
838                                         action = actions.defaultAction;
839                                 }
840                                 action(element);
841                         }
842
843                         function recurse(element) {
844                                 t.splitSafeEach(element.childNodes, processElement);
845                         }
846
847                         function brAtEdgeOfSelection(container, offset) {
848                                 return offset >= 0 && container.hasChildNodes() && offset < container.childNodes.length &&
849                                                 container.childNodes[offset].tagName === 'BR';
850                         }
851
852                         function isInTable() {
853                                 var n = sel.getNode();
854                                 var p = dom.getParent(n, 'td');
855                                 return p !== null;
856                         }
857
858                         selectedBlocks = actions.elements;
859
860                         r = sel.getRng(true);
861                         if (!r.collapsed) {
862                                 if (brAtEdgeOfSelection(r.endContainer, r.endOffset - 1)) {
863                                         r.setEnd(r.endContainer, r.endOffset - 1);
864                                         sel.setRng(r);
865                                 }
866                                 if (brAtEdgeOfSelection(r.startContainer, r.startOffset)) {
867                                         r.setStart(r.startContainer, r.startOffset + 1);
868                                         sel.setRng(r);
869                                 }
870                         }
871
872
873                         if (tinymce.isIE8) {
874                                 // append a zero sized nbsp so that caret is restored correctly using bookmark
875                                 var s = t.ed.selection.getNode();
876                                 if (s.tagName === 'LI' && !(s.parentNode.lastChild === s)) {
877                                         var i = t.ed.getDoc().createTextNode('\uFEFF');
878                                         s.appendChild(i);
879                                 }
880                         }
881
882                         bookmark = sel.getBookmark();
883                         actions.OL = actions.UL = recurse;
884                         t.splitSafeEach(selectedBlocks, processElement);
885                         sel.moveToBookmark(bookmark);
886                         bookmark = null;
887
888                         // we avoid doing repaint in a table as this will move the caret out of the table in Firefox 3.6
889                         if (!isInTable()) {
890                                 // Avoids table or image handles being left behind in Firefox.
891                                 t.ed.execCommand('mceRepaint');
892                         }
893                 },
894
895                 splitSafeEach: function(elements, f) {
896                         if (tinymce.isGecko && (/Firefox\/[12]\.[0-9]/.test(navigator.userAgent) ||
897                                         /Firefox\/3\.[0-4]/.test(navigator.userAgent))) {
898                                 this.classBasedEach(elements, f);
899                         } else {
900                                 each(elements, f);
901                         }
902                 },
903
904                 classBasedEach: function(elements, f) {
905                         var dom = this.ed.dom, nodes, element;
906                         // Mark nodes
907                         each(elements, function(element) {
908                                 dom.addClass(element, '_mce_act_on');
909                         });
910                         nodes = dom.select('._mce_act_on');
911                         while (nodes.length > 0) {
912                                 element = nodes.shift();
913                                 dom.removeClass(element, '_mce_act_on');
914                                 f(element);
915                                 nodes = dom.select('._mce_act_on');
916                         }
917                 },
918
919                 adjustPaddingFunction: function(isIndent) {
920                         var indentAmount, indentUnits, ed = this.ed;
921                         indentAmount = ed.settings.indentation;
922                         indentUnits = /[a-z%]+/i.exec(indentAmount);
923                         indentAmount = parseInt(indentAmount, 10);
924                         return function(element) {
925                                 var currentIndent, newIndentAmount;
926                                 currentIndent = parseInt(ed.dom.getStyle(element, 'margin-left') || 0, 10) + parseInt(ed.dom.getStyle(element, 'padding-left') || 0, 10);
927                                 if (isIndent) {
928                                         newIndentAmount = currentIndent + indentAmount;
929                                 } else {
930                                         newIndentAmount = currentIndent - indentAmount;
931                                 }
932                                 ed.dom.setStyle(element, 'padding-left', '');
933                                 ed.dom.setStyle(element, 'margin-left', newIndentAmount > 0 ? newIndentAmount + indentUnits : '');
934                         };
935                 },
936
937                 selectedBlocks: function() {
938                         var ed = this.ed
939                         var selectedBlocks = ed.selection.getSelectedBlocks();
940                         return selectedBlocks.length == 0 ? [ ed.dom.getRoot() ] : selectedBlocks;
941                 },
942
943                 getInfo: function() {
944                         return {
945                                 longname : 'Lists',
946                                 author : 'Moxiecode Systems AB',
947                                 authorurl : 'http://tinymce.moxiecode.com',
948                                 infourl : 'http://wiki.moxiecode.com/index.php/TinyMCE:Plugins/lists',
949                                 version : tinymce.majorVersion + "." + tinymce.minorVersion
950                         };
951                 }
952         });
953         tinymce.PluginManager.add("lists", tinymce.plugins.Lists);
954 }());