2 * editor_plugin_src.js
\r
4 * Copyright 2011, Moxiecode Systems AB
\r
5 * Released under LGPL License.
\r
7 * License: http://tinymce.moxiecode.com/license
\r
8 * Contributing: http://tinymce.moxiecode.com/contributing
\r
12 var each = tinymce.each, Event = tinymce.dom.Event, bookmark;
\r
14 // Skips text nodes that only contain whitespace since they aren't semantically important.
\r
15 function skipWhitespaceNodes(e, next) {
\r
16 while (e && (e.nodeType === 8 || (e.nodeType === 3 && /^[ \t\n\r]*$/.test(e.nodeValue)))) {
\r
22 function skipWhitespaceNodesBackwards(e) {
\r
23 return skipWhitespaceNodes(e, function(e) {
\r
24 return e.previousSibling;
\r
28 function skipWhitespaceNodesForwards(e) {
\r
29 return skipWhitespaceNodes(e, function(e) {
\r
30 return e.nextSibling;
\r
34 function hasParentInList(ed, e, list) {
\r
35 return ed.dom.getParent(e, function(p) {
\r
36 return tinymce.inArray(list, p) !== -1;
\r
40 function isList(e) {
\r
41 return e && (e.tagName === 'OL' || e.tagName === 'UL');
\r
44 function splitNestedLists(element, dom) {
\r
45 var tmp, nested, wrapItem;
\r
46 tmp = skipWhitespaceNodesBackwards(element.lastChild);
\r
47 while (isList(tmp)) {
\r
49 tmp = skipWhitespaceNodesBackwards(nested.previousSibling);
\r
52 wrapItem = dom.create('li', { style: 'list-style-type: none;'});
\r
53 dom.split(element, nested);
\r
54 dom.insertAfter(wrapItem, nested);
\r
55 wrapItem.appendChild(nested);
\r
56 wrapItem.appendChild(nested);
\r
57 element = wrapItem.previousSibling;
\r
62 function attemptMergeWithAdjacent(e, allowDifferentListStyles, mergeParagraphs) {
\r
63 e = attemptMergeWithPrevious(e, allowDifferentListStyles, mergeParagraphs);
\r
64 return attemptMergeWithNext(e, allowDifferentListStyles, mergeParagraphs);
\r
67 function attemptMergeWithPrevious(e, allowDifferentListStyles, mergeParagraphs) {
\r
68 var prev = skipWhitespaceNodesBackwards(e.previousSibling);
\r
70 return attemptMerge(prev, e, allowDifferentListStyles ? prev : false, mergeParagraphs);
\r
76 function attemptMergeWithNext(e, allowDifferentListStyles, mergeParagraphs) {
\r
77 var next = skipWhitespaceNodesForwards(e.nextSibling);
\r
79 return attemptMerge(e, next, allowDifferentListStyles ? next : false, mergeParagraphs);
\r
85 function attemptMerge(e1, e2, differentStylesMasterElement, mergeParagraphs) {
\r
86 if (canMerge(e1, e2, !!differentStylesMasterElement, mergeParagraphs)) {
\r
87 return merge(e1, e2, differentStylesMasterElement);
\r
88 } else if (e1 && e1.tagName === 'LI' && isList(e2)) {
\r
89 // Fix invalidly nested lists.
\r
95 function canMerge(e1, e2, allowDifferentListStyles, mergeParagraphs) {
\r
98 } else if (e1.tagName === 'LI' && e2.tagName === 'LI') {
\r
99 return e2.style.listStyleType === 'none' || containsOnlyAList(e2);
\r
100 } else if (isList(e1)) {
\r
101 return (e1.tagName === e2.tagName && (allowDifferentListStyles || e1.style.listStyleType === e2.style.listStyleType)) || isListForIndent(e2);
\r
102 } else return mergeParagraphs && e1.tagName === 'P' && e2.tagName === 'P';
\r
105 function isListForIndent(e) {
\r
106 var firstLI = skipWhitespaceNodesForwards(e.firstChild), lastLI = skipWhitespaceNodesBackwards(e.lastChild);
\r
107 return firstLI && lastLI && isList(e) && firstLI === lastLI && (isList(firstLI) || firstLI.style.listStyleType === 'none' || containsOnlyAList(firstLI));
\r
110 function containsOnlyAList(e) {
\r
111 var firstChild = skipWhitespaceNodesForwards(e.firstChild), lastChild = skipWhitespaceNodesBackwards(e.lastChild);
\r
112 return firstChild && lastChild && firstChild === lastChild && isList(firstChild);
\r
115 function merge(e1, e2, masterElement) {
\r
116 var lastOriginal = skipWhitespaceNodesBackwards(e1.lastChild), firstNew = skipWhitespaceNodesForwards(e2.firstChild);
\r
117 if (e1.tagName === 'P') {
\r
118 e1.appendChild(e1.ownerDocument.createElement('br'));
\r
120 while (e2.firstChild) {
\r
121 e1.appendChild(e2.firstChild);
\r
123 if (masterElement) {
\r
124 e1.style.listStyleType = masterElement.style.listStyleType;
\r
126 e2.parentNode.removeChild(e2);
\r
127 attemptMerge(lastOriginal, firstNew, false);
\r
131 function findItemToOperateOn(e, dom) {
\r
133 if (!dom.is(e, 'li,ol,ul')) {
\r
134 item = dom.getParent(e, 'li');
\r
142 tinymce.create('tinymce.plugins.Lists', {
\r
143 init: function(ed) {
\r
144 var LIST_TABBING = 'TABBING';
\r
145 var LIST_EMPTY_ITEM = 'EMPTY';
\r
146 var LIST_ESCAPE = 'ESCAPE';
\r
147 var LIST_PARAGRAPH = 'PARAGRAPH';
\r
148 var LIST_UNKNOWN = 'UNKNOWN';
\r
149 var state = LIST_UNKNOWN;
\r
151 function isTabInList(e) {
\r
152 // Don't indent on Ctrl+Tab or Alt+Tab
\r
153 return e.keyCode === tinymce.VK.TAB && !(e.altKey || e.ctrlKey) &&
\r
154 (ed.queryCommandState('InsertUnorderedList') || ed.queryCommandState('InsertOrderedList'));
\r
157 function isOnLastListItem() {
\r
159 var grandParent = li.parentNode.parentNode;
\r
160 var isLastItem = li.parentNode.lastChild === li;
\r
161 return isLastItem && !isNestedList(grandParent) && isEmptyListItem(li);
\r
164 function isNestedList(grandParent) {
\r
165 if (isList(grandParent)) {
\r
166 return grandParent.parentNode && grandParent.parentNode.tagName === 'LI';
\r
168 return grandParent.tagName === 'LI';
\r
172 function isInEmptyListItem() {
\r
173 return ed.selection.isCollapsed() && isEmptyListItem(getLi());
\r
177 var n = ed.selection.getStart();
\r
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
\r
179 return ((n.tagName == 'BR' || n.tagName == '') && n.parentNode.tagName == 'LI') ? n.parentNode : n;
\r
182 function isEmptyListItem(li) {
\r
183 var numChildren = li.childNodes.length;
\r
184 if (li.tagName === 'LI') {
\r
185 return numChildren == 0 ? true : numChildren == 1 && (li.firstChild.tagName == '' || li.firstChild.tagName == 'BR' || isEmptyIE9Li(li));
\r
190 function isEmptyIE9Li(li) {
\r
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
\r
192 var lis = tinymce.grep(li.parentNode.childNodes, function(n) {return n.tagName == 'LI'});
\r
193 var isLastLi = li == lis[lis.length - 1];
\r
194 var child = li.firstChild;
\r
195 return tinymce.isIE9 && isLastLi && (child.nodeValue == String.fromCharCode(160) || child.nodeValue == String.fromCharCode(32));
\r
198 function isEnter(e) {
\r
199 return e.keyCode === tinymce.VK.ENTER;
\r
202 function isEnterWithoutShift(e) {
\r
203 return isEnter(e) && !e.shiftKey;
\r
206 function getListKeyState(e) {
\r
207 if (isTabInList(e)) {
\r
208 return LIST_TABBING;
\r
209 } else if (isEnterWithoutShift(e) && isOnLastListItem()) {
\r
210 // Returns LIST_UNKNOWN since breaking out of lists is handled by the EnterKey.js logic now
\r
211 //return LIST_ESCAPE;
\r
212 return LIST_UNKNOWN;
\r
213 } else if (isEnterWithoutShift(e) && isInEmptyListItem()) {
\r
214 return LIST_EMPTY_ITEM;
\r
216 return LIST_UNKNOWN;
\r
220 function cancelDefaultEvents(ed, e) {
\r
221 // list escape is done manually using outdent as it does not create paragraphs correctly in td's
\r
222 if (state == LIST_TABBING || state == LIST_EMPTY_ITEM || tinymce.isGecko && state == LIST_ESCAPE) {
\r
227 function isCursorAtEndOfContainer() {
\r
228 var range = ed.selection.getRng(true);
\r
229 var startContainer = range.startContainer;
\r
230 if (startContainer.nodeType == 3) {
\r
231 var value = startContainer.nodeValue;
\r
232 if (tinymce.isIE9 && value.length > 1 && value.charCodeAt(value.length-1) == 32) {
\r
233 // IE9 places a space on the end of the text in some cases so ignore last char
\r
234 return (range.endOffset == value.length-1);
\r
236 return (range.endOffset == value.length);
\r
238 } else if (startContainer.nodeType == 1) {
\r
239 return range.endOffset == startContainer.childNodes.length;
\r
245 If we are at the end of a list item surrounded with an element, pressing enter should create a
\r
246 new list item instead without splitting the element e.g. don't want to create new P or H1 tag
\r
248 function isEndOfListItem() {
\r
249 var node = ed.selection.getNode();
\r
250 var validElements = 'h1,h2,h3,h4,h5,h6,p,div';
\r
251 var isLastParagraphOfLi = ed.dom.is(node, validElements) && node.parentNode.tagName === 'LI' && node.parentNode.lastChild === node;
\r
252 return ed.selection.isCollapsed() && isLastParagraphOfLi && isCursorAtEndOfContainer();
\r
255 // Creates a new list item after the current selection's list item parent
\r
256 function createNewLi(ed, e) {
\r
257 if (isEnterWithoutShift(e) && isEndOfListItem()) {
\r
258 var node = ed.selection.getNode();
\r
259 var li = ed.dom.create("li");
\r
260 var parentLi = ed.dom.getParent(node, 'li');
\r
261 ed.dom.insertAfter(li, parentLi);
\r
263 // Move caret to new list element.
\r
264 if (tinymce.isIE6 || tinymce.isIE7 || tinyMCE.isIE8) {
\r
265 // Removed this line since it would create an odd < > tag and placing the caret inside an empty LI is handled and should be handled by the selection logic
\r
266 //li.appendChild(ed.dom.create(" ")); // IE needs an element within the bullet point
\r
267 ed.selection.setCursorLocation(li, 1);
\r
269 ed.selection.setCursorLocation(li, 0);
\r
271 e.preventDefault();
\r
275 function imageJoiningListItem(ed, e) {
\r
278 if (!tinymce.isGecko)
\r
281 var n = ed.selection.getStart();
\r
282 if (e.keyCode != tinymce.VK.BACKSPACE || n.tagName !== 'IMG')
\r
285 function lastLI(node) {
\r
286 var child = node.firstChild;
\r
292 if (child.tagName === 'LI')
\r
294 } while (child = child.nextSibling);
\r
299 function addChildren(parentNode, destination) {
\r
300 while (parentNode.childNodes.length > 0)
\r
301 destination.appendChild(parentNode.childNodes[0]);
\r
304 // Check if there is a previous sibling
\r
305 prevSibling = n.parentNode.previousSibling;
\r
310 if (prevSibling.tagName === 'UL' || prevSibling.tagName === 'OL')
\r
312 else if (prevSibling.previousSibling && (prevSibling.previousSibling.tagName === 'UL' || prevSibling.previousSibling.tagName === 'OL'))
\r
313 ul = prevSibling.previousSibling;
\r
317 var li = lastLI(ul);
\r
319 // move the caret to the end of the list item
\r
320 var rng = ed.dom.createRng();
\r
321 rng.setStart(li, 1);
\r
323 ed.selection.setRng(rng);
\r
324 ed.selection.collapse(true);
\r
326 // save a bookmark at the end of the list item
\r
327 var bookmark = ed.selection.getBookmark();
\r
329 // copy the image an its text to the list item
\r
330 var clone = n.parentNode.cloneNode(true);
\r
331 if (clone.tagName === 'P' || clone.tagName === 'DIV')
\r
332 addChildren(clone, li);
\r
334 li.appendChild(clone);
\r
336 // remove the old copy of the image
\r
337 n.parentNode.parentNode.removeChild(n.parentNode);
\r
339 // move the caret where we saved the bookmark
\r
340 ed.selection.moveToBookmark(bookmark);
\r
343 // fix the cursor position to ensure it is correct in IE
\r
344 function setCursorPositionToOriginalLi(li) {
\r
345 var list = ed.dom.getParent(li, 'ol,ul');
\r
346 if (list != null) {
\r
347 var lastLi = list.lastChild;
\r
348 // Removed this line since IE9 would report an DOM character error and placing the caret inside an empty LI is handled and should be handled by the selection logic
\r
349 //lastLi.appendChild(ed.getDoc().createElement(''));
\r
350 ed.selection.setCursorLocation(lastLi, 0);
\r
355 ed.addCommand('Indent', this.indent, this);
\r
356 ed.addCommand('Outdent', this.outdent, this);
\r
357 ed.addCommand('InsertUnorderedList', function() {
\r
358 this.applyList('UL', 'OL');
\r
360 ed.addCommand('InsertOrderedList', function() {
\r
361 this.applyList('OL', 'UL');
\r
364 ed.onInit.add(function() {
\r
365 ed.editorCommands.addCommands({
\r
366 'outdent': function() {
\r
367 var sel = ed.selection, dom = ed.dom;
\r
369 function hasStyleIndent(n) {
\r
370 n = dom.getParent(n, dom.isBlock);
\r
371 return n && (parseInt(ed.dom.getStyle(n, 'margin-left') || 0, 10) + parseInt(ed.dom.getStyle(n, 'padding-left') || 0, 10)) > 0;
\r
374 return hasStyleIndent(sel.getStart()) || hasStyleIndent(sel.getEnd()) || ed.queryCommandState('InsertOrderedList') || ed.queryCommandState('InsertUnorderedList');
\r
379 ed.onKeyUp.add(function(ed, e) {
\r
380 if (state == LIST_TABBING) {
\r
381 ed.execCommand(e.shiftKey ? 'Outdent' : 'Indent', true, null);
\r
382 state = LIST_UNKNOWN;
\r
383 return Event.cancel(e);
\r
384 } else if (state == LIST_EMPTY_ITEM) {
\r
386 var shouldOutdent = ed.settings.list_outdent_on_enter === true || e.shiftKey;
\r
387 ed.execCommand(shouldOutdent ? 'Outdent' : 'Indent', true, null);
\r
388 if (tinymce.isIE) {
\r
389 setCursorPositionToOriginalLi(li);
\r
392 return Event.cancel(e);
\r
393 } else if (state == LIST_ESCAPE) {
\r
394 if (tinymce.isIE6 || tinymce.isIE7 || tinymce.isIE8) {
\r
395 // append a zero sized nbsp so that caret is positioned correctly in IE after escaping and applying formatting.
\r
396 // if there is no text then applying formatting for e.g a H1 to the P tag immediately following list after
\r
397 // escaping from it will cause the caret to be positioned on the last li instead of staying the in P tag.
\r
398 var n = ed.getDoc().createTextNode('\uFEFF');
\r
399 ed.selection.getNode().appendChild(n);
\r
400 } else if (tinymce.isIE9 || tinymce.isGecko) {
\r
401 // IE9 does not escape the list so we use outdent to do this and cancel the default behaviour
\r
402 // Gecko does not create a paragraph outdenting inside a TD so default behaviour is cancelled and we outdent ourselves
\r
403 ed.execCommand('Outdent');
\r
404 return Event.cancel(e);
\r
409 function fixListItem(parent, reference) {
\r
410 // a zero-sized non-breaking space is placed in the empty list item so that the nested list is
\r
411 // displayed on the below line instead of next to it
\r
412 var n = ed.getDoc().createTextNode('\uFEFF');
\r
413 parent.insertBefore(n, reference);
\r
414 ed.selection.setCursorLocation(n, 0);
\r
415 // repaint to remove rendering artifact. only visible when creating new list
\r
416 ed.execCommand('mceRepaint');
\r
419 function fixIndentedListItemForGecko(ed, e) {
\r
423 var parent = li.parentNode;
\r
424 var grandParent = parent && parent.parentNode;
\r
425 if (grandParent && grandParent.nodeName == 'LI' && grandParent.firstChild == parent && li == parent.firstChild) {
\r
426 fixListItem(grandParent, parent);
\r
432 function fixIndentedListItemForIE8(ed, e) {
\r
435 if (ed.dom.select('ul li', li).length === 1) {
\r
436 var list = li.firstChild;
\r
437 fixListItem(li, list);
\r
442 function fixDeletingFirstCharOfList(ed, e) {
\r
443 function listElements(list, li) {
\r
445 var walker = new tinymce.dom.TreeWalker(li, list);
\r
446 for (var node = walker.current(); node; node = walker.next()) {
\r
447 if (ed.dom.is(node, 'ol,ul,li')) {
\r
448 elements.push(node);
\r
454 if (e.keyCode == tinymce.VK.BACKSPACE) {
\r
457 var list = ed.dom.getParent(li, 'ol,ul');
\r
458 if (list && list.firstChild === li) {
\r
459 var elements = listElements(list, li);
\r
460 ed.execCommand("Outdent", false, elements);
\r
461 ed.undoManager.add();
\r
462 return Event.cancel(e);
\r
468 function fixDeletingEmptyLiInWebkit(ed, e) {
\r
470 if (e.keyCode === tinymce.VK.BACKSPACE && ed.dom.is(li, 'li') && li.parentNode.firstChild!==li) {
\r
471 if (ed.dom.select('ul,ol', li).length === 1) {
\r
472 var prevLi = li.previousSibling;
\r
473 ed.dom.remove(ed.dom.select('br', li));
\r
474 ed.dom.remove(li, true);
\r
475 var textNodes = tinymce.grep(prevLi.childNodes, function(n){ return n.nodeType === 3 });
\r
476 if (textNodes.length === 1) {
\r
477 var textNode = textNodes[0]
\r
478 ed.selection.setCursorLocation(textNode, textNode.length);
\r
480 ed.undoManager.add();
\r
481 return Event.cancel(e);
\r
486 ed.onKeyDown.add(function(_, e) { state = getListKeyState(e); });
\r
487 ed.onKeyDown.add(cancelDefaultEvents);
\r
488 ed.onKeyDown.add(imageJoiningListItem);
\r
489 ed.onKeyDown.add(createNewLi);
\r
491 if (tinymce.isGecko) {
\r
492 ed.onKeyUp.add(fixIndentedListItemForGecko);
\r
494 if (tinymce.isIE8) {
\r
495 ed.onKeyUp.add(fixIndentedListItemForIE8);
\r
497 if (tinymce.isGecko || tinymce.isWebKit) {
\r
498 ed.onKeyDown.add(fixDeletingFirstCharOfList);
\r
500 if (tinymce.isWebKit) {
\r
501 ed.onKeyDown.add(fixDeletingEmptyLiInWebkit);
\r
505 applyList: function(targetListType, oppositeListType) {
\r
506 var t = this, ed = t.ed, dom = ed.dom, applied = [], hasSameType = false, hasOppositeType = false, hasNonList = false, actions,
\r
507 selectedBlocks = ed.selection.getSelectedBlocks();
\r
509 function cleanupBr(e) {
\r
510 if (e && e.tagName === 'BR') {
\r
515 function makeList(element) {
\r
516 var list = dom.create(targetListType), li;
\r
518 function adjustIndentForNewList(element) {
\r
519 // If there's a margin-left, outdent one level to account for the extra list margin.
\r
520 if (element.style.marginLeft || element.style.paddingLeft) {
\r
521 t.adjustPaddingFunction(false)(element);
\r
525 if (element.tagName === 'LI') {
\r
526 // No change required.
\r
527 } else if (element.tagName === 'P' || element.tagName === 'DIV' || element.tagName === 'BODY') {
\r
528 processBrs(element, function(startSection, br) {
\r
529 doWrapList(startSection, br, element.tagName === 'BODY' ? null : startSection.parentNode);
\r
530 li = startSection.parentNode;
\r
531 adjustIndentForNewList(li);
\r
535 if (li.tagName === 'LI' && (element.tagName === 'P' || selectedBlocks.length > 1)) {
\r
536 dom.split(li.parentNode.parentNode, li.parentNode);
\r
538 attemptMergeWithAdjacent(li.parentNode, true);
\r
542 // Put the list around the element.
\r
543 li = dom.create('li');
\r
544 dom.insertAfter(li, element);
\r
545 li.appendChild(element);
\r
546 adjustIndentForNewList(element);
\r
549 dom.insertAfter(list, element);
\r
550 list.appendChild(element);
\r
551 attemptMergeWithAdjacent(list, true);
\r
552 applied.push(element);
\r
555 function doWrapList(start, end, template) {
\r
556 var li, n = start, tmp;
\r
557 while (!dom.isBlock(start.parentNode) && start.parentNode !== dom.getRoot()) {
\r
558 start = dom.split(start.parentNode, start.previousSibling);
\r
559 start = start.nextSibling;
\r
563 li = template.cloneNode(true);
\r
564 start.parentNode.insertBefore(li, start);
\r
565 while (li.firstChild) dom.remove(li.firstChild);
\r
566 li = dom.rename(li, 'li');
\r
568 li = dom.create('li');
\r
569 start.parentNode.insertBefore(li, start);
\r
571 while (n && n != end) {
\r
572 tmp = n.nextSibling;
\r
576 if (li.childNodes.length === 0) {
\r
577 li.innerHTML = '<br _mce_bogus="1" />';
\r
582 function processBrs(element, callback) {
\r
583 var startSection, previousBR, END_TO_START = 3, START_TO_END = 1,
\r
584 breakElements = 'br,ul,ol,p,div,h1,h2,h3,h4,h5,h6,table,blockquote,address,pre,form,center,dl';
\r
586 function isAnyPartSelected(start, end) {
\r
587 var r = dom.createRng(), sel;
\r
588 bookmark.keep = true;
\r
589 ed.selection.moveToBookmark(bookmark);
\r
590 bookmark.keep = false;
\r
591 sel = ed.selection.getRng(true);
\r
593 end = start.parentNode.lastChild;
\r
595 r.setStartBefore(start);
\r
596 r.setEndAfter(end);
\r
597 return !(r.compareBoundaryPoints(END_TO_START, sel) > 0 || r.compareBoundaryPoints(START_TO_END, sel) <= 0);
\r
600 function nextLeaf(br) {
\r
601 if (br.nextSibling)
\r
602 return br.nextSibling;
\r
603 if (!dom.isBlock(br.parentNode) && br.parentNode !== dom.getRoot())
\r
604 return nextLeaf(br.parentNode);
\r
607 // Split on BRs within the range and process those.
\r
608 startSection = element.firstChild;
\r
609 // First mark the BRs that have any part of the previous section selected.
\r
610 var trailingContentSelected = false;
\r
611 each(dom.select(breakElements, element), function(br) {
\r
612 if (br.hasAttribute && br.hasAttribute('_mce_bogus')) {
\r
613 return true; // Skip the bogus Brs that are put in to appease Firefox and Safari.
\r
615 if (isAnyPartSelected(startSection, br)) {
\r
616 dom.addClass(br, '_mce_tagged_br');
\r
617 startSection = nextLeaf(br);
\r
620 trailingContentSelected = (startSection && isAnyPartSelected(startSection, undefined));
\r
621 startSection = element.firstChild;
\r
622 each(dom.select(breakElements, element), function(br) {
\r
623 // Got a section from start to br.
\r
624 var tmp = nextLeaf(br);
\r
625 if (br.hasAttribute && br.hasAttribute('_mce_bogus')) {
\r
626 return true; // Skip the bogus Brs that are put in to appease Firefox and Safari.
\r
628 if (dom.hasClass(br, '_mce_tagged_br')) {
\r
629 callback(startSection, br, previousBR);
\r
634 startSection = tmp;
\r
636 if (trailingContentSelected) {
\r
637 callback(startSection, undefined, previousBR);
\r
641 function wrapList(element) {
\r
642 processBrs(element, function(startSection, br, previousBR) {
\r
643 // Need to indent this part
\r
644 doWrapList(startSection, br);
\r
646 cleanupBr(previousBR);
\r
650 function changeList(element) {
\r
651 if (tinymce.inArray(applied, element) !== -1) {
\r
654 if (element.parentNode.tagName === oppositeListType) {
\r
655 dom.split(element.parentNode, element);
\r
657 attemptMergeWithNext(element.parentNode, false);
\r
659 applied.push(element);
\r
662 function convertListItemToParagraph(element) {
\r
663 var child, nextChild, mergedElement, splitLast;
\r
664 if (tinymce.inArray(applied, element) !== -1) {
\r
667 element = splitNestedLists(element, dom);
\r
668 while (dom.is(element.parentNode, 'ol,ul,li')) {
\r
669 dom.split(element.parentNode, element);
\r
671 // Push the original element we have from the selection, not the renamed one.
\r
672 applied.push(element);
\r
673 element = dom.rename(element, 'p');
\r
674 mergedElement = attemptMergeWithAdjacent(element, false, ed.settings.force_br_newlines);
\r
675 if (mergedElement === element) {
\r
676 // Now split out any block elements that can't be contained within a P.
\r
677 // Manually iterate to ensure we handle modifications correctly (doesn't work with tinymce.each)
\r
678 child = element.firstChild;
\r
680 if (dom.isBlock(child)) {
\r
681 child = dom.split(child.parentNode, child);
\r
683 nextChild = child.nextSibling && child.nextSibling.firstChild;
\r
685 nextChild = child.nextSibling;
\r
686 if (splitLast && child.tagName === 'BR') {
\r
696 each(selectedBlocks, function(e) {
\r
697 e = findItemToOperateOn(e, dom);
\r
698 if (e.tagName === oppositeListType || (e.tagName === 'LI' && e.parentNode.tagName === oppositeListType)) {
\r
699 hasOppositeType = true;
\r
700 } else if (e.tagName === targetListType || (e.tagName === 'LI' && e.parentNode.tagName === targetListType)) {
\r
701 hasSameType = true;
\r
707 if (hasNonList &&!hasSameType || hasOppositeType || selectedBlocks.length === 0) {
\r
718 'DIV': selectedBlocks.length > 1 ? makeList : wrapList,
\r
719 defaultAction: wrapList,
\r
720 elements: this.selectedBlocks()
\r
724 defaultAction: convertListItemToParagraph,
\r
725 elements: this.selectedBlocks()
\r
728 this.process(actions);
\r
731 indent: function() {
\r
732 var ed = this.ed, dom = ed.dom, indented = [];
\r
734 function createWrapItem(element) {
\r
735 var wrapItem = dom.create('li', { style: 'list-style-type: none;'});
\r
736 dom.insertAfter(wrapItem, element);
\r
740 function createWrapList(element) {
\r
741 var wrapItem = createWrapItem(element),
\r
742 list = dom.getParent(element, 'ol,ul'),
\r
743 listType = list.tagName,
\r
744 listStyle = dom.getStyle(list, 'list-style-type'),
\r
747 if (listStyle !== '') {
\r
748 attrs.style = 'list-style-type: ' + listStyle + ';';
\r
750 wrapList = dom.create(listType, attrs);
\r
751 wrapItem.appendChild(wrapList);
\r
755 function indentLI(element) {
\r
756 if (!hasParentInList(ed, element, indented)) {
\r
757 element = splitNestedLists(element, dom);
\r
758 var wrapList = createWrapList(element);
\r
759 wrapList.appendChild(element);
\r
760 attemptMergeWithAdjacent(wrapList.parentNode, false);
\r
761 attemptMergeWithAdjacent(wrapList, false);
\r
762 indented.push(element);
\r
768 defaultAction: this.adjustPaddingFunction(true),
\r
769 elements: this.selectedBlocks()
\r
774 outdent: function(ui, elements) {
\r
775 var t = this, ed = t.ed, dom = ed.dom, outdented = [];
\r
777 function outdentLI(element) {
\r
778 var listElement, targetParent, align;
\r
779 if (!hasParentInList(ed, element, outdented)) {
\r
780 if (dom.getStyle(element, 'margin-left') !== '' || dom.getStyle(element, 'padding-left') !== '') {
\r
781 return t.adjustPaddingFunction(false)(element);
\r
783 align = dom.getStyle(element, 'text-align', true);
\r
784 if (align === 'center' || align === 'right') {
\r
785 dom.setStyle(element, 'text-align', 'left');
\r
788 element = splitNestedLists(element, dom);
\r
789 listElement = element.parentNode;
\r
790 targetParent = element.parentNode.parentNode;
\r
791 if (targetParent.tagName === 'P') {
\r
792 dom.split(targetParent, element.parentNode);
\r
794 dom.split(listElement, element);
\r
795 if (targetParent.tagName === 'LI') {
\r
796 // Nested list, need to split the LI and go back out to the OL/UL element.
\r
797 dom.split(targetParent, element);
\r
798 } else if (!dom.is(targetParent, 'ol,ul')) {
\r
799 dom.rename(element, 'p');
\r
802 outdented.push(element);
\r
806 var listElements = elements && tinymce.is(elements, 'array') ? elements : this.selectedBlocks();
\r
809 defaultAction: this.adjustPaddingFunction(false),
\r
810 elements: listElements
\r
813 each(outdented, attemptMergeWithAdjacent);
\r
816 process: function(actions) {
\r
817 var t = this, sel = t.ed.selection, dom = t.ed.dom, selectedBlocks, r;
\r
819 function isEmptyElement(element) {
\r
820 var excludeBrsAndBookmarks = tinymce.grep(element.childNodes, function(n) {
\r
821 return !(n.nodeName === 'BR' || n.nodeName === 'SPAN' && dom.getAttrib(n, 'data-mce-type') == 'bookmark'
\r
822 || n.nodeType == 3 && (n.nodeValue == String.fromCharCode(160) || n.nodeValue == ''));
\r
824 return excludeBrsAndBookmarks.length === 0;
\r
827 function processElement(element) {
\r
828 dom.removeClass(element, '_mce_act_on');
\r
829 if (!element || element.nodeType !== 1 || selectedBlocks.length > 1 && isEmptyElement(element)) {
\r
832 element = findItemToOperateOn(element, dom);
\r
833 var action = actions[element.tagName];
\r
835 action = actions.defaultAction;
\r
840 function recurse(element) {
\r
841 t.splitSafeEach(element.childNodes, processElement);
\r
844 function brAtEdgeOfSelection(container, offset) {
\r
845 return offset >= 0 && container.hasChildNodes() && offset < container.childNodes.length &&
\r
846 container.childNodes[offset].tagName === 'BR';
\r
849 function isInTable() {
\r
850 var n = sel.getNode();
\r
851 var p = dom.getParent(n, 'td');
\r
855 selectedBlocks = actions.elements;
\r
857 r = sel.getRng(true);
\r
858 if (!r.collapsed) {
\r
859 if (brAtEdgeOfSelection(r.endContainer, r.endOffset - 1)) {
\r
860 r.setEnd(r.endContainer, r.endOffset - 1);
\r
863 if (brAtEdgeOfSelection(r.startContainer, r.startOffset)) {
\r
864 r.setStart(r.startContainer, r.startOffset + 1);
\r
870 if (tinymce.isIE8) {
\r
871 // append a zero sized nbsp so that caret is restored correctly using bookmark
\r
872 var s = t.ed.selection.getNode();
\r
873 if (s.tagName === 'LI' && !(s.parentNode.lastChild === s)) {
\r
874 var i = t.ed.getDoc().createTextNode('\uFEFF');
\r
879 bookmark = sel.getBookmark();
\r
880 actions.OL = actions.UL = recurse;
\r
881 t.splitSafeEach(selectedBlocks, processElement);
\r
882 sel.moveToBookmark(bookmark);
\r
885 // we avoid doing repaint in a table as this will move the caret out of the table in Firefox 3.6
\r
886 if (!isInTable()) {
\r
887 // Avoids table or image handles being left behind in Firefox.
\r
888 t.ed.execCommand('mceRepaint');
\r
892 splitSafeEach: function(elements, f) {
\r
893 if (tinymce.isGecko && (/Firefox\/[12]\.[0-9]/.test(navigator.userAgent) ||
\r
894 /Firefox\/3\.[0-4]/.test(navigator.userAgent))) {
\r
895 this.classBasedEach(elements, f);
\r
901 classBasedEach: function(elements, f) {
\r
902 var dom = this.ed.dom, nodes, element;
\r
904 each(elements, function(element) {
\r
905 dom.addClass(element, '_mce_act_on');
\r
907 nodes = dom.select('._mce_act_on');
\r
908 while (nodes.length > 0) {
\r
909 element = nodes.shift();
\r
910 dom.removeClass(element, '_mce_act_on');
\r
912 nodes = dom.select('._mce_act_on');
\r
916 adjustPaddingFunction: function(isIndent) {
\r
917 var indentAmount, indentUnits, ed = this.ed;
\r
918 indentAmount = ed.settings.indentation;
\r
919 indentUnits = /[a-z%]+/i.exec(indentAmount);
\r
920 indentAmount = parseInt(indentAmount, 10);
\r
921 return function(element) {
\r
922 var currentIndent, newIndentAmount;
\r
923 currentIndent = parseInt(ed.dom.getStyle(element, 'margin-left') || 0, 10) + parseInt(ed.dom.getStyle(element, 'padding-left') || 0, 10);
\r
925 newIndentAmount = currentIndent + indentAmount;
\r
927 newIndentAmount = currentIndent - indentAmount;
\r
929 ed.dom.setStyle(element, 'padding-left', '');
\r
930 ed.dom.setStyle(element, 'margin-left', newIndentAmount > 0 ? newIndentAmount + indentUnits : '');
\r
934 selectedBlocks: function() {
\r
936 var selectedBlocks = ed.selection.getSelectedBlocks();
\r
937 return selectedBlocks.length == 0 ? [ ed.dom.getRoot() ] : selectedBlocks;
\r
940 getInfo: function() {
\r
942 longname : 'Lists',
\r
943 author : 'Moxiecode Systems AB',
\r
944 authorurl : 'http://tinymce.moxiecode.com',
\r
945 infourl : 'http://wiki.moxiecode.com/index.php/TinyMCE:Plugins/lists',
\r
946 version : tinymce.majorVersion + "." + tinymce.minorVersion
\r
950 tinymce.PluginManager.add("lists", tinymce.plugins.Lists);
\r