4 * Copyright 2011, Moxiecode Systems AB
5 * Released under LGPL License.
7 * License: http://tinymce.moxiecode.com/license
8 * Contributing: http://tinymce.moxiecode.com/contributing
12 var each = tinymce.each, Event = tinymce.dom.Event, bookmark;
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)))) {
22 function skipWhitespaceNodesBackwards(e) {
23 return skipWhitespaceNodes(e, function(e) { return e.previousSibling; });
26 function skipWhitespaceNodesForwards(e) {
27 return skipWhitespaceNodes(e, function(e) { return e.nextSibling; });
30 function hasParentInList(ed, e, list) {
31 return ed.dom.getParent(e, function(p) {
32 return tinymce.inArray(list, p) !== -1;
37 return e && (e.tagName === 'OL' || e.tagName === 'UL');
40 function splitNestedLists(element, dom) {
41 var tmp, nested, wrapItem;
42 tmp = skipWhitespaceNodesBackwards(element.lastChild);
45 tmp = skipWhitespaceNodesBackwards(nested.previousSibling);
48 wrapItem = dom.create('li', { style: 'list-style-type: none;'});
49 dom.split(element, nested);
50 dom.insertAfter(wrapItem, nested);
51 wrapItem.appendChild(nested);
52 wrapItem.appendChild(nested);
53 element = wrapItem.previousSibling;
58 function attemptMergeWithAdjacent(e, allowDifferentListStyles, mergeParagraphs) {
59 e = attemptMergeWithPrevious(e, allowDifferentListStyles, mergeParagraphs);
60 return attemptMergeWithNext(e, allowDifferentListStyles, mergeParagraphs);
63 function attemptMergeWithPrevious(e, allowDifferentListStyles, mergeParagraphs) {
64 var prev = skipWhitespaceNodesBackwards(e.previousSibling);
66 return attemptMerge(prev, e, allowDifferentListStyles ? prev : false, mergeParagraphs);
72 function attemptMergeWithNext(e, allowDifferentListStyles, mergeParagraphs) {
73 var next = skipWhitespaceNodesForwards(e.nextSibling);
75 return attemptMerge(e, next, allowDifferentListStyles ? next : false, mergeParagraphs);
81 function attemptMerge(e1, e2, differentStylesMasterElement, mergeParagraphs) {
82 if (canMerge(e1, e2, !!differentStylesMasterElement, mergeParagraphs)) {
83 return merge(e1, e2, differentStylesMasterElement);
84 } else if (e1 && e1.tagName === 'LI' && isList(e2)) {
85 // Fix invalidly nested lists.
91 function canMerge(e1, e2, allowDifferentListStyles, mergeParagraphs) {
94 } else if (e1.tagName === 'LI' && e2.tagName === 'LI') {
95 return e2.style.listStyleType === 'none' || containsOnlyAList(e2);
96 } else if (isList(e1)) {
97 return (e1.tagName === e2.tagName && (allowDifferentListStyles || e1.style.listStyleType === e2.style.listStyleType)) || isListForIndent(e2);
98 } else if (mergeParagraphs && e1.tagName === 'P' && e2.tagName === 'P') {
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));
110 function containsOnlyAList(e) {
111 var firstChild = skipWhitespaceNodesForwards(e.firstChild), lastChild = skipWhitespaceNodesBackwards(e.lastChild);
112 return firstChild && lastChild && firstChild === lastChild && isList(firstChild);
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'));
120 while (e2.firstChild) {
121 e1.appendChild(e2.firstChild);
124 e1.style.listStyleType = masterElement.style.listStyleType;
126 e2.parentNode.removeChild(e2);
127 attemptMerge(lastOriginal, firstNew, false);
131 function findItemToOperateOn(e, dom) {
133 if (!dom.is(e, 'li,ol,ul')) {
134 item = dom.getParent(e, 'li');
142 tinymce.create('tinymce.plugins.Lists', {
143 init: function(ed, url) {
144 var enterDownInEmptyList = false;
145 function isTriggerKey(e) {
146 return e.keyCode === 9 && (ed.queryCommandState('InsertUnorderedList') || ed.queryCommandState('InsertOrderedList'));
148 function isEnterInEmptyListItem(ed, e) {
149 var sel = ed.selection, n;
150 if (e.keyCode === 13) {
152 enterDownInEmptyList = sel.isCollapsed() && n && n.tagName === 'LI' && n.childNodes.length === 0;
153 return enterDownInEmptyList;
156 function cancelKeys(ed, e) {
157 if (isTriggerKey(e) || isEnterInEmptyListItem(ed, e)) {
158 return Event.cancel(e);
163 ed.addCommand('Indent', this.indent, this);
164 ed.addCommand('Outdent', this.outdent, this);
165 ed.addCommand('InsertUnorderedList', function() {
166 this.applyList('UL', 'OL');
168 ed.addCommand('InsertOrderedList', function() {
169 this.applyList('OL', 'UL');
172 ed.onInit.add(function() {
173 ed.editorCommands.addCommands({
174 'outdent': function() {
175 var sel = ed.selection, dom = ed.dom;
176 function hasStyleIndent(n) {
177 n = dom.getParent(n, dom.isBlock);
178 return n && (parseInt(ed.dom.getStyle(n, 'margin-left') || 0, 10) + parseInt(ed.dom.getStyle(n, 'padding-left') || 0, 10)) > 0;
180 return hasStyleIndent(sel.getStart()) || hasStyleIndent(sel.getEnd()) || ed.queryCommandState('InsertOrderedList') || ed.queryCommandState('InsertUnorderedList');
185 ed.onKeyUp.add(function(ed, e) {
187 if (isTriggerKey(e)) {
188 ed.execCommand(e.shiftKey ? 'Outdent' : 'Indent', true, null);
189 return Event.cancel(e);
190 } else if (enterDownInEmptyList && isEnterInEmptyListItem(ed, e)) {
191 if (ed.queryCommandState('InsertOrderedList')) {
192 ed.execCommand('InsertOrderedList');
194 ed.execCommand('InsertUnorderedList');
196 n = ed.selection.getStart();
197 if (n && n.tagName === 'LI') {
198 // Fix the caret position on IE since it jumps back up to the previous list item.
199 n = ed.dom.getParent(n, 'ol,ul').nextSibling;
200 if (n && n.tagName === 'P') {
202 n.appendChild(ed.getDoc().createTextNode(''));
204 rng = ed.dom.createRng();
205 rng.setStart(n.firstChild, 1);
206 rng.setEnd(n.firstChild, 1);
207 ed.selection.setRng(rng);
210 return Event.cancel(e);
213 ed.onKeyPress.add(cancelKeys);
214 ed.onKeyDown.add(cancelKeys);
217 applyList: function(targetListType, oppositeListType) {
218 var t = this, ed = t.ed, dom = ed.dom, applied = [], hasSameType = false, hasOppositeType = false, hasNonList = false, actions,
219 selectedBlocks = ed.selection.getSelectedBlocks();
221 function cleanupBr(e) {
222 if (e && e.tagName === 'BR') {
227 function makeList(element) {
228 var list = dom.create(targetListType), li;
229 function adjustIndentForNewList(element) {
230 // If there's a margin-left, outdent one level to account for the extra list margin.
231 if (element.style.marginLeft || element.style.paddingLeft) {
232 t.adjustPaddingFunction(false)(element);
236 if (element.tagName === 'LI') {
237 // No change required.
238 } else if (element.tagName === 'P' || element.tagName === 'DIV' || element.tagName === 'BODY') {
239 processBrs(element, function(startSection, br, previousBR) {
240 doWrapList(startSection, br, element.tagName === 'BODY' ? null : startSection.parentNode);
241 li = startSection.parentNode;
242 adjustIndentForNewList(li);
245 if (element.tagName === 'P' || selectedBlocks.length > 1) {
246 dom.split(li.parentNode.parentNode, li.parentNode);
248 attemptMergeWithAdjacent(li.parentNode, true);
251 // Put the list around the element.
252 li = dom.create('li');
253 dom.insertAfter(li, element);
254 li.appendChild(element);
255 adjustIndentForNewList(element);
258 dom.insertAfter(list, element);
259 list.appendChild(element);
260 attemptMergeWithAdjacent(list, true);
261 applied.push(element);
264 function doWrapList(start, end, template) {
265 var li, n = start, tmp, i;
266 while (!dom.isBlock(start.parentNode) && start.parentNode !== dom.getRoot()) {
267 start = dom.split(start.parentNode, start.previousSibling);
268 start = start.nextSibling;
272 li = template.cloneNode(true);
273 start.parentNode.insertBefore(li, start);
274 while (li.firstChild) dom.remove(li.firstChild);
275 li = dom.rename(li, 'li');
277 li = dom.create('li');
278 start.parentNode.insertBefore(li, start);
280 while (n && n != end) {
285 if (li.childNodes.length === 0) {
286 li.innerHTML = '<br _mce_bogus="1" />';
291 function processBrs(element, callback) {
292 var startSection, previousBR, END_TO_START = 3, START_TO_END = 1,
293 breakElements = 'br,ul,ol,p,div,h1,h2,h3,h4,h5,h6,table,blockquote,address,pre,form,center,dl';
294 function isAnyPartSelected(start, end) {
295 var r = dom.createRng(), sel;
296 bookmark.keep = true;
297 ed.selection.moveToBookmark(bookmark);
298 bookmark.keep = false;
299 sel = ed.selection.getRng(true);
301 end = start.parentNode.lastChild;
303 r.setStartBefore(start);
305 return !(r.compareBoundaryPoints(END_TO_START, sel) > 0 || r.compareBoundaryPoints(START_TO_END, sel) <= 0);
307 function nextLeaf(br) {
309 return br.nextSibling;
310 if (!dom.isBlock(br.parentNode) && br.parentNode !== dom.getRoot())
311 return nextLeaf(br.parentNode);
313 // Split on BRs within the range and process those.
314 startSection = element.firstChild;
315 // First mark the BRs that have any part of the previous section selected.
316 var trailingContentSelected = false;
317 each(dom.select(breakElements, element), function(br) {
319 if (br.hasAttribute && br.hasAttribute('_mce_bogus')) {
320 return true; // Skip the bogus Brs that are put in to appease Firefox and Safari.
322 if (isAnyPartSelected(startSection, br)) {
323 dom.addClass(br, '_mce_tagged_br');
324 startSection = nextLeaf(br);
327 trailingContentSelected = (startSection && isAnyPartSelected(startSection, undefined));
328 startSection = element.firstChild;
329 each(dom.select(breakElements, element), function(br) {
330 // Got a section from start to br.
331 var tmp = nextLeaf(br);
332 if (br.hasAttribute && br.hasAttribute('_mce_bogus')) {
333 return true; // Skip the bogus Brs that are put in to appease Firefox and Safari.
335 if (dom.hasClass(br, '_mce_tagged_br')) {
336 callback(startSection, br, previousBR);
343 if (trailingContentSelected) {
344 callback(startSection, undefined, previousBR);
348 function wrapList(element) {
349 processBrs(element, function(startSection, br, previousBR) {
350 // Need to indent this part
351 doWrapList(startSection, br);
353 cleanupBr(previousBR);
357 function changeList(element) {
358 if (tinymce.inArray(applied, element) !== -1) {
361 if (element.parentNode.tagName === oppositeListType) {
362 dom.split(element.parentNode, element);
364 attemptMergeWithNext(element.parentNode, false);
366 applied.push(element);
369 function convertListItemToParagraph(element) {
370 var child, nextChild, mergedElement, splitLast;
371 if (tinymce.inArray(applied, element) !== -1) {
374 element = splitNestedLists(element, dom);
375 while (dom.is(element.parentNode, 'ol,ul,li')) {
376 dom.split(element.parentNode, element);
378 // Push the original element we have from the selection, not the renamed one.
379 applied.push(element);
380 element = dom.rename(element, 'p');
381 mergedElement = attemptMergeWithAdjacent(element, false, ed.settings.force_br_newlines);
382 if (mergedElement === element) {
383 // Now split out any block elements that can't be contained within a P.
384 // Manually iterate to ensure we handle modifications correctly (doesn't work with tinymce.each)
385 child = element.firstChild;
387 if (dom.isBlock(child)) {
388 child = dom.split(child.parentNode, child);
390 nextChild = child.nextSibling && child.nextSibling.firstChild;
392 nextChild = child.nextSibling;
393 if (splitLast && child.tagName === 'BR') {
403 each(selectedBlocks, function(e) {
404 e = findItemToOperateOn(e, dom);
405 if (e.tagName === oppositeListType || (e.tagName === 'LI' && e.parentNode.tagName === oppositeListType)) {
406 hasOppositeType = true;
407 } else if (e.tagName === targetListType || (e.tagName === 'LI' && e.parentNode.tagName === targetListType)) {
414 if (hasNonList || hasOppositeType || selectedBlocks.length === 0) {
425 'DIV': selectedBlocks.length > 1 ? makeList : wrapList,
426 defaultAction: wrapList
430 defaultAction: convertListItemToParagraph
433 this.process(actions);
437 var ed = this.ed, dom = ed.dom, indented = [];
439 function createWrapItem(element) {
440 var wrapItem = dom.create('li', { style: 'list-style-type: none;'});
441 dom.insertAfter(wrapItem, element);
445 function createWrapList(element) {
446 var wrapItem = createWrapItem(element),
447 list = dom.getParent(element, 'ol,ul'),
448 listType = list.tagName,
449 listStyle = dom.getStyle(list, 'list-style-type'),
452 if (listStyle !== '') {
453 attrs.style = 'list-style-type: ' + listStyle + ';';
455 wrapList = dom.create(listType, attrs);
456 wrapItem.appendChild(wrapList);
460 function indentLI(element) {
461 if (!hasParentInList(ed, element, indented)) {
462 element = splitNestedLists(element, dom);
463 var wrapList = createWrapList(element);
464 wrapList.appendChild(element);
465 attemptMergeWithAdjacent(wrapList.parentNode, false);
466 attemptMergeWithAdjacent(wrapList, false);
467 indented.push(element);
473 defaultAction: this.adjustPaddingFunction(true)
478 outdent: function() {
479 var t = this, ed = t.ed, dom = ed.dom, outdented = [];
481 function outdentLI(element) {
482 var listElement, targetParent, align;
483 if (!hasParentInList(ed, element, outdented)) {
484 if (dom.getStyle(element, 'margin-left') !== '' || dom.getStyle(element, 'padding-left') !== '') {
485 return t.adjustPaddingFunction(false)(element);
487 align = dom.getStyle(element, 'text-align', true);
488 if (align === 'center' || align === 'right') {
489 dom.setStyle(element, 'text-align', 'left');
492 element = splitNestedLists(element, dom);
493 listElement = element.parentNode;
494 targetParent = element.parentNode.parentNode;
495 if (targetParent.tagName === 'P') {
496 dom.split(targetParent, element.parentNode);
498 dom.split(listElement, element);
499 if (targetParent.tagName === 'LI') {
500 // Nested list, need to split the LI and go back out to the OL/UL element.
501 dom.split(targetParent, element);
502 } else if (!dom.is(targetParent, 'ol,ul')) {
503 dom.rename(element, 'p');
506 outdented.push(element);
512 defaultAction: this.adjustPaddingFunction(false)
515 each(outdented, attemptMergeWithAdjacent);
518 process: function(actions) {
519 var t = this, sel = t.ed.selection, dom = t.ed.dom, selectedBlocks, r;
520 function processElement(element) {
521 dom.removeClass(element, '_mce_act_on');
522 if (!element || element.nodeType !== 1) {
525 element = findItemToOperateOn(element, dom);
526 var action = actions[element.tagName];
528 action = actions.defaultAction;
532 function recurse(element) {
533 t.splitSafeEach(element.childNodes, processElement);
535 function brAtEdgeOfSelection(container, offset) {
536 return offset >= 0 && container.hasChildNodes() && offset < container.childNodes.length &&
537 container.childNodes[offset].tagName === 'BR';
539 selectedBlocks = sel.getSelectedBlocks();
540 if (selectedBlocks.length === 0) {
541 selectedBlocks = [ dom.getRoot() ];
544 r = sel.getRng(true);
546 if (brAtEdgeOfSelection(r.endContainer, r.endOffset - 1)) {
547 r.setEnd(r.endContainer, r.endOffset - 1);
550 if (brAtEdgeOfSelection(r.startContainer, r.startOffset)) {
551 r.setStart(r.startContainer, r.startOffset + 1);
555 bookmark = sel.getBookmark();
556 actions.OL = actions.UL = recurse;
557 t.splitSafeEach(selectedBlocks, processElement);
558 sel.moveToBookmark(bookmark);
560 // Avoids table or image handles being left behind in Firefox.
561 t.ed.execCommand('mceRepaint');
564 splitSafeEach: function(elements, f) {
565 if (tinymce.isGecko && (/Firefox\/[12]\.[0-9]/.test(navigator.userAgent) ||
566 /Firefox\/3\.[0-4]/.test(navigator.userAgent))) {
567 this.classBasedEach(elements, f);
573 classBasedEach: function(elements, f) {
574 var dom = this.ed.dom, nodes, element;
576 each(elements, function(element) {
577 dom.addClass(element, '_mce_act_on');
579 nodes = dom.select('._mce_act_on');
580 while (nodes.length > 0) {
581 element = nodes.shift();
582 dom.removeClass(element, '_mce_act_on');
584 nodes = dom.select('._mce_act_on');
588 adjustPaddingFunction: function(isIndent) {
589 var indentAmount, indentUnits, ed = this.ed;
590 indentAmount = ed.settings.indentation;
591 indentUnits = /[a-z%]+/i.exec(indentAmount);
592 indentAmount = parseInt(indentAmount, 10);
593 return function(element) {
594 var currentIndent, newIndentAmount;
595 currentIndent = parseInt(ed.dom.getStyle(element, 'margin-left') || 0, 10) + parseInt(ed.dom.getStyle(element, 'padding-left') || 0, 10);
597 newIndentAmount = currentIndent + indentAmount;
599 newIndentAmount = currentIndent - indentAmount;
601 ed.dom.setStyle(element, 'padding-left', '');
602 ed.dom.setStyle(element, 'margin-left', newIndentAmount > 0 ? newIndentAmount + indentUnits : '');
606 getInfo: function() {
609 author : 'Moxiecode Systems AB',
610 authorurl : 'http://tinymce.moxiecode.com',
611 infourl : 'http://wiki.moxiecode.com/index.php/TinyMCE:Plugins/lists',
612 version : tinymce.majorVersion + "." + tinymce.minorVersion
616 tinymce.PluginManager.add("lists", tinymce.plugins.Lists);