From a2cf7c41b97a587d90188b83e4d15da1567a54b4 Mon Sep 17 00:00:00 2001 From: Thomas Bruederli <thomas@roundcube.net> Date: Wed, 09 Apr 2014 02:48:28 -0400 Subject: [PATCH] Fix accidental key replacements --- program/js/tiny_mce/tiny_mce_src.js | 911 +++++++++++++++++++++++++++++++++++++++++++------------- 1 files changed, 698 insertions(+), 213 deletions(-) diff --git a/program/js/tiny_mce/tiny_mce_src.js b/program/js/tiny_mce/tiny_mce_src.js index 32129db..86b162b 100644 --- a/program/js/tiny_mce/tiny_mce_src.js +++ b/program/js/tiny_mce/tiny_mce_src.js @@ -6,18 +6,20 @@ var tinymce = { majorVersion : '3', - minorVersion : '5.4.1', + minorVersion : '5.10', - releaseDate : '2012-06-24', + releaseDate : '2013-10-24', _init : function() { var t = this, d = document, na = navigator, ua = na.userAgent, i, nl, n, base, p, v; + + t.isIE11 = ua.indexOf('Trident/') != -1 && (ua.indexOf('rv:') != -1 || na.appName.indexOf('Netscape') != -1); t.isOpera = win.opera && opera.buildNumber; t.isWebKit = /WebKit/.test(ua); - t.isIE = !t.isWebKit && !t.isOpera && (/MSIE/gi).test(ua) && (/Explorer/gi).test(na.appName); + t.isIE = !t.isWebKit && !t.isOpera && (/MSIE/gi).test(ua) && (/Explorer/gi).test(na.appName) || t.isIE11; t.isIE6 = t.isIE && /MSIE [56]/.test(ua); @@ -27,7 +29,7 @@ t.isIE9 = t.isIE && /MSIE [9]/.test(ua); - t.isGecko = !t.isWebKit && /Gecko/.test(ua); + t.isGecko = !t.isWebKit && !t.isIE11 && /Gecko/.test(ua); t.isMac = ua.indexOf('Mac') != -1; @@ -107,10 +109,14 @@ if (!t) return o !== undef; - if (t == 'array' && (o.hasOwnProperty && o instanceof Array)) + if (t == 'array' && tinymce.isArray(o)) return true; return typeof(o) == t; + }, + + isArray: Array.isArray || function(obj) { + return Object.prototype.toString.call(obj) === "[object Array]"; }, makeMap : function(items, delim, map) { @@ -921,7 +927,7 @@ } if (t == 'object') { - if (o.hasOwnProperty && o instanceof Array) { + if (o.hasOwnProperty && Object.prototype.toString.call(o) === '[object Array]') { for (i=0, v = '['; i<o.length; i++) v += (i > 0 ? ',' : '') + serialize(o[i], quote); @@ -1086,13 +1092,15 @@ }, metaKeyPressed: function(e) { - return tinymce.isMac ? e.metaKey : e.ctrlKey; + // Check if ctrl or meta key is pressed also check if alt is false for Polish users + return tinymce.isMac ? e.metaKey : e.ctrlKey && !e.altKey; } }; })(tinymce); tinymce.util.Quirks = function(editor) { - var VK = tinymce.VK, BACKSPACE = VK.BACKSPACE, DELETE = VK.DELETE, dom = editor.dom, selection = editor.selection, settings = editor.settings; + var VK = tinymce.VK, BACKSPACE = VK.BACKSPACE, DELETE = VK.DELETE, dom = editor.dom, selection = editor.selection, + settings = editor.settings, parser = editor.parser, serializer = editor.serializer, each = tinymce.each; function setEditorCommandState(cmd, state) { try { @@ -1108,56 +1116,81 @@ return documentMode ? documentMode : 6; }; + function isDefaultPrevented(e) { + return e.isDefaultPrevented(); + }; + function cleanupStylesWhenDeleting() { function removeMergedFormatSpans(isDelete) { - var rng, blockElm, node, clonedSpan; + var rng, blockElm, wrapperElm, bookmark, container, offset, elm; + + function isAtStartOrEndOfElm() { + if (container.nodeType == 3) { + if (isDelete && offset == container.length) { + return true; + } + + if (!isDelete && offset === 0) { + return true; + } + } + } rng = selection.getRng(); + var tmpRng = [rng.startContainer, rng.startOffset, rng.endContainer, rng.endOffset]; - // Find root block - blockElm = dom.getParent(rng.startContainer, dom.isBlock); + if (!rng.collapsed) { + isDelete = true; + } - // On delete clone the root span of the next block element - if (isDelete) - blockElm = dom.getNext(blockElm, dom.isBlock); + container = rng[(isDelete ? 'start' : 'end') + 'Container']; + offset = rng[(isDelete ? 'start' : 'end') + 'Offset']; - // Locate root span element and clone it since it would otherwise get merged by the "apple-style-span" on delete/backspace - if (blockElm) { - node = blockElm.firstChild; + if (container.nodeType == 3) { + blockElm = dom.getParent(rng.startContainer, dom.isBlock); - // Ignore empty text nodes - while (node && node.nodeType == 3 && node.nodeValue.length === 0) - node = node.nextSibling; + // On delete clone the root span of the next block element + if (isDelete) { + blockElm = dom.getNext(blockElm, dom.isBlock); + } - if (node && node.nodeName === 'SPAN') { - clonedSpan = node.cloneNode(false); + if (blockElm && (isAtStartOrEndOfElm() || !rng.collapsed)) { + // Wrap children of block in a EM and let WebKit stick is + // runtime styles junk into that EM + wrapperElm = dom.create('em', {'id': '__mceDel'}); + + each(tinymce.grep(blockElm.childNodes), function(node) { + wrapperElm.appendChild(node); + }); + + blockElm.appendChild(wrapperElm); } } // Do the backspace/delete action + rng = dom.createRng(); + rng.setStart(tmpRng[0], tmpRng[1]); + rng.setEnd(tmpRng[2], tmpRng[3]); + selection.setRng(rng); editor.getDoc().execCommand(isDelete ? 'ForwardDelete' : 'Delete', false, null); - // Find all odd apple-style-spans - blockElm = dom.getParent(rng.startContainer, dom.isBlock); - tinymce.each(dom.select('span.Apple-style-span,font.Apple-style-span', blockElm), function(span) { - var bm = selection.getBookmark(); + // Remove temp wrapper element + if (wrapperElm) { + bookmark = selection.getBookmark(); - if (clonedSpan) { - dom.replace(clonedSpan.cloneNode(false), span, true); - } else { - dom.remove(span, true); + while (elm = dom.get('__mceDel')) { + dom.remove(elm, true); } - // Restore the selection - selection.moveToBookmark(bm); - }); - }; + selection.moveToBookmark(bookmark); + } + } editor.onKeyDown.add(function(editor, e) { var isDelete; isDelete = e.keyCode == DELETE; - if (!e.isDefaultPrevented() && (isDelete || e.keyCode == BACKSPACE) && !VK.modifierPressed(e)) { + if (!isDefaultPrevented(e) && (isDelete || e.keyCode == BACKSPACE) && !VK.modifierPressed(e)) { e.preventDefault(); removeMergedFormatSpans(isDelete); } @@ -1180,7 +1213,7 @@ var allRng = dom.createRng(); allRng.selectNode(editor.getBody()); - var allSelection = serializeRng(allRng);//console.log(selection, "----", allSelection); + var allSelection = serializeRng(allRng); return selection === allSelection; } @@ -1188,7 +1221,7 @@ var keyCode = e.keyCode, isCollapsed; // Empty the editor if it's needed for example backspace at <p><b>|</b></p> - if (!e.isDefaultPrevented() && (keyCode == DELETE || keyCode == BACKSPACE)) { + if (!isDefaultPrevented(e) && (keyCode == DELETE || keyCode == BACKSPACE)) { isCollapsed = editor.selection.isCollapsed(); // Selection is collapsed but the editor isn't empty @@ -1216,7 +1249,7 @@ function selectAll() { editor.onKeyDown.add(function(editor, e) { - if (e.keyCode == 65 && VK.metaKeyPressed(e)) { + if (!isDefaultPrevented(e) && e.keyCode == 65 && VK.metaKeyPressed(e)) { e.preventDefault(); editor.execCommand('SelectAll'); } @@ -1242,7 +1275,7 @@ function removeHrOnBackspace() { editor.onKeyDown.add(function(editor, e) { - if (!e.isDefaultPrevented() && e.keyCode === BACKSPACE) { + if (!isDefaultPrevented(e) && e.keyCode === BACKSPACE) { if (selection.isCollapsed() && selection.getRng(true).startOffset === 0) { var node = selection.getNode(); var previousSibling = node.previousSibling; @@ -1261,7 +1294,7 @@ // wouldn't get proper focus if the user clicked on the HTML element if (!Range.prototype.getClientRects) { // Detect getClientRects got introduced in FF 4 editor.onMouseDown.add(function(editor, e) { - if (e.target.nodeName === "HTML") { + if (!isDefaultPrevented(e) && e.target.nodeName === "HTML") { var body = editor.getBody(); // Blur the body it's focused but not correctly focused @@ -1305,7 +1338,7 @@ if (target !== editor.getBody()) { dom.setAttrib(target, "style", null); - tinymce.each(template, function(attr) { + each(template, function(attr) { target.setAttributeNode(attr.cloneNode(true)); }); } @@ -1313,7 +1346,7 @@ } function isSelectionAcrossElements() { - return !selection.isCollapsed() && selection.getStart() != selection.getEnd(); + return !selection.isCollapsed() && dom.getParent(selection.getStart(), dom.isBlock) != dom.getParent(selection.getEnd(), dom.isBlock); } function blockEvent(editor, e) { @@ -1324,7 +1357,7 @@ editor.onKeyPress.add(function(editor, e) { var applyAttributes; - if ((e.keyCode == 8 || e.keyCode == 46) && isSelectionAcrossElements()) { + if (!isDefaultPrevented(e) && (e.keyCode == 8 || e.keyCode == 46) && isSelectionAcrossElements()) { applyAttributes = getAttributeApplyFunction(); editor.getDoc().execCommand('delete', false, null); applyAttributes(); @@ -1336,7 +1369,7 @@ dom.bind(editor.getDoc(), 'cut', function(e) { var applyAttributes; - if (isSelectionAcrossElements()) { + if (!isDefaultPrevented(e) && isSelectionAcrossElements()) { applyAttributes = getAttributeApplyFunction(); editor.onKeyUp.addToTop(blockEvent); @@ -1375,7 +1408,7 @@ function disableBackspaceIntoATable() { editor.onKeyDown.add(function(editor, e) { - if (!e.isDefaultPrevented() && e.keyCode === BACKSPACE) { + if (!isDefaultPrevented(e) && e.keyCode === BACKSPACE) { if (selection.isCollapsed() && selection.getRng(true).startOffset === 0) { var previousSibling = selection.getNode().previousSibling; if (previousSibling && previousSibling.nodeName && previousSibling.nodeName.toLowerCase() === "table") { @@ -1399,7 +1432,7 @@ dom.addClass(editor.getBody(), 'mceHideBrInPre'); // Adds a \n before all BR elements in PRE to get them visual - editor.parser.addNodeFilter('pre', function(nodes, name) { + parser.addNodeFilter('pre', function(nodes, name) { var i = nodes.length, brNodes, j, brElm, sibling; while (i--) { @@ -1420,7 +1453,7 @@ }); // Removes any \n before BR elements in PRE since other browsers and in contentEditable=false mode they will be visible - editor.serializer.addNodeFilter('pre', function(nodes, name) { + serializer.addNodeFilter('pre', function(nodes, name) { var i = nodes.length, brNodes, j, brElm, sibling; while (i--) { @@ -1463,7 +1496,7 @@ var isDelete, rng, container, offset, brElm, sibling, collapsed; isDelete = e.keyCode == DELETE; - if (!e.isDefaultPrevented() && (isDelete || e.keyCode == BACKSPACE) && !VK.modifierPressed(e)) { + if (!isDefaultPrevented(e) && (isDelete || e.keyCode == BACKSPACE) && !VK.modifierPressed(e)) { rng = selection.getRng(); container = rng.startContainer; offset = rng.startOffset; @@ -1472,6 +1505,12 @@ // Override delete if the start container is a text node and is at the beginning of text or // just before/after the last character to be deleted in collapsed mode if (container.nodeType == 3 && container.nodeValue.length > 0 && ((offset === 0 && !collapsed) || (collapsed && offset === (isDelete ? 0 : 1)))) { + // Edge case when deleting <p><b><img> |x</b></p> + sibling = container.previousSibling; + if (sibling && sibling.nodeName == "IMG") { + return; + } + nonEmptyElements = editor.schema.getNonEmptyElements(); // Prevent default logic since it's broken @@ -1503,7 +1542,7 @@ editor.onKeyDown.add(function(editor, e) { var rng, container, offset, root, parent; - if (e.isDefaultPrevented() || e.keyCode != VK.BACKSPACE) { + if (isDefaultPrevented(e) || e.keyCode != VK.BACKSPACE) { return; } @@ -1527,10 +1566,10 @@ editor.formatter.toggle('blockquote', null, parent); // Move the caret to the beginning of container + rng = dom.createRng(); rng.setStart(container, 0); rng.setEnd(container, 0); selection.setRng(rng); - selection.collapse(false); } }); }; @@ -1555,7 +1594,7 @@ function addBrAfterLastLinks() { function fixLinks(editor, o) { - tinymce.each(dom.select('a'), function(node) { + each(dom.select('a'), function(node) { var parentNode = node.parentNode, root = dom.getRoot(); if (parentNode.lastChild === node) { @@ -1605,7 +1644,7 @@ editor.onKeyDown.add(function(editor, e) { var rng; - if (!e.isDefaultPrevented() && e.keyCode == BACKSPACE) { + if (!isDefaultPrevented(e) && e.keyCode == BACKSPACE) { rng = editor.getDoc().selection.createRange(); if (rng && rng.item) { e.preventDefault(); @@ -1623,7 +1662,7 @@ // IE10+ if (getDocumentMode() >= 10) { emptyBlocksCSS = ''; - tinymce.each('p div h1 h2 h3 h4 h5 h6'.split(' '), function(name, i) { + each('p div h1 h2 h3 h4 h5 h6'.split(' '), function(name, i) { emptyBlocksCSS += (i > 0 ? ',' : '') + name + ':empty'; }); @@ -1632,77 +1671,288 @@ }; function fakeImageResize() { - var mouseDownImg, startX, startY, startW, startH; + var selectedElmX, selectedElmY, selectedElm, selectedElmGhost, selectedHandle, startX, startY, startW, startH, ratio, + resizeHandles, width, height, rootDocument = document, editableDoc = editor.getDoc(); if (!settings.object_resizing || settings.webkit_fake_resize === false) { return; } - editor.contentStyles.push('.mceResizeImages img {cursor: se-resize !important}'); + // Try disabling object resizing if WebKit implements resizing in the future + setEditorCommandState("enableObjectResizing", false); - function resizeImage(e) { - var deltaX, deltaY, ratio, width, height; + // Details about each resize handle how to scale etc + resizeHandles = { + // Name: x multiplier, y multiplier, delta size x, delta size y + n: [.5, 0, 0, -1], + e: [1, .5, 1, 0], + s: [.5, 1, 0, 1], + w: [0, .5, -1, 0], + nw: [0, 0, -1, -1], + ne: [1, 0, 1, -1], + se: [1, 1, 1, 1], + sw : [0, 1, -1, 1] + }; - if (mouseDownImg) { - deltaX = e.screenX - startX; - deltaY = e.screenY - startY; - ratio = Math.max((startW + deltaX) / startW, (startH + deltaY) / startH); + function resizeElement(e) { + var deltaX, deltaY; - // Only update styles if the user draged one pixel or more - if (Math.abs(deltaX) > 1 || Math.abs(deltaY) > 1) { - // Constrain proportions - width = Math.round(startW * ratio); - height = Math.round(startH * ratio); + // Calc new width/height + deltaX = e.screenX - startX; + deltaY = e.screenY - startY; + // Calc new size + width = deltaX * selectedHandle[2] + startW; + height = deltaY * selectedHandle[3] + startH; + + // Never scale down lower than 5 pixels + width = width < 5 ? 5 : width; + height = height < 5 ? 5 : height; + + // Constrain proportions when modifier key is pressed or if the nw, ne, sw, se corners are moved on an image + if (VK.modifierPressed(e) || (selectedElm.nodeName == "IMG" && selectedHandle[2] * selectedHandle[3] !== 0)) { + width = Math.round(height / ratio); + height = Math.round(width * ratio); + } + + // Update ghost size + dom.setStyles(selectedElmGhost, { + width: width, + height: height + }); + + // Update ghost X position if needed + if (selectedHandle[2] < 0 && selectedElmGhost.clientWidth <= width) { + dom.setStyle(selectedElmGhost, 'left', selectedElmX + (startW - width)); + } + + // Update ghost Y position if needed + if (selectedHandle[3] < 0 && selectedElmGhost.clientHeight <= height) { + dom.setStyle(selectedElmGhost, 'top', selectedElmY + (startH - height)); + } + } + + function endResize() { + function setSizeProp(name, value) { + if (value) { // Resize by using style or attribute - if (mouseDownImg.style.width) { - dom.setStyle(mouseDownImg, 'width', width); + if (selectedElm.style[name] || !editor.schema.isValid(selectedElm.nodeName.toLowerCase(), name)) { + dom.setStyle(selectedElm, name, value); } else { - dom.setAttrib(mouseDownImg, 'width', width); - } - - // Resize by using style or attribute - if (mouseDownImg.style.height) { - dom.setStyle(mouseDownImg, 'height', height); - } else { - dom.setAttrib(mouseDownImg, 'height', height); - } - - if (!dom.hasClass(editor.getBody(), 'mceResizeImages')) { - dom.addClass(editor.getBody(), 'mceResizeImages'); + dom.setAttrib(selectedElm, name, value); } } } - }; - editor.onMouseDown.add(function(editor, e) { - var target = e.target; + // Set width/height properties + setSizeProp('width', width); + setSizeProp('height', height); - if (target.nodeName == "IMG") { - mouseDownImg = target; - startX = e.screenX; - startY = e.screenY; - startW = mouseDownImg.clientWidth; - startH = mouseDownImg.clientHeight; - dom.bind(editor.getDoc(), 'mousemove', resizeImage); - e.preventDefault(); - } - }); + dom.unbind(editableDoc, 'mousemove', resizeElement); + dom.unbind(editableDoc, 'mouseup', endResize); - // Unbind events on node change and restore resize cursor - editor.onNodeChange.add(function() { - if (mouseDownImg) { - mouseDownImg = null; - dom.unbind(editor.getDoc(), 'mousemove', resizeImage); + if (rootDocument != editableDoc) { + dom.unbind(rootDocument, 'mousemove', resizeElement); + dom.unbind(rootDocument, 'mouseup', endResize); } - if (selection.getNode().nodeName == "IMG") { - dom.addClass(editor.getBody(), 'mceResizeImages'); + // Remove ghost and update resize handle positions + dom.remove(selectedElmGhost); + showResizeRect(selectedElm); + } + + function showResizeRect(targetElm) { + var position, targetWidth, targetHeight; + + hideResizeRect(); + + // Get position and size of target + position = dom.getPos(targetElm); + selectedElmX = position.x; + selectedElmY = position.y; + targetWidth = targetElm.offsetWidth; + targetHeight = targetElm.offsetHeight; + + // Reset width/height if user selects a new image/table + if (selectedElm != targetElm) { + selectedElm = targetElm; + width = height = 0; + } + + each(resizeHandles, function(handle, name) { + var handleElm; + + // Get existing or render resize handle + handleElm = dom.get('mceResizeHandle' + name); + if (!handleElm) { + handleElm = dom.add(editableDoc.documentElement, 'div', { + id: 'mceResizeHandle' + name, + 'class': 'mceResizeHandle', + style: 'cursor:' + name + '-resize; margin:0; padding:0' + }); + + dom.bind(handleElm, 'mousedown', function(e) { + e.preventDefault(); + + endResize(); + + startX = e.screenX; + startY = e.screenY; + startW = selectedElm.clientWidth; + startH = selectedElm.clientHeight; + ratio = startH / startW; + selectedHandle = handle; + + selectedElmGhost = selectedElm.cloneNode(true); + dom.addClass(selectedElmGhost, 'mceClonedResizable'); + dom.setStyles(selectedElmGhost, { + left: selectedElmX, + top: selectedElmY, + margin: 0 + }); + + editableDoc.documentElement.appendChild(selectedElmGhost); + + dom.bind(editableDoc, 'mousemove', resizeElement); + dom.bind(editableDoc, 'mouseup', endResize); + + if (rootDocument != editableDoc) { + dom.bind(rootDocument, 'mousemove', resizeElement); + dom.bind(rootDocument, 'mouseup', endResize); + } + }); + } else { + dom.show(handleElm); + } + + // Position element + dom.setStyles(handleElm, { + left: (targetWidth * handle[0] + selectedElmX) - (handleElm.offsetWidth / 2), + top: (targetHeight * handle[1] + selectedElmY) - (handleElm.offsetHeight / 2) + }); + }); + + // Only add resize rectangle on WebKit and only on images + if (!tinymce.isOpera && selectedElm.nodeName == "IMG") { + selectedElm.setAttribute('data-mce-selected', '1'); + } + } + + function hideResizeRect() { + if (selectedElm) { + selectedElm.removeAttribute('data-mce-selected'); + } + + for (var name in resizeHandles) { + dom.hide('mceResizeHandle' + name); + } + } + + // Add CSS for resize handles, cloned element and selected + editor.contentStyles.push( + '.mceResizeHandle {' + + 'position: absolute;' + + 'border: 1px solid black;' + + 'background: #FFF;' + + 'width: 5px;' + + 'height: 5px;' + + 'z-index: 10000' + + '}' + + '.mceResizeHandle:hover {' + + 'background: #000' + + '}' + + 'img[data-mce-selected] {' + + 'outline: 1px solid black' + + '}' + + 'img.mceClonedResizable, table.mceClonedResizable {' + + 'position: absolute;' + + 'outline: 1px dashed black;' + + 'opacity: .5;' + + 'z-index: 10000' + + '}' + ); + + function updateResizeRect() { + var controlElm = dom.getParent(selection.getNode(), 'table,img'); + + // Remove data-mce-selected from all elements since they might have been copied using Ctrl+c/v + each(dom.select('img[data-mce-selected]'), function(img) { + img.removeAttribute('data-mce-selected'); + }); + + if (controlElm) { + showResizeRect(controlElm); } else { - dom.removeClass(editor.getBody(), 'mceResizeImages'); + hideResizeRect(); + } + } + + // Show/hide resize rect when image is selected + editor.onNodeChange.add(updateResizeRect); + + // Fixes WebKit quirk where it returns IMG on getNode if caret is after last image in container + dom.bind(editableDoc, 'selectionchange', updateResizeRect); + + // Remove the internal attribute when serializing the DOM + editor.serializer.addAttributeFilter('data-mce-selected', function(nodes, name) { + var i = nodes.length; + + while (i--) { + nodes[i].attr(name, null); } }); - }; + } + + function keepNoScriptContents() { + if (getDocumentMode() < 9) { + parser.addNodeFilter('noscript', function(nodes) { + var i = nodes.length, node, textNode; + + while (i--) { + node = nodes[i]; + textNode = node.firstChild; + + if (textNode) { + node.attr('data-mce-innertext', textNode.value); + } + } + }); + + serializer.addNodeFilter('noscript', function(nodes) { + var i = nodes.length, node, textNode, value; + + while (i--) { + node = nodes[i]; + textNode = nodes[i].firstChild; + + if (textNode) { + textNode.value = tinymce.html.Entities.decode(textNode.value); + } else { + // Old IE can't retain noscript value so an attribute is used to store it + value = node.attributes.map['data-mce-innertext']; + if (value) { + node.attr('data-mce-innertext', null); + textNode = new tinymce.html.Node('#text', 3); + textNode.value = value; + textNode.raw = true; + node.append(textNode); + } + } + } + }); + } + } + + function bodyHeight() { + editor.contentStyles.push('body {min-height: 100px}'); + editor.onClick.add(function(ed, e) { + if (e.target.nodeName == 'HTML') { + editor.execCommand('SelectAll'); + editor.selection.collapse(true); + editor.nodeChanged(); + } + }); + } // All browsers disableBackspaceIntoATable(); @@ -1727,23 +1977,34 @@ } // IE - if (tinymce.isIE) { + if (tinymce.isIE && !tinymce.isIE11) { removeHrOnBackspace(); ensureBodyHasRoleApplication(); addNewLinesBeforeBrInPre(); removePreSerializedStylesWhenSelectingControls(); deleteControlItemOnBackSpace(); renderEmptyBlocksFix(); + keepNoScriptContents(); + } + + // IE 11+ + if (tinymce.isIE11) { + bodyHeight(); } // Gecko - if (tinymce.isGecko) { + if (tinymce.isGecko && !tinymce.isIE11) { removeHrOnBackspace(); focusBody(); removeStylesWhenDeletingAccrossBlockElements(); setGeckoEditingOptions(); addBrAfterLastLinks(); removeGhostSelection(); + } + + // Opera + if (tinymce.isOpera) { + fakeImageResize(); } }; (function(tinymce) { @@ -1968,6 +2229,12 @@ function compress(prefix, suffix) { var top, right, bottom, left; + + // IE 11 will produce a border-image: none when getting the style attribute from <p style="border: 1px solid red"></p> + // So lets asume it shouldn't be there + if (styles['border-image'] === 'none') { + delete styles['border-image']; + } // Get values and check it it needs compressing top = styles[prefix + '-top' + suffix]; @@ -2283,7 +2550,7 @@ 'form[A|accept-charset|action|autocomplete|enctype|method|name|novalidate|target][C]' + 'fieldset[A|disabled|form|name][C|legend]' + 'label[A|form|for][B]' + - 'input[A|type|accept|alt|autocomplete|checked|disabled|form|formaction|formenctype|formmethod|formnovalidate|formtarget|height|list|max|maxlength|min|' + + 'input[A|type|accept|alt|autocomplete|autofocus|checked|disabled|form|formaction|formenctype|formmethod|formnovalidate|formtarget|height|list|max|maxlength|min|' + 'multiple|pattern|placeholder|readonly|required|size|src|step|width|files|value|name][]' + 'button[A|autofocus|disabled|form|formaction|formenctype|formmethod|formnovalidate|formtarget|name|value|type][B]' + 'select[A|autofocus|disabled|form|multiple|name|size][option|optgroup]' + @@ -2487,14 +2754,15 @@ } // Setup map objects - whiteSpaceElementsMap = createLookupTable('whitespace_elements', 'pre script style textarea'); + whiteSpaceElementsMap = createLookupTable('whitespace_elements', 'pre script noscript style textarea'); selfClosingElementsMap = createLookupTable('self_closing_elements', 'colgroup dd dt li option p td tfoot th thead tr'); shortEndedElementsMap = createLookupTable('short_ended_elements', 'area base basefont br col frame hr img input isindex link meta param embed source wbr'); boolAttrMap = createLookupTable('boolean_attributes', 'checked compact declare defer disabled ismap multiple nohref noresize noshade nowrap readonly selected autoplay loop controls'); - nonEmptyElementsMap = createLookupTable('non_empty_elements', 'td th iframe video audio object', shortEndedElementsMap); - blockElementsMap = createLookupTable('block_elements', 'h1 h2 h3 h4 h5 h6 hr p div address pre form table tbody thead tfoot ' + - 'th tr td li ol ul caption blockquote center dl dt dd dir fieldset ' + - 'noscript menu isindex samp header footer article section hgroup aside nav figure option datalist select optgroup'); + nonEmptyElementsMap = createLookupTable('non_empty_elements', 'td th iframe video audio object script', shortEndedElementsMap); + textBlockElementsMap = createLookupTable('text_block_elements', 'h1 h2 h3 h4 h5 h6 p div address pre form ' + + 'blockquote center dir fieldset header footer article section hgroup aside nav figure'); + blockElementsMap = createLookupTable('block_elements', 'hr table tbody thead tfoot ' + + 'th tr td li ol ul caption dl dt dd noscript menu isindex samp option datalist select optgroup', textBlockElementsMap); // Converts a wildcard expression string to a regexp for example *a will become /.*a/. function patternToRegExp(str) { @@ -2668,8 +2936,15 @@ customElementsMap[name] = cloneName; // If it's not marked as inline then add it to valid block elements - if (!inline) + if (!inline) { + blockElementsMap[name.toUpperCase()] = {}; blockElementsMap[name] = {}; + } + + // Add elements clone if needed + if (!elements[name]) { + elements[name] = elements[cloneName]; + } // Add custom elements at span/div positions each(children, function(element, child) { @@ -2794,6 +3069,10 @@ return blockElementsMap; }; + self.getTextBlockElements = function() { + return textBlockElementsMap; + }; + self.getShortEndedElements = function() { return shortEndedElementsMap; }; @@ -2859,6 +3138,8 @@ self.addCustomElements = addCustomElements; self.addValidChildren = addValidChildren; + + self.elements = elements; }; })(tinymce); @@ -2957,10 +3238,10 @@ '(?:!DOCTYPE([\\w\\W]*?)>)|' + // DOCTYPE '(?:\\?([^\\s\\/<>]+) ?([\\w\\W]*?)[?/]>)|' + // PI '(?:\\/([^>]+)>)|' + // End element - '(?:([A-Za-z0-9\\-\\:]+)((?:\\s+[^"\'>]+(?:(?:"[^"]*")|(?:\'[^\']*\')|[^>]*))*|\\/|\\s+)>)' + // Start element + '(?:([A-Za-z0-9\\-\\:\\.]+)((?:\\s+[^"\'>]+(?:(?:"[^"]*")|(?:\'[^\']*\')|[^>]*))*|\\/|\\s+)>)' + // Start element ')', 'g'); - attrRegExp = /([\w:\-]+)(?:\s*=\s*(?:(?:\"((?:\\.|[^\"])*)\")|(?:\'((?:\\.|[^\'])*)\')|([^>\s]+)))?/g; + attrRegExp = /([\w:\-]+)(?:\s*=\s*(?:(?:\"((?:[^\"])*)\")|(?:\'((?:[^\'])*)\')|([^>\s]+)))?/g; specialElements = { 'script' : /<\/script[^>]*>/gi, 'style' : /<\/style[^>]*>/gi, @@ -3443,7 +3724,7 @@ i = node.attributes.length; while (i--) { name = node.attributes[i].name; - if (name === "name" || name.indexOf('data-') === 0) + if (name === "name" || name.indexOf('data-mce-') === 0) return false; } } @@ -3499,17 +3780,40 @@ function fixInvalidChildren(nodes) { var ni, node, parent, parents, newParent, currentNode, tempNode, childNode, i, - childClone, nonEmptyElements, nonSplitableElements, sibling, nextNode; + childClone, nonEmptyElements, nonSplitableElements, textBlockElements, sibling, nextNode; nonSplitableElements = tinymce.makeMap('tr,td,th,tbody,thead,tfoot,table'); nonEmptyElements = schema.getNonEmptyElements(); + textBlockElements = schema.getTextBlockElements(); for (ni = 0; ni < nodes.length; ni++) { node = nodes[ni]; - // Already removed - if (!node.parent) + // Already removed or fixed + if (!node.parent || node.fixed) continue; + + // If the invalid element is a text block and the text block is within a parent LI element + // Then unwrap the first text block and convert other sibling text blocks to LI elements similar to Word/Open Office + if (textBlockElements[node.name] && node.parent.name == 'li') { + // Move sibling text blocks after LI element + sibling = node.next; + while (sibling) { + if (textBlockElements[sibling.name]) { + sibling.name = 'li'; + sibling.fixed = true; + node.parent.insert(sibling, node.parent); + } else { + break; + } + + sibling = sibling.next; + } + + // Unwrap current text block + node.unwrap(node); + continue; + } // Get list of all parent nodes until we find a valid parent to stick the child into parents = [node]; @@ -3888,7 +4192,8 @@ } // Trim start white space - textNode = node.prev; + // Removed due to: #5424 + /*textNode = node.prev; if (textNode && textNode.type === 3) { text = textNode.value.replace(startWhiteSpaceRegExp, ''); @@ -3896,7 +4201,7 @@ textNode.value = text; else textNode.remove(); - } + }*/ } // Check if we exited a whitespace preserved element @@ -4404,6 +4709,12 @@ event_utils.domLoaded = true; callback(event); } + } + + // Page already loaded then fire it directly + if (doc.readyState == "complete") { + readyHandler(); + return; } // Use W3C method @@ -4949,6 +5260,11 @@ blockElementsMap = s.schema ? s.schema.getBlockElements() : {}; t.isBlock = function(node) { + // Fix for #5446 + if (!node) { + return false; + } + // This function is called in module pattern style since it might be executed with the wrong this scope var type = node.nodeType; @@ -4963,7 +5279,7 @@ fixDoc: function(doc) { var settings = this.settings, name; - if (isIE && settings.schema) { + if (isIE && !tinymce.isIE11 && settings.schema) { // Add missing HTML 4/5 elements to IE ('abbr article aside audio canvas ' + 'details figcaption figure footer ' + @@ -4984,7 +5300,7 @@ var self = this, clone, doc; // TODO: Add feature detection here in the future - if (!isIE || node.nodeType !== 1 || deep) { + if (!isIE || tinymce.isIE11 || node.nodeType !== 1 || deep) { return node.cloneNode(deep); } @@ -5257,7 +5573,7 @@ switch (na) { case 'opacity': // IE specific opacity - if (isIE) { + if (isIE && ! tinymce.isIE11) { s.filter = v === '' ? '' : "alpha(opacity=" + (v * 100) + ")"; if (!n.currentStyle || !n.currentStyle.hasLayout) @@ -5269,7 +5585,7 @@ break; case 'float': - isIE ? s.styleFloat = v : s.cssFloat = v; + (isIE && ! tinymce.isIE11) ? s.styleFloat = v : s.cssFloat = v; break; default: @@ -5599,7 +5915,7 @@ styleElm.id = 'mceDefaultStyles'; styleElm.type = 'text/css'; - head = doc.getElementsByTagName('head')[0] + head = doc.getElementsByTagName('head')[0]; if (head.firstChild) { head.insertBefore(styleElm, head.firstChild); } else { @@ -5635,7 +5951,7 @@ // IE 8 has a bug where dynamically loading stylesheets would produce a 1 item remaining bug // This fix seems to resolve that issue by realcing the document ones a stylesheet finishes loading // It's ugly but it seems to work fine. - if (isIE && d.documentMode && d.recalc) { + if (isIE && !tinymce.isIE11 && d.documentMode && d.recalc) { link.onload = function() { if (d.recalc) d.recalc(); @@ -5954,7 +6270,12 @@ // Import case 3: - addClasses(r.styleSheet); + try { + addClasses(r.styleSheet); + } catch (ex) { + // Ignore + } + break; } }); @@ -7349,7 +7670,8 @@ }; this.addRange = function(rng) { - var ieRng, ctrlRng, startContainer, startOffset, endContainer, endOffset, sibling, doc = selection.dom.doc, body = doc.body; + var ieRng, ctrlRng, startContainer, startOffset, endContainer, endOffset, sibling, + doc = selection.dom.doc, body = doc.body, nativeRng, ctrlElm; function setEndPoint(start) { var container, offset, marker, tmpRng, nodes; @@ -7435,10 +7757,17 @@ if (startOffset == endOffset - 1) { try { + ctrlElm = startContainer.childNodes[startOffset]; ctrlRng = body.createControlRange(); - ctrlRng.addElement(startContainer.childNodes[startOffset]); + ctrlRng.addElement(ctrlElm); ctrlRng.select(); - return; + + // Check if the range produced is on the correct element and is a control range + // On IE 8 it will select the parent contentEditable container if you select an inner element see: #5398 + nativeRng = selection.getRng(); + if (nativeRng.item && ctrlElm === nativeRng.item(0)) { + return; + } } catch (ex) { // Ignore } @@ -9048,7 +9377,7 @@ if (!t.win.getSelection) t.tridentSel = new tinymce.dom.TridentSelection(t); - if (tinymce.isIE && dom.boxModel) + if (tinymce.isIE && ! tinymce.isIE11 && dom.boxModel) this._fixIESelection(); // Prevent leaks @@ -9340,8 +9669,20 @@ } // Handle simple range - if (type) - return {rng : t.getRng()}; + if (type) { + rng = t.getRng(); + + if (rng.setStart) { + rng = { + startContainer: rng.startContainer, + startOffset: rng.startOffset, + endContainer: rng.endContainer, + endOffset: rng.endOffset + }; + } + + return {rng : rng}; + } rng = t.getRng(); id = dom.uniqueId(); @@ -9407,7 +9748,7 @@ }, moveToBookmark : function(bookmark) { - var t = this, dom = t.dom, marker1, marker2, rng, root, startContainer, endContainer, startOffset, endOffset; + var t = this, dom = t.dom, marker1, marker2, rng, rng2, root, startContainer, endContainer, startOffset, endOffset; function setEndPoint(start) { var point = bookmark[start ? 'start' : 'end'], i, node, offset, children; @@ -9537,8 +9878,24 @@ } } else if (bookmark.name) { t.select(dom.select(bookmark.name)[bookmark.index]); - } else if (bookmark.rng) - t.setRng(bookmark.rng); + } else if (bookmark.rng) { + rng = bookmark.rng; + + if (rng.startContainer) { + rng2 = t.dom.createRng(); + + try { + rng2.setStart(rng.startContainer, rng.startOffset); + rng2.setEnd(rng.endContainer, rng.endOffset); + } catch (e) { + // Might fail with index error + } + + rng = rng2; + } + + t.setRng(rng); + } } }, @@ -9637,7 +9994,7 @@ } // We have W3C ranges and it's IE then fake control selection since IE9 doesn't handle that correctly yet - if (tinymce.isIE && rng && rng.setStart && doc.selection.createRange().item) { + if (tinymce.isIE && ! tinymce.isIE11 && rng && rng.setStart && doc.selection.createRange().item) { elm = doc.selection.createRange().item(0); rng = doc.createRange(); rng.setStartBefore(elm); @@ -10050,6 +10407,16 @@ return self; }, + scrollIntoView: function(elm) { + var y, viewPort, self = this, dom = self.dom; + + viewPort = dom.getViewPort(self.editor.getWin()); + y = dom.getPos(elm).y; + if (y < viewPort.y || y + 25 > viewPort.y + viewPort.h) { + self.editor.getWin().scrollTo(0, y < viewPort.y ? y : y - viewPort.h + 25); + } + }, + destroy : function(manual) { var self = this; @@ -10222,6 +10589,18 @@ } }); + htmlParser.addNodeFilter('noscript', function(nodes) { + var i = nodes.length, node; + + while (i--) { + node = nodes[i].firstChild; + + if (node) { + node.value = tinymce.html.Entities.decode(node.value); + } + } + }); + // Force script into CDATA sections and remove the mce- prefix also add comments around styles htmlParser.addNodeFilter('script,style', function(nodes, name) { var i = nodes.length, node, value; @@ -10379,7 +10758,7 @@ // Replace all BOM characters for now until we can find a better solution if (!args.cleanup) - args.content = args.content.replace(/\uFEFF|\u200B/g, ''); + args.content = args.content.replace(/\uFEFF/g, ''); // Post process if (!args.no_events) @@ -10480,7 +10859,7 @@ // Add onload listener for non IE browsers since IE9 // fires onload event before the script is parsed and executed - if (!tinymce.isIE) + if (!tinymce.isIE || tinymce.isIE11) elm.onload = done; // Add onerror event will get fired on some browsers but not all of them @@ -10893,18 +11272,22 @@ switch (evt.keyCode) { case DOM_VK_LEFT: if (enableLeftRight) t.moveFocus(-1); + Event.cancel(evt); break; case DOM_VK_RIGHT: if (enableLeftRight) t.moveFocus(1); + Event.cancel(evt); break; case DOM_VK_UP: if (enableUpDown) t.moveFocus(-1); + Event.cancel(evt); break; case DOM_VK_DOWN: if (enableUpDown) t.moveFocus(1); + Event.cancel(evt); break; case DOM_VK_ESCAPE: @@ -11601,7 +11984,7 @@ else h += '<span class="mceIcon ' + s['class'] + '"></span>' + (l ? '<span class="' + cp + 'Label">' + l + '</span>' : ''); - h += '<span class="mceVoiceLabel mceIconOnly" style="display: none;" id="' + this.id + '_voice">' + s.title + '</span>'; + h += '<span class="mceVoiceLabel mceIconOnly" style="display: none;" id="' + this.id + '_voice">' + s.title + '</span>'; h += '</a>'; return h; }, @@ -11626,9 +12009,11 @@ return s.onclick.call(s.scope, e); } }); - tinymce.dom.Event.add(t.id, 'keyup', function(e) { - if (!t.isDisabled() && e.keyCode==tinymce.VK.SPACEBAR) + tinymce.dom.Event.add(t.id, 'keydown', function(e) { + if (!t.isDisabled() && e.keyCode==tinymce.VK.SPACEBAR) { + tinymce.dom.Event.cancel(e); return s.onclick.call(s.scope, e); + } }); } }); @@ -12041,7 +12426,7 @@ // Accessibility keyhandler Event.add(t.id, 'keydown', function(e) { - var bf; + var bf, DOM_VK_LEFT = 37, DOM_VK_RIGHT = 39, DOM_VK_UP = 38, DOM_VK_DOWN = 40, DOM_VK_RETURN = 13, DOM_VK_SPACE = 32; Event.remove(t.id, 'change', ch); changeListenerAdded = false; @@ -12053,14 +12438,12 @@ Event.remove(t.id, 'blur', bf); }); - //prevent default left and right keys on chrome - so that the keyboard navigation is used. - if (tinymce.isWebKit && (e.keyCode==37 ||e.keyCode==39)) { - return Event.prevent(e); - } - - if (e.keyCode == 13 || e.keyCode == 32) { + if (e.keyCode == DOM_VK_RETURN || e.keyCode == DOM_VK_SPACE) { onChange(e); return Event.cancel(e); + } else if (e.keyCode == DOM_VK_DOWN || e.keyCode == DOM_VK_UP) { + // allow native implementation (navigate select element options) + e.stopImmediatePropagation(); } }); @@ -12863,6 +13246,9 @@ if (id === undef) return this.editors; + if (!this.editors.hasOwnProperty(id)) + return undef; + return this.editors[id]; }, @@ -12946,7 +13332,7 @@ ed.render(); // Fix IE memory leaks - if (tinymce.isIE) { + if (tinymce.isIE && ! tinymce.isIE11) { w.attachEvent('onunload', clr); } @@ -13344,9 +13730,15 @@ // Store away the selection when it's changed to it can be restored later with a editor.focus() call if (isIE) { t.onInit.add(function(ed) { - ed.dom.bind(ed.getBody(), 'beforedeactivate keydown', function() { - ed.lastIERng = ed.selection.getRng(); + ed.dom.bind(ed.getBody(), 'beforedeactivate keydown keyup', function() { + ed.bookmark = ed.selection.getBookmark(1); }); + }); + + t.onNodeChange.add(function(ed) { + if (document.activeElement.id == ed.id + "_ifr") { + ed.bookmark = ed.selection.getBookmark(1); + } }); } } @@ -13359,6 +13751,11 @@ each(explode(s.content_css), function(u) { t.contentCSS.push(t.documentBaseURI.toAbsolute(u)); }); + } + + // Load specified content CSS last + if (s.content_style) { + t.contentStyles.push(s.content_style); } // Content editable mode ends here @@ -13379,10 +13776,12 @@ t.iframeHTML += '<base href="' + t.documentBaseURI.getURI() + '" />'; // IE8 doesn't support carets behind images setting ie7_compat would force IE8+ to run in IE7 compat mode. - if (s.ie7_compat) - t.iframeHTML += '<meta http-equiv="X-UA-Compatible" content="IE=7" />'; - else - t.iframeHTML += '<meta http-equiv="X-UA-Compatible" content="IE=edge" />'; + if (tinymce.isIE8) { + if (s.ie7_compat) + t.iframeHTML += '<meta http-equiv="X-UA-Compatible" content="IE=7" />'; + else + t.iframeHTML += '<meta http-equiv="X-UA-Compatible" content="IE=edge" />'; + } t.iframeHTML += '<meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />'; @@ -13660,8 +14059,9 @@ var oed, self = this, selection = self.selection, contentEditable = self.settings.content_editable, ieRng, controlElm, doc = self.getDoc(), body; if (!skip_focus) { - if (self.lastIERng) { - selection.setRng(self.lastIERng); + if (self.bookmark) { + selection.moveToBookmark(self.bookmark); + self.bookmark = null; } // Get selected control element @@ -13682,7 +14082,7 @@ body = self.getBody(); // Check for setActive since it doesn't scroll to the element - if (body.setActive) { + if (body.setActive && ! tinymce.isIE11) { body.setActive(); } else { body.focus(); @@ -14010,6 +14410,8 @@ // We must save before we hide so Safari doesn't crash self.save(); + + // defer the call to hide to prevent an IE9 crash #4921 DOM.hide(self.getContainer()); DOM.setStyle(self.id, 'display', self.orgDisplay); }, @@ -14135,7 +14537,7 @@ }, getContent : function(args) { - var self = this, content; + var self = this, content, body = self.getBody(); // Setup args object args = args || {}; @@ -14149,11 +14551,18 @@ // Get raw contents or by default the cleaned contents if (args.format == 'raw') - content = self.getBody().innerHTML; + content = body.innerHTML; + else if (args.format == 'text') + content = body.innerText || body.textContent; else - content = self.serializer.serialize(self.getBody(), args); + content = self.serializer.serialize(body, args); - args.content = tinymce.trim(content); + // Trim whitespace in beginning/end of HTML + if (args.format != 'text') { + args.content = tinymce.trim(content); + } else { + args.content = content; + } // Do post processing if (!args.no_events) @@ -14282,11 +14691,19 @@ }, remove : function() { - var self = this, elm = self.getContainer(); + var self = this, elm = self.getContainer(), doc = self.getDoc(); if (!self.removed) { self.removed = 1; // Cancels post remove event execution - self.hide(); + + // Fixed bug where IE has a blinking cursor left from the editor + if (isIE && doc) + doc.execCommand('SelectAll'); + + // We must save before we hide so Safari doesn't crash + self.save(); + + DOM.setStyle(self.id, 'display', self.orgDisplay); // Don't clear the window or document if content editable // is enabled since other instances might still be present @@ -14969,7 +15386,7 @@ // Insert bookmark node and get the parent selection.setContent(bookmarkHtml); - parentNode = editor.selection.getNode(); + parentNode = selection.getNode(); rootNode = editor.getBody(); // Opera will return the document node when selection is in root @@ -15136,10 +15553,15 @@ selectAll : function() { var root = dom.getRoot(), rng = dom.createRng(); - rng.setStart(root, 0); - rng.setEnd(root, root.childNodes.length); + // Old IE does a better job with selectall than new versions + if (selection.getRng().setStart) { + rng.setStart(root, 0); + rng.setEnd(root, root.childNodes.length); - editor.selection.setRng(rng); + selection.setRng(rng); + } else { + execNativeCommand('SelectAll'); + } } }); @@ -15178,7 +15600,10 @@ }, 'InsertUnorderedList,InsertOrderedList' : function(command) { - return dom.getParent(selection.getNode(), command == 'insertunorderedlist' ? 'UL' : 'OL'); + var list = dom.getParent(selection.getNode(), 'ul,ol'); + return list && + (command === 'insertunorderedlist' && list.tagName === 'UL' + || command === 'insertorderedlist' && list.tagName === 'OL'); } }, 'state'); @@ -15271,7 +15696,7 @@ // Add undo level on save contents, drag end and blur/focusout editor.onSaveContent.add(addNonTypingUndoLevel); editor.dom.bind(editor.dom.getRoot(), 'dragend', addNonTypingUndoLevel); - editor.dom.bind(editor.getDoc(), tinymce.isGecko ? 'blur' : 'focusout', function(e) { + editor.dom.bind(editor.getBody(), 'focusout', function(e) { if (!editor.removed && self.typing) { addNonTypingUndoLevel(); } @@ -15479,6 +15904,14 @@ node = rootNode.firstChild; while (node) { if (node.nodeType === 3 || (node.nodeType == 1 && !blockElements[node.nodeName])) { + // Remove empty text nodes + if (node.nodeType === 3 && node.nodeValue.length == 0) { + tempNode = node; + node = node.nextSibling; + dom.remove(tempNode); + continue; + } + if (!rootBlockNode) { rootBlockNode = dom.create(settings.forced_root_block); node.parentNode.insertBefore(rootBlockNode, node); @@ -16046,10 +16479,11 @@ TreeWalker = tinymce.dom.TreeWalker, rangeUtils = new tinymce.dom.RangeUtils(dom), isValid = ed.schema.isValidChild, + isArray = tinymce.isArray, isBlock = dom.isBlock, forcedRootBlock = ed.settings.forced_root_block, nodeIndex = dom.nodeIndex, - INVISIBLE_CHAR = tinymce.isGecko ? '\u200B' : '\uFEFF', + INVISIBLE_CHAR = '\uFEFF', MCE_ATTR_RE = /^(src|href|style)$/, FALSE = false, TRUE = true, @@ -16057,9 +16491,13 @@ undef, getContentEditable = dom.getContentEditable; - function isArray(obj) { - return obj instanceof Array; - }; + function isTextBlock(name) { + if (name.nodeType) { + name = name.nodeName; + } + + return !!ed.schema.getTextBlockElements()[name.toLowerCase()]; + } function getParents(node, selector) { return dom.getParents(node, selector, dom.getRoot()); @@ -16419,7 +16857,7 @@ // Is it valid to wrap this item if (contentEditable && !hasContentEditableState && isValid(wrapName, nodeName) && isValid(parentName, wrapName) && - !(!node_specific && node.nodeType === 3 && node.nodeValue.length === 1 && node.nodeValue.charCodeAt(0) === 65279) && !isCaretNode(node)) { + !(!node_specific && node.nodeType === 3 && node.nodeValue.length === 1 && node.nodeValue.charCodeAt(0) === 65279) && !isCaretNode(node) && (!format.inline || !isBlock(node))) { // Start wrapping if (!currentWrapElm) { // Wrap the node @@ -16624,6 +17062,11 @@ // Merges the styles for each node function process(node) { var children, i, l, localContentEditable, lastContentEditable, hasContentEditableState; + + // Skip on text nodes as they have neither format to remove nor children + if (node.nodeType === 3) { + return; + } // Node has a contentEditable value if (node.nodeType === 1 && getContentEditable(node)) { @@ -16964,7 +17407,7 @@ return FALSE; }; - function formatChanged(formats, callback) { + function formatChanged(formats, callback, similar) { var currentFormats; // Setup format node change logic @@ -16978,7 +17421,7 @@ // Check for new formats each(formatChangeData, function(callbacks, format) { each(parents, function(node) { - if (matchNode(node, format, {}, true)) { + if (matchNode(node, format, {}, callbacks.similar)) { if (!currentFormats[format]) { // Execute callbacks each(callbacks, function(callback) { @@ -17011,6 +17454,7 @@ each(formats.split(','), function(format) { if (!formatChangeData[format]) { formatChangeData[format] = []; + formatChangeData[format].similar = similar; } formatChangeData[format].push(callback); @@ -17117,6 +17561,10 @@ siblingName = start ? 'previousSibling' : 'nextSibling'; root = dom.getRoot(); + function isBogusBr(node) { + return node.nodeName == "BR" && node.getAttribute('data-mce-bogus') && !node.nextSibling; + }; + // If it's a text node and the offset is inside the text if (container.nodeType == 3 && !isWhiteSpaceNode(container)) { if (start ? startOffset > 0 : endOffset < container.nodeValue.length) { @@ -17131,7 +17579,7 @@ // Walk left/right for (sibling = parent[siblingName]; sibling; sibling = sibling[siblingName]) { - if (!isBookmarkNode(sibling) && !isWhiteSpaceNode(sibling)) { + if (!isBookmarkNode(sibling) && !isWhiteSpaceNode(sibling) && !isBogusBr(sibling)) { return parent; } } @@ -17290,7 +17738,7 @@ // Expand to first wrappable block element or any block element if (!node) - node = dom.getParent(container.nodeType == 3 ? container.parentNode : container, isBlock); + node = dom.getParent(container.nodeType == 3 ? container.parentNode : container, isTextBlock); // Exclude inner lists from wrapping if (node && format[0].wrapper) @@ -17689,10 +18137,6 @@ return next; }; - function isTextBlock(name) { - return /^(h[1-6]|p|div|pre|address|dl|dt|dd)$/.test(name); - }; - function getContainer(rng, start) { var container, offset, lastIdx, walker; @@ -17928,11 +18372,23 @@ node.appendChild(dom.doc.createTextNode(INVISIBLE_CHAR)); node = node.firstChild; - // Insert caret container after the formated node - dom.insertAfter(caretContainer, formatNode); + var block = dom.getParent(formatNode, isTextBlock); + + if (block && dom.isEmpty(block)) { + // Replace formatNode with caretContainer when removing format from empty block like <p><b>|</b></p> + formatNode.parentNode.replaceChild(caretContainer, formatNode); + } else { + // Insert caret container after the formated node + dom.insertAfter(caretContainer, formatNode); + } // Move selection to text node selection.setCursorLocation(node, 1); + + // If the formatNode is empty, we can remove it safely. + if (dom.isEmpty(formatNode)) { + dom.remove(formatNode); + } } }; @@ -18106,7 +18562,7 @@ var dom = editor.dom, selection = editor.selection, settings = editor.settings, undoManager = editor.undoManager, nonEmptyElementsMap = editor.schema.getNonEmptyElements(); function handleEnterKey(evt) { - var rng = selection.getRng(true), tmpRng, editableRoot, container, offset, parentBlock, documentMode, + var rng = selection.getRng(true), tmpRng, editableRoot, container, offset, parentBlock, documentMode, shiftKey, newBlock, fragment, containerBlock, parentBlockName, containerBlockName, newBlockName, isAfterLastNodeInContainer; // Returns true if the block can be split into two blocks or not @@ -18122,7 +18578,7 @@ function renderBlockOnIE(block) { var oldRng; - if (tinymce.isIE && dom.isBlock(block)) { + if (tinymce.isIE && !tinymce.isIE11 && dom.isBlock(block)) { oldRng = selection.getRng(); block.appendChild(dom.create('span', null, '\u00a0')); selection.select(block); @@ -18151,6 +18607,11 @@ node = firstChilds[i]; if (!node.hasChildNodes() || (node.firstChild == node.lastChild && node.firstChild.nodeValue === '')) { dom.remove(node); + } else { + // Remove <a> </a> see #5381 + if (node.nodeName == "A" && (node.innerText || node.textContent) === ' ') { + dom.remove(node); + } } } }; @@ -18232,6 +18693,11 @@ if (settings.keep_styles !== false) { do { if (/^(SPAN|STRONG|B|EM|I|FONT|STRIKE|U)$/.test(node.nodeName)) { + // Never clone a caret containers + if (node.id == '_mce_caret') { + continue; + } + clonedNode = node.cloneNode(false); dom.setAttrib(clonedNode, 'id', ''); // Remove ID since it needs to be document unique @@ -18247,8 +18713,8 @@ } // BR is needed in empty blocks on non IE browsers - if (!tinymce.isIE) { - caretNode.innerHTML = '<br>'; + if (!tinymce.isIE || tinymce.isIE11) { + caretNode.innerHTML = '<br data-mce-bogus="1">'; } return block; @@ -18409,27 +18875,25 @@ undoManager.add(); }; - // Walks the parent block to the right and look for BR elements - function hasRightSideBr() { + // Walks the parent block to the right and look for any contents + function hasRightSideContent() { var walker = new TreeWalker(container, parentBlock), node; - while (node = walker.current()) { - if (node.nodeName == 'BR') { + while (node = walker.next()) { + if (nonEmptyElementsMap[node.nodeName.toLowerCase()] || node.length > 0) { return true; } - - node = walker.next(); } } - + // Inserts a BR element if the forced_root_block option is set to false or empty string function insertBr() { - var brElm, extraBr; + var brElm, extraBr, marker; if (container && container.nodeType == 3 && offset >= container.nodeValue.length) { // Insert extra BR element at the end block elements - if (!tinymce.isIE && !hasRightSideBr()) { - brElm = dom.create('br') + if ((!tinymce.isIE || tinymce.isIE11) && !hasRightSideContent()) { + brElm = dom.create('br'); rng.insertNode(brElm); rng.setStartAfter(brElm); rng.setEndAfter(brElm); @@ -18441,9 +18905,15 @@ rng.insertNode(brElm); // Rendering modes below IE8 doesn't display BR elements in PRE unless we have a \n before it - if (tinymce.isIE && parentBlockName == 'PRE' && (!documentMode || documentMode < 8)) { + if ((tinymce.isIE && !tinymce.isIE11) && parentBlockName == 'PRE' && (!documentMode || documentMode < 8)) { brElm.parentNode.insertBefore(dom.doc.createTextNode('\r'), brElm); } + + // Insert temp marker and scroll to that + marker = dom.create('span', {}, ' '); + brElm.parentNode.insertBefore(marker, brElm); + selection.scrollIntoView(marker); + dom.remove(marker); if (!extraBr) { rng.setStartAfter(brElm); @@ -18489,7 +18959,7 @@ var lastChild; // IE will render the blocks correctly other browsers needs a BR - if (!tinymce.isIE) { + if (!tinymce.isIE || tinymce.isIE11) { block.normalize(); // Remove empty text nodes that got left behind by the extract // Check if the block is empty or contains a floated last child @@ -18514,9 +18984,10 @@ // Setup range items and newBlockName container = rng.startContainer; offset = rng.startOffset; - newBlockName = settings.forced_root_block; + newBlockName = (settings.force_p_newlines ? 'p' : '') || settings.forced_root_block; newBlockName = newBlockName ? newBlockName.toUpperCase() : ''; documentMode = dom.doc.documentMode; + shiftKey = evt.shiftKey; // Resolve node index if (container.nodeType == 1 && container.hasChildNodes()) { @@ -18541,7 +19012,7 @@ // If editable root isn't block nor the root of the editor if (!dom.isBlock(editableRoot) && editableRoot != dom.getRoot()) { - if (!newBlockName || evt.shiftKey) { + if (!newBlockName || shiftKey) { insertBr(); } @@ -18551,7 +19022,7 @@ // Wrap the current node and it's sibling in a default block if it's needed. // for example this <td>text|<b>text2</b></td> will become this <td><p>text|<b>text2</p></b></td> // This won't happen if root blocks are disabled or the shiftKey is pressed - if ((newBlockName && !evt.shiftKey) || (!newBlockName && evt.shiftKey)) { + if ((newBlockName && !shiftKey) || (!newBlockName && shiftKey)) { container = wrapSelfAndSiblingsInDefaultBlock(container, offset); } @@ -18563,26 +19034,40 @@ parentBlockName = parentBlock ? parentBlock.nodeName.toUpperCase() : ''; // IE < 9 & HTML5 containerBlockName = containerBlock ? containerBlock.nodeName.toUpperCase() : ''; // IE < 9 & HTML5 - // Handle enter inside an empty list item - if (parentBlockName == 'LI' && dom.isEmpty(parentBlock)) { - // Let the list plugin or browser handle nested lists for now - if (/^(UL|OL|LI)$/.test(containerBlock.parentNode.nodeName)) { - return false; + // Enter inside block contained within a LI then split or insert before/after LI + if (containerBlockName == 'LI' && !evt.ctrlKey) { + parentBlock = containerBlock; + parentBlockName = containerBlockName; + } + + // Handle enter in LI + if (parentBlockName == 'LI') { + if (!newBlockName && shiftKey) { + insertBr(); + return; } - handleEmptyListItem(); - return; + // Handle enter inside an empty list item + if (dom.isEmpty(parentBlock)) { + // Let the list plugin or browser handle nested lists for now + if (/^(UL|OL|LI)$/.test(containerBlock.parentNode.nodeName)) { + return false; + } + + handleEmptyListItem(); + return; + } } // Don't split PRE tags but insert a BR instead easier when writing code samples etc if (parentBlockName == 'PRE' && settings.br_in_pre !== false) { - if (!evt.shiftKey) { + if (!shiftKey) { insertBr(); return; } } else { // If no root block is configured then insert a BR by default or if the shiftKey is pressed - if ((!newBlockName && !evt.shiftKey && parentBlockName != 'LI') || (newBlockName && evt.shiftKey)) { + if ((!newBlockName && !shiftKey && parentBlockName != 'LI') || (newBlockName && shiftKey)) { insertBr(); return; } -- Gitblit v1.9.1