Subversion-Projekte lars-tiefland.content-management

Revision

Details | Letzte Änderung | Log anzeigen | RSS feed

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