var tinymce = {\r
majorVersion : '3',\r
\r
- minorVersion : '5.0.1',\r
+ minorVersion : '5.8',\r
\r
- releaseDate : '2012-05-10',\r
+ releaseDate : '2012-11-20',\r
\r
_init : function() {\r
var t = this, d = document, na = navigator, ua = na.userAgent, i, nl, n, base, p, v;\r
if (!t)\r
return o !== undef;\r
\r
- if (t == 'array' && (o.hasOwnProperty && o instanceof Array))\r
+ if (t == 'array' && tinymce.isArray(o))\r
return true;\r
\r
return typeof(o) == t;\r
},\r
\r
+ isArray: Array.isArray || function(obj) {\r
+ return Object.prototype.toString.call(obj) === "[object Array]";\r
+ },\r
+\r
makeMap : function(items, delim, map) {\r
var i;\r
\r
((s) ? "; secure" : "");\r
},\r
\r
- remove : function(n, p) {\r
- var d = new Date();\r
+ remove : function(name, path, domain) {\r
+ var date = new Date();\r
\r
- d.setTime(d.getTime() - 1000);\r
+ date.setTime(date.getTime() - 1000);\r
\r
- this.set(n, '', d, p, d);\r
+ this.set(name, '', date, path, domain);\r
}\r
});\r
})();\r
}\r
\r
if (t == 'object') {\r
- if (o.hasOwnProperty && o instanceof Array) {\r
+ if (o.hasOwnProperty && Object.prototype.toString.call(o) === '[object Array]') {\r
for (i=0, v = '['; i<o.length; i++)\r
v += (i > 0 ? ',' : '') + serialize(o[i], quote);\r
\r
\r
modifierPressed: function (e) {\r
return e.shiftKey || e.ctrlKey || e.altKey;\r
+ },\r
+\r
+ metaKeyPressed: function(e) {\r
+ // Check if ctrl or meta key is pressed also check if alt is false for Polish users\r
+ return tinymce.isMac ? e.metaKey : e.ctrlKey && !e.altKey;\r
}\r
};\r
})(tinymce);\r
\r
tinymce.util.Quirks = function(editor) {\r
- var VK = tinymce.VK, BACKSPACE = VK.BACKSPACE, DELETE = VK.DELETE, dom = editor.dom, selection = editor.selection, settings = editor.settings;\r
+ var VK = tinymce.VK, BACKSPACE = VK.BACKSPACE, DELETE = VK.DELETE, dom = editor.dom, selection = editor.selection,\r
+ settings = editor.settings, parser = editor.parser, serializer = editor.serializer, each = tinymce.each;\r
\r
function setEditorCommandState(cmd, state) {\r
try {\r
}\r
}\r
\r
+ function getDocumentMode() {\r
+ var documentMode = editor.getDoc().documentMode;\r
+\r
+ return documentMode ? documentMode : 6;\r
+ };\r
+\r
+ function isDefaultPrevented(e) {\r
+ return e.isDefaultPrevented();\r
+ };\r
+\r
function cleanupStylesWhenDeleting() {\r
function removeMergedFormatSpans(isDelete) {\r
var rng, blockElm, node, clonedSpan;\r
blockElm = dom.getParent(rng.startContainer, dom.isBlock);\r
\r
// On delete clone the root span of the next block element\r
- if (isDelete)\r
+ if (isDelete) {\r
blockElm = dom.getNext(blockElm, dom.isBlock);\r
+ }\r
\r
// Locate root span element and clone it since it would otherwise get merged by the "apple-style-span" on delete/backspace\r
if (blockElm) {\r
node = blockElm.firstChild;\r
\r
// Ignore empty text nodes\r
- while (node && node.nodeType == 3 && node.nodeValue.length === 0)\r
+ while (node && node.nodeType == 3 && node.nodeValue.length === 0) {\r
node = node.nextSibling;\r
+ }\r
\r
if (node && node.nodeName === 'SPAN') {\r
clonedSpan = node.cloneNode(false);\r
}\r
}\r
\r
+ each(dom.select('span', blockElm), function(span) {\r
+ span.setAttribute('data-mce-mark', '1');\r
+ });\r
+\r
// Do the backspace/delete action\r
editor.getDoc().execCommand(isDelete ? 'ForwardDelete' : 'Delete', false, null);\r
\r
// Find all odd apple-style-spans\r
blockElm = dom.getParent(rng.startContainer, dom.isBlock);\r
- tinymce.each(dom.select('span.Apple-style-span,font.Apple-style-span', blockElm), function(span) {\r
+ each(dom.select('span', blockElm), function(span) {\r
var bm = selection.getBookmark();\r
\r
if (clonedSpan) {\r
dom.replace(clonedSpan.cloneNode(false), span, true);\r
- } else {\r
+ } else if (!span.getAttribute('data-mce-mark')) {\r
dom.remove(span, true);\r
+ } else {\r
+ span.removeAttribute('data-mce-mark');\r
}\r
\r
// Restore the selection\r
selection.moveToBookmark(bm);\r
});\r
- };\r
+ }\r
\r
editor.onKeyDown.add(function(editor, e) {\r
var isDelete;\r
\r
isDelete = e.keyCode == DELETE;\r
- if (!e.isDefaultPrevented() && (isDelete || e.keyCode == BACKSPACE) && !VK.modifierPressed(e)) {\r
+ if (!isDefaultPrevented(e) && (isDelete || e.keyCode == BACKSPACE) && !VK.modifierPressed(e)) {\r
e.preventDefault();\r
removeMergedFormatSpans(isDelete);\r
}\r
};\r
\r
function emptyEditorWhenDeleting() {\r
- function getEndPointNode(rng, start) {\r
- var container, offset, prefix = start ? 'start' : 'end';\r
-\r
- container = rng[prefix + 'Container'];\r
- offset = rng[prefix + 'Offset'];\r
+ function serializeRng(rng) {\r
+ var body = dom.create("body");\r
+ var contents = rng.cloneContents();\r
+ body.appendChild(contents);\r
+ return selection.serializer.serialize(body, {format: 'html'});\r
+ }\r
\r
- // Resolve indexed container\r
- if (container.nodeType == 1 && container.hasChildNodes()) {\r
- container = container.childNodes[Math.min(start ? offset : (offset > 0 ? offset - 1 : 0), container.childNodes.length - 1)]\r
- }\r
+ function allContentsSelected(rng) {\r
+ var selection = serializeRng(rng);\r
\r
- return container;\r
- };\r
+ var allRng = dom.createRng();\r
+ allRng.selectNode(editor.getBody());\r
\r
- function isAtStartEndOfBody(rng, start) {\r
- var container, offset, root, childNode, prefix = start ? 'start' : 'end', isAfter;\r
+ var allSelection = serializeRng(allRng);\r
+ return selection === allSelection;\r
+ }\r
\r
- container = rng[prefix + 'Container'];\r
- offset = rng[prefix + 'Offset'];\r
- root = dom.getRoot();\r
+ editor.onKeyDown.add(function(editor, e) {\r
+ var keyCode = e.keyCode, isCollapsed;\r
\r
- // Resolve indexed container\r
- if (container.nodeType == 1) {\r
- isAfter = offset >= container.childNodes.length;\r
- container = getEndPointNode(rng, start);\r
+ // Empty the editor if it's needed for example backspace at <p><b>|</b></p>\r
+ if (!isDefaultPrevented(e) && (keyCode == DELETE || keyCode == BACKSPACE)) {\r
+ isCollapsed = editor.selection.isCollapsed();\r
\r
- if (container.nodeType == 3) {\r
- offset = start && !isAfter ? 0 : container.nodeValue.length;\r
+ // Selection is collapsed but the editor isn't empty\r
+ if (isCollapsed && !dom.isEmpty(editor.getBody())) {\r
+ return;\r
}\r
- }\r
-\r
- // Check if start/end is in the middle of text\r
- if (container.nodeType == 3 && ((start && offset > 0) || (!start && offset < container.nodeValue.length))) {\r
- return false;\r
- }\r
\r
- // Walk up the DOM tree to see if the endpoint is at the beginning/end of body\r
- while (container !== root) {\r
- childNode = container.parentNode[start ? 'firstChild' : 'lastChild'];\r
-\r
- // If first/last element is a BR then jump to it's sibling in case: <p>x<br></p>\r
- if (childNode.nodeName == "BR") {\r
- childNode = childNode[start ? 'nextSibling' : 'previousSibling'] || childNode;\r
+ // IE deletes all contents correctly when everything is selected\r
+ if (tinymce.isIE && !isCollapsed) {\r
+ return;\r
}\r
\r
- // If the childNode isn't the container node then break in case <p><span>A</span>[X]</p>\r
- if (childNode !== container) {\r
- return false;\r
+ // Selection isn't collapsed but not all the contents is selected\r
+ if (!isCollapsed && !allContentsSelected(editor.selection.getRng())) {\r
+ return;\r
}\r
\r
- container = container.parentNode;\r
+ // Manually empty the editor\r
+ editor.setContent('');\r
+ editor.selection.setCursorLocation(editor.getBody(), 0);\r
+ editor.nodeChanged();\r
}\r
+ });\r
+ };\r
\r
- return true;\r
- };\r
-\r
- editor.onKeyDown.addToTop(function(editor, e) {\r
- var rng, keyCode = e.keyCode;\r
-\r
- if (!e.isDefaultPrevented() && (keyCode == DELETE || keyCode == BACKSPACE)) {\r
- rng = selection.getRng(true);\r
-\r
- if (isAtStartEndOfBody(rng, true) && isAtStartEndOfBody(rng, false) &&\r
- (rng.collapsed || dom.findCommonAncestor(getEndPointNode(rng, true), getEndPointNode(rng)) === dom.getRoot())) {\r
- editor.setContent('');\r
- editor.nodeChanged();\r
- e.preventDefault();\r
- }\r
+ function selectAll() {\r
+ editor.onKeyDown.add(function(editor, e) {\r
+ if (!isDefaultPrevented(e) && e.keyCode == 65 && VK.metaKeyPressed(e)) {\r
+ e.preventDefault();\r
+ editor.execCommand('SelectAll');\r
}\r
});\r
};\r
\r
function removeHrOnBackspace() {\r
editor.onKeyDown.add(function(editor, e) {\r
- if (!e.isDefaultPrevented() && e.keyCode === BACKSPACE) {\r
+ if (!isDefaultPrevented(e) && e.keyCode === BACKSPACE) {\r
if (selection.isCollapsed() && selection.getRng(true).startOffset === 0) {\r
var node = selection.getNode();\r
var previousSibling = node.previousSibling;\r
// wouldn't get proper focus if the user clicked on the HTML element\r
if (!Range.prototype.getClientRects) { // Detect getClientRects got introduced in FF 4\r
editor.onMouseDown.add(function(editor, e) {\r
- if (e.target.nodeName === "HTML") {\r
+ if (!isDefaultPrevented(e) && e.target.nodeName === "HTML") {\r
var body = editor.getBody();\r
\r
// Blur the body it's focused but not correctly focused\r
if (target !== editor.getBody()) {\r
dom.setAttrib(target, "style", null);\r
\r
- tinymce.each(template, function(attr) {\r
+ each(template, function(attr) {\r
target.setAttributeNode(attr.cloneNode(true));\r
});\r
}\r
}\r
\r
function isSelectionAcrossElements() {\r
- return !selection.isCollapsed() && selection.getStart() != selection.getEnd();\r
+ return !selection.isCollapsed() && dom.getParent(selection.getStart(), dom.isBlock) != dom.getParent(selection.getEnd(), dom.isBlock);\r
}\r
\r
function blockEvent(editor, e) {\r
editor.onKeyPress.add(function(editor, e) {\r
var applyAttributes;\r
\r
- if ((e.keyCode == 8 || e.keyCode == 46) && isSelectionAcrossElements()) {\r
+ if (!isDefaultPrevented(e) && (e.keyCode == 8 || e.keyCode == 46) && isSelectionAcrossElements()) {\r
applyAttributes = getAttributeApplyFunction();\r
editor.getDoc().execCommand('delete', false, null);\r
applyAttributes();\r
dom.bind(editor.getDoc(), 'cut', function(e) {\r
var applyAttributes;\r
\r
- if (isSelectionAcrossElements()) {\r
+ if (!isDefaultPrevented(e) && isSelectionAcrossElements()) {\r
applyAttributes = getAttributeApplyFunction();\r
editor.onKeyUp.addToTop(blockEvent);\r
\r
\r
function disableBackspaceIntoATable() {\r
editor.onKeyDown.add(function(editor, e) {\r
- if (!e.isDefaultPrevented() && e.keyCode === BACKSPACE) {\r
+ if (!isDefaultPrevented(e) && e.keyCode === BACKSPACE) {\r
if (selection.isCollapsed() && selection.getRng(true).startOffset === 0) {\r
var previousSibling = selection.getNode().previousSibling;\r
if (previousSibling && previousSibling.nodeName && previousSibling.nodeName.toLowerCase() === "table") {\r
}\r
\r
function addNewLinesBeforeBrInPre() {\r
- var documentMode = editor.getDoc().documentMode;\r
-\r
// IE8+ rendering mode does the right thing with BR in PRE\r
- if (documentMode && documentMode > 7) {\r
+ if (getDocumentMode() > 7) {\r
return;\r
}\r
\r
// Enable display: none in area and add a specific class that hides all BR elements in PRE to\r
// avoid the caret from getting stuck at the BR elements while pressing the right arrow key\r
setEditorCommandState('RespectVisibilityInDesign', true);\r
+ editor.contentStyles.push('.mceHideBrInPre pre br {display: none}');\r
dom.addClass(editor.getBody(), 'mceHideBrInPre');\r
\r
// Adds a \n before all BR elements in PRE to get them visual\r
- editor.parser.addNodeFilter('pre', function(nodes, name) {\r
+ parser.addNodeFilter('pre', function(nodes, name) {\r
var i = nodes.length, brNodes, j, brElm, sibling;\r
\r
while (i--) {\r
});\r
\r
// Removes any \n before BR elements in PRE since other browsers and in contentEditable=false mode they will be visible\r
- editor.serializer.addNodeFilter('pre', function(nodes, name) {\r
+ serializer.addNodeFilter('pre', function(nodes, name) {\r
var i = nodes.length, brNodes, j, brElm, sibling;\r
\r
while (i--) {\r
var isDelete, rng, container, offset, brElm, sibling, collapsed;\r
\r
isDelete = e.keyCode == DELETE;\r
- if (!e.isDefaultPrevented() && (isDelete || e.keyCode == BACKSPACE) && !VK.modifierPressed(e)) {\r
+ if (!isDefaultPrevented(e) && (isDelete || e.keyCode == BACKSPACE) && !VK.modifierPressed(e)) {\r
rng = selection.getRng();\r
container = rng.startContainer;\r
offset = rng.startOffset;\r
editor.onKeyDown.add(function(editor, e) {\r
var rng, container, offset, root, parent;\r
\r
- if (e.isDefaultPrevented() || e.keyCode != VK.BACKSPACE) {\r
+ if (isDefaultPrevented(e) || e.keyCode != VK.BACKSPACE) {\r
return;\r
}\r
\r
editor.formatter.toggle('blockquote', null, parent);\r
\r
// Move the caret to the beginning of container\r
+ rng = dom.createRng();\r
rng.setStart(container, 0);\r
rng.setEnd(container, 0);\r
selection.setRng(rng);\r
- selection.collapse(false);\r
}\r
});\r
};\r
\r
function addBrAfterLastLinks() {\r
function fixLinks(editor, o) {\r
- tinymce.each(dom.select('a'), function(node) {\r
+ each(dom.select('a'), function(node) {\r
var parentNode = node.parentNode, root = dom.getRoot();\r
\r
if (parentNode.lastChild === node) {\r
editor.onSetContent.add(selection.onSetContent.add(fixLinks));\r
};\r
\r
+ function setDefaultBlockType() {\r
+ if (settings.forced_root_block) {\r
+ editor.onInit.add(function() {\r
+ setEditorCommandState('DefaultParagraphSeparator', settings.forced_root_block);\r
+ });\r
+ }\r
+ }\r
+\r
function removeGhostSelection() {\r
function repaint(sender, args) {\r
if (!sender || !args.initial) {\r
editor.onSetContent.add(repaint);\r
};\r
\r
- function deleteImageOnBackSpace() {\r
+ function deleteControlItemOnBackSpace() {\r
editor.onKeyDown.add(function(editor, e) {\r
- if (!e.isDefaultPrevented() && e.keyCode == 8 && selection.getNode().nodeName == 'IMG') {\r
- e.preventDefault();\r
- editor.undoManager.beforeChange();\r
- dom.remove(selection.getNode());\r
- editor.undoManager.add();\r
+ var rng;\r
+\r
+ if (!isDefaultPrevented(e) && e.keyCode == BACKSPACE) {\r
+ rng = editor.getDoc().selection.createRange();\r
+ if (rng && rng.item) {\r
+ e.preventDefault();\r
+ editor.undoManager.beforeChange();\r
+ dom.remove(rng.item(0));\r
+ editor.undoManager.add();\r
+ }\r
}\r
});\r
};\r
\r
+ function renderEmptyBlocksFix() {\r
+ var emptyBlocksCSS;\r
+\r
+ // IE10+\r
+ if (getDocumentMode() >= 10) {\r
+ emptyBlocksCSS = '';\r
+ each('p div h1 h2 h3 h4 h5 h6'.split(' '), function(name, i) {\r
+ emptyBlocksCSS += (i > 0 ? ',' : '') + name + ':empty';\r
+ });\r
+\r
+ editor.contentStyles.push(emptyBlocksCSS + '{padding-right: 1px !important}');\r
+ }\r
+ };\r
+\r
+ function fakeImageResize() {\r
+ var selectedElmX, selectedElmY, selectedElm, selectedElmGhost, selectedHandle, startX, startY, startW, startH, ratio,\r
+ resizeHandles, width, height, rootDocument = document, editableDoc = editor.getDoc();\r
+\r
+ if (!settings.object_resizing || settings.webkit_fake_resize === false) {\r
+ return;\r
+ }\r
+\r
+ // Try disabling object resizing if WebKit implements resizing in the future\r
+ setEditorCommandState("enableObjectResizing", false);\r
+\r
+ // Details about each resize handle how to scale etc\r
+ resizeHandles = {\r
+ // Name: x multiplier, y multiplier, delta size x, delta size y\r
+ n: [.5, 0, 0, -1],\r
+ e: [1, .5, 1, 0],\r
+ s: [.5, 1, 0, 1],\r
+ w: [0, .5, -1, 0],\r
+ nw: [0, 0, -1, -1],\r
+ ne: [1, 0, 1, -1],\r
+ se: [1, 1, 1, 1],\r
+ sw : [0, 1, -1, 1]\r
+ };\r
+\r
+ function resizeElement(e) {\r
+ var deltaX, deltaY;\r
+\r
+ // Calc new width/height\r
+ deltaX = e.screenX - startX;\r
+ deltaY = e.screenY - startY;\r
+\r
+ // Calc new size\r
+ width = deltaX * selectedHandle[2] + startW;\r
+ height = deltaY * selectedHandle[3] + startH;\r
+\r
+ // Never scale down lower than 5 pixels\r
+ width = width < 5 ? 5 : width;\r
+ height = height < 5 ? 5 : height;\r
+\r
+ // Constrain proportions when modifier key is pressed or if the nw, ne, sw, se corners are moved on an image\r
+ if (VK.modifierPressed(e) || (selectedElm.nodeName == "IMG" && selectedHandle[2] * selectedHandle[3] !== 0)) {\r
+ width = Math.round(height / ratio);\r
+ height = Math.round(width * ratio);\r
+ }\r
+\r
+ // Update ghost size\r
+ dom.setStyles(selectedElmGhost, {\r
+ width: width,\r
+ height: height\r
+ });\r
+\r
+ // Update ghost X position if needed\r
+ if (selectedHandle[2] < 0 && selectedElmGhost.clientWidth <= width) {\r
+ dom.setStyle(selectedElmGhost, 'left', selectedElmX + (startW - width));\r
+ }\r
+\r
+ // Update ghost Y position if needed\r
+ if (selectedHandle[3] < 0 && selectedElmGhost.clientHeight <= height) {\r
+ dom.setStyle(selectedElmGhost, 'top', selectedElmY + (startH - height));\r
+ }\r
+ }\r
+\r
+ function endResize() {\r
+ function setSizeProp(name, value) {\r
+ if (value) {\r
+ // Resize by using style or attribute\r
+ if (selectedElm.style[name] || !editor.schema.isValid(selectedElm.nodeName.toLowerCase(), name)) {\r
+ dom.setStyle(selectedElm, name, value);\r
+ } else {\r
+ dom.setAttrib(selectedElm, name, value);\r
+ }\r
+ }\r
+ }\r
+\r
+ // Set width/height properties\r
+ setSizeProp('width', width);\r
+ setSizeProp('height', height);\r
+\r
+ dom.unbind(editableDoc, 'mousemove', resizeElement);\r
+ dom.unbind(editableDoc, 'mouseup', endResize);\r
+\r
+ if (rootDocument != editableDoc) {\r
+ dom.unbind(rootDocument, 'mousemove', resizeElement);\r
+ dom.unbind(rootDocument, 'mouseup', endResize);\r
+ }\r
+\r
+ // Remove ghost and update resize handle positions\r
+ dom.remove(selectedElmGhost);\r
+ showResizeRect(selectedElm);\r
+ }\r
+\r
+ function showResizeRect(targetElm) {\r
+ var position, targetWidth, targetHeight;\r
+\r
+ hideResizeRect();\r
+\r
+ // Get position and size of target\r
+ position = dom.getPos(targetElm);\r
+ selectedElmX = position.x;\r
+ selectedElmY = position.y;\r
+ targetWidth = targetElm.offsetWidth;\r
+ targetHeight = targetElm.offsetHeight;\r
+\r
+ // Reset width/height if user selects a new image/table\r
+ if (selectedElm != targetElm) {\r
+ selectedElm = targetElm;\r
+ width = height = 0;\r
+ }\r
+\r
+ each(resizeHandles, function(handle, name) {\r
+ var handleElm;\r
+\r
+ // Get existing or render resize handle\r
+ handleElm = dom.get('mceResizeHandle' + name);\r
+ if (!handleElm) {\r
+ handleElm = dom.add(editableDoc.documentElement, 'div', {\r
+ id: 'mceResizeHandle' + name,\r
+ 'class': 'mceResizeHandle',\r
+ style: 'cursor:' + name + '-resize; margin:0; padding:0'\r
+ });\r
+\r
+ dom.bind(handleElm, 'mousedown', function(e) {\r
+ e.preventDefault();\r
+\r
+ endResize();\r
+\r
+ startX = e.screenX;\r
+ startY = e.screenY;\r
+ startW = selectedElm.clientWidth;\r
+ startH = selectedElm.clientHeight;\r
+ ratio = startH / startW;\r
+ selectedHandle = handle;\r
+\r
+ selectedElmGhost = selectedElm.cloneNode(true);\r
+ dom.addClass(selectedElmGhost, 'mceClonedResizable');\r
+ dom.setStyles(selectedElmGhost, {\r
+ left: selectedElmX,\r
+ top: selectedElmY,\r
+ margin: 0\r
+ });\r
+\r
+ editableDoc.documentElement.appendChild(selectedElmGhost);\r
+\r
+ dom.bind(editableDoc, 'mousemove', resizeElement);\r
+ dom.bind(editableDoc, 'mouseup', endResize);\r
+\r
+ if (rootDocument != editableDoc) {\r
+ dom.bind(rootDocument, 'mousemove', resizeElement);\r
+ dom.bind(rootDocument, 'mouseup', endResize);\r
+ }\r
+ });\r
+ } else {\r
+ dom.show(handleElm);\r
+ }\r
+\r
+ // Position element\r
+ dom.setStyles(handleElm, {\r
+ left: (targetWidth * handle[0] + selectedElmX) - (handleElm.offsetWidth / 2),\r
+ top: (targetHeight * handle[1] + selectedElmY) - (handleElm.offsetHeight / 2)\r
+ });\r
+ });\r
+\r
+ // Only add resize rectangle on WebKit and only on images\r
+ if (!tinymce.isOpera && selectedElm.nodeName == "IMG") {\r
+ selectedElm.setAttribute('data-mce-selected', '1');\r
+ }\r
+ }\r
+\r
+ function hideResizeRect() {\r
+ if (selectedElm) {\r
+ selectedElm.removeAttribute('data-mce-selected');\r
+ }\r
+\r
+ for (var name in resizeHandles) {\r
+ dom.hide('mceResizeHandle' + name);\r
+ }\r
+ }\r
+\r
+ // Add CSS for resize handles, cloned element and selected\r
+ editor.contentStyles.push(\r
+ '.mceResizeHandle {' +\r
+ 'position: absolute;' +\r
+ 'border: 1px solid black;' +\r
+ 'background: #FFF;' +\r
+ 'width: 5px;' +\r
+ 'height: 5px;' +\r
+ 'z-index: 10000' +\r
+ '}' +\r
+ '.mceResizeHandle:hover {' +\r
+ 'background: #000' +\r
+ '}' +\r
+ 'img[data-mce-selected] {' +\r
+ 'outline: 1px solid black' +\r
+ '}' +\r
+ 'img.mceClonedResizable, table.mceClonedResizable {' +\r
+ 'position: absolute;' +\r
+ 'outline: 1px dashed black;' +\r
+ 'opacity: .5;' +\r
+ 'z-index: 10000' +\r
+ '}'\r
+ );\r
+\r
+ function updateResizeRect() {\r
+ var controlElm = dom.getParent(selection.getNode(), 'table,img');\r
+\r
+ // Remove data-mce-selected from all elements since they might have been copied using Ctrl+c/v\r
+ each(dom.select('img[data-mce-selected]'), function(img) {\r
+ img.removeAttribute('data-mce-selected');\r
+ });\r
+\r
+ if (controlElm) {\r
+ showResizeRect(controlElm);\r
+ } else {\r
+ hideResizeRect();\r
+ }\r
+ }\r
+\r
+ // Show/hide resize rect when image is selected\r
+ editor.onNodeChange.add(updateResizeRect);\r
+\r
+ // Fixes WebKit quirk where it returns IMG on getNode if caret is after last image in container\r
+ dom.bind(editableDoc, 'selectionchange', updateResizeRect);\r
+\r
+ // Remove the internal attribute when serializing the DOM\r
+ editor.serializer.addAttributeFilter('data-mce-selected', function(nodes, name) {\r
+ var i = nodes.length;\r
+\r
+ while (i--) {\r
+ nodes[i].attr(name, null);\r
+ }\r
+ });\r
+ }\r
+\r
+ function keepNoScriptContents() {\r
+ if (getDocumentMode() < 9) {\r
+ parser.addNodeFilter('noscript', function(nodes) {\r
+ var i = nodes.length, node, textNode;\r
+\r
+ while (i--) {\r
+ node = nodes[i];\r
+ textNode = node.firstChild;\r
+\r
+ if (textNode) {\r
+ node.attr('data-mce-innertext', textNode.value);\r
+ }\r
+ }\r
+ });\r
+\r
+ serializer.addNodeFilter('noscript', function(nodes) {\r
+ var i = nodes.length, node, textNode, value;\r
+\r
+ while (i--) {\r
+ node = nodes[i];\r
+ textNode = nodes[i].firstChild;\r
+\r
+ if (textNode) {\r
+ textNode.value = tinymce.html.Entities.decode(textNode.value);\r
+ } else {\r
+ // Old IE can't retain noscript value so an attribute is used to store it\r
+ value = node.attributes.map['data-mce-innertext'];\r
+ if (value) {\r
+ node.attr('data-mce-innertext', null);\r
+ textNode = new tinymce.html.Node('#text', 3);\r
+ textNode.value = value;\r
+ textNode.raw = true;\r
+ node.append(textNode);\r
+ }\r
+ }\r
+ }\r
+ });\r
+ }\r
+ }\r
+\r
// All browsers\r
disableBackspaceIntoATable();\r
removeBlockQuoteOnBackSpace();\r
cleanupStylesWhenDeleting();\r
inputMethodFocus();\r
selectControlElements();\r
+ setDefaultBlockType();\r
\r
// iOS\r
if (tinymce.isIDevice) {\r
selectionChangeNodeChanged();\r
+ } else {\r
+ fakeImageResize();\r
+ selectAll();\r
}\r
}\r
\r
ensureBodyHasRoleApplication();\r
addNewLinesBeforeBrInPre();\r
removePreSerializedStylesWhenSelectingControls();\r
- deleteImageOnBackSpace();\r
+ deleteControlItemOnBackSpace();\r
+ renderEmptyBlocksFix();\r
+ keepNoScriptContents();\r
}\r
\r
// Gecko\r
addBrAfterLastLinks();\r
removeGhostSelection();\r
}\r
+\r
+ // Opera\r
+ if (tinymce.isOpera) {\r
+ fakeImageResize();\r
+ }\r
};\r
(function(tinymce) {\r
var namedEntities, baseEntities, reverseEntities,\r
\r
if (!html5) {\r
html5 = mapCache.html5 = unpack({\r
- A : 'id|accesskey|class|dir|draggable|item|hidden|itemprop|role|spellcheck|style|subject|title',\r
- B : '#|a|abbr|area|audio|b|bdo|br|button|canvas|cite|code|command|datalist|del|dfn|em|embed|i|iframe|img|input|ins|kbd|keygen|label|link|map|mark|meta|meter|noscript|object|output|progress|q|ruby|samp|script|select|small|span|strong|sub|sup|svg|textarea|time|var|video',\r
- C : '#|a|abbr|area|address|article|aside|audio|b|bdo|blockquote|br|button|canvas|cite|code|command|datalist|del|details|dfn|dialog|div|dl|em|embed|fieldset|figure|footer|form|h1|h2|h3|h4|h5|h6|header|hgroup|hr|i|iframe|img|input|ins|kbd|keygen|label|link|map|mark|menu|meta|meter|nav|noscript|ol|object|output|p|pre|progress|q|ruby|samp|script|section|select|small|span|strong|style|sub|sup|svg|table|textarea|time|ul|var|video'\r
+ A : 'id|accesskey|class|dir|draggable|item|hidden|itemprop|role|spellcheck|style|subject|title|onclick|ondblclick|onmousedown|onmouseup|onmouseover|onmousemove|onmouseout|onkeypress|onkeydown|onkeyup',\r
+ B : '#|a|abbr|area|audio|b|bdo|br|button|canvas|cite|code|command|datalist|del|dfn|em|embed|i|iframe|img|input|ins|kbd|keygen|label|link|map|mark|meta|' +\r
+ 'meter|noscript|object|output|progress|q|ruby|samp|script|select|small|span|strong|sub|sup|svg|textarea|time|var|video|wbr',\r
+ C : '#|a|abbr|area|address|article|aside|audio|b|bdo|blockquote|br|button|canvas|cite|code|command|datalist|del|details|dfn|dialog|div|dl|em|embed|fieldset|' +\r
+ 'figure|footer|form|h1|h2|h3|h4|h5|h6|header|hgroup|hr|i|iframe|img|input|ins|kbd|keygen|label|link|map|mark|menu|meta|meter|nav|noscript|ol|object|output|' +\r
+ 'p|pre|progress|q|ruby|samp|script|section|select|small|span|strong|style|sub|sup|svg|table|textarea|time|ul|var|video'\r
}, 'html[A|manifest][body|head]' +\r
'head[A][base|command|link|meta|noscript|script|style|title]' +\r
'title[A][#]' +\r
'dl[A][dd|dt]' +\r
'dt[A][B]' +\r
'dd[A][C]' +\r
- 'a[A|href|target|ping|rel|media|type][C]' +\r
+ 'a[A|href|target|ping|rel|media|type][B]' +\r
'em[A][B]' +\r
'strong[A][B]' +\r
'small[A][B]' +\r
'form[A|accept-charset|action|autocomplete|enctype|method|name|novalidate|target][C]' +\r
'fieldset[A|disabled|form|name][C|legend]' +\r
'label[A|form|for][B]' +\r
- 'input[A|type|accept|alt|autocomplete|checked|disabled|form|formaction|formenctype|formmethod|formnovalidate|formtarget|height|list|max|maxlength|min|multiple|pattern|placeholder|readonly|required|size|src|step|width|files|value][]' +\r
+ 'input[A|type|accept|alt|autocomplete|autofocus|checked|disabled|form|formaction|formenctype|formmethod|formnovalidate|formtarget|height|list|max|maxlength|min|' +\r
+ 'multiple|pattern|placeholder|readonly|required|size|src|step|width|files|value|name][]' +\r
'button[A|autofocus|disabled|form|formaction|formenctype|formmethod|formnovalidate|formtarget|name|value|type][B]' +\r
'select[A|autofocus|disabled|form|multiple|name|size][option|optgroup]' +\r
'datalist[A][B|option]' +\r
'area[A|shape|coords|href|alt|target|media|rel|ping|type][]' +\r
'mathml[A][]' +\r
'svg[A][]' +\r
- 'table[A|summary][caption|colgroup|thead|tfoot|tbody|tr]' +\r
+ 'table[A|border][caption|colgroup|thead|tfoot|tbody|tr]' +\r
'caption[A][C]' +\r
'colgroup[A|span][col]' +\r
'col[A|span][]' +\r
'tbody[A][tr]' +\r
'tr[A][th|td]' +\r
'th[A|headers|rowspan|colspan|scope][B]' +\r
- 'td[A|headers|rowspan|colspan][C]'\r
+ 'td[A|headers|rowspan|colspan][C]' +\r
+ 'wbr[A][]'\r
);\r
}\r
\r
}\r
\r
// Setup map objects\r
- whiteSpaceElementsMap = createLookupTable('whitespace_elements', 'pre script style textarea');\r
- selfClosingElementsMap = createLookupTable('self_closing_elements', 'colgroup dd dt li options p td tfoot th thead tr');\r
- shortEndedElementsMap = createLookupTable('short_ended_elements', 'area base basefont br col frame hr img input isindex link meta param embed source');\r
+ whiteSpaceElementsMap = createLookupTable('whitespace_elements', 'pre script noscript style textarea');\r
+ selfClosingElementsMap = createLookupTable('self_closing_elements', 'colgroup dd dt li option p td tfoot th thead tr');\r
+ shortEndedElementsMap = createLookupTable('short_ended_elements', 'area base basefont br col frame hr img input isindex link meta param embed source wbr');\r
boolAttrMap = createLookupTable('boolean_attributes', 'checked compact declare defer disabled ismap multiple nohref noresize noshade nowrap readonly selected autoplay loop controls');\r
nonEmptyElementsMap = createLookupTable('non_empty_elements', 'td th iframe video audio object', shortEndedElementsMap);\r
- blockElementsMap = createLookupTable('block_elements', 'h1 h2 h3 h4 h5 h6 hr p div address pre form table tbody thead tfoot ' + \r
- 'th tr td li ol ul caption blockquote center dl dt dd dir fieldset ' + \r
- 'noscript menu isindex samp header footer article section hgroup aside nav figure');\r
+ textBlockElementsMap = createLookupTable('text_block_elements', 'h1 h2 h3 h4 h5 h6 p div address pre form ' + \r
+ 'blockquote center dir fieldset header footer article section hgroup aside nav figure');\r
+ blockElementsMap = createLookupTable('block_elements', 'hr table tbody thead tfoot ' + \r
+ 'th tr td li ol ul caption dl dt dd noscript menu isindex samp option datalist select optgroup', textBlockElementsMap);\r
\r
// Converts a wildcard expression string to a regexp for example *a will become /.*a/.\r
function patternToRegExp(str) {\r
customElementsMap[name] = cloneName;\r
\r
// If it's not marked as inline then add it to valid block elements\r
- if (!inline)\r
+ if (!inline) {\r
+ blockElementsMap[name.toUpperCase()] = {};\r
blockElementsMap[name] = {};\r
+ }\r
+\r
+ // Add elements clone if needed\r
+ if (!elements[name]) {\r
+ elements[name] = elements[cloneName];\r
+ }\r
\r
// Add custom elements at span/div positions\r
each(children, function(element, child) {\r
return blockElementsMap;\r
};\r
\r
+ self.getTextBlockElements = function() {\r
+ return textBlockElementsMap;\r
+ };\r
+\r
self.getShortEndedElements = function() {\r
return shortEndedElementsMap;\r
};\r
return !!(parent && parent[child]);\r
};\r
\r
+ self.isValid = function(name, attr) {\r
+ var attrPatterns, i, rule = getElementRule(name);\r
+\r
+ // Check if it's a valid element\r
+ if (rule) {\r
+ if (attr) {\r
+ // Check if attribute name exists\r
+ if (rule.attributes[attr]) {\r
+ return true;\r
+ }\r
+\r
+ // Check if attribute matches a regexp pattern\r
+ attrPatterns = rule.attributePatterns;\r
+ if (attrPatterns) {\r
+ i = attrPatterns.length;\r
+ while (i--) {\r
+ if (attrPatterns[i].pattern.test(name)) {\r
+ return true;\r
+ }\r
+ }\r
+ }\r
+ } else {\r
+ return true;\r
+ }\r
+ }\r
+\r
+ // No match\r
+ return false;\r
+ };\r
+ \r
self.getElementRule = getElementRule;\r
\r
self.getCustomElements = function() {\r
self.addCustomElements = addCustomElements;\r
\r
self.addValidChildren = addValidChildren;\r
+\r
+ self.elements = elements;\r
};\r
})(tinymce);\r
\r
'(?:!DOCTYPE([\\w\\W]*?)>)|' + // DOCTYPE\r
'(?:\\?([^\\s\\/<>]+) ?([\\w\\W]*?)[?/]>)|' + // PI\r
'(?:\\/([^>]+)>)|' + // End element\r
- '(?:([A-Za-z0-9\\-\\:]+)((?:\\s+[^"\'>]+(?:(?:"[^"]*")|(?:\'[^\']*\')|[^>]*))*|\\/|\\s+)>)' + // Start element\r
+ '(?:([A-Za-z0-9\\-\\:\\.]+)((?:\\s+[^"\'>]+(?:(?:"[^"]*")|(?:\'[^\']*\')|[^>]*))*|\\/|\\s+)>)' + // Start element\r
')', 'g');\r
\r
- attrRegExp = /([\w:\-]+)(?:\s*=\s*(?:(?:\"((?:\\.|[^\"])*)\")|(?:\'((?:\\.|[^\'])*)\')|([^>\s]+)))?/g;\r
+ attrRegExp = /([\w:\-]+)(?:\s*=\s*(?:(?:\"((?:[^\"])*)\")|(?:\'((?:[^\'])*)\')|([^>\s]+)))?/g;\r
specialElements = {\r
'script' : /<\/script[^>]*>/gi,\r
'style' : /<\/style[^>]*>/gi,\r
\r
// Setup lookup tables for empty elements and boolean attributes\r
shortEndedElements = schema.getShortEndedElements();\r
- selfClosing = schema.getSelfClosingElements();\r
+ selfClosing = settings.self_closing_elements || schema.getSelfClosingElements();\r
fillAttrsMap = schema.getBoolAttrs();\r
validate = settings.validate;\r
removeInternalElements = settings.remove_internals;\r
i = node.attributes.length;\r
while (i--) {\r
name = node.attributes[i].name;\r
- if (name === "name" || name.indexOf('data-') === 0)\r
+ if (name === "name" || name.indexOf('data-mce-') === 0)\r
return false;\r
}\r
}\r
\r
function fixInvalidChildren(nodes) {\r
var ni, node, parent, parents, newParent, currentNode, tempNode, childNode, i,\r
- childClone, nonEmptyElements, nonSplitableElements, sibling, nextNode;\r
+ childClone, nonEmptyElements, nonSplitableElements, textBlockElements, sibling, nextNode;\r
\r
nonSplitableElements = tinymce.makeMap('tr,td,th,tbody,thead,tfoot,table');\r
nonEmptyElements = schema.getNonEmptyElements();\r
+ textBlockElements = schema.getTextBlockElements();\r
\r
for (ni = 0; ni < nodes.length; ni++) {\r
node = nodes[ni];\r
\r
- // Already removed\r
- if (!node.parent)\r
+ // Already removed or fixed\r
+ if (!node.parent || node.fixed)\r
+ continue;\r
+\r
+ // If the invalid element is a text block and the text block is within a parent LI element\r
+ // Then unwrap the first text block and convert other sibling text blocks to LI elements similar to Word/Open Office\r
+ if (textBlockElements[node.name] && node.parent.name == 'li') {\r
+ // Move sibling text blocks after LI element\r
+ sibling = node.next;\r
+ while (sibling) {\r
+ if (textBlockElements[sibling.name]) {\r
+ sibling.name = 'li';\r
+ sibling.fixed = true;\r
+ node.parent.insert(sibling, node.parent);\r
+ } else {\r
+ break;\r
+ }\r
+\r
+ sibling = sibling.next;\r
+ }\r
+\r
+ // Unwrap current text block\r
+ node.unwrap(node);\r
continue;\r
+ }\r
\r
// Get list of all parent nodes until we find a valid parent to stick the child into\r
parents = [node];\r
}\r
};\r
\r
+ function cloneAndExcludeBlocks(input) {\r
+ var name, output = {};\r
+\r
+ for (name in input) {\r
+ if (name !== 'li' && name != 'p') {\r
+ output[name] = input[name];\r
+ }\r
+ }\r
+\r
+ return output;\r
+ };\r
+\r
parser = new tinymce.html.SaxParser({\r
validate : validate,\r
- fix_self_closing : !validate, // Let the DOM parser handle <li> in <li> or <p> in <p> for better results\r
+\r
+ // Exclude P and LI from DOM parsing since it's treated better by the DOM parser\r
+ self_closing_elements: cloneAndExcludeBlocks(schema.getSelfClosingElements()),\r
\r
cdata: function(text) {\r
node.append(createNode('#cdata', 4)).value = text;\r
}\r
\r
// Trim start white space\r
- textNode = node.prev;\r
+ // Removed due to: #5424\r
+ /*textNode = node.prev;\r
if (textNode && textNode.type === 3) {\r
text = textNode.value.replace(startWhiteSpaceRegExp, '');\r
\r
textNode.value = text;\r
else\r
textNode.remove();\r
- }\r
+ }*/\r
}\r
\r
// Check if we exited a whitespace preserved element\r
node.empty().append(new Node('#text', '3')).value = '\u00a0';\r
else {\r
// Leave nodes that have a name like <a name="name">\r
- if (!node.attributes.map.name) {\r
+ if (!node.attributes.map.name && !node.attributes.map.id) {\r
tempNode = node.parent;\r
node.empty().remove();\r
node = tempNode;\r
\r
// Force anchor names closed, unless the setting "allow_html_in_named_anchor" is explicitly included.\r
if (!settings.allow_html_in_named_anchor) {\r
- self.addAttributeFilter('name', function(nodes, name) {\r
+ self.addAttributeFilter('id,name', function(nodes, name) {\r
var i = nodes.length, sibling, prevSibling, parent, node;\r
\r
while (i--) {\r
node = nodes[i];\r
- if (node.name === 'a' && node.firstChild) {\r
+ if (node.name === 'a' && node.firstChild && !node.attr('href')) {\r
parent = node.parent;\r
\r
// Move children after current node\r
}\r
}\r
\r
+ // Page already loaded then fire it directly\r
+ if (doc.readyState == "complete") {\r
+ readyHandler();\r
+ return;\r
+ }\r
+\r
// Use W3C method\r
if (w3cEventModel) {\r
addEvent(win, 'DOMContentLoaded', readyHandler);\r
\r
// Old API supported multiple targets\r
if (target && target instanceof Array) {\r
- var i = target;\r
+ var i = target.length;\r
\r
while (i--) {\r
self.add(target[i], events, func, scope);\r
};\r
\r
self.prevent = function(e) {\r
+ if (!e.preventDefault) {\r
+ e = fix(e);\r
+ }\r
+\r
e.preventDefault();\r
\r
return false;\r
};\r
\r
self.stop = function(e) {\r
+ if (!e.stopPropagation) {\r
+ e = fix(e);\r
+ }\r
+\r
e.stopPropagation();\r
\r
return false;\r
blockElementsMap = s.schema ? s.schema.getBlockElements() : {};\r
\r
t.isBlock = function(node) {\r
+ // Fix for #5446\r
+ if (!node) {\r
+ return false;\r
+ }\r
+\r
// This function is called in module pattern style since it might be executed with the wrong this scope\r
var type = node.nodeType;\r
\r
return this.styles.serialize(o, name);\r
},\r
\r
+ addStyle: function(cssText) {\r
+ var doc = this.doc, head;\r
+\r
+ // Create style element if needed\r
+ styleElm = doc.getElementById('mceDefaultStyles');\r
+ if (!styleElm) {\r
+ styleElm = doc.createElement('style'),\r
+ styleElm.id = 'mceDefaultStyles';\r
+ styleElm.type = 'text/css';\r
+\r
+ head = doc.getElementsByTagName('head')[0];\r
+ if (head.firstChild) {\r
+ head.insertBefore(styleElm, head.firstChild);\r
+ } else {\r
+ head.appendChild(styleElm);\r
+ }\r
+ }\r
+\r
+ // Append style data to old or new style element\r
+ if (styleElm.styleSheet) {\r
+ styleElm.styleSheet.cssText += cssText;\r
+ } else {\r
+ styleElm.appendChild(doc.createTextNode(cssText));\r
+ }\r
+ },\r
+\r
loadCSS : function(u) {\r
var t = this, d = t.doc, head;\r
\r
// This seems to fix this problem\r
\r
// Create new div with HTML contents and a BR infront to keep comments\r
- element = self.create('div');\r
- element.innerHTML = '<br />' + html;\r
+ var newElement = self.create('div');\r
+ newElement.innerHTML = '<br />' + html;\r
\r
// Add all children from div to target\r
- each (element.childNodes, function(node, i) {\r
+ each (tinymce.grep(newElement.childNodes), function(node, i) {\r
// Skip br element\r
- if (i)\r
+ if (i && element.canHaveHTML)\r
element.appendChild(node);\r
});\r
}\r
cloneContents : cloneContents,\r
insertNode : insertNode,\r
surroundContents : surroundContents,\r
- cloneRange : cloneRange\r
+ cloneRange : cloneRange,\r
+ toStringIE : toStringIE\r
});\r
\r
function createDocumentFragment() {\r
\r
n.parentNode.removeChild(n);\r
};\r
+\r
+ function toStringIE() {\r
+ return dom.create('body', null, cloneContents()).outerText;\r
+ }\r
+ \r
+ return t;\r
};\r
\r
ns.Range = Range;\r
+\r
+ // Older IE versions doesn't let you override toString by it's constructor so we have to stick it in the prototype\r
+ Range.prototype.toString = function() {\r
+ return this.toStringIE();\r
+ };\r
})(tinymce.dom);\r
\r
(function() {\r
};\r
\r
this.addRange = function(rng) {\r
- var ieRng, ctrlRng, startContainer, startOffset, endContainer, endOffset, doc = selection.dom.doc, body = doc.body;\r
+ var ieRng, ctrlRng, startContainer, startOffset, endContainer, endOffset, sibling,\r
+ doc = selection.dom.doc, body = doc.body, nativeRng, ctrlElm;\r
\r
function setEndPoint(start) {\r
var container, offset, marker, tmpRng, nodes;\r
// Trick to place the caret inside an empty block element like <p></p>\r
if (startOffset == endOffset && !startContainer.hasChildNodes()) {\r
if (startContainer.canHaveHTML) {\r
+ // Check if previous sibling is an empty block if it is then we need to render it\r
+ // IE would otherwise move the caret into the sibling instead of the empty startContainer see: #5236\r
+ // Example this: <p></p><p>|</p> would become this: <p>|</p><p></p>\r
+ sibling = startContainer.previousSibling;\r
+ if (sibling && !sibling.hasChildNodes() && dom.isBlock(sibling)) {\r
+ sibling.innerHTML = '\uFEFF';\r
+ } else {\r
+ sibling = null;\r
+ }\r
+\r
startContainer.innerHTML = '<span>\uFEFF</span><span>\uFEFF</span>';\r
ieRng.moveToElementText(startContainer.lastChild);\r
ieRng.select();\r
dom.doc.selection.clear();\r
startContainer.innerHTML = '';\r
+\r
+ if (sibling) {\r
+ sibling.innerHTML = '';\r
+ }\r
return;\r
} else {\r
startOffset = dom.nodeIndex(startContainer);\r
\r
if (startOffset == endOffset - 1) {\r
try {\r
+ ctrlElm = startContainer.childNodes[startOffset];\r
ctrlRng = body.createControlRange();\r
- ctrlRng.addElement(startContainer.childNodes[startOffset]);\r
+ ctrlRng.addElement(ctrlElm);\r
ctrlRng.select();\r
- return;\r
+\r
+ // Check if the range produced is on the correct element and is a control range\r
+ // On IE 8 it will select the parent contentEditable container if you select an inner element see: #5398\r
+ nativeRng = selection.getRng();\r
+ if (nativeRng.item && ctrlElm === nativeRng.item(0)) {\r
+ return;\r
+ }\r
} catch (ex) {\r
// Ignore\r
}\r
var is = tinymce.is, isIE = tinymce.isIE, each = tinymce.each, TreeWalker = tinymce.dom.TreeWalker;\r
\r
tinymce.create('tinymce.dom.Selection', {\r
- Selection : function(dom, win, serializer) {\r
+ Selection : function(dom, win, serializer, editor) {\r
var t = this;\r
\r
t.dom = dom;\r
t.win = win;\r
t.serializer = serializer;\r
+ t.editor = editor;\r
\r
// Add events\r
each([\r
},\r
\r
getStart : function() {\r
- var rng = this.getRng(), startElement, parentElement, checkRng, node;\r
+ var self = this, rng = self.getRng(), startElement, parentElement, checkRng, node;\r
\r
if (rng.duplicate || rng.item) {\r
// Control selection, return first item\r
checkRng = rng.duplicate();\r
checkRng.collapse(1);\r
startElement = checkRng.parentElement();\r
+ if (startElement.ownerDocument !== self.dom.doc) {\r
+ startElement = self.dom.getRoot();\r
+ }\r
\r
// Check if range parent is inside the start element, then return the inner parent element\r
// This will fix issues when a single element is selected, IE would otherwise return the wrong start element\r
},\r
\r
getEnd : function() {\r
- var t = this, r = t.getRng(), e, eo;\r
+ var self = this, rng = self.getRng(), endElement, endOffset;\r
\r
- if (r.duplicate || r.item) {\r
- if (r.item)\r
- return r.item(0);\r
+ if (rng.duplicate || rng.item) {\r
+ if (rng.item)\r
+ return rng.item(0);\r
\r
- r = r.duplicate();\r
- r.collapse(0);\r
- e = r.parentElement();\r
+ rng = rng.duplicate();\r
+ rng.collapse(0);\r
+ endElement = rng.parentElement();\r
+ if (endElement.ownerDocument !== self.dom.doc) {\r
+ endElement = self.dom.getRoot();\r
+ }\r
\r
- if (e && e.nodeName == 'BODY')\r
- return e.lastChild || e;\r
+ if (endElement && endElement.nodeName == 'BODY')\r
+ return endElement.lastChild || endElement;\r
\r
- return e;\r
+ return endElement;\r
} else {\r
- e = r.endContainer;\r
- eo = r.endOffset;\r
+ endElement = rng.endContainer;\r
+ endOffset = rng.endOffset;\r
\r
- if (e.nodeType == 1 && e.hasChildNodes())\r
- e = e.childNodes[eo > 0 ? eo - 1 : eo];\r
+ if (endElement.nodeType == 1 && endElement.hasChildNodes())\r
+ endElement = endElement.childNodes[endOffset > 0 ? endOffset - 1 : endOffset];\r
\r
- if (e && e.nodeType == 3)\r
- return e.parentNode;\r
+ if (endElement && endElement.nodeType == 3)\r
+ return endElement.parentNode;\r
\r
- return e;\r
+ return endElement;\r
}\r
},\r
\r
}\r
},\r
\r
- destroy : function(s) {\r
- var t = this;\r
+ selectorChanged: function(selector, callback) {\r
+ var self = this, currentSelectors;\r
+\r
+ if (!self.selectorChangedData) {\r
+ self.selectorChangedData = {};\r
+ currentSelectors = {};\r
+\r
+ self.editor.onNodeChange.addToTop(function(ed, cm, node) {\r
+ var dom = self.dom, parents = dom.getParents(node, null, dom.getRoot()), matchedSelectors = {};\r
+\r
+ // Check for new matching selectors\r
+ each(self.selectorChangedData, function(callbacks, selector) {\r
+ each(parents, function(node) {\r
+ if (dom.is(node, selector)) {\r
+ if (!currentSelectors[selector]) {\r
+ // Execute callbacks\r
+ each(callbacks, function(callback) {\r
+ callback(true, {node: node, selector: selector, parents: parents});\r
+ });\r
+\r
+ currentSelectors[selector] = callbacks;\r
+ }\r
+\r
+ matchedSelectors[selector] = callbacks;\r
+ return false;\r
+ }\r
+ });\r
+ });\r
+\r
+ // Check if current selectors still match\r
+ each(currentSelectors, function(callbacks, selector) {\r
+ if (!matchedSelectors[selector]) {\r
+ delete currentSelectors[selector];\r
+\r
+ each(callbacks, function(callback) {\r
+ callback(false, {node: node, selector: selector, parents: parents});\r
+ });\r
+ }\r
+ });\r
+ });\r
+ }\r
+\r
+ // Add selector listeners\r
+ if (!self.selectorChangedData[selector]) {\r
+ self.selectorChangedData[selector] = [];\r
+ }\r
+\r
+ self.selectorChangedData[selector].push(callback);\r
\r
- t.win = null;\r
+ return self;\r
+ },\r
+\r
+ scrollIntoView: function(elm) {\r
+ var y, viewPort, self = this, dom = self.dom;\r
+\r
+ viewPort = dom.getViewPort(self.editor.getWin());\r
+ y = dom.getPos(elm).y;\r
+ if (y < viewPort.y || y + 25 > viewPort.y + viewPort.h) {\r
+ self.editor.getWin().scrollTo(0, y < viewPort.y ? y : y - viewPort.h + 25);\r
+ }\r
+ },\r
+\r
+ destroy : function(manual) {\r
+ var self = this;\r
+\r
+ self.win = null;\r
\r
// Manual destroy then remove unload handler\r
- if (!s)\r
- tinymce.removeUnload(t.destroy);\r
+ if (!manual)\r
+ tinymce.removeUnload(self.destroy);\r
},\r
\r
// IE has an issue where you can't select/move the caret by clicking outside the body if the document is in standards mode\r
}\r
});\r
\r
+ htmlParser.addNodeFilter('noscript', function(nodes) {\r
+ var i = nodes.length, node;\r
+\r
+ while (i--) {\r
+ node = nodes[i].firstChild;\r
+\r
+ if (node) {\r
+ node.value = tinymce.html.Entities.decode(node.value);\r
+ }\r
+ }\r
+ });\r
+\r
// Force script into CDATA sections and remove the mce- prefix also add comments around styles\r
htmlParser.addNodeFilter('script,style', function(nodes, name) {\r
var i = nodes.length, node, value;\r
\r
// Replace all BOM characters for now until we can find a better solution\r
if (!args.cleanup)\r
- args.content = args.content.replace(/\uFEFF|\u200B/g, '');\r
+ args.content = args.content.replace(/\uFEFF/g, '');\r
\r
// Post process\r
if (!args.no_events)\r
}\r
\r
// Create new script element\r
- elm = dom.create('script', {\r
- id : id,\r
- type : 'text/javascript',\r
- src : tinymce._addVer(url)\r
- });\r
+ elm = document.createElement('script');\r
+ elm.id = id;\r
+ elm.type = 'text/javascript';\r
+ elm.src = tinymce._addVer(url);\r
\r
// Add onload listener for non IE browsers since IE9\r
// fires onload event before the script is parsed and executed\r
\r
t.destroy = function() {\r
each(items, function(item) {\r
- dom.unbind(dom.get(item.id), 'focus', itemFocussed);\r
- dom.unbind(dom.get(item.id), 'blur', itemBlurred);\r
+ var elm = dom.get(item.id);\r
+\r
+ dom.unbind(elm, 'focus', itemFocussed);\r
+ dom.unbind(elm, 'blur', itemBlurred);\r
});\r
\r
- dom.unbind(dom.get(root), 'focus', rootFocussed);\r
- dom.unbind(dom.get(root), 'keydown', rootKeydown);\r
+ var rootElm = dom.get(root);\r
+ dom.unbind(rootElm, 'focus', rootFocussed);\r
+ dom.unbind(rootElm, 'keydown', rootKeydown);\r
\r
items = dom = root = t.focus = itemFocussed = itemBlurred = rootKeydown = rootFocussed = null;\r
t.destroy = function() {};\r
\r
// Set up state and listeners for each item.\r
each(items, function(item, idx) {\r
- var tabindex;\r
+ var tabindex, elm;\r
\r
if (!item.id) {\r
item.id = dom.uniqueId('_mce_item_');\r
}\r
\r
+ elm = dom.get(item.id);\r
+\r
if (excludeFromTabOrder) {\r
- dom.bind(item.id, 'blur', itemBlurred);\r
+ dom.bind(elm, 'blur', itemBlurred);\r
tabindex = '-1';\r
} else {\r
tabindex = (idx === 0 ? '0' : '-1');\r
}\r
\r
- dom.setAttrib(item.id, 'tabindex', tabindex);\r
- dom.bind(dom.get(item.id), 'focus', itemFocussed);\r
+ elm.setAttribute('tabindex', tabindex);\r
+ dom.bind(elm, 'focus', itemFocussed);\r
});\r
\r
// Setup initial state for root element.\r
}\r
\r
dom.setAttrib(root, 'tabindex', '-1');\r
- \r
+\r
// Setup listeners for root element.\r
- dom.bind(dom.get(root), 'focus', rootFocussed);\r
- dom.bind(dom.get(root), 'keydown', rootKeydown);\r
+ var rootElm = dom.get(root);\r
+ dom.bind(rootElm, 'focus', rootFocussed);\r
+ dom.bind(rootElm, 'keydown', rootKeydown);\r
}\r
});\r
})(tinymce);\r
l = DOM.encode(s.label || '');\r
h = '<a role="button" id="' + this.id + '" href="javascript:;" class="' + cp + ' ' + cp + 'Enabled ' + s['class'] + (l ? ' ' + cp + 'Labeled' : '') +'" onmousedown="return false;" onclick="return false;" aria-labelledby="' + this.id + '_voice" title="' + DOM.encode(s.title) + '">';\r
if (s.image && !(this.editor &&this.editor.forcedHighContrastMode) )\r
- h += '<img class="mceIcon" src="' + s.image + '" alt="' + DOM.encode(s.title) + '" />' + l;\r
+ h += '<span class="mceIcon ' + s['class'] + '"><img class="mceIcon" src="' + s.image + '" alt="' + DOM.encode(s.title) + '" /></span>' + (l ? '<span class="' + cp + 'Label">' + l + '</span>' : '');\r
else\r
h += '<span class="mceIcon ' + s['class'] + '"></span>' + (l ? '<span class="' + cp + 'Label">' + l + '</span>' : '');\r
\r
DOM.select('a', t.id + '_menu')[0].focus(); // Select first link\r
}\r
\r
+ t.keyboardNav = new tinymce.ui.KeyboardNavigation({\r
+ root: t.id + '_menu',\r
+ items: DOM.select('a', t.id + '_menu'),\r
+ onCancel: function() {\r
+ t.hideMenu();\r
+ t.focus();\r
+ }\r
+ });\r
+\r
+ t.keyboardNav.focus();\r
t.isMenuVisible = 1;\r
},\r
\r
\r
t.isMenuVisible = 0;\r
t.onHideMenu.dispatch();\r
+ t.keyboardNav.destroy();\r
}\r
},\r
\r
}\r
\r
DOM.addClass(m, 'mceColorSplitMenu');\r
- \r
- new tinymce.ui.KeyboardNavigation({\r
- root: t.id + '_menu',\r
- items: DOM.select('a', t.id + '_menu'),\r
- onCancel: function() {\r
- t.hideMenu();\r
- t.focus();\r
- }\r
- });\r
\r
// Prevent IE from scrolling and hindering click to occur #4019\r
Event.add(t.id + '_menu', 'mousedown', function(e) {return Event.cancel(e);});\r
},\r
\r
destroy : function() {\r
- this.parent();\r
+ var self = this;\r
+\r
+ self.parent();\r
+\r
+ Event.clear(self.id + '_menu');\r
+ Event.clear(self.id + '_more');\r
+ DOM.remove(self.id + '_menu');\r
\r
- Event.clear(this.id + '_menu');\r
- Event.clear(this.id + '_more');\r
- DOM.remove(this.id + '_menu');\r
+ if (self.keyboardNav) {\r
+ self.keyboardNav.destroy();\r
+ }\r
}\r
});\r
})(tinymce);\r
return c.constructor === RegExp ? c.test(n.className) : DOM.hasClass(n, c);\r
};\r
\r
- s = extend({\r
- theme : "simple",\r
- language : "en"\r
- }, s);\r
-\r
t.settings = s;\r
\r
// Legacy call\r
if (id === undef)\r
return this.editors;\r
\r
+ if (!this.editors.hasOwnProperty(id))\r
+ return undef;\r
+\r
return this.editors[id];\r
},\r
\r
self.settings = settings = extend({\r
id : id,\r
language : 'en',\r
- theme : 'simple',\r
+ theme : 'advanced',\r
skin : 'default',\r
delta_width : 0,\r
delta_height : 0,\r
inline_styles : TRUE,\r
convert_fonts_to_spans : TRUE,\r
indent : 'simple',\r
- indent_before : 'p,h1,h2,h3,h4,h5,h6,blockquote,div,title,style,pre,script,td,ul,li,area,table,thead,tfoot,tbody,tr,section,article,hgroup,aside,figure',\r
- indent_after : 'p,h1,h2,h3,h4,h5,h6,blockquote,div,title,style,pre,script,td,ul,li,area,table,thead,tfoot,tbody,tr,section,article,hgroup,aside,figure',\r
+ indent_before : 'p,h1,h2,h3,h4,h5,h6,blockquote,div,title,style,pre,script,td,ul,li,area,table,thead,tfoot,tbody,tr,section,article,hgroup,aside,figure,option,optgroup,datalist',\r
+ indent_after : 'p,h1,h2,h3,h4,h5,h6,blockquote,div,title,style,pre,script,td,ul,li,area,table,thead,tfoot,tbody,tr,section,article,hgroup,aside,figure,option,optgroup,datalist',\r
validate : TRUE,\r
entity_encoding : 'named',\r
url_converter : self.convertURL,\r
\r
self.contentCSS = [];\r
\r
+ self.contentStyles = [];\r
+\r
// Creates all events like onClick, onSetContent etc see Editor.Events.js for the actual logic\r
self.setupEvents();\r
\r
if (!/TEXTAREA|INPUT/i.test(t.getElement().nodeName) && s.hidden_input && DOM.getParent(id, 'form'))\r
DOM.insertAfter(DOM.create('input', {type : 'hidden', name : id}), id);\r
\r
+ // Hide target element early to prevent content flashing\r
+ if (!s.content_editable) {\r
+ t.orgVisibility = t.getElement().style.visibility;\r
+ t.getElement().style.visibility = 'hidden';\r
+ }\r
+\r
if (tinymce.WindowManager)\r
t.windowManager = new tinymce.WindowManager(t);\r
\r
if (s.language && s.language_load !== false)\r
sl.add(tinymce.baseURL + '/langs/' + s.language + '.js');\r
\r
- if (s.theme && s.theme.charAt(0) != '-' && !ThemeManager.urls[s.theme])\r
+ if (s.theme && typeof s.theme != "function" && s.theme.charAt(0) != '-' && !ThemeManager.urls[s.theme])\r
ThemeManager.load(s.theme, 'themes/' + s.theme + '/editor_template' + tinymce.suffix + '.js');\r
\r
each(explode(s.plugins), function(p) {\r
},\r
\r
init : function() {\r
- var n, t = this, s = t.settings, w, h, e = t.getElement(), o, ti, u, bi, bc, re, i, initializedPlugins = [];\r
+ var n, t = this, s = t.settings, w, h, mh, e = t.getElement(), o, ti, u, bi, bc, re, i, initializedPlugins = [];\r
\r
tinymce.add(t);\r
\r
s.aria_label = s.aria_label || DOM.getAttrib(e, 'aria-label', t.getLang('aria.rich_text_area'));\r
\r
if (s.theme) {\r
- s.theme = s.theme.replace(/-/, '');\r
- o = ThemeManager.get(s.theme);\r
- t.theme = new o();\r
+ if (typeof s.theme != "function") {\r
+ s.theme = s.theme.replace(/-/, '');\r
+ o = ThemeManager.get(s.theme);\r
+ t.theme = new o();\r
\r
- if (t.theme.init)\r
- t.theme.init(t, ThemeManager.urls[s.theme] || tinymce.documentBaseURL.replace(/\/$/, ''));\r
+ if (t.theme.init)\r
+ t.theme.init(t, ThemeManager.urls[s.theme] || tinymce.documentBaseURL.replace(/\/$/, ''));\r
+ } else {\r
+ t.theme = s.theme;\r
+ }\r
}\r
+\r
function initPlugin(p) {\r
var c = PluginManager.get(p), u = PluginManager.urls[p] || tinymce.documentBaseURL.replace(/\/$/, ''), po;\r
if (c && tinymce.inArray(initializedPlugins,p) === -1) {\r
\r
t.controlManager = new tinymce.ControlManager(t);\r
\r
- t.onExecCommand.add(function(ed, c) {\r
- // Don't refresh the select lists until caret move\r
- if (!/^(FontName|FontSize)$/.test(c))\r
- t.nodeChanged();\r
- });\r
-\r
// Enables users to override the control factory\r
t.onBeforeRenderUI.dispatch(t, t.controlManager);\r
\r
// Measure box\r
if (s.render_ui && t.theme) {\r
- w = s.width || e.style.width || e.offsetWidth;\r
- h = s.height || e.style.height || e.offsetHeight;\r
t.orgDisplay = e.style.display;\r
- re = /^[0-9\.]+(|px)$/i;\r
\r
- if (re.test('' + w))\r
- w = Math.max(parseInt(w, 10) + (o.deltaWidth || 0), 100);\r
+ if (typeof s.theme != "function") {\r
+ w = s.width || e.style.width || e.offsetWidth;\r
+ h = s.height || e.style.height || e.offsetHeight;\r
+ mh = s.min_height || 100;\r
+ re = /^[0-9\.]+(|px)$/i;\r
+\r
+ if (re.test('' + w))\r
+ w = Math.max(parseInt(w, 10) + (o.deltaWidth || 0), 100);\r
+\r
+ if (re.test('' + h))\r
+ h = Math.max(parseInt(h, 10) + (o.deltaHeight || 0), mh);\r
+\r
+ // Render UI\r
+ o = t.theme.renderUI({\r
+ targetNode : e,\r
+ width : w,\r
+ height : h,\r
+ deltaWidth : s.delta_width,\r
+ deltaHeight : s.delta_height\r
+ });\r
\r
- if (re.test('' + h))\r
- h = Math.max(parseInt(h, 10) + (o.deltaHeight || 0), 100);\r
+ // Resize editor\r
+ DOM.setStyles(o.sizeContainer || o.editorContainer, {\r
+ width : w,\r
+ height : h\r
+ });\r
\r
- // Render UI\r
- o = t.theme.renderUI({\r
- targetNode : e,\r
- width : w,\r
- height : h,\r
- deltaWidth : s.delta_width,\r
- deltaHeight : s.delta_height\r
- });\r
+ h = (o.iframeHeight || h) + (typeof(h) == 'number' ? (o.deltaHeight || 0) : '');\r
+ if (h < mh)\r
+ h = mh;\r
+ } else {\r
+ o = s.theme(t, e);\r
+\r
+ // Convert element type to id:s\r
+ if (o.editorContainer.nodeType) {\r
+ o.editorContainer = o.editorContainer.id = o.editorContainer.id || t.id + "_parent";\r
+ }\r
+\r
+ // Convert element type to id:s\r
+ if (o.iframeContainer.nodeType) {\r
+ o.iframeContainer = o.iframeContainer.id = o.iframeContainer.id || t.id + "_iframecontainer";\r
+ }\r
+\r
+ // Use specified iframe height or the targets offsetHeight\r
+ h = o.iframeHeight || e.offsetHeight;\r
+\r
+ // Store away the selection when it's changed to it can be restored later with a editor.focus() call\r
+ if (isIE) {\r
+ t.onInit.add(function(ed) {\r
+ ed.dom.bind(ed.getBody(), 'beforedeactivate keydown', function() {\r
+ ed.lastIERng = ed.selection.getRng();\r
+ });\r
+ });\r
+ }\r
+ }\r
\r
t.editorContainer = o.editorContainer;\r
}\r
});\r
}\r
\r
+ // Load specified content CSS last\r
+ if (s.content_style) {\r
+ t.contentStyles.push(s.content_style);\r
+ }\r
+\r
// Content editable mode ends here\r
if (s.content_editable) {\r
e = n = o = null; // Fix IE leak\r
if (document.domain && location.hostname != document.domain)\r
tinymce.relaxedDomain = document.domain;\r
\r
- // Resize editor\r
- DOM.setStyles(o.sizeContainer || o.editorContainer, {\r
- width : w,\r
- height : h\r
- });\r
-\r
- h = (o.iframeHeight || h) + (typeof(h) == 'number' ? (o.deltaHeight || 0) : '');\r
- if (h < 100)\r
- h = 100;\r
-\r
t.iframeHTML = s.doctype + '<html><head xmlns="http://www.w3.org/1999/xhtml">';\r
\r
// We only need to override paths if we have to\r
t.iframeHTML += '<base href="' + t.documentBaseURI.getURI() + '" />';\r
\r
// IE8 doesn't support carets behind images setting ie7_compat would force IE8+ to run in IE7 compat mode.\r
- if (s.ie7_compat)\r
- t.iframeHTML += '<meta http-equiv="X-UA-Compatible" content="IE=7" />';\r
- else\r
- t.iframeHTML += '<meta http-equiv="X-UA-Compatible" content="IE=edge" />';\r
+ if (tinymce.isIE8) {\r
+ if (s.ie7_compat)\r
+ t.iframeHTML += '<meta http-equiv="X-UA-Compatible" content="IE=7" />';\r
+ else\r
+ t.iframeHTML += '<meta http-equiv="X-UA-Compatible" content="IE=edge" />';\r
+ }\r
\r
t.iframeHTML += '<meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />';\r
\r
});\r
\r
t.contentAreaContainer = o.iframeContainer;\r
- DOM.get(o.editorContainer).style.display = t.orgDisplay;\r
+\r
+ if (o.editorContainer) {\r
+ DOM.get(o.editorContainer).style.display = t.orgDisplay;\r
+ }\r
+\r
+ // Restore visibility on target element\r
+ e.style.visibility = t.orgVisibility;\r
+\r
DOM.get(t.id).style.display = 'none';\r
DOM.setAttrib(t.id, 'aria-hidden', true);\r
\r
},\r
\r
initContentBody : function() {\r
- var self = this, settings = self.settings, targetElm = DOM.get(self.id), doc = self.getDoc(), html, body;\r
+ var self = this, settings = self.settings, targetElm = DOM.get(self.id), doc = self.getDoc(), html, body, contentCssText;\r
\r
// Setup iframe body\r
if ((!isIE || !tinymce.relaxedDomain) && !settings.content_editable) {\r
\r
self.serializer = new tinymce.dom.Serializer(settings, self.dom, self.schema);\r
\r
- self.selection = new tinymce.dom.Selection(self.dom, self.getWin(), self.serializer);\r
+ self.selection = new tinymce.dom.Selection(self.dom, self.getWin(), self.serializer, self);\r
\r
self.formatter = new tinymce.Formatter(self);\r
\r
self.enterKey = new tinymce.EnterKey(self);\r
self.editorCommands = new tinymce.EditorCommands(self);\r
\r
+ self.onExecCommand.add(function(editor, command) {\r
+ // Don't refresh the select lists until caret move\r
+ if (!/^(FontName|FontSize)$/.test(command))\r
+ self.nodeChanged();\r
+ });\r
+\r
// Pass through\r
self.serializer.onPreProcess.add(function(se, o) {\r
return self.onPreProcess.dispatch(self, o, se);\r
\r
self.onPreInit.dispatch(self);\r
\r
- if (!settings.gecko_spellcheck)\r
+ if (!settings.browser_spellcheck && !settings.gecko_spellcheck)\r
doc.body.spellcheck = false;\r
\r
if (!settings.readonly) {\r
self.focus(true);\r
self.nodeChanged({initial : true});\r
\r
+ // Add editor specific CSS styles\r
+ if (self.contentStyles.length > 0) {\r
+ contentCssText = '';\r
+\r
+ each(self.contentStyles, function(style) {\r
+ contentCssText += style + "\r\n";\r
+ });\r
+\r
+ self.dom.addStyle(contentCssText);\r
+ }\r
+\r
// Load specified content CSS last\r
each(self.contentCSS, function(url) {\r
self.dom.loadCSS(url);\r
var oed, self = this, selection = self.selection, contentEditable = self.settings.content_editable, ieRng, controlElm, doc = self.getDoc(), body;\r
\r
if (!skip_focus) {\r
+ if (self.lastIERng) {\r
+ selection.setRng(self.lastIERng);\r
+ }\r
+\r
// Get selected control element\r
ieRng = selection.getRng();\r
if (ieRng.item) {\r
\r
// We must save before we hide so Safari doesn't crash\r
self.save();\r
+\r
+ // defer the call to hide to prevent an IE9 crash #4921\r
DOM.hide(self.getContainer());\r
DOM.setStyle(self.id, 'display', self.orgDisplay);\r
},\r
if (!args.no_events)\r
self.onSetContent.dispatch(self, args);\r
\r
- self.selection.normalize();\r
+ // Don't normalize selection if the focused element isn't the body in content editable mode since it will steal focus otherwise\r
+ if (!self.settings.content_editable || document.activeElement === self.getBody()) {\r
+ self.selection.normalize();\r
+ }\r
\r
return args.content;\r
},\r
\r
getContent : function(args) {\r
- var self = this, content;\r
+ var self = this, content, body = self.getBody();\r
\r
// Setup args object\r
args = args || {};\r
\r
// Get raw contents or by default the cleaned contents\r
if (args.format == 'raw')\r
- content = self.getBody().innerHTML;\r
+ content = body.innerHTML;\r
+ else if (args.format == 'text')\r
+ content = body.innerText || body.textContent;\r
else\r
- content = self.serializer.serialize(self.getBody(), args);\r
+ content = self.serializer.serialize(body, args);\r
\r
- args.content = tinymce.trim(content);\r
+ // Trim whitespace in beginning/end of HTML\r
+ if (args.format != 'text') {\r
+ args.content = tinymce.trim(content);\r
+ } else {\r
+ args.content = content;\r
+ }\r
\r
// Do post processing\r
if (!args.no_events)\r
return;\r
\r
case 'A':\r
- value = dom.getAttrib(elm, 'name');\r
- cls = 'mceItemAnchor';\r
+ if (!dom.getAttrib(elm, 'href', false)) {\r
+ value = dom.getAttrib(elm, 'name') || elm.id;\r
+ cls = 'mceItemAnchor';\r
\r
- if (value) {\r
- if (self.hasVisual)\r
- dom.addClass(elm, cls);\r
- else\r
- dom.removeClass(elm, cls);\r
+ if (value) {\r
+ if (self.hasVisual)\r
+ dom.addClass(elm, cls);\r
+ else\r
+ dom.removeClass(elm, cls);\r
+ }\r
}\r
\r
return;\r
},\r
\r
remove : function() {\r
- var self = this, elm = self.getContainer();\r
+ var self = this, elm = self.getContainer(), doc = self.getDoc();\r
\r
if (!self.removed) {\r
self.removed = 1; // Cancels post remove event execution\r
- self.hide();\r
+\r
+ // Fixed bug where IE has a blinking cursor left from the editor\r
+ if (isIE && doc)\r
+ doc.execCommand('SelectAll');\r
+\r
+ // We must save before we hide so Safari doesn't crash\r
+ self.save();\r
+\r
+ DOM.setStyle(self.id, 'display', self.orgDisplay);\r
\r
// Don't clear the window or document if content editable\r
// is enabled since other instances might still be present\r
if (!self.settings.content_editable) {\r
- Event.clear(self.getWin());\r
- Event.clear(self.getDoc());\r
+ Event.unbind(self.getWin());\r
+ Event.unbind(self.getDoc());\r
}\r
\r
- Event.clear(self.getBody());\r
- Event.clear(self.formElement);\r
- Event.unbind(elm);\r
+ Event.unbind(self.getBody());\r
+ Event.clear(elm);\r
\r
self.execCallback('remove_instance_callback', self);\r
self.onRemove.dispatch(self);\r
// Handle legacy handle_event_callback option\r
if (settings.handle_event_callback) {\r
self.onEvent.add(function(ed, e, o) {\r
- if (self.execCallback('handle_event_callback', e, ed, o) === false)\r
- Event.cancel(e);\r
+ if (self.execCallback('handle_event_callback', e, ed, o) === false) {\r
+ e.preventDefault();\r
+ e.stopPropagation();\r
+ }\r
});\r
}\r
\r
self.focus(true);\r
};\r
\r
- function nodeChanged() {\r
- // Normalize selection for example <b>a</b><i>|a</i> becomes <b>a|</b><i>a</i>\r
- self.selection.normalize();\r
+ function nodeChanged(ed, e) {\r
+ // Normalize selection for example <b>a</b><i>|a</i> becomes <b>a|</b><i>a</i> except for Ctrl+A since it selects everything\r
+ if (e.keyCode != 65 || !tinymce.VK.metaKeyPressed(e)) {\r
+ self.selection.normalize();\r
+ }\r
+\r
self.nodeChanged();\r
}\r
\r
var keyCode = e.keyCode;\r
\r
if ((keyCode >= 33 && keyCode <= 36) || (keyCode >= 37 && keyCode <= 40) || keyCode == 13 || keyCode == 45 || keyCode == 46 || keyCode == 8 || (tinymce.isMac && (keyCode == 91 || keyCode == 93)) || e.ctrlKey)\r
- nodeChanged();\r
+ nodeChanged(ed, e);\r
});\r
\r
// Add reset handler\r
\r
// Insert bookmark node and get the parent\r
selection.setContent(bookmarkHtml);\r
- parentNode = editor.selection.getNode();\r
+ parentNode = selection.getNode();\r
rootNode = editor.getBody();\r
\r
// Opera will return the document node when selection is in root\r
editor.setContent(editor.getContent().replace(/tiny_mce_marker/g, function() { return value }));\r
},\r
\r
+ mceToggleFormat : function(command, ui, value) {\r
+ toggleFormat(value);\r
+ },\r
+\r
mceSetContent : function(command, ui, value) {\r
editor.setContent(value);\r
},\r
selectAll : function() {\r
var root = dom.getRoot(), rng = dom.createRng();\r
\r
- rng.setStart(root, 0);\r
- rng.setEnd(root, root.childNodes.length);\r
+ // Old IE does a better job with selectall than new versions\r
+ if (selection.getRng().setStart) {\r
+ rng.setStart(root, 0);\r
+ rng.setEnd(root, root.childNodes.length);\r
\r
- editor.selection.setRng(rng);\r
+ selection.setRng(rng);\r
+ } else {\r
+ execNativeCommand('SelectAll');\r
+ }\r
}\r
});\r
\r
},\r
\r
'InsertUnorderedList,InsertOrderedList' : function(command) {\r
- return dom.getParent(selection.getNode(), command == 'insertunorderedlist' ? 'UL' : 'OL');\r
+ var list = dom.getParent(selection.getNode(), 'ul,ol');\r
+ return list && \r
+ (command === 'insertunorderedlist' && list.tagName === 'UL'\r
+ || command === 'insertorderedlist' && list.tagName === 'OL');\r
}\r
}, 'state');\r
\r
};\r
\r
// Create event instances\r
- onAdd = new Dispatcher(self);\r
- onUndo = new Dispatcher(self);\r
- onRedo = new Dispatcher(self);\r
+ onBeforeAdd = new Dispatcher(self);\r
+ onAdd = new Dispatcher(self);\r
+ onUndo = new Dispatcher(self);\r
+ onRedo = new Dispatcher(self);\r
\r
// Pass though onAdd event from UndoManager to Editor as onChange\r
onAdd.add(function(undoman, level) {\r
// Add undo level on save contents, drag end and blur/focusout\r
editor.onSaveContent.add(addNonTypingUndoLevel);\r
editor.dom.bind(editor.dom.getRoot(), 'dragend', addNonTypingUndoLevel);\r
- editor.dom.bind(editor.getDoc(), tinymce.isGecko ? 'blur' : 'focusout', function(e) {\r
+ editor.dom.bind(editor.getBody(), 'focusout', function(e) {\r
if (!editor.removed && self.typing) {\r
addNonTypingUndoLevel();\r
}\r
data : data,\r
\r
typing : false,\r
+ \r
+ onBeforeAdd: onBeforeAdd,\r
\r
onAdd : onAdd,\r
\r
\r
level = level || {};\r
level.content = getContent();\r
+ \r
+ self.onBeforeAdd.dispatch(self, level);\r
\r
// Add undo level if needed\r
lastLevel = data[index];\r
var settings = editor.settings, dom = editor.dom, selection = editor.selection, blockElements = editor.schema.getBlockElements();\r
\r
function addRootBlocks() {\r
- var node = selection.getStart(), rootNode = editor.getBody(), rng, startContainer, startOffset, endContainer, endOffset, rootBlockNode, tempNode, offset = -0xFFFFFF, wrapped;\r
+ var node = selection.getStart(), rootNode = editor.getBody(), rng, startContainer, startOffset, endContainer, endOffset, rootBlockNode, tempNode, offset = -0xFFFFFF, wrapped, isInEditorDocument;\r
\r
if (!node || node.nodeType !== 1 || !settings.forced_root_block)\r
return;\r
rng.moveToElementText(node);\r
}\r
\r
+ isInEditorDocument = rng.parentElement().ownerDocument === editor.getDoc();\r
tmpRng = rng.duplicate();\r
tmpRng.collapse(true);\r
startOffset = tmpRng.move('character', offset) * -1;\r
node = rootNode.firstChild;\r
while (node) {\r
if (node.nodeType === 3 || (node.nodeType == 1 && !blockElements[node.nodeName])) {\r
+ // Remove empty text nodes\r
+ if (node.nodeType === 3 && node.nodeValue.length == 0) {\r
+ tempNode = node;\r
+ node = node.nextSibling;\r
+ dom.remove(tempNode);\r
+ continue;\r
+ }\r
+\r
if (!rootBlockNode) {\r
rootBlockNode = dom.create(settings.forced_root_block);\r
node.parentNode.insertBefore(rootBlockNode, node);\r
}\r
}\r
\r
- if (rng.setStart) {\r
- rng.setStart(startContainer, startOffset);\r
- rng.setEnd(endContainer, endOffset);\r
- selection.setRng(rng);\r
- } else {\r
- try {\r
- rng = editor.getDoc().body.createTextRange();\r
- rng.moveToElementText(rootNode);\r
- rng.collapse(true);\r
- rng.moveStart('character', startOffset);\r
+ if (wrapped) {\r
+ if (rng.setStart) {\r
+ rng.setStart(startContainer, startOffset);\r
+ rng.setEnd(endContainer, endOffset);\r
+ selection.setRng(rng);\r
+ } else {\r
+ // Only select if the previous selection was inside the document to prevent auto focus in quirks mode\r
+ if (isInEditorDocument) {\r
+ try {\r
+ rng = editor.getDoc().body.createTextRange();\r
+ rng.moveToElementText(rootNode);\r
+ rng.collapse(true);\r
+ rng.moveStart('character', startOffset);\r
\r
- if (endOffset > 0)\r
- rng.moveEnd('character', endOffset);\r
+ if (endOffset > 0)\r
+ rng.moveEnd('character', endOffset);\r
\r
- rng.select();\r
- } catch (ex) {\r
- // Ignore\r
+ rng.select();\r
+ } catch (ex) {\r
+ // Ignore\r
+ }\r
+ }\r
}\r
- }\r
\r
- // Only trigger nodeChange when we wrapped nodes to prevent a forever loop\r
- if (wrapped) {\r
editor.nodeChanged();\r
}\r
};\r
return c;\r
},\r
\r
- createControl : function(n) {\r
- var c, t = this, ed = t.editor;\r
+ createControl : function(name) {\r
+ var ctrl, i, l, self = this, editor = self.editor, factories, ctrlName;\r
\r
- each(ed.plugins, function(p) {\r
- if (p.createControl) {\r
- c = p.createControl(n, t);\r
+ // Build control factory cache\r
+ if (!self.controlFactories) {\r
+ self.controlFactories = [];\r
+ each(editor.plugins, function(plugin) {\r
+ if (plugin.createControl) {\r
+ self.controlFactories.push(plugin);\r
+ }\r
+ });\r
+ }\r
\r
- if (c)\r
- return false;\r
+ // Create controls by asking cached factories\r
+ factories = self.controlFactories;\r
+ for (i = 0, l = factories.length; i < l; i++) {\r
+ ctrl = factories[i].createControl(name, self);\r
+\r
+ if (ctrl) {\r
+ return self.add(ctrl);\r
}\r
- });\r
+ }\r
\r
- switch (n) {\r
- case "|":\r
- case "separator":\r
- return t.createSeparator();\r
+ // Create sepearator\r
+ if (name === "|" || name === "separator") {\r
+ return self.createSeparator();\r
}\r
\r
- if (!c && ed.buttons && (c = ed.buttons[n]))\r
- return t.createButton(n, c);\r
+ // Create control from button collection\r
+ if (editor.buttons && (ctrl = editor.buttons[name])) {\r
+ return self.createButton(name, ctrl);\r
+ }\r
\r
- return t.add(c);\r
+ return self.add(ctrl);\r
},\r
\r
createDropMenu : function(id, s, cc) {\r
TreeWalker = tinymce.dom.TreeWalker,\r
rangeUtils = new tinymce.dom.RangeUtils(dom),\r
isValid = ed.schema.isValidChild,\r
+ isArray = tinymce.isArray,\r
isBlock = dom.isBlock,\r
forcedRootBlock = ed.settings.forced_root_block,\r
nodeIndex = dom.nodeIndex,\r
- INVISIBLE_CHAR = tinymce.isGecko ? '\u200B' : '\uFEFF',\r
+ INVISIBLE_CHAR = '\uFEFF',\r
MCE_ATTR_RE = /^(src|href|style)$/,\r
FALSE = false,\r
TRUE = true,\r
+ formatChangeData,\r
undef,\r
getContentEditable = dom.getContentEditable;\r
\r
- function isArray(obj) {\r
- return obj instanceof Array;\r
- };\r
+ function isTextBlock(name) {\r
+ return !!ed.schema.getTextBlocks()[name.toLowerCase()];\r
+ }\r
\r
function getParents(node, selector) {\r
return dom.getParents(node, selector, dom.getRoot());\r
function process(node) {\r
var children, i, l, localContentEditable, lastContentEditable, hasContentEditableState;\r
\r
+ // Skip on text nodes as they have neither format to remove nor children\r
+ if (node.nodeType === 3) {\r
+ return;\r
+ }\r
+\r
// Node has a contentEditable value\r
if (node.nodeType === 1 && getContentEditable(node)) {\r
lastContentEditable = contentEditable;\r
matchedFormatNames.push(name);\r
}\r
}\r
- });\r
+ }, dom.getRoot());\r
\r
return matchedFormatNames;\r
};\r
return FALSE;\r
};\r
\r
+ function formatChanged(formats, callback, similar) {\r
+ var currentFormats;\r
+\r
+ // Setup format node change logic\r
+ if (!formatChangeData) {\r
+ formatChangeData = {};\r
+ currentFormats = {};\r
+\r
+ ed.onNodeChange.addToTop(function(ed, cm, node) {\r
+ var parents = getParents(node), matchedFormats = {};\r
+\r
+ // Check for new formats\r
+ each(formatChangeData, function(callbacks, format) {\r
+ each(parents, function(node) {\r
+ if (matchNode(node, format, {}, callbacks.similar)) {\r
+ if (!currentFormats[format]) {\r
+ // Execute callbacks\r
+ each(callbacks, function(callback) {\r
+ callback(true, {node: node, format: format, parents: parents});\r
+ });\r
+\r
+ currentFormats[format] = callbacks;\r
+ }\r
+\r
+ matchedFormats[format] = callbacks;\r
+ return false;\r
+ }\r
+ });\r
+ });\r
+\r
+ // Check if current formats still match\r
+ each(currentFormats, function(callbacks, format) {\r
+ if (!matchedFormats[format]) {\r
+ delete currentFormats[format];\r
+\r
+ each(callbacks, function(callback) {\r
+ callback(false, {node: node, format: format, parents: parents});\r
+ });\r
+ }\r
+ });\r
+ });\r
+ }\r
+\r
+ // Add format listeners\r
+ each(formats.split(','), function(format) {\r
+ if (!formatChangeData[format]) {\r
+ formatChangeData[format] = [];\r
+ formatChangeData[format].similar = similar;\r
+ }\r
+\r
+ formatChangeData[format].push(callback);\r
+ });\r
+\r
+ return this;\r
+ };\r
+\r
// Expose to public\r
tinymce.extend(this, {\r
get : get,\r
match : match,\r
matchAll : matchAll,\r
matchNode : matchNode,\r
- canApply : canApply\r
+ canApply : canApply,\r
+ formatChanged: formatChanged\r
});\r
\r
// Initialize\r
siblingName = start ? 'previousSibling' : 'nextSibling';\r
root = dom.getRoot();\r
\r
+ function isBogusBr(node) {\r
+ return node.nodeName == "BR" && node.getAttribute('data-mce-bogus') && !node.nextSibling;\r
+ };\r
+\r
// If it's a text node and the offset is inside the text\r
if (container.nodeType == 3 && !isWhiteSpaceNode(container)) {\r
if (start ? startOffset > 0 : endOffset < container.nodeValue.length) {\r
\r
// Walk left/right\r
for (sibling = parent[siblingName]; sibling; sibling = sibling[siblingName]) {\r
- if (!isBookmarkNode(sibling) && !isWhiteSpaceNode(sibling)) {\r
+ if (!isBookmarkNode(sibling) && !isWhiteSpaceNode(sibling) && !isBogusBr(sibling)) {\r
return parent;\r
}\r
}\r
\r
// Expand to first wrappable block element or any block element\r
if (!node)\r
- node = dom.getParent(container.nodeType == 3 ? container.parentNode : container, isBlock);\r
+ node = dom.getParent(container.nodeType == 3 ? container.parentNode : container, isTextBlock);\r
\r
// Exclude inner lists from wrapping\r
if (node && format[0].wrapper)\r
}\r
};\r
\r
+ // Checks if the parent caret container node isn't empty if that is the case it\r
+ // will remove the bogus state on all children that isn't empty\r
+ function unmarkBogusCaretParents() {\r
+ var i, caretContainer, node;\r
+\r
+ caretContainer = getParentCaretContainer(selection.getStart());\r
+ if (caretContainer && !dom.isEmpty(caretContainer)) {\r
+ tinymce.walk(caretContainer, function(node) {\r
+ if (node.nodeType == 1 && node.id !== caretContainerId && !dom.isEmpty(node)) {\r
+ dom.setAttrib(node, 'data-mce-bogus', null);\r
+ }\r
+ }, 'childNodes');\r
+ }\r
+ };\r
+\r
// Only bind the caret events once\r
if (!self._hasCaretEvents) {\r
// Mark current caret container elements as bogus when getting the contents so we don't end up with empty elements\r
tinymce.each('onMouseUp onKeyUp'.split(' '), function(name) {\r
ed[name].addToTop(function() {\r
removeCaretContainer();\r
+ unmarkBogusCaretParents();\r
});\r
});\r
\r
if (keyCode == 8 || keyCode == 37 || keyCode == 39) {\r
removeCaretContainer(getParentCaretContainer(selection.getStart()));\r
}\r
+\r
+ unmarkBogusCaretParents();\r
});\r
\r
// Remove bogus state if they got filled by contents using editor.selection.setContent\r
- selection.onSetContent.add(function() {\r
- dom.getParent(selection.getStart(), function(node) {\r
- if (node.id !== caretContainerId && dom.getAttrib(node, 'data-mce-bogus') && !dom.isEmpty(node)) {\r
- dom.setAttrib(node, 'data-mce-bogus', null);\r
- }\r
- });\r
- });\r
+ selection.onSetContent.add(unmarkBogusCaretParents);\r
\r
self._hasCaretEvents = true;\r
}\r
var TreeWalker = tinymce.dom.TreeWalker;\r
\r
tinymce.EnterKey = function(editor) {\r
- var dom = editor.dom, selection = editor.selection, settings = editor.settings, undoManager = editor.undoManager;\r
+ var dom = editor.dom, selection = editor.selection, settings = editor.settings, undoManager = editor.undoManager, nonEmptyElementsMap = editor.schema.getNonEmptyElements();\r
\r
function handleEnterKey(evt) {\r
- var rng = selection.getRng(true), tmpRng, editableRoot, container, offset, parentBlock, documentMode,\r
+ var rng = selection.getRng(true), tmpRng, editableRoot, container, offset, parentBlock, documentMode, shiftKey,\r
newBlock, fragment, containerBlock, parentBlockName, containerBlockName, newBlockName, isAfterLastNodeInContainer;\r
\r
// Returns true if the block can be split into two blocks or not\r
function canSplitBlock(node) {\r
return node &&\r
dom.isBlock(node) &&\r
- !/^(TD|TH|CAPTION)$/.test(node.nodeName) &&\r
+ !/^(TD|TH|CAPTION|FORM)$/.test(node.nodeName) &&\r
!/^(fixed|absolute)/i.test(node.style.position) && \r
dom.getContentEditable(node) !== "true";\r
};\r
\r
+ // Renders empty block on IE\r
+ function renderBlockOnIE(block) {\r
+ var oldRng;\r
+\r
+ if (tinymce.isIE && dom.isBlock(block)) {\r
+ oldRng = selection.getRng();\r
+ block.appendChild(dom.create('span', null, '\u00a0'));\r
+ selection.select(block);\r
+ block.lastChild.outerHTML = '';\r
+ selection.setRng(oldRng);\r
+ }\r
+ };\r
+\r
+ // Remove the first empty inline element of the block so this: <p><b><em></em></b>x</p> becomes this: <p>x</p>\r
+ function trimInlineElementsOnLeftSideOfBlock(block) {\r
+ var node = block, firstChilds = [], i;\r
+\r
+ // Find inner most first child ex: <p><i><b>*</b></i></p>\r
+ while (node = node.firstChild) {\r
+ if (dom.isBlock(node)) {\r
+ return;\r
+ }\r
+\r
+ if (node.nodeType == 1 && !nonEmptyElementsMap[node.nodeName.toLowerCase()]) {\r
+ firstChilds.push(node);\r
+ }\r
+ }\r
+\r
+ i = firstChilds.length;\r
+ while (i--) {\r
+ node = firstChilds[i];\r
+ if (!node.hasChildNodes() || (node.firstChild == node.lastChild && node.firstChild.nodeValue === '')) {\r
+ dom.remove(node);\r
+ } else {\r
+ // Remove <a> </a> see #5381\r
+ if (node.nodeName == "A" && (node.innerText || node.textContent) === ' ') {\r
+ dom.remove(node);\r
+ }\r
+ }\r
+ }\r
+ };\r
+ \r
// Moves the caret to a suitable position within the root for example in the first non pure whitespace text node or before an image\r
function moveToCaretPosition(root) {\r
var walker, node, rng, y, viewPort, lastNode = root, tempElm;\r
break;\r
}\r
\r
- if (/^(BR|IMG)$/.test(node.nodeName)) {\r
+ if (nonEmptyElementsMap[node.nodeName.toLowerCase()]) {\r
rng.setStartBefore(node);\r
rng.setEndBefore(node);\r
break;\r
if (settings.keep_styles !== false) {\r
do {\r
if (/^(SPAN|STRONG|B|EM|I|FONT|STRIKE|U)$/.test(node.nodeName)) {\r
+ // Never clone a caret containers\r
+ if (node.id == '_mce_caret') {\r
+ continue;\r
+ }\r
+\r
clonedNode = node.cloneNode(false);\r
dom.setAttrib(clonedNode, 'id', ''); // Remove ID since it needs to be document unique\r
\r
\r
// BR is needed in empty blocks on non IE browsers\r
if (!tinymce.isIE) {\r
- caretNode.innerHTML = '<br>';\r
+ caretNode.innerHTML = '<br data-mce-bogus="1">';\r
}\r
\r
return block;\r
return true;\r
}\r
\r
+ // If the caret if before the first element in parentBlock\r
+ if (start && container.nodeType == 1 && container == parentBlock.firstChild) {\r
+ return true;\r
+ }\r
+\r
// Caret can be before/after a table\r
if (container.nodeName === "TABLE" || (container.previousSibling && container.previousSibling.nodeName == "TABLE")) {\r
return (isAfterLastNodeInContainer && !start) || (!isAfterLastNodeInContainer && start);\r
\r
// Walk the DOM and look for text nodes or non empty elements\r
walker = new TreeWalker(container, parentBlock);\r
- while (node = (start ? walker.prev() : walker.next())) {\r
+ \r
+ // If caret is in beginning or end of a text block then jump to the next/previous node\r
+ if (container.nodeType == 3) {\r
+ if (start && offset == 0) {\r
+ walker.prev();\r
+ } else if (!start && offset == container.nodeValue.length) {\r
+ walker.next();\r
+ }\r
+ }\r
+\r
+ while (node = walker.current()) {\r
if (node.nodeType === 1) {\r
// Ignore bogus elements\r
- if (node.getAttribute('data-mce-bogus')) {\r
- continue;\r
- }\r
-\r
- // Keep empty elements like <img />\r
- name = node.nodeName.toLowerCase();\r
- if (name === 'IMG') {\r
- return false;\r
+ if (!node.getAttribute('data-mce-bogus')) {\r
+ // Keep empty elements like <img /> <input /> but not trailing br:s like <p>text|<br></p>\r
+ name = node.nodeName.toLowerCase();\r
+ if (nonEmptyElementsMap[name] && name !== 'br') {\r
+ return false;\r
+ }\r
}\r
} else if (node.nodeType === 3 && !/^[ \t\r\n]*$/.test(node.nodeValue)) {\r
return false;\r
}\r
+\r
+ if (start) {\r
+ walker.prev();\r
+ } else {\r
+ walker.next();\r
+ }\r
}\r
\r
return true;\r
} else if (isFirstOrLastLi()) {\r
// Last LI in list then temove LI and add text block after list\r
dom.insertAfter(newBlock, containerBlock);\r
+ renderBlockOnIE(newBlock);\r
} else {\r
// Middle LI in list the split the list and insert a text block in the middle\r
// Extract after fragment and insert it after the current block\r
\r
// Inserts a BR element if the forced_root_block option is set to false or empty string\r
function insertBr() {\r
- var brElm, extraBr;\r
+ var brElm, extraBr, marker;\r
\r
if (container && container.nodeType == 3 && offset >= container.nodeValue.length) {\r
// Insert extra BR element at the end block elements\r
if (!tinymce.isIE && !hasRightSideBr()) {\r
- brElm = dom.create('br')\r
+ brElm = dom.create('br');\r
rng.insertNode(brElm);\r
rng.setStartAfter(brElm);\r
rng.setEndAfter(brElm);\r
brElm.parentNode.insertBefore(dom.doc.createTextNode('\r'), brElm);\r
}\r
\r
+ // Insert temp marker and scroll to that\r
+ marker = dom.create('span', {}, ' ');\r
+ brElm.parentNode.insertBefore(marker, brElm);\r
+ selection.scrollIntoView(marker);\r
+ dom.remove(marker);\r
+\r
if (!extraBr) {\r
rng.setStartAfter(brElm);\r
rng.setEndAfter(brElm);\r
return parent !== root ? editableRoot : root;\r
};\r
\r
+ // Adds a BR at the end of blocks that only contains an IMG or INPUT since these might be floated and then they won't expand the block\r
+ function addBrToBlockIfNeeded(block) {\r
+ var lastChild;\r
+\r
+ // IE will render the blocks correctly other browsers needs a BR\r
+ if (!tinymce.isIE) {\r
+ block.normalize(); // Remove empty text nodes that got left behind by the extract\r
+\r
+ // Check if the block is empty or contains a floated last child\r
+ lastChild = block.lastChild;\r
+ if (!lastChild || (/^(left|right)$/gi.test(dom.getStyle(lastChild, 'float', true)))) {\r
+ dom.add(block, 'br');\r
+ }\r
+ }\r
+ };\r
+\r
// Delete any selected contents\r
if (!rng.collapsed) {\r
editor.execCommand('Delete');\r
// Setup range items and newBlockName\r
container = rng.startContainer;\r
offset = rng.startOffset;\r
- newBlockName = settings.forced_root_block;\r
+ newBlockName = (settings.force_p_newlines ? 'p' : '') || settings.forced_root_block;\r
newBlockName = newBlockName ? newBlockName.toUpperCase() : '';\r
documentMode = dom.doc.documentMode;\r
+ shiftKey = evt.shiftKey;\r
\r
// Resolve node index\r
if (container.nodeType == 1 && container.hasChildNodes()) {\r
isAfterLastNodeInContainer = offset > container.childNodes.length - 1;\r
container = container.childNodes[Math.min(offset, container.childNodes.length - 1)] || container;\r
- offset = 0;\r
+ if (isAfterLastNodeInContainer && container.nodeType == 3) {\r
+ offset = container.nodeValue.length;\r
+ } else {\r
+ offset = 0;\r
+ }\r
}\r
\r
// Get editable root node normaly the body element but sometimes a div or span\r
\r
// If editable root isn't block nor the root of the editor\r
if (!dom.isBlock(editableRoot) && editableRoot != dom.getRoot()) {\r
- if (!newBlockName || evt.shiftKey) {\r
+ if (!newBlockName || shiftKey) {\r
insertBr();\r
}\r
\r
// Wrap the current node and it's sibling in a default block if it's needed.\r
// for example this <td>text|<b>text2</b></td> will become this <td><p>text|<b>text2</p></b></td>\r
// This won't happen if root blocks are disabled or the shiftKey is pressed\r
- if ((newBlockName && !evt.shiftKey) || (!newBlockName && evt.shiftKey)) {\r
+ if ((newBlockName && !shiftKey) || (!newBlockName && shiftKey)) {\r
container = wrapSelfAndSiblingsInDefaultBlock(container, offset);\r
}\r
\r
parentBlockName = parentBlock ? parentBlock.nodeName.toUpperCase() : ''; // IE < 9 & HTML5\r
containerBlockName = containerBlock ? containerBlock.nodeName.toUpperCase() : ''; // IE < 9 & HTML5\r
\r
- // Handle enter inside an empty list item\r
- if (parentBlockName == 'LI' && dom.isEmpty(parentBlock)) {\r
- // Let the list plugin or browser handle nested lists for now\r
- if (/^(UL|OL|LI)$/.test(containerBlock.parentNode.nodeName)) {\r
- return false;\r
+ // Enter inside block contained within a LI then split or insert before/after LI\r
+ if (containerBlockName == 'LI' && !evt.ctrlKey) {\r
+ parentBlock = containerBlock;\r
+ parentBlockName = containerBlockName;\r
+ }\r
+\r
+ // Handle enter in LI\r
+ if (parentBlockName == 'LI') {\r
+ if (!newBlockName && shiftKey) {\r
+ insertBr();\r
+ return;\r
}\r
\r
- handleEmptyListItem();\r
- return;\r
+ // Handle enter inside an empty list item\r
+ if (dom.isEmpty(parentBlock)) {\r
+ // Let the list plugin or browser handle nested lists for now\r
+ if (/^(UL|OL|LI)$/.test(containerBlock.parentNode.nodeName)) {\r
+ return false;\r
+ }\r
+\r
+ handleEmptyListItem();\r
+ return;\r
+ }\r
}\r
\r
// Don't split PRE tags but insert a BR instead easier when writing code samples etc\r
if (parentBlockName == 'PRE' && settings.br_in_pre !== false) {\r
- if (!evt.shiftKey) {\r
+ if (!shiftKey) {\r
insertBr();\r
return;\r
}\r
} else {\r
// If no root block is configured then insert a BR by default or if the shiftKey is pressed\r
- if ((!newBlockName && !evt.shiftKey && parentBlockName != 'LI') || (newBlockName && evt.shiftKey)) {\r
+ if ((!newBlockName && !shiftKey && parentBlockName != 'LI') || (newBlockName && shiftKey)) {\r
insertBr();\r
return;\r
}\r
} else {\r
dom.insertAfter(newBlock, parentBlock);\r
}\r
+\r
+ moveToCaretPosition(newBlock);\r
} else if (isCaretAtStartOrEndOfBlock(true)) {\r
// Insert new block before\r
newBlock = parentBlock.parentNode.insertBefore(createNewBlock(), parentBlock);\r
+ renderBlockOnIE(newBlock);\r
} else {\r
// Extract after fragment and insert it after the current block\r
tmpRng = rng.cloneRange();\r
trimLeadingLineBreaks(fragment);\r
newBlock = fragment.firstChild;\r
dom.insertAfter(fragment, parentBlock);\r
+ trimInlineElementsOnLeftSideOfBlock(newBlock);\r
+ addBrToBlockIfNeeded(parentBlock);\r
+ moveToCaretPosition(newBlock);\r
}\r
\r
dom.setAttrib(newBlock, 'id', ''); // Remove ID since it needs to be document unique\r
- moveToCaretPosition(newBlock);\r
undoManager.add();\r
}\r
\r