| | |
| | | return e2.style.listStyleType === 'none' || containsOnlyAList(e2); |
| | | } else if (isList(e1)) { |
| | | return (e1.tagName === e2.tagName && (allowDifferentListStyles || e1.style.listStyleType === e2.style.listStyleType)) || isListForIndent(e2); |
| | | } else if (mergeParagraphs && e1.tagName === 'P' && e2.tagName === 'P') { |
| | | return true; |
| | | } else { |
| | | return false; |
| | | } |
| | | } else return mergeParagraphs && e1.tagName === 'P' && e2.tagName === 'P'; |
| | | } |
| | | |
| | | function isListForIndent(e) { |
| | |
| | | } |
| | | |
| | | tinymce.create('tinymce.plugins.Lists', { |
| | | init: function(ed, url) { |
| | | var LIST_TABBING = 0; |
| | | var LIST_EMPTY_ITEM = 1; |
| | | var LIST_ESCAPE = 2; |
| | | var LIST_UNKNOWN = 3; |
| | | init: function(ed) { |
| | | var LIST_TABBING = 'TABBING'; |
| | | var LIST_EMPTY_ITEM = 'EMPTY'; |
| | | var LIST_ESCAPE = 'ESCAPE'; |
| | | var LIST_PARAGRAPH = 'PARAGRAPH'; |
| | | var LIST_UNKNOWN = 'UNKNOWN'; |
| | | var state = LIST_UNKNOWN; |
| | | |
| | | function isTabInList(e) { |
| | | return e.keyCode === 9 && (ed.queryCommandState('InsertUnorderedList') || ed.queryCommandState('InsertOrderedList')); |
| | | // Don't indent on Ctrl+Tab or Alt+Tab |
| | | return e.keyCode === tinymce.VK.TAB && !(e.altKey || e.ctrlKey) && |
| | | (ed.queryCommandState('InsertUnorderedList') || ed.queryCommandState('InsertOrderedList')); |
| | | } |
| | | |
| | | function isOnLastListItem() { |
| | |
| | | function isEmptyListItem(li) { |
| | | var numChildren = li.childNodes.length; |
| | | if (li.tagName === 'LI') { |
| | | return numChildren == 0 ? true : numChildren == 1 && (li.firstChild.tagName == '' || isEmptyWebKitLi(li) || isEmptyIE9Li(li)); |
| | | return numChildren == 0 ? true : numChildren == 1 && (li.firstChild.tagName == '' || li.firstChild.tagName == 'BR' || isEmptyIE9Li(li)); |
| | | } |
| | | return false; |
| | | } |
| | | |
| | | function isEmptyWebKitLi(li) { |
| | | // 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 |
| | | return tinymce.isWebKit && li.firstChild.nodeName == 'BR'; |
| | | } |
| | | |
| | | function isEmptyIE9Li(li) { |
| | | // 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 |
| | | var lis = tinymce.grep(li.parentNode.childNodes, function(n) {return n.nodeName == 'LI'}); |
| | | var lis = tinymce.grep(li.parentNode.childNodes, function(n) {return n.tagName == 'LI'}); |
| | | var isLastLi = li == lis[lis.length - 1]; |
| | | var child = li.firstChild; |
| | | return tinymce.isIE9 && isLastLi && (child.nodeValue == String.fromCharCode(160) || child.nodeValue == String.fromCharCode(32)); |
| | | } |
| | | |
| | | function isEnter(e) { |
| | | return e.keyCode === 13; |
| | | return e.keyCode === tinymce.VK.ENTER; |
| | | } |
| | | |
| | | function isEnterWithoutShift(e) { |
| | | return isEnter(e) && !e.shiftKey; |
| | | } |
| | | |
| | | function getListKeyState(e) { |
| | | if (isTabInList(e)) { |
| | | return LIST_TABBING; |
| | | } else if (isEnter(e) && isOnLastListItem()) { |
| | | return LIST_ESCAPE; |
| | | } else if (isEnter(e) && isInEmptyListItem()) { |
| | | } else if (isEnterWithoutShift(e) && isOnLastListItem()) { |
| | | // Returns LIST_UNKNOWN since breaking out of lists is handled by the EnterKey.js logic now |
| | | //return LIST_ESCAPE; |
| | | return LIST_UNKNOWN; |
| | | } else if (isEnterWithoutShift(e) && isInEmptyListItem()) { |
| | | return LIST_EMPTY_ITEM; |
| | | } else { |
| | | return LIST_UNKNOWN; |
| | | } |
| | | } |
| | | |
| | | function cancelEnterAndTab(_, e) { |
| | | if (state == LIST_TABBING || state == LIST_EMPTY_ITEM) { |
| | | return Event.cancel(e); |
| | | function cancelDefaultEvents(ed, e) { |
| | | // list escape is done manually using outdent as it does not create paragraphs correctly in td's |
| | | if (state == LIST_TABBING || state == LIST_EMPTY_ITEM || tinymce.isGecko && state == LIST_ESCAPE) { |
| | | Event.cancel(e); |
| | | } |
| | | } |
| | | |
| | | function isCursorAtEndOfContainer() { |
| | | var range = ed.selection.getRng(true); |
| | | var startContainer = range.startContainer; |
| | | if (startContainer.nodeType == 3) { |
| | | var value = startContainer.nodeValue; |
| | | if (tinymce.isIE9 && value.length > 1 && value.charCodeAt(value.length-1) == 32) { |
| | | // IE9 places a space on the end of the text in some cases so ignore last char |
| | | return (range.endOffset == value.length-1); |
| | | } else { |
| | | return (range.endOffset == value.length); |
| | | } |
| | | } else if (startContainer.nodeType == 1) { |
| | | return range.endOffset == startContainer.childNodes.length; |
| | | } |
| | | return false; |
| | | } |
| | | |
| | | /* |
| | | If we are at the end of a list item surrounded with an element, pressing enter should create a |
| | | new list item instead without splitting the element e.g. don't want to create new P or H1 tag |
| | | */ |
| | | function isEndOfListItem() { |
| | | var node = ed.selection.getNode(); |
| | | var validElements = 'h1,h2,h3,h4,h5,h6,p,div'; |
| | | var isLastParagraphOfLi = ed.dom.is(node, validElements) && node.parentNode.tagName === 'LI' && node.parentNode.lastChild === node; |
| | | return ed.selection.isCollapsed() && isLastParagraphOfLi && isCursorAtEndOfContainer(); |
| | | } |
| | | |
| | | // Creates a new list item after the current selection's list item parent |
| | | function createNewLi(ed, e) { |
| | | if (isEnterWithoutShift(e) && isEndOfListItem()) { |
| | | var node = ed.selection.getNode(); |
| | | var li = ed.dom.create("li"); |
| | | var parentLi = ed.dom.getParent(node, 'li'); |
| | | ed.dom.insertAfter(li, parentLi); |
| | | |
| | | // Move caret to new list element. |
| | | if (tinymce.isIE6 || tinymce.isIE7 || tinyMCE.isIE8) { |
| | | // 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 |
| | | //li.appendChild(ed.dom.create(" ")); // IE needs an element within the bullet point |
| | | ed.selection.setCursorLocation(li, 1); |
| | | } else { |
| | | ed.selection.setCursorLocation(li, 0); |
| | | } |
| | | e.preventDefault(); |
| | | } |
| | | } |
| | | |
| | |
| | | return; |
| | | |
| | | var n = ed.selection.getStart(); |
| | | if (e.keyCode != 8 || n.tagName !== 'IMG') |
| | | if (e.keyCode != tinymce.VK.BACKSPACE || n.tagName !== 'IMG') |
| | | return; |
| | | |
| | | function lastLI(node) { |
| | |
| | | var list = ed.dom.getParent(li, 'ol,ul'); |
| | | if (list != null) { |
| | | var lastLi = list.lastChild; |
| | | lastLi.appendChild(ed.getDoc().createElement('')); |
| | | // 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 |
| | | //lastLi.appendChild(ed.getDoc().createElement('')); |
| | | ed.selection.setCursorLocation(lastLi, 0); |
| | | } |
| | | } |
| | |
| | | if (tinymce.isIE) { |
| | | setCursorPositionToOriginalLi(li); |
| | | } |
| | | |
| | | return Event.cancel(e); |
| | | } else if (state == LIST_ESCAPE) { |
| | | if (tinymce.isIE8) { |
| | | // append a zero sized nbsp so that caret is positioned correctly in IE8 after escaping and applying formatting. |
| | | if (tinymce.isIE6 || tinymce.isIE7 || tinymce.isIE8) { |
| | | // append a zero sized nbsp so that caret is positioned correctly in IE after escaping and applying formatting. |
| | | // if there is no text then applying formatting for e.g a H1 to the P tag immediately following list after |
| | | // escaping from it will cause the caret to be positioned on the last li instead of staying the in P tag. |
| | | var n = ed.getDoc().createTextNode('\uFEFF'); |
| | | ed.selection.getNode().appendChild(n); |
| | | } else if (tinymce.isIE9) { |
| | | } else if (tinymce.isIE9 || tinymce.isGecko) { |
| | | // IE9 does not escape the list so we use outdent to do this and cancel the default behaviour |
| | | // Gecko does not create a paragraph outdenting inside a TD so default behaviour is cancelled and we outdent ourselves |
| | | ed.execCommand('Outdent'); |
| | | return Event.cancel(e); |
| | | } |
| | | } |
| | | }); |
| | | |
| | | function fixListItem(parent, reference) { |
| | | // a zero-sized non-breaking space is placed in the empty list item so that the nested list is |
| | | // displayed on the below line instead of next to it |
| | | var n = ed.getDoc().createTextNode('\uFEFF'); |
| | | parent.insertBefore(n, reference); |
| | | ed.selection.setCursorLocation(n, 0); |
| | | // repaint to remove rendering artifact. only visible when creating new list |
| | | ed.execCommand('mceRepaint'); |
| | | } |
| | | |
| | | function fixIndentedListItemForGecko(ed, e) { |
| | | if (isEnter(e)) { |
| | | var li = getLi(); |
| | | if (li) { |
| | | var parent = li.parentNode; |
| | | var grandParent = parent && parent.parentNode; |
| | | if (grandParent && grandParent.nodeName == 'LI' && grandParent.firstChild == parent && li == parent.firstChild) { |
| | | fixListItem(grandParent, parent); |
| | | } |
| | | } |
| | | } |
| | | } |
| | | |
| | | function fixIndentedListItemForIE8(ed, e) { |
| | | if (isEnter(e)) { |
| | | var li = getLi(); |
| | | if (ed.dom.select('ul li', li).length === 1) { |
| | | var list = li.firstChild; |
| | | fixListItem(li, list); |
| | | } |
| | | } |
| | | } |
| | | |
| | | function fixDeletingFirstCharOfList(ed, e) { |
| | | function listElements(li) { |
| | | var elements = []; |
| | | var walker = new tinymce.dom.TreeWalker(li.firstChild, li); |
| | | for (var node = walker.current(); node; node = walker.next()) { |
| | | if (ed.dom.is(node, 'ol,ul,li')) { |
| | | elements.push(node); |
| | | } |
| | | } |
| | | return elements; |
| | | } |
| | | |
| | | if (e.keyCode == tinymce.VK.BACKSPACE) { |
| | | var li = getLi(); |
| | | if (li) { |
| | | var list = ed.dom.getParent(li, 'ol,ul'), |
| | | rng = ed.selection.getRng(); |
| | | if (list && list.firstChild === li && rng.startOffset == 0) { |
| | | var elements = listElements(li); |
| | | elements.unshift(li); |
| | | ed.execCommand("Outdent", false, elements); |
| | | ed.undoManager.add(); |
| | | return Event.cancel(e); |
| | | } |
| | | } |
| | | } |
| | | } |
| | | |
| | | function fixDeletingEmptyLiInWebkit(ed, e) { |
| | | var li = getLi(); |
| | | if (e.keyCode === tinymce.VK.BACKSPACE && ed.dom.is(li, 'li') && li.parentNode.firstChild!==li) { |
| | | if (ed.dom.select('ul,ol', li).length === 1) { |
| | | var prevLi = li.previousSibling; |
| | | ed.dom.remove(ed.dom.select('br', li)); |
| | | ed.dom.remove(li, true); |
| | | var textNodes = tinymce.grep(prevLi.childNodes, function(n){ return n.nodeType === 3 }); |
| | | if (textNodes.length === 1) { |
| | | var textNode = textNodes[0]; |
| | | ed.selection.setCursorLocation(textNode, textNode.length); |
| | | } |
| | | ed.undoManager.add(); |
| | | return Event.cancel(e); |
| | | } |
| | | } |
| | | } |
| | | |
| | | ed.onKeyDown.add(function(_, e) { state = getListKeyState(e); }); |
| | | ed.onKeyDown.add(cancelEnterAndTab); |
| | | ed.onKeyDown.add(cancelDefaultEvents); |
| | | ed.onKeyDown.add(imageJoiningListItem); |
| | | ed.onKeyPress.add(cancelEnterAndTab); |
| | | ed.onKeyDown.add(createNewLi); |
| | | |
| | | if (tinymce.isGecko) { |
| | | ed.onKeyUp.add(fixIndentedListItemForGecko); |
| | | } |
| | | if (tinymce.isIE8) { |
| | | ed.onKeyUp.add(fixIndentedListItemForIE8); |
| | | } |
| | | if (tinymce.isGecko || tinymce.isWebKit) { |
| | | ed.onKeyDown.add(fixDeletingFirstCharOfList); |
| | | } |
| | | if (tinymce.isWebKit) { |
| | | ed.onKeyDown.add(fixDeletingEmptyLiInWebkit); |
| | | } |
| | | }, |
| | | |
| | | applyList: function(targetListType, oppositeListType) { |
| | |
| | | if (element.tagName === 'LI') { |
| | | // No change required. |
| | | } else if (element.tagName === 'P' || element.tagName === 'DIV' || element.tagName === 'BODY') { |
| | | processBrs(element, function(startSection, br, previousBR) { |
| | | processBrs(element, function(startSection, br) { |
| | | doWrapList(startSection, br, element.tagName === 'BODY' ? null : startSection.parentNode); |
| | | li = startSection.parentNode; |
| | | adjustIndentForNewList(li); |
| | | cleanupBr(br); |
| | | }); |
| | | if (element.tagName === 'P' || selectedBlocks.length > 1) { |
| | | dom.split(li.parentNode.parentNode, li.parentNode); |
| | | if (li) { |
| | | if (li.tagName === 'LI' && (element.tagName === 'P' || selectedBlocks.length > 1)) { |
| | | dom.split(li.parentNode.parentNode, li.parentNode); |
| | | } |
| | | attemptMergeWithAdjacent(li.parentNode, true); |
| | | } |
| | | attemptMergeWithAdjacent(li.parentNode, true); |
| | | return; |
| | | } else { |
| | | // Put the list around the element. |
| | |
| | | } |
| | | |
| | | function doWrapList(start, end, template) { |
| | | var li, n = start, tmp, i; |
| | | var li, n = start, tmp; |
| | | while (!dom.isBlock(start.parentNode) && start.parentNode !== dom.getRoot()) { |
| | | start = dom.split(start.parentNode, start.previousSibling); |
| | | start = start.nextSibling; |
| | |
| | | // First mark the BRs that have any part of the previous section selected. |
| | | var trailingContentSelected = false; |
| | | each(dom.select(breakElements, element), function(br) { |
| | | var b; |
| | | if (br.hasAttribute && br.hasAttribute('_mce_bogus')) { |
| | | return true; // Skip the bogus Brs that are put in to appease Firefox and Safari. |
| | | } |
| | |
| | | } |
| | | }); |
| | | |
| | | if (hasNonList || hasOppositeType || selectedBlocks.length === 0) { |
| | | if (hasNonList &&!hasSameType || hasOppositeType || selectedBlocks.length === 0) { |
| | | actions = { |
| | | 'LI': changeList, |
| | | 'H1': makeList, |
| | |
| | | 'P': makeList, |
| | | 'BODY': makeList, |
| | | 'DIV': selectedBlocks.length > 1 ? makeList : wrapList, |
| | | defaultAction: wrapList |
| | | defaultAction: wrapList, |
| | | elements: this.selectedBlocks() |
| | | }; |
| | | } else { |
| | | actions = { |
| | | defaultAction: convertListItemToParagraph |
| | | defaultAction: convertListItemToParagraph, |
| | | elements: this.selectedBlocks(), |
| | | processEvenIfEmpty: true |
| | | }; |
| | | } |
| | | this.process(actions); |
| | |
| | | |
| | | this.process({ |
| | | 'LI': indentLI, |
| | | defaultAction: this.adjustPaddingFunction(true) |
| | | defaultAction: this.adjustPaddingFunction(true), |
| | | elements: this.selectedBlocks() |
| | | }); |
| | | |
| | | }, |
| | | |
| | | outdent: function() { |
| | | outdent: function(ui, elements) { |
| | | var t = this, ed = t.ed, dom = ed.dom, outdented = []; |
| | | |
| | | function outdentLI(element) { |
| | |
| | | } |
| | | } |
| | | |
| | | var listElements = elements && tinymce.is(elements, 'array') ? elements : this.selectedBlocks(); |
| | | this.process({ |
| | | 'LI': outdentLI, |
| | | defaultAction: this.adjustPaddingFunction(false) |
| | | defaultAction: this.adjustPaddingFunction(false), |
| | | elements: listElements |
| | | }); |
| | | |
| | | each(outdented, attemptMergeWithAdjacent); |
| | |
| | | process: function(actions) { |
| | | var t = this, sel = t.ed.selection, dom = t.ed.dom, selectedBlocks, r; |
| | | |
| | | function isEmptyElement(element) { |
| | | var excludeBrsAndBookmarks = tinymce.grep(element.childNodes, function(n) { |
| | | return !(n.nodeName === 'BR' || n.nodeName === 'SPAN' && dom.getAttrib(n, 'data-mce-type') == 'bookmark' |
| | | || n.nodeType == 3 && (n.nodeValue == String.fromCharCode(160) || n.nodeValue == '')); |
| | | }); |
| | | return excludeBrsAndBookmarks.length === 0; |
| | | } |
| | | |
| | | function processElement(element) { |
| | | dom.removeClass(element, '_mce_act_on'); |
| | | if (!element || element.nodeType !== 1) { |
| | | if (!element || element.nodeType !== 1 || ! actions.processEvenIfEmpty && selectedBlocks.length > 1 && isEmptyElement(element)) { |
| | | return; |
| | | } |
| | | element = findItemToOperateOn(element, dom); |
| | |
| | | } |
| | | |
| | | function recurse(element) { |
| | | t.splitSafeEach(element.childNodes, processElement); |
| | | t.splitSafeEach(element.childNodes, processElement, true); |
| | | } |
| | | |
| | | function brAtEdgeOfSelection(container, offset) { |
| | |
| | | container.childNodes[offset].tagName === 'BR'; |
| | | } |
| | | |
| | | selectedBlocks = sel.getSelectedBlocks(); |
| | | if (selectedBlocks.length === 0) { |
| | | selectedBlocks = [ dom.getRoot() ]; |
| | | function isInTable() { |
| | | var n = sel.getNode(); |
| | | var p = dom.getParent(n, 'td'); |
| | | return p !== null; |
| | | } |
| | | |
| | | selectedBlocks = actions.elements; |
| | | |
| | | r = sel.getRng(true); |
| | | if (!r.collapsed) { |
| | |
| | | t.splitSafeEach(selectedBlocks, processElement); |
| | | sel.moveToBookmark(bookmark); |
| | | bookmark = null; |
| | | // Avoids table or image handles being left behind in Firefox. |
| | | t.ed.execCommand('mceRepaint'); |
| | | |
| | | // we avoid doing repaint in a table as this will move the caret out of the table in Firefox 3.6 |
| | | if (!isInTable()) { |
| | | // Avoids table or image handles being left behind in Firefox. |
| | | t.ed.execCommand('mceRepaint'); |
| | | } |
| | | }, |
| | | |
| | | splitSafeEach: function(elements, f) { |
| | | if (tinymce.isGecko && (/Firefox\/[12]\.[0-9]/.test(navigator.userAgent) || |
| | | /Firefox\/3\.[0-4]/.test(navigator.userAgent))) { |
| | | splitSafeEach: function(elements, f, forceClassBase) { |
| | | if (forceClassBase || |
| | | (tinymce.isGecko && |
| | | (/Firefox\/[12]\.[0-9]/.test(navigator.userAgent) || |
| | | /Firefox\/3\.[0-4]/.test(navigator.userAgent)))) { |
| | | this.classBasedEach(elements, f); |
| | | } else { |
| | | each(elements, f); |
| | |
| | | }; |
| | | }, |
| | | |
| | | selectedBlocks: function() { |
| | | var ed = this.ed, selectedBlocks = ed.selection.getSelectedBlocks(); |
| | | return selectedBlocks.length == 0 ? [ ed.dom.getRoot() ] : selectedBlocks; |
| | | }, |
| | | |
| | | getInfo: function() { |
| | | return { |
| | | longname : 'Lists', |