var tinymce = {\r
majorVersion : '3',\r
\r
- minorVersion : '4.5',\r
+ minorVersion : '4.9',\r
\r
- releaseDate : '2011-09-06',\r
+ releaseDate : '2012-02-23',\r
\r
_init : function() {\r
var t = this, d = document, na = navigator, ua = na.userAgent, i, nl, n, base, p, v;\r
// And this is also more efficient\r
for (i = 0; i<li.length; i++) {\r
c = li[i];\r
- s = c.cb.apply(c.scope, a);\r
+ s = c.cb.apply(c.scope, a.length > 0 ? a : [c.scope]);\r
\r
if (s === false)\r
break;\r
\r
// Parse URL (Credits goes to Steave, http://blog.stevenlevithan.com/archives/parseuri)\r
u = u.replace(/@@/g, '(mce_at)'); // Zope 3 workaround, they use @@something\r
- u = /^(?:(?![^:@]+:[^:@\/]*@)([^:\/?#.]+):)?(?:\/\/)?((?:(([^:@]*):?([^:@]*))?@)?([^:\/?#]*)(?::(\d*))?)(((\/(?:[^?#](?![^?#\/]*\.[^?#\/.]+(?:[?#]|$)))*\/?)?([^?#\/]*))(?:\?([^#]*))?(?:#(.*))?)/.exec(u);\r
+ u = /^(?:(?![^:@]+:[^:@\/]*@)([^:\/?#.]+):)?(?:\/\/)?((?:(([^:@\/]*):?([^:@\/]*))?@)?([^:\/?#]*)(?::(\d*))?)(((\/(?:[^?#](?![^?#\/]*\.[^?#\/.]+(?:[?#]|$)))*\/?)?([^?#\/]*))(?:\?([^#]*))?(?:#(.*))?)/.exec(u);\r
each(["source","protocol","authority","userInfo","user","password","host","port","relative","path","directory","file","query","anchor"], function(v, i) {\r
var s = u[i];\r
\r
\r
v = '{';\r
\r
- for (i in o)\r
- v += typeof o[i] != 'function' ? (v.length > 1 ? ',' + quote : quote) + i + quote +':' + serialize(o[i], quote) : '';\r
+ for (i in o) {\r
+ if (o.hasOwnProperty(i)) {\r
+ v += typeof o[i] != 'function' ? (v.length > 1 ? ',' + quote : quote) + i + quote +':' + serialize(o[i], quote) : '';\r
+ }\r
+ }\r
\r
return v + '}';\r
}\r
\r
};\r
})();\r
+\r
tinymce.create('static tinymce.util.XHR', {\r
send : function(o) {\r
var x, t, w = window, c = 0;\r
}());\r
(function(tinymce){\r
tinymce.VK = {\r
- DELETE:46,\r
- BACKSPACE:8\r
- \r
+ DELETE: 46,\r
+ BACKSPACE: 8,\r
+ ENTER: 13,\r
+ TAB: 9,\r
+ SPACEBAR: 32,\r
+ UP: 38,\r
+ DOWN: 40,\r
+ modifierPressed: function (e) {\r
+ return e.shiftKey || e.ctrlKey || e.altKey;\r
+ }\r
}\r
-\r
})(tinymce);\r
\r
(function(tinymce) {\r
var rng, blockElm, node, clonedSpan, isDelete;\r
\r
isDelete = e.keyCode == DELETE;\r
- if (isDelete || e.keyCode == BACKSPACE) {\r
+ if ((isDelete || e.keyCode == BACKSPACE) && !VK.modifierPressed(e)) {\r
e.preventDefault();\r
rng = selection.getRng();\r
\r
if (blockElm) {\r
node = blockElm.firstChild;\r
\r
+ // Ignore empty text nodes\r
+ while (node && node.nodeType == 3 && node.nodeValue.length == 0)\r
+ node = node.nextSibling;\r
+\r
if (node && node.nodeName === 'SPAN') {\r
clonedSpan = node.cloneNode(false);\r
}\r
}\r
\r
- // Do the backspace/delete actiopn\r
+ // Do the backspace/delete action\r
ed.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
- var rng = dom.createRng();\r
-\r
- // Set range selection before the span we are about to remove\r
- rng.setStartBefore(span);\r
- rng.setEndBefore(span);\r
+ var bm = selection.getBookmark();\r
\r
if (clonedSpan) {\r
dom.replace(clonedSpan.cloneNode(false), span, true);\r
}\r
\r
// Restore the selection\r
- selection.setRng(rng);\r
+ selection.moveToBookmark(bm);\r
});\r
}\r
});\r
};\r
\r
function emptyEditorWhenDeleting(ed) {\r
- ed.onKeyUp.add(function(ed, e) {\r
- var keyCode = e.keyCode;\r
\r
+ function serializeRng(rng) {\r
+ var body = ed.dom.create("body");\r
+ var contents = rng.cloneContents();\r
+ body.appendChild(contents);\r
+ return ed.selection.serializer.serialize(body, {format: 'html'});\r
+ }\r
+\r
+ function allContentsSelected(rng) {\r
+ var selection = serializeRng(rng);\r
+\r
+ var allRng = ed.dom.createRng();\r
+ allRng.selectNode(ed.getBody());\r
+\r
+ var allSelection = serializeRng(allRng);\r
+ return selection === allSelection;\r
+ }\r
+\r
+ ed.onKeyDown.addToTop(function(ed, e) {\r
+ var keyCode = e.keyCode;\r
if (keyCode == DELETE || keyCode == BACKSPACE) {\r
- if (ed.dom.isEmpty(ed.getBody())) {\r
+ var rng = ed.selection.getRng(true);\r
+ if (!rng.collapsed && allContentsSelected(rng)) {\r
ed.setContent('', {format : 'raw'});\r
ed.nodeChanged();\r
- return;\r
+ e.preventDefault();\r
}\r
}\r
});\r
+\r
+ };\r
+\r
+ function inputMethodFocus(ed) {\r
+ ed.dom.bind(ed.getDoc(), 'focusin', function() {\r
+ ed.selection.setRng(ed.selection.getRng());\r
+ });\r
};\r
+\r
+ function removeHrOnBackspace(ed) {\r
+ ed.onKeyDown.add(function(ed, e) {\r
+ if (e.keyCode === BACKSPACE) {\r
+ if (ed.selection.isCollapsed() && ed.selection.getRng(true).startOffset === 0) {\r
+ var node = ed.selection.getNode();\r
+ var previousSibling = node.previousSibling;\r
+ if (previousSibling && previousSibling.nodeName && previousSibling.nodeName.toLowerCase() === "hr") {\r
+ ed.dom.remove(previousSibling);\r
+ tinymce.dom.Event.cancel(e);\r
+ }\r
+ }\r
+ }\r
+ })\r
+ }\r
+\r
+ function focusBody(ed) {\r
+ // Fix for a focus bug in FF 3.x where the body element\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
+ ed.onMouseDown.add(function(ed, e) {\r
+ if (e.target.nodeName === "HTML") {\r
+ var body = ed.getBody();\r
+\r
+ // Blur the body it's focused but not correctly focused\r
+ body.blur();\r
+\r
+ // Refocus the body after a little while\r
+ setTimeout(function() {\r
+ body.focus();\r
+ }, 0);\r
+ }\r
+ });\r
+ }\r
+ };\r
+\r
+ function selectControlElements(ed) {\r
+ ed.onClick.add(function(ed, e) {\r
+ e = e.target;\r
+\r
+ // Workaround for bug, http://bugs.webkit.org/show_bug.cgi?id=12250\r
+ // WebKit can't even do simple things like selecting an image\r
+ // Needs tobe the setBaseAndExtend or it will fail to select floated images\r
+ if (/^(IMG|HR)$/.test(e.nodeName))\r
+ ed.selection.getSel().setBaseAndExtent(e, 0, e, 1);\r
+\r
+ if (e.nodeName == 'A' && ed.dom.hasClass(e, 'mceItemAnchor'))\r
+ ed.selection.select(e);\r
+\r
+ ed.nodeChanged();\r
+ });\r
+ };\r
+\r
+ function removeStylesOnPTagsInheritedFromHeadingTag(ed) {\r
+ ed.onKeyDown.add(function(ed, event) {\r
+ function checkInHeadingTag(ed) {\r
+ var currentNode = ed.selection.getNode();\r
+ var headingTags = 'h1,h2,h3,h4,h5,h6';\r
+ return ed.dom.is(currentNode, headingTags) || ed.dom.getParent(currentNode, headingTags) !== null;\r
+ }\r
+\r
+ if (event.keyCode === VK.ENTER && !VK.modifierPressed(event) && checkInHeadingTag(ed)) {\r
+ setTimeout(function() {\r
+ var currentNode = ed.selection.getNode();\r
+ if (ed.dom.is(currentNode, 'p')) {\r
+ ed.dom.setAttrib(currentNode, 'style', null);\r
+ // While tiny's content is correct after this method call, the content shown is not representative of it and needs to be 'repainted'\r
+ ed.execCommand('mceCleanup');\r
+ }\r
+ }, 0);\r
+ }\r
+ });\r
+ }\r
+ function selectionChangeNodeChanged(ed) {\r
+ var lastRng, selectionTimer;\r
+\r
+ ed.dom.bind(ed.getDoc(), 'selectionchange', function() {\r
+ if (selectionTimer) {\r
+ clearTimeout(selectionTimer);\r
+ selectionTimer = 0;\r
+ }\r
+\r
+ selectionTimer = window.setTimeout(function() {\r
+ var rng = ed.selection.getRng();\r
+\r
+ // Compare the ranges to see if it was a real change or not\r
+ if (!lastRng || !tinymce.dom.RangeUtils.compareRanges(rng, lastRng)) {\r
+ ed.nodeChanged();\r
+ lastRng = rng;\r
+ }\r
+ }, 50);\r
+ });\r
+ }\r
+\r
+ function ensureBodyHasRoleApplication(ed) {\r
+ document.body.setAttribute("role", "application");\r
+ }\r
\r
tinymce.create('tinymce.util.Quirks', {\r
Quirks: function(ed) {\r
- // Load WebKit specific fixed\r
+ // WebKit\r
if (tinymce.isWebKit) {\r
cleanupStylesWhenDeleting(ed);\r
emptyEditorWhenDeleting(ed);\r
+ inputMethodFocus(ed);\r
+ selectControlElements(ed);\r
+\r
+ // iOS\r
+ if (tinymce.isIDevice) {\r
+ selectionChangeNodeChanged(ed);\r
+ }\r
}\r
\r
- // Load IE specific fixes\r
+ // IE\r
if (tinymce.isIE) {\r
+ removeHrOnBackspace(ed);\r
emptyEditorWhenDeleting(ed);\r
+ ensureBodyHasRoleApplication(ed);\r
+ removeStylesOnPTagsInheritedFromHeadingTag(ed)\r
+ }\r
+\r
+ // Gecko\r
+ if (tinymce.isGecko) {\r
+ removeHrOnBackspace(ed);\r
+ focusBody(ed);\r
}\r
}\r
});\r
})(tinymce);\r
+\r
(function(tinymce) {\r
var namedEntities, baseEntities, reverseEntities,\r
- attrsCharsRegExp = /[&<>\"\u007E-\uD7FF]|[\uD800-\uDBFF][\uDC00-\uDFFF]/g,\r
- textCharsRegExp = /[<>&\u007E-\uD7FF]|[\uD800-\uDBFF][\uDC00-\uDFFF]/g,\r
+ attrsCharsRegExp = /[&<>\"\u007E-\uD7FF\uE000-\uFFEF]|[\uD800-\uDBFF][\uDC00-\uDFFF]/g,\r
+ textCharsRegExp = /[<>&\u007E-\uD7FF\uE000-\uFFEF]|[\uD800-\uDBFF][\uDC00-\uDFFF]/g,\r
rawCharsRegExp = /[<>&\"\']/g,\r
entityRegExp = /&(#x|#)?([\w]+);/g,\r
asciiMap = {\r
'(?:!DOCTYPE([\\w\\W]*?)>)|' + // DOCTYPE\r
'(?:\\?([^\\s\\/<>]+) ?([\\w\\W]*?)[?/]>)|' + // PI\r
'(?:\\/([^>]+)>)|' + // End element\r
- '(?:([^\\s\\/<>]+)\\s*((?:[^"\'>]+(?:(?:"[^"]*")|(?:\'[^\']*\')|[^>]*))*)>)' + // Start element\r
+ '(?:([^\\s\\/<>]+)((?:\\s+[^"\'>]+(?:(?:"[^"]*")|(?:\'[^\']*\')|[^>]*))*|\\/)>)' + // Start element\r
')', 'g');\r
\r
attrRegExp = /([\w:\-]+)(?:\s*=\s*(?:(?:\"((?:\\.|[^\"])*)\")|(?:\'((?:\\.|[^\'])*)\')|([^>\s]+)))?/g;\r
}\r
}\r
\r
+ // Keep comments\r
+ if (node.type === 8)\r
+ return false;\r
+ \r
// Keep non whitespace text nodes\r
if ((node.type === 3 && !whiteSpaceRegExp.test(node.value)))\r
return false;\r
\r
return this.run(e, function(e) {\r
var s = t.settings;\r
+ var originalValue = e.getAttribute(n);\r
+ if (v !== null) {\r
+ switch (n) {\r
+ case "style":\r
+ if (!is(v, 'string')) {\r
+ each(v, function(v, n) {\r
+ t.setStyle(e, n, v);\r
+ });\r
\r
- switch (n) {\r
- case "style":\r
- if (!is(v, 'string')) {\r
- each(v, function(v, n) {\r
- t.setStyle(e, n, v);\r
- });\r
-\r
- return;\r
- }\r
+ return;\r
+ }\r
\r
- // No mce_style for elements with these since they might get resized by the user\r
- if (s.keep_values) {\r
- if (v && !t._isRes(v))\r
- e.setAttribute('data-mce-style', v, 2);\r
- else\r
- e.removeAttribute('data-mce-style', 2);\r
- }\r
+ // No mce_style for elements with these since they might get resized by the user\r
+ if (s.keep_values) {\r
+ if (v && !t._isRes(v))\r
+ e.setAttribute('data-mce-style', v, 2);\r
+ else\r
+ e.removeAttribute('data-mce-style', 2);\r
+ }\r
\r
- e.style.cssText = v;\r
- break;\r
+ e.style.cssText = v;\r
+ break;\r
\r
- case "class":\r
- e.className = v || ''; // Fix IE null bug\r
- break;\r
+ case "class":\r
+ e.className = v || ''; // Fix IE null bug\r
+ break;\r
\r
- case "src":\r
- case "href":\r
- if (s.keep_values) {\r
- if (s.url_converter)\r
- v = s.url_converter.call(s.url_converter_scope || t, v, n, e);\r
+ case "src":\r
+ case "href":\r
+ if (s.keep_values) {\r
+ if (s.url_converter)\r
+ v = s.url_converter.call(s.url_converter_scope || t, v, n, e);\r
\r
- t.setAttrib(e, 'data-mce-' + n, v, 2);\r
- }\r
+ t.setAttrib(e, 'data-mce-' + n, v, 2);\r
+ }\r
\r
- break;\r
+ break;\r
\r
- case "shape":\r
- e.setAttribute('data-mce-style', v);\r
- break;\r
+ case "shape":\r
+ e.setAttribute('data-mce-style', v);\r
+ break;\r
+ }\r
}\r
-\r
if (is(v) && v !== null && v.length !== 0)\r
e.setAttribute(n, '' + v, 2);\r
else\r
e.removeAttribute(n, 2);\r
+\r
+ // fire onChangeAttrib event for attributes that have changed\r
+ if (tinyMCE.activeEditor && originalValue != v) {\r
+ var ed = tinyMCE.activeEditor;\r
+ ed.onSetAttrib.dispatch(ed, e, n, v);\r
+ }\r
});\r
},\r
\r
}\r
}\r
\r
+ // Keep comment nodes\r
+ if (type == 8)\r
+ return false;\r
+\r
// Keep non whitespace text nodes\r
if ((type === 3 && !whiteSpaceRegExp.test(node.nodeValue)))\r
return false;\r
function trim(node) {\r
var i, children = node.childNodes, type = node.nodeType;\r
\r
+ function surroundedBySpans(node) {\r
+ var previousIsSpan = node.previousSibling && node.previousSibling.nodeName == 'SPAN';\r
+ var nextIsSpan = node.nextSibling && node.nextSibling.nodeName == 'SPAN';\r
+ return previousIsSpan && nextIsSpan;\r
+ }\r
+\r
if (type == 1 && node.getAttribute('data-mce-type') == 'bookmark')\r
return;\r
\r
// Keep non whitespace text nodes\r
if (type == 3 && node.nodeValue.length > 0) {\r
// If parent element isn't a block or there isn't any useful contents for example "<p> </p>"\r
- if (!t.isBlock(node.parentNode) || tinymce.trim(node.nodeValue).length > 0)\r
+ // Also keep text nodes with only spaces if surrounded by spans.\r
+ // eg. "<p><span>a</span> <span>b</span></p>" should keep space between a and b\r
+ var trimmedLength = tinymce.trim(node.nodeValue).length;\r
+ if (!t.isBlock(node.parentNode) || trimmedLength > 0 || trimmedLength == 0 && surroundedBySpans(node))\r
return;\r
} else if (type == 1) {\r
// If the only child is a bookmark then move it up\r
\r
// Insert middle chunk\r
if (re)\r
- pa.replaceChild(re, e);\r
- else\r
- pa.insertBefore(e, pe);\r
+ pa.replaceChild(re, e);\r
+ else\r
+ pa.insertBefore(e, pe);\r
\r
// Insert after chunk\r
pa.insertBefore(trim(aft), pe);\r
parent = node.parentNode;\r
root = dom.getRoot().parentNode;\r
\r
- while (parent != root) {\r
+ while (parent != root && parent.nodeType !== 9) {\r
children = parent.children;\r
\r
i = children.length;\r
return;\r
}\r
\r
+ // When loaded asynchronously, the DOM Content may already be loaded\r
+ if (doc.readyState === 'complete') {\r
+ t._pageInit(win);\r
+ return;\r
+ }\r
+\r
// Use IE method\r
if (doc.attachEvent) {\r
doc.attachEvent("onreadystatechange", function() {\r
}\r
\r
s.addRange(r);\r
- t.selectedRange = s.getRangeAt(0);\r
+ // adding range isn't always successful so we need to check range count otherwise an exception can occur\r
+ t.selectedRange = s.rangeCount > 0 ? s.getRangeAt(0) : null;\r
}\r
} else {\r
// Is W3C Range\r
if (sb && eb && sb != eb) {\r
n = sb;\r
\r
- while ((n = n.nextSibling) && n != eb) {\r
+ var walker = new tinymce.dom.TreeWalker(sb, dom.getRoot());\r
+ while ((n = walker.next()) && n != eb) {\r
if (dom.isBlock(n))\r
bl.push(n);\r
}\r
normalize : function() {\r
var self = this, rng, normalized;\r
\r
+ // TODO:\r
+ // Retain selection direction.\r
+ // Lean left/right on Gecko for inline elements.\r
+ // Run this on mouse up/key up when the user manually moves the selection\r
+ \r
// Normalize only on non IE browsers for now\r
if (tinymce.isIE)\r
return;\r
if (node.nodeType === 3) {\r
offset = start ? 0 : node.nodeValue.length - 1;\r
container = node;\r
+ normalized = true;\r
break;\r
}\r
\r
- // Found a BR element that we can place the caret before\r
- if (node.nodeName === 'BR') {\r
+ // Found a BR/IMG element that we can place the caret before\r
+ if (/^(BR|IMG)$/.test(node.nodeName)) {\r
offset = dom.nodeIndex(node);\r
container = node.parentNode;\r
+\r
+ // Put caret after image when moving the end point\r
+ if (node.nodeName == "IMG" && !start) {\r
+ offset++;\r
+ }\r
+\r
+ normalized = true;\r
break;\r
}\r
} while (node = (start ? walker.next() : walker.prev()));\r
-\r
- normalized = true;\r
}\r
}\r
}\r
// Normalize the end points\r
normalizeEndPoint(true);\r
\r
- if (rng.collapsed)\r
+ if (!rng.collapsed)\r
normalizeEndPoint();\r
\r
// Set the selection if it was normalized\r
if (!settings.apply_source_formatting)\r
settings.indent = false;\r
\r
- settings.remove_trailing_brs = true;\r
-\r
// Default DOM and Schema if they are undefined\r
dom = dom || tinymce.DOM;\r
schema = schema || new tinymce.html.Schema(settings);\r
settings.entity_encoding = settings.entity_encoding || 'named';\r
+ settings.remove_trailing_brs = "remove_trailing_brs" in settings ? settings.remove_trailing_brs : true;\r
\r
onPreProcess = new tinymce.util.Dispatcher(self);\r
\r
function trim(value) {\r
return value.replace(/(<!--\[CDATA\[|\]\]-->)/g, '\n')\r
.replace(/^[\r\n]*|[\r\n]*$/g, '')\r
- .replace(/^\s*(\/\/\s*<!--|\/\/\s*<!\[CDATA\[|<!--|<!\[CDATA\[)[\r\n]*/g, '')\r
- .replace(/\s*(\/\/\s*\]\]>|\/\/\s*-->|\]\]>|-->|\]\]-->)\s*$/g, '');\r
+ .replace(/^\s*((<!--)?(\s*\/\/)?\s*<!\[CDATA\[|(<!--\s*)?\/\*\s*<!\[CDATA\[\s*\*\/|(\/\/)?\s*<!--|\/\*\s*<!--\s*\*\/)\s*[\r\n]*/gi, '')\r
+ .replace(/\s*(\/\*\s*\]\]>\s*\*\/(-->)?|\s*\/\/\s*\]\]>(-->)?|\/\/\s*(-->)?|\]\]>|\/\*\s*-->\s*\*\/|\s*-->\s*)\s*$/g, '');\r
};\r
\r
while (i--) {\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/g, '');\r
+ args.content = args.content.replace(/\uFEFF|\u200B/g, '');\r
\r
// Post process\r
if (!args.no_events)\r
return;\r
}\r
\r
+ function exclude(nodes) {\r
+ var node;\r
+\r
+ // First node is excluded\r
+ node = nodes[0];\r
+ if (node.nodeType === 3 && node === startContainer && startOffset >= node.nodeValue.length) {\r
+ nodes.splice(0, 1);\r
+ }\r
+\r
+ // Last node is excluded\r
+ node = nodes[nodes.length - 1];\r
+ if (endOffset === 0 && nodes.length > 0 && node === endContainer && node.nodeType === 3) {\r
+ nodes.splice(nodes.length - 1, 1);\r
+ }\r
+\r
+ return nodes;\r
+ };\r
+\r
function collectSiblings(node, name, end_node) {\r
var siblings = [];\r
\r
if (!next)\r
siblings.reverse();\r
\r
- callback(siblings);\r
+ callback(exclude(siblings));\r
}\r
}\r
};\r
if (endContainer.nodeType == 1 && endContainer.hasChildNodes())\r
endContainer = endContainer.childNodes[Math.min(endOffset - 1, endContainer.childNodes.length - 1)];\r
\r
- // Find common ancestor and end points\r
- ancestor = dom.findCommonAncestor(startContainer, endContainer);\r
-\r
// Same container\r
if (startContainer == endContainer)\r
- return callback([startContainer]);\r
+ return callback(exclude([startContainer]));\r
\r
+ // Find common ancestor and end points\r
+ ancestor = dom.findCommonAncestor(startContainer, endContainer);\r
+ \r
// Process left side\r
for (node = startContainer; node; node = node.parentNode) {\r
- if (node == endContainer)\r
+ if (node === endContainer)\r
return walkBoundary(startContainer, ancestor, true);\r
\r
- if (node == ancestor)\r
+ if (node === ancestor)\r
break;\r
}\r
\r
// Process right side\r
for (node = endContainer; node; node = node.parentNode) {\r
- if (node == startContainer)\r
+ if (node === startContainer)\r
return walkBoundary(endContainer, ancestor);\r
\r
- if (node == ancestor)\r
+ if (node === ancestor)\r
break;\r
}\r
\r
);\r
\r
if (siblings.length)\r
- callback(siblings);\r
+ callback(exclude(siblings));\r
\r
// Walk right leaf\r
walkBoundary(endContainer, endPoint);\r
};\r
\r
- /* this.split = function(rng) {\r
+ this.split = function(rng) {\r
var startContainer = rng.startContainer,\r
startOffset = rng.startOffset,\r
endContainer = rng.endContainer,\r
endOffset = rng.endOffset;\r
\r
function splitText(node, offset) {\r
- if (offset == node.nodeValue.length)\r
- node.appendData(INVISIBLE_CHAR);\r
-\r
- node = node.splitText(offset);\r
-\r
- if (node.nodeValue === INVISIBLE_CHAR)\r
- node.nodeValue = '';\r
-\r
- return node;\r
+ return node.splitText(offset);\r
};\r
\r
// Handle single text node\r
- if (startContainer == endContainer) {\r
- if (startContainer.nodeType == 3) {\r
- if (startOffset != 0)\r
- startContainer = endContainer = splitText(startContainer, startOffset);\r
-\r
- if (endOffset - startOffset != startContainer.nodeValue.length)\r
- splitText(startContainer, endOffset - startOffset);\r
+ if (startContainer == endContainer && startContainer.nodeType == 3) {\r
+ if (startOffset > 0 && startOffset < startContainer.nodeValue.length) {\r
+ endContainer = splitText(startContainer, startOffset);\r
+ startContainer = endContainer.previousSibling;\r
+\r
+ if (endOffset > startOffset) {\r
+ endOffset = endOffset - startOffset;\r
+ startContainer = endContainer = splitText(endContainer, endOffset).previousSibling;\r
+ endOffset = endContainer.nodeValue.length;\r
+ startOffset = 0;\r
+ } else {\r
+ endOffset = 0;\r
+ }\r
}\r
} else {\r
// Split startContainer text node if needed\r
- if (startContainer.nodeType == 3 && startOffset != 0) {\r
+ if (startContainer.nodeType == 3 && startOffset > 0 && startOffset < startContainer.nodeValue.length) {\r
startContainer = splitText(startContainer, startOffset);\r
startOffset = 0;\r
}\r
\r
// Split endContainer text node if needed\r
- if (endContainer.nodeType == 3 && endOffset != endContainer.nodeValue.length) {\r
+ if (endContainer.nodeType == 3 && endOffset > 0 && endOffset < endContainer.nodeValue.length) {\r
endContainer = splitText(endContainer, endOffset).previousSibling;\r
endOffset = endContainer.nodeValue.length;\r
}\r
endOffset : endOffset\r
};\r
};\r
-*/\r
+\r
};\r
\r
tinymce.dom.RangeUtils.compareRanges = function(rng1, rng2) {\r
}\r
});\r
})(tinymce);\r
+\r
(function(tinymce) {\r
// Shorten class names\r
var DOM = tinymce.DOM, is = tinymce.is;\r
// Internal functions\r
_setupKeyboardNav : function(){\r
var contextMenu, menuItems, t=this; \r
- contextMenu = DOM.select('#menu_' + t.id)[0];\r
+ contextMenu = DOM.get('menu_' + t.id);\r
menuItems = DOM.select('a[role=option]', 'menu_' + t.id);\r
menuItems.splice(0,0,contextMenu);\r
t.keyboardNav = new tinymce.ui.KeyboardNavigation({\r
},\r
\r
postRender : function() {\r
- var t = this, s = t.settings;\r
-\r
+ var t = this, s = t.settings, imgBookmark;\r
+\r
+ // In IE a large image that occupies the entire editor area will be deselected when a button is clicked, so\r
+ // need to keep the selection in case the selection is lost\r
+ if (tinymce.isIE && t.editor) {\r
+ tinymce.dom.Event.add(t.id, 'mousedown', function(e) {\r
+ var nodeName = t.editor.selection.getNode().nodeName;\r
+ imgBookmark = nodeName === 'IMG' ? t.editor.selection.getBookmark() : null;\r
+ });\r
+ }\r
tinymce.dom.Event.add(t.id, 'click', function(e) {\r
- if (!t.isDisabled())\r
+ if (!t.isDisabled()) {\r
+ // restore the selection in case the selection is lost in IE\r
+ if (tinymce.isIE && t.editor && imgBookmark !== null) {\r
+ t.editor.selection.moveToBookmark(imgBookmark);\r
+ }\r
+ return s.onclick.call(s.scope, e);\r
+ }\r
+ });\r
+ tinymce.dom.Event.add(t.id, 'keyup', function(e) {\r
+ if (!t.isDisabled() && e.keyCode==tinymce.VK.SPACEBAR)\r
return s.onclick.call(s.scope, e);\r
});\r
}\r
return t.selectByIndex(-1);\r
\r
// Is string or number make function selector\r
- if (va && va.call)\r
+ if (va && typeof(va)=="function")\r
f = va;\r
else {\r
f = function(v) {\r
},\r
\r
selectByIndex : function(idx) {\r
- var t = this, e, o;\r
+ var t = this, e, o, label;\r
\r
if (idx != t.selectedIndex) {\r
e = DOM.get(t.id + '_text');\r
+ label = DOM.get(t.id + '_voiceDesc');\r
o = t.items[idx];\r
\r
if (o) {\r
t.selectedValue = o.value;\r
t.selectedIndex = idx;\r
DOM.setHTML(e, DOM.encode(o.title));\r
+ DOM.setHTML(label, t.settings.title + " - " + o.title);\r
DOM.removeClass(e, 'mceTitle');\r
DOM.setAttrib(t.id, 'aria-valuenow', o.title);\r
} else {\r
DOM.setHTML(e, DOM.encode(t.settings.title));\r
+ DOM.setHTML(label, DOM.encode(t.settings.title));\r
DOM.addClass(e, 'mceTitle');\r
t.selectedValue = t.selectedIndex = null;\r
DOM.setAttrib(t.id, 'aria-valuenow', t.settings.title);\r
renderHTML : function() {\r
var h = '', t = this, s = t.settings, cp = t.classPrefix;\r
\r
- h = '<span role="button" aria-haspopup="true" aria-labelledby="' + t.id +'_text" aria-describedby="' + t.id + '_voiceDesc"><table role="presentation" tabindex="0" id="' + t.id + '" cellpadding="0" cellspacing="0" class="' + cp + ' ' + cp + 'Enabled' + (s['class'] ? (' ' + s['class']) : '') + '"><tbody><tr>';\r
+ h = '<span role="listbox" aria-haspopup="true" aria-labelledby="' + t.id +'_voiceDesc" aria-describedby="' + t.id + '_voiceDesc"><table role="presentation" tabindex="0" id="' + t.id + '" cellpadding="0" cellspacing="0" class="' + cp + ' ' + cp + 'Enabled' + (s['class'] ? (' ' + s['class']) : '') + '"><tbody><tr>';\r
h += '<td>' + DOM.createHTML('span', {id: t.id + '_voiceDesc', 'class': 'voiceLabel', style:'display:none;'}, t.settings.title); \r
h += DOM.createHTML('a', {id : t.id + '_text', tabindex : -1, href : 'javascript:;', 'class' : 'mceText', onclick : "return false;", onmousedown : 'return false;'}, DOM.encode(t.settings.title)) + '</td>';\r
h += '<td>' + DOM.createHTML('a', {id : t.id + '_open', tabindex : -1, href : 'javascript:;', 'class' : 'mceOpen', onclick : "return false;", onmousedown : 'return false;'}, '<span><span style="display:none;" class="mceIconOnly" aria-hidden="true">\u25BC</span></span>') + '</td>';\r
if (o.value === undefined) {\r
m.add({\r
title : o.title,\r
+ role : "option",\r
'class' : 'mceMenuItemTitle',\r
onclick : function() {\r
if (t.settings.onselect('') !== false)\r
});\r
} else {\r
o.id = DOM.uniqueId();\r
+ o.role= "option";\r
o.onclick = function() {\r
if (t.settings.onselect(o.value) !== false)\r
t.select(o.value); // Must be runned after\r
}\r
});\r
})(tinymce);\r
+\r
(function(tinymce) {\r
var DOM = tinymce.DOM, Event = tinymce.dom.Event, each = tinymce.each, Dispatcher = tinymce.util.Dispatcher;\r
\r
return t.selectByIndex(-1);\r
\r
// Is string or number make function selector\r
- if (va && va.call)\r
+ if (va && typeof(va)=="function")\r
f = va;\r
else {\r
f = function(v) {\r
h += '<td >' + DOM.createHTML('a', {role: 'button', id : t.id + '_open', tabindex: '-1', href : 'javascript:;', 'class' : 'mceOpen ' + s['class'], onclick : "return false;", onmousedown : 'return false;', title : s.title}, h1) + '</td>';\r
\r
h += '</tr></tbody>';\r
- h = DOM.createHTML('table', {id : t.id, role: 'presentation', tabindex: '0', 'class' : 'mceSplitButton mceSplitButtonEnabled ' + s['class'], cellpadding : '0', cellspacing : '0', title : s.title}, h);\r
- return DOM.createHTML('span', {role: 'button', 'aria-labelledby': t.id + '_voice', 'aria-haspopup': 'true'}, h);\r
+ h = DOM.createHTML('table', { role: 'presentation', 'class' : 'mceSplitButton mceSplitButtonEnabled ' + s['class'], cellpadding : '0', cellspacing : '0', title : s.title}, h);\r
+ return DOM.createHTML('div', {id : t.id, role: 'button', tabindex: '0', 'aria-labelledby': t.id + '_voice', 'aria-haspopup': 'true'}, h);\r
},\r
\r
postRender : function() {\r
}\r
\r
n = DOM.add(tr, 'td');\r
- n = DOM.add(n, 'a', {\r
- role : 'option',\r
+ var settings = {\r
href : 'javascript:;',\r
style : {\r
backgroundColor : '#' + c\r
},\r
'title': t.editor.getLang('colors.' + c, c),\r
'data-mce-color' : '#' + c\r
- });\r
+ };\r
+\r
+ // adding a proper ARIA role = button causes JAWS to read things incorrectly on IE.\r
+ if (!tinymce.isIE ) {\r
+ settings['role']= 'option';\r
+ }\r
+\r
+ n = DOM.add(n, 'a', settings);\r
\r
if (t.editor.forcedHighContrastMode) {\r
n = DOM.add(n, 'canvas', { width: 16, height: 16, 'aria-hidden': 'true' });\r
},\r
\r
focus : function() {\r
- this.keyNav.focus();\r
+ var t = this;\r
+ dom.get(t.id).focus();\r
},\r
\r
postRender : function() {\r
root: t.id,\r
items: items,\r
onCancel: function() {\r
+ //Move focus if webkit so that navigation back will read the item.\r
+ if (tinymce.isWebKit) {\r
+ dom.get(t.editor.id+"_ifr").focus();\r
+ }\r
t.editor.focus();\r
},\r
excludeFromTabOrder: !t.settings.tab_focus_toolbar\r
Dispatcher = tinymce.util.Dispatcher, each = tinymce.each, isGecko = tinymce.isGecko,\r
isIE = tinymce.isIE, isWebKit = tinymce.isWebKit, is = tinymce.is,\r
ThemeManager = tinymce.ThemeManager, PluginManager = tinymce.PluginManager,\r
- inArray = tinymce.inArray, grep = tinymce.grep, explode = tinymce.explode;\r
+ inArray = tinymce.inArray, grep = tinymce.grep, explode = tinymce.explode, VK = tinymce.VK;\r
\r
tinymce.create('tinymce.Editor', {\r
Editor : function(id, s) {\r
\r
'onPostRender',\r
\r
+ 'onLoad',\r
+\r
'onInit',\r
\r
'onRemove',\r
\r
'onVisualAid',\r
\r
- 'onSetProgressState'\r
+ 'onSetProgressState',\r
+\r
+ 'onSetAttrib'\r
], function(e) {\r
t[e] = new Dispatcher(t);\r
});\r
visual_table_class : 'mceItemTable',\r
visual : 1,\r
font_size_style_values : 'xx-small,x-small,small,medium,large,x-large,xx-large',\r
+ font_size_legacy_values : 'xx-small,small,medium,large,x-large,xx-large,300%', // See: http://www.w3.org/TR/CSS2/fonts.html#propdef-font-size\r
apply_source_formatting : 1,\r
directionality : 'ltr',\r
forced_root_block : 'p',\r
\r
t.iframeHTML += '<meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />';\r
\r
+ // Load the CSS by injecting them into the HTML this will reduce "flicker"\r
+ for (i = 0; i < t.contentCSS.length; i++) {\r
+ t.iframeHTML += '<link type="text/css" rel="stylesheet" href="' + t.contentCSS[i] + '" />';\r
+ }\r
+\r
+ t.contentCSS = [];\r
+\r
bi = s.body_id || 'tinymce';\r
if (bi.indexOf('=') != -1) {\r
bi = t.getParam('body_id', '', 'hash');\r
bc = bc[t.id] || '';\r
}\r
\r
- t.iframeHTML += '</head><body id="' + bi + '" class="mceContentBody ' + bc + '"><br></body></html>';\r
+ t.iframeHTML += '</head><body id="' + bi + '" class="mceContentBody ' + bc + '" onload="window.parent.tinyMCE.get(\'' + t.id + '\').onLoad.dispatch();"><br></body></html>';\r
\r
// Domain relaxing enabled, then set document domain\r
if (tinymce.relaxedDomain && (isIE || (tinymce.isOpera && parseFloat(opera.version()) < 11))) {\r
// We need to write the contents here in IE since multiple writes messes up refresh button and back button\r
- u = 'javascript:(function(){document.open();document.domain="' + document.domain + '";var ed = window.parent.tinyMCE.get("' + t.id + '");document.write(ed.iframeHTML);document.close();ed.setupIframe();})()'; \r
+ u = 'javascript:(function(){document.open();document.domain="' + document.domain + '";var ed = window.parent.tinyMCE.get("' + t.id + '");document.write(ed.iframeHTML);document.close();ed.setupIframe();})()';\r
}\r
\r
// Create iframe\r
\r
// Setup iframe body\r
if (!isIE || !tinymce.relaxedDomain) {\r
- // Fix for a focus bug in FF 3.x where the body element\r
- // wouldn't get proper focus if the user clicked on the HTML element\r
- if (isGecko && !Range.prototype.getClientRects) { // Detect getClientRects got introduced in FF 4\r
- t.onMouseDown.add(function(ed, e) {\r
- if (e.target.nodeName === "HTML") {\r
- var body = t.getBody();\r
-\r
- // Blur the body it's focused but not correctly focused\r
- body.blur();\r
-\r
- // Refocus the body after a little while\r
- setTimeout(function() {\r
- body.focus();\r
- }, 0);\r
- }\r
- });\r
- }\r
-\r
d.open();\r
d.write(t.iframeHTML);\r
d.close();\r
\r
// Keep scripts from executing\r
t.parser.addNodeFilter('script', function(nodes, name) {\r
- var i = nodes.length;\r
+ var i = nodes.length, node;\r
\r
- while (i--)\r
- nodes[i].attr('type', 'mce-text/javascript');\r
+ while (i--) {\r
+ node = nodes[i];\r
+ node.attr('type', 'mce-' + (node.attr('type') || 'text/javascript'));\r
+ }\r
});\r
\r
t.parser.addNodeFilter('#cdata', function(nodes, name) {\r
}\r
\r
t._refreshContentEditable();\r
- selection.normalize();\r
\r
// Is not content editable\r
if (!ce)\r
if (!/^(mceAddUndoLevel|mceEndUndoLevel|mceBeginUndoLevel|mceRepaint|SelectAll)$/.test(cmd) && (!a || !a.skip_focus))\r
t.focus();\r
\r
- o = {};\r
- t.onBeforeExecCommand.dispatch(t, cmd, ui, val, o);\r
- if (o.terminate)\r
+ a = extend({}, a);\r
+ t.onBeforeExecCommand.dispatch(t, cmd, ui, val, a);\r
+ if (a.terminate)\r
return false;\r
\r
// Command callback\r
t.onMouseDown.add(setOpts);\r
}\r
\r
- t.onClick.add(function(ed, e) {\r
- e = e.target;\r
-\r
- // Workaround for bug, http://bugs.webkit.org/show_bug.cgi?id=12250\r
- // WebKit can't even do simple things like selecting an image\r
- // Needs tobe the setBaseAndExtend or it will fail to select floated images\r
- if (tinymce.isWebKit && e.nodeName == 'IMG')\r
- t.selection.getSel().setBaseAndExtent(e, 0, e, 1);\r
-\r
- if (e.nodeName == 'A' && dom.hasClass(e, 'mceItemAnchor'))\r
- t.selection.select(e);\r
-\r
- t.nodeChanged();\r
- });\r
-\r
// Add node change handlers\r
t.onMouseUp.add(t.nodeChanged);\r
//t.onClick.add(t.nodeChanged);\r
\r
// Add block quote deletion handler\r
t.onKeyDown.add(function(ed, e) {\r
- // Was the BACKSPACE key pressed?\r
- if (e.keyCode != 8)\r
+ if (e.keyCode != VK.BACKSPACE)\r
+ return;\r
+\r
+ var rng = ed.selection.getRng();\r
+ if (!rng.collapsed)\r
return;\r
\r
- var n = ed.selection.getRng().startContainer;\r
- var offset = ed.selection.getRng().startOffset;\r
+ var n = rng.startContainer;\r
+ var offset = rng.startOffset;\r
\r
while (n && n.nodeType && n.nodeType != 1 && n.parentNode)\r
n = n.parentNode;\r
- \r
+\r
// Is the cursor at the beginning of a blockquote?\r
if (n && n.parentNode && n.parentNode.tagName === 'BLOCKQUOTE' && n.parentNode.firstChild == n && offset == 0) {\r
// Remove the blockquote\r
ed.formatter.toggle('blockquote', null, n.parentNode);\r
\r
// Move the caret to the beginning of n\r
- var rng = ed.selection.getRng();\r
rng.setStart(n, 0);\r
rng.setEnd(n, 0);\r
ed.selection.setRng(rng);\r
ed.selection.collapse(false);\r
}\r
});\r
- \r
+\r
\r
\r
// Add reset handler\r
t.undoManager.add();\r
};\r
\r
- dom.bind(t.getDoc(), 'focusout', function(e) {\r
+ var focusLostFunc = tinymce.isGecko ? 'blur' : 'focusout';\r
+ dom.bind(t.getDoc(), focusLostFunc, function(e){\r
if (!t.removed && t.undoManager.typing)\r
addUndo();\r
});\r
});\r
}\r
\r
- // Fire a nodeChanged when the selection is changed on WebKit this fixes selection issues on iOS5\r
- // It only fires the nodeChange event every 50ms since it would other wise update the UI when you type and it hogs the CPU\r
- if (tinymce.isWebKit) {\r
- dom.bind(t.getDoc(), 'selectionchange', function() {\r
- if (t.selectionTimer) {\r
- clearTimeout(t.selectionTimer);\r
- t.selectionTimer = 0;\r
- }\r
-\r
- t.selectionTimer = window.setTimeout(function() {\r
- t.nodeChanged();\r
- }, 50);\r
- });\r
- }\r
-\r
// Bug fix for FireFox keeping styles from end of selection instead of start.\r
if (tinymce.isGecko) {\r
function getAttributeApplyFunction() {\r
var target = t.selection.getStart();\r
\r
if (target !== t.getBody()) {\r
- t.dom.removeAllAttribs(target);\r
+ t.dom.setAttrib(target, "style", null);\r
\r
- each(template, function(attr) {\r
- target.setAttributeNode(attr.cloneNode(true));\r
- });\r
+ each(template, function(attr) {\r
+ target.setAttributeNode(attr.cloneNode(true));\r
+ });\r
}\r
};\r
}\r
var parser, serializer, parentNode, rootNode, fragment, args,\r
marker, nodeRect, viewPortRect, rng, node, node2, bookmarkHtml, viewportBodyElement;\r
\r
+ //selection.normalize();\r
+\r
// Setup parser and serializer\r
parser = editor.parser;\r
serializer = new tinymce.html.Serializer({}, editor.schema);\r
addCommands({\r
// Override justify commands\r
'JustifyLeft,JustifyCenter,JustifyRight,JustifyFull' : function(command) {\r
- return isFormatMatch('align' + command.substring(7));\r
+ var name = 'align' + command.substring(7);\r
+ // Use Formatter.matchNode instead of Formatter.match so that we don't match on parent node. This fixes bug where for both left\r
+ // and right align buttons can be active. This could occur when selected nodes have align right and the parent has align left.\r
+ var nodes = selection.isCollapsed() ? [selection.getNode()] : selection.getSelectedBlocks();\r
+ var matches = tinymce.map(nodes, function(node) {\r
+ return !!formatter.matchNode(node, name);\r
+ });\r
+ return tinymce.inArray(matches, TRUE) !== -1;\r
},\r
\r
'Bold,Italic,Underline,Strikethrough,Superscript,Subscript' : function(command) {\r
\r
id = t.prefix + id;\r
\r
- if (ed.settings.use_native_selects)\r
+\r
+ function useNativeListForAccessibility(ed) {\r
+ return ed.settings.use_accessible_selects && !tinymce.isGecko\r
+ }\r
+\r
+ if (ed.settings.use_native_selects || useNativeListForAccessibility(ed))\r
c = new tinymce.ui.NativeListBox(id, s);\r
else {\r
cls = cc || t._cls.listbox || tinymce.ui.ListBox;\r
MCE_ATTR_RE = /^(src|href|style)$/,\r
FALSE = false,\r
TRUE = true,\r
- undefined,\r
- pendingFormats = {apply : [], remove : []};\r
+ undefined;\r
\r
function isArray(obj) {\r
return obj instanceof Array;\r
};\r
\r
function isCaretNode(node) {\r
- return node.nodeType === 1 && (node.face === 'mceinline' || node.style.fontFamily === 'mceinline');\r
+ return node.nodeType === 1 && node.id === '_mce_caret';\r
};\r
\r
// Public functions\r
function apply(name, vars, node) {\r
var formatList = get(name), format = formatList[0], bookmark, rng, i, isCollapsed = selection.isCollapsed();\r
\r
- function moveStart(rng) {\r
- var container = rng.startContainer,\r
- offset = rng.startOffset,\r
- walker, node;\r
-\r
- // Move startContainer/startOffset in to a suitable node\r
- if (container.nodeType == 1 || container.nodeValue === "") {\r
- container = container.nodeType == 1 ? container.childNodes[offset] : container;\r
-\r
- // Might fail if the offset is behind the last element in it's container\r
- if (container) {\r
- walker = new TreeWalker(container, container.parentNode);\r
- for (node = walker.current(); node; node = walker.next()) {\r
- if (node.nodeType == 3 && !isWhiteSpaceNode(node)) {\r
- rng.setStart(node, 0);\r
- break;\r
- }\r
- }\r
- }\r
- }\r
-\r
- return rng;\r
- };\r
-\r
function setElementFormat(elm, fmt) {\r
fmt = fmt || format;\r
\r
}\r
};\r
\r
- function applyRngStyle(rng, bookmark) {\r
+ function applyRngStyle(rng, bookmark, node_specific) {\r
var newWrappers = [], wrapName, wrapElm;\r
\r
// Setup wrapper element\r
\r
// Is it valid to wrap this item\r
if (isValid(wrapName, nodeName) && isValid(parentName, wrapName) &&\r
- !(node.nodeType === 3 && node.nodeValue.length === 1 && node.nodeValue.charCodeAt(0) === 65279)) {\r
+ !(!node_specific && node.nodeType === 3 && node.nodeValue.length === 1 && node.nodeValue.charCodeAt(0) === 65279) && !isCaretNode(node)) {\r
// Start wrapping\r
if (!currentWrapElm) {\r
// Wrap the node\r
\r
if (format) {\r
if (node) {\r
- rng = dom.createRng();\r
-\r
- rng.setStartBefore(node);\r
- rng.setEndAfter(node);\r
-\r
- applyRngStyle(expandRng(rng, formatList));\r
+ if (node.nodeType) {\r
+ rng = dom.createRng();\r
+ rng.setStartBefore(node);\r
+ rng.setEndAfter(node);\r
+ applyRngStyle(expandRng(rng, formatList), null, true);\r
+ } else {\r
+ applyRngStyle(node, null, true);\r
+ }\r
} else {\r
if (!isCollapsed || !format.inline || dom.select('td.mceSelected,th.mceSelected').length) {\r
// Obtain selection node before selection is unselected by applyRngStyle()\r
}\r
\r
selection.moveToBookmark(bookmark);\r
- selection.setRng(moveStart(selection.getRng(TRUE)));\r
+ moveStart(selection.getRng(TRUE));\r
ed.nodeChanged();\r
} else\r
performCaretAction('apply', name, vars);\r
\r
function remove(name, vars, node) {\r
var formatList = get(name), format = formatList[0], bookmark, i, rng;\r
- function moveStart(rng) {\r
- var container = rng.startContainer,\r
- offset = rng.startOffset,\r
- walker, node, nodes, tmpNode;\r
-\r
- // Convert text node into index if possible\r
- if (container.nodeType == 3 && offset >= container.nodeValue.length - 1) {\r
- container = container.parentNode;\r
- offset = nodeIndex(container) + 1;\r
- }\r
-\r
- // Move startContainer/startOffset in to a suitable node\r
- if (container.nodeType == 1) {\r
- nodes = container.childNodes;\r
- container = nodes[Math.min(offset, nodes.length - 1)];\r
- walker = new TreeWalker(container);\r
-\r
- // If offset is at end of the parent node walk to the next one\r
- if (offset > nodes.length - 1)\r
- walker.next();\r
-\r
- for (node = walker.current(); node; node = walker.next()) {\r
- if (node.nodeType == 3 && !isWhiteSpaceNode(node)) {\r
- // IE has a "neat" feature where it moves the start node into the closest element\r
- // we can avoid this by inserting an element before it and then remove it after we set the selection\r
- tmpNode = dom.create('a', null, INVISIBLE_CHAR);\r
- node.parentNode.insertBefore(tmpNode, node);\r
-\r
- // Set selection and remove tmpNode\r
- rng.setStart(node, 0);\r
- selection.setRng(rng);\r
- dom.remove(tmpNode);\r
-\r
- return;\r
- }\r
- }\r
- }\r
- };\r
\r
// Merges the styles for each node\r
function process(node) {\r
\r
// Handle node\r
if (node) {\r
- rng = dom.createRng();\r
- rng.setStartBefore(node);\r
- rng.setEndAfter(node);\r
- removeRngStyle(rng);\r
+ if (node.nodeType) {\r
+ rng = dom.createRng();\r
+ rng.setStartBefore(node);\r
+ rng.setEndAfter(node);\r
+ removeRngStyle(rng);\r
+ } else {\r
+ removeRngStyle(node);\r
+ }\r
+\r
return;\r
}\r
\r
selection.moveToBookmark(bookmark);\r
\r
// Check if start element still has formatting then we are at: "<b>text|</b>text" and need to move the start into the next text node\r
- if (match(name, vars, selection.getStart())) {\r
+ if (format.inline && match(name, vars, selection.getStart())) {\r
moveStart(selection.getRng(true));\r
}\r
\r
ed.nodeChanged();\r
} else\r
performCaretAction('remove', name, vars);\r
+\r
+ // When you remove formatting from a table cell in WebKit (cell, not the contents of a cell) there is a rendering issue with column width\r
+ if (tinymce.isWebKit) {\r
+ ed.execCommand('mceCleanup');\r
+ }\r
};\r
\r
function toggle(name, vars, node) {\r
};\r
\r
function match(name, vars, node) {\r
- var startNode, i;\r
+ var startNode;\r
\r
function matchParents(node) {\r
// Find first node with similar format settings\r
if (node)\r
return matchParents(node);\r
\r
- // Check pending formats\r
- if (selection.isCollapsed()) {\r
- for (i = pendingFormats.apply.length - 1; i >= 0; i--) {\r
- if (pendingFormats.apply[i].name == name)\r
- return true;\r
- }\r
-\r
- for (i = pendingFormats.remove.length - 1; i >= 0; i--) {\r
- if (pendingFormats.remove[i].name == name)\r
- return false;\r
- }\r
-\r
- return matchParents(selection.getNode());\r
- }\r
-\r
// Check selected node\r
node = selection.getNode();\r
if (matchParents(node))\r
function matchAll(names, vars) {\r
var startElement, matchedFormatNames = [], checkedMap = {}, i, ni, name;\r
\r
- // If the selection is collapsed then check pending formats\r
- if (selection.isCollapsed()) {\r
- for (ni = 0; ni < names.length; ni++) {\r
- // If the name is to be removed, then stop it from being added\r
- for (i = pendingFormats.remove.length - 1; i >= 0; i--) {\r
- name = names[ni];\r
-\r
- if (pendingFormats.remove[i].name == name) {\r
- checkedMap[name] = true;\r
- break;\r
- }\r
- }\r
- }\r
-\r
- // If the format is to be applied\r
- for (i = pendingFormats.apply.length - 1; i >= 0; i--) {\r
- for (ni = 0; ni < names.length; ni++) {\r
- name = names[ni];\r
-\r
- if (!checkedMap[name] && pendingFormats.apply[i].name == name) {\r
- checkedMap[name] = true;\r
- matchedFormatNames.push(name);\r
- }\r
- }\r
- }\r
- }\r
-\r
// Check start of selection for formats\r
startElement = selection.getStart();\r
dom.getParent(startElement, function(node) {\r
};\r
\r
function isWhiteSpaceNode(node) {\r
- return node && node.nodeType === 3 && /^([\s\r\n]+|)$/.test(node.nodeValue);\r
+ return node && node.nodeType === 3 && /^([\t \r\n]+|)$/.test(node.nodeValue);\r
};\r
\r
function wrap(node, name, attrs) {\r
var startContainer = rng.startContainer,\r
startOffset = rng.startOffset,\r
endContainer = rng.endContainer,\r
- endOffset = rng.endOffset, sibling, lastIdx, leaf;\r
+ endOffset = rng.endOffset, sibling, lastIdx, leaf, endPoint;\r
\r
// This function walks up the tree if there is no siblings before/after the node\r
- function findParentContainer(container, child_name, sibling_name, root) {\r
- var parent, child;\r
-\r
- root = root || dom.getRoot();\r
+ function findParentContainer(start) {\r
+ var container, parent, child, sibling, siblingName;\r
\r
- for (;;) {\r
- // Check if we can move up are we at root level or body level\r
- parent = container.parentNode;\r
+ container = parent = start ? startContainer : endContainer;\r
+ siblingName = start ? 'previousSibling' : 'nextSibling';\r
+ root = dom.getRoot();\r
\r
- // Stop expanding on block elements or root depending on format\r
- if (parent == root || (!format[0].block_expand && isBlock(parent)))\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
return container;\r
+ }\r
+ }\r
\r
- for (sibling = parent[child_name]; sibling && sibling != container; sibling = sibling[sibling_name]) {\r
- if (sibling.nodeType == 1 && !isBookmarkNode(sibling))\r
- return container;\r
+ for (;;) {\r
+ // Stop expanding on block elements\r
+ if (!format[0].block_expand && isBlock(parent))\r
+ return parent;\r
+\r
+ // Walk left/right\r
+ for (sibling = parent[siblingName]; sibling; sibling = sibling[siblingName]) {\r
+ if (!isBookmarkNode(sibling) && !isWhiteSpaceNode(sibling)) {\r
+ return parent;\r
+ }\r
+ }\r
\r
- if (sibling.nodeType == 3 && !isWhiteSpaceNode(sibling))\r
- return container;\r
+ // Check if we can move up are we at root level or body level\r
+ if (parent.parentNode == root) {\r
+ container = parent;\r
+ break;\r
}\r
\r
- container = container.parentNode;\r
+ parent = parent.parentNode;\r
}\r
\r
return container;\r
}\r
\r
// Exclude bookmark nodes if possible\r
- if (isBookmarkNode(startContainer.parentNode))\r
- startContainer = startContainer.parentNode;\r
-\r
- if (isBookmarkNode(startContainer))\r
+ if (isBookmarkNode(startContainer.parentNode) || isBookmarkNode(startContainer)) {\r
+ startContainer = isBookmarkNode(startContainer) ? startContainer : startContainer.parentNode;\r
startContainer = startContainer.nextSibling || startContainer;\r
\r
- if (isBookmarkNode(endContainer.parentNode)) {\r
- endOffset = dom.nodeIndex(endContainer);\r
- endContainer = endContainer.parentNode;\r
+ if (startContainer.nodeType == 3)\r
+ startOffset = 0;\r
}\r
\r
- if (isBookmarkNode(endContainer) && endContainer.previousSibling) {\r
- endContainer = endContainer.previousSibling;\r
- endOffset = endContainer.length;\r
+ if (isBookmarkNode(endContainer.parentNode) || isBookmarkNode(endContainer)) {\r
+ endContainer = isBookmarkNode(endContainer) ? endContainer : endContainer.parentNode;\r
+ endContainer = endContainer.previousSibling || endContainer;\r
+\r
+ if (endContainer.nodeType == 3)\r
+ endOffset = endContainer.length;\r
}\r
\r
if (format[0].inline) {\r
+ if (rng.collapsed) {\r
+ function findWordEndPoint(container, offset, start) {\r
+ var walker, node, pos, lastTextNode;\r
+\r
+ function findSpace(node, offset) {\r
+ var pos, pos2, str = node.nodeValue;\r
+\r
+ if (typeof(offset) == "undefined") {\r
+ offset = start ? str.length : 0;\r
+ }\r
+\r
+ if (start) {\r
+ pos = str.lastIndexOf(' ', offset);\r
+ pos2 = str.lastIndexOf('\u00a0', offset);\r
+ pos = pos > pos2 ? pos : pos2;\r
+\r
+ // Include the space on remove to avoid tag soup\r
+ if (pos !== -1 && !remove) {\r
+ pos++;\r
+ }\r
+ } else {\r
+ pos = str.indexOf(' ', offset);\r
+ pos2 = str.indexOf('\u00a0', offset);\r
+ pos = pos !== -1 && (pos2 === -1 || pos < pos2) ? pos : pos2;\r
+ }\r
+\r
+ return pos;\r
+ };\r
+\r
+ if (container.nodeType === 3) {\r
+ pos = findSpace(container, offset);\r
+\r
+ if (pos !== -1) {\r
+ return {container : container, offset : pos};\r
+ }\r
+\r
+ lastTextNode = container;\r
+ }\r
+\r
+ // Walk the nodes inside the block\r
+ walker = new TreeWalker(container, dom.getParent(container, isBlock) || ed.getBody());\r
+ while (node = walker[start ? 'prev' : 'next']()) {\r
+ if (node.nodeType === 3) {\r
+ lastTextNode = node;\r
+ pos = findSpace(node);\r
+\r
+ if (pos !== -1) {\r
+ return {container : node, offset : pos};\r
+ }\r
+ } else if (isBlock(node)) {\r
+ break;\r
+ }\r
+ }\r
+\r
+ if (lastTextNode) {\r
+ if (start) {\r
+ offset = 0;\r
+ } else {\r
+ offset = lastTextNode.length;\r
+ }\r
+\r
+ return {container: lastTextNode, offset: offset};\r
+ }\r
+ }\r
+\r
+ // Expand left to closest word boundery\r
+ endPoint = findWordEndPoint(startContainer, startOffset, true);\r
+ if (endPoint) {\r
+ startContainer = endPoint.container;\r
+ startOffset = endPoint.offset;\r
+ }\r
+\r
+ // Expand right to closest word boundery\r
+ endPoint = findWordEndPoint(endContainer, endOffset);\r
+ if (endPoint) {\r
+ endContainer = endPoint.container;\r
+ endOffset = endPoint.offset;\r
+ }\r
+ }\r
+\r
// Avoid applying formatting to a trailing space.\r
leaf = findLeaf(endContainer, endOffset);\r
if (leaf.node) {\r
endContainer = leaf.node;\r
endContainer.splitText(leaf.offset - 1);\r
} else if (leaf.node.previousSibling) {\r
- endContainer = leaf.node.previousSibling;\r
+ // TODO: Figure out why this is in here\r
+ //endContainer = leaf.node.previousSibling;\r
}\r
}\r
}\r
}\r
- \r
+\r
// Move start/end point up the tree if the leaves are sharp and if we are in different containers\r
// Example * becomes !: !<p><b><i>*text</i><i>text*</i></b></p>!\r
// This will reduce the number of wrapper elements that needs to be created\r
// Move start point up the tree\r
if (format[0].inline || format[0].block_expand) {\r
- startContainer = findParentContainer(startContainer, 'firstChild', 'nextSibling');\r
- endContainer = findParentContainer(endContainer, 'lastChild', 'previousSibling');\r
+ if (!format[0].inline || (startContainer.nodeType != 3 || startOffset === 0)) {\r
+ startContainer = findParentContainer(true);\r
+ }\r
+\r
+ if (!format[0].inline || (endContainer.nodeType != 3 || endOffset === endContainer.nodeValue.length)) {\r
+ endContainer = findParentContainer();\r
+ }\r
}\r
\r
// Expand start/end container to matching selector\r
// Non block element then try to expand up the leaf\r
if (format[0].block) {\r
if (!isBlock(startContainer))\r
- startContainer = findParentContainer(startContainer, 'firstChild', 'nextSibling');\r
+ startContainer = findParentContainer(true);\r
\r
if (!isBlock(endContainer))\r
- endContainer = findParentContainer(endContainer, 'lastChild', 'previousSibling');\r
+ endContainer = findParentContainer();\r
}\r
}\r
\r
};\r
\r
function getContainer(rng, start) {\r
- var container, offset, lastIdx;\r
+ var container, offset, lastIdx, walker;\r
\r
container = rng[start ? 'startContainer' : 'endContainer'];\r
offset = rng[start ? 'startOffset' : 'endOffset'];\r
container = container.childNodes[offset > lastIdx ? lastIdx : offset];\r
}\r
\r
+ // If start text node is excluded then walk to the next node\r
+ if (container.nodeType === 3 && start && offset >= container.nodeValue.length) {\r
+ container = new TreeWalker(container, ed.getBody()).next() || container;\r
+ }\r
+\r
+ // If end text node is excluded then walk to the previous node\r
+ if (container.nodeType === 3 && !start && offset == 0) {\r
+ container = new TreeWalker(container, ed.getBody()).prev() || container;\r
+ }\r
+\r
return container;\r
};\r
\r
function performCaretAction(type, name, vars) {\r
- var i, currentPendingFormats = pendingFormats[type],\r
- otherPendingFormats = pendingFormats[type == 'apply' ? 'remove' : 'apply'];\r
+ var invisibleChar, caretContainerId = '_mce_caret', debug = ed.settings.caret_debug;\r
+\r
+ // Setup invisible character use zero width space on Gecko since it doesn't change the heigt of the container\r
+ invisibleChar = tinymce.isGecko ? '\u200B' : INVISIBLE_CHAR;\r
+\r
+ // Creates a caret container bogus element\r
+ function createCaretContainer(fill) {\r
+ var caretContainer = dom.create('span', {id: caretContainerId, 'data-mce-bogus': true, style: debug ? 'color:red' : ''});\r
+\r
+ if (fill) {\r
+ caretContainer.appendChild(ed.getDoc().createTextNode(invisibleChar));\r
+ }\r
+\r
+ return caretContainer;\r
+ };\r
+\r
+ function isCaretContainerEmpty(node, nodes) {\r
+ while (node) {\r
+ if ((node.nodeType === 3 && node.nodeValue !== invisibleChar) || node.childNodes.length > 1) {\r
+ return false;\r
+ }\r
\r
- function hasPending() {\r
- return pendingFormats.apply.length || pendingFormats.remove.length;\r
+ // Collect nodes\r
+ if (nodes && node.nodeType === 1) {\r
+ nodes.push(node);\r
+ }\r
+\r
+ node = node.firstChild;\r
+ }\r
+\r
+ return true;\r
};\r
+ \r
+ // Returns any parent caret container element\r
+ function getParentCaretContainer(node) {\r
+ while (node) {\r
+ if (node.id === caretContainerId) {\r
+ return node;\r
+ }\r
\r
- function resetPending() {\r
- pendingFormats.apply = [];\r
- pendingFormats.remove = [];\r
+ node = node.parentNode;\r
+ }\r
};\r
\r
- function perform(caret_node) {\r
- // Apply pending formats\r
- each(pendingFormats.apply.reverse(), function(item) {\r
- apply(item.name, item.vars, caret_node);\r
+ // Finds the first text node in the specified node\r
+ function findFirstTextNode(node) {\r
+ var walker;\r
\r
- // Colored nodes should be underlined so that the color of the underline matches the text color.\r
- if (item.name === 'forecolor' && item.vars.value)\r
- processUnderlineAndColor(caret_node.parentNode);\r
- });\r
+ if (node) {\r
+ walker = new TreeWalker(node, node);\r
\r
- // Remove pending formats\r
- each(pendingFormats.remove.reverse(), function(item) {\r
- remove(item.name, item.vars, caret_node);\r
- });\r
+ for (node = walker.current(); node; node = walker.next()) {\r
+ if (node.nodeType === 3) {\r
+ return node;\r
+ }\r
+ }\r
+ }\r
+ };\r
+\r
+ // Removes the caret container for the specified node or all on the current document\r
+ function removeCaretContainer(node, move_caret) {\r
+ var child, rng;\r
+\r
+ if (!node) {\r
+ node = getParentCaretContainer(selection.getStart());\r
+\r
+ if (!node) {\r
+ while (node = dom.get(caretContainerId)) {\r
+ removeCaretContainer(node, false);\r
+ }\r
+ }\r
+ } else {\r
+ rng = selection.getRng(true);\r
+\r
+ if (isCaretContainerEmpty(node)) {\r
+ if (move_caret !== false) {\r
+ rng.setStartBefore(node);\r
+ rng.setEndBefore(node);\r
+ }\r
+\r
+ dom.remove(node);\r
+ } else {\r
+ child = findFirstTextNode(node);\r
+\r
+ if (child.nodeValue.charAt(0) === INVISIBLE_CHAR) {\r
+ child = child.deleteData(0, 1);\r
+ }\r
+\r
+ dom.remove(node, 1);\r
+ }\r
+\r
+ selection.setRng(rng);\r
+ }\r
+ };\r
+ \r
+ // Applies formatting to the caret postion\r
+ function applyCaretFormat() {\r
+ var rng, caretContainer, textNode, offset, bookmark, container, text;\r
+\r
+ rng = selection.getRng(true);\r
+ offset = rng.startOffset;\r
+ container = rng.startContainer;\r
+ text = container.nodeValue;\r
\r
- dom.remove(caret_node, 1);\r
- resetPending();\r
+ caretContainer = getParentCaretContainer(selection.getStart());\r
+ if (caretContainer) {\r
+ textNode = findFirstTextNode(caretContainer);\r
+ }\r
+\r
+ // Expand to word is caret is in the middle of a text node and the char before/after is a alpha numeric character\r
+ if (text && offset > 0 && offset < text.length && /\w/.test(text.charAt(offset)) && /\w/.test(text.charAt(offset - 1))) {\r
+ // Get bookmark of caret position\r
+ bookmark = selection.getBookmark();\r
+\r
+ // Collapse bookmark range (WebKit)\r
+ rng.collapse(true);\r
+\r
+ // Expand the range to the closest word and split it at those points\r
+ rng = expandRng(rng, get(name));\r
+ rng = rangeUtils.split(rng);\r
+\r
+ // Apply the format to the range\r
+ apply(name, vars, rng);\r
+\r
+ // Move selection back to caret position\r
+ selection.moveToBookmark(bookmark);\r
+ } else {\r
+ if (!caretContainer || textNode.nodeValue !== invisibleChar) {\r
+ caretContainer = createCaretContainer(true);\r
+ textNode = caretContainer.firstChild;\r
+\r
+ rng.insertNode(caretContainer);\r
+ offset = 1;\r
+\r
+ apply(name, vars, caretContainer);\r
+ } else {\r
+ apply(name, vars, caretContainer);\r
+ }\r
+\r
+ // Move selection to text node\r
+ selection.setCursorLocation(textNode, offset);\r
+ }\r
};\r
\r
- // Check if it already exists then ignore it\r
- for (i = currentPendingFormats.length - 1; i >= 0; i--) {\r
- if (currentPendingFormats[i].name == name)\r
+ function removeCaretFormat() {\r
+ var rng = selection.getRng(true), container, offset, bookmark,\r
+ hasContentAfter, node, formatNode, parents = [], i, caretContainer;\r
+\r
+ container = rng.startContainer;\r
+ offset = rng.startOffset;\r
+ node = container;\r
+\r
+ if (container.nodeType == 3) {\r
+ if (offset != container.nodeValue.length || container.nodeValue === invisibleChar) {\r
+ hasContentAfter = true;\r
+ }\r
+\r
+ node = node.parentNode;\r
+ }\r
+\r
+ while (node) {\r
+ if (matchNode(node, name, vars)) {\r
+ formatNode = node;\r
+ break;\r
+ }\r
+\r
+ if (node.nextSibling) {\r
+ hasContentAfter = true;\r
+ }\r
+\r
+ parents.push(node);\r
+ node = node.parentNode;\r
+ }\r
+\r
+ // Node doesn't have the specified format\r
+ if (!formatNode) {\r
return;\r
- }\r
+ }\r
\r
- currentPendingFormats.push({name : name, vars : vars});\r
+ // Is there contents after the caret then remove the format on the element\r
+ if (hasContentAfter) {\r
+ // Get bookmark of caret position\r
+ bookmark = selection.getBookmark();\r
\r
- // Check if it's in the other type, then remove it\r
- for (i = otherPendingFormats.length - 1; i >= 0; i--) {\r
- if (otherPendingFormats[i].name == name)\r
- otherPendingFormats.splice(i, 1);\r
- }\r
+ // Collapse bookmark range (WebKit)\r
+ rng.collapse(true);\r
\r
- // Pending apply or remove formats\r
- if (hasPending()) {\r
- ed.getDoc().execCommand('FontName', false, 'mceinline');\r
- pendingFormats.lastRng = selection.getRng();\r
+ // Expand the range to the closest word and split it at those points\r
+ rng = expandRng(rng, get(name), true);\r
+ rng = rangeUtils.split(rng);\r
\r
- // IE will convert the current word\r
- each(dom.select('font,span'), function(node) {\r
- var bookmark;\r
+ // Remove the format from the range\r
+ remove(name, vars, rng);\r
\r
- if (isCaretNode(node)) {\r
- bookmark = selection.getBookmark();\r
- perform(node);\r
- selection.moveToBookmark(bookmark);\r
- ed.nodeChanged();\r
+ // Move selection back to caret position\r
+ selection.moveToBookmark(bookmark);\r
+ } else {\r
+ caretContainer = createCaretContainer();\r
+\r
+ node = caretContainer;\r
+ for (i = parents.length - 1; i >= 0; i--) {\r
+ node.appendChild(parents[i].cloneNode(false));\r
+ node = node.firstChild;\r
+ }\r
+\r
+ // Insert invisible character into inner most format element\r
+ node.appendChild(dom.doc.createTextNode(invisibleChar));\r
+ node = node.firstChild;\r
+\r
+ // Insert caret container after the formated node\r
+ dom.insertAfter(caretContainer, formatNode);\r
+\r
+ // Move selection to text node\r
+ selection.setCursorLocation(node, 1);\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
+ ed.onBeforeGetContent.addToTop(function() {\r
+ var nodes = [], i;\r
+\r
+ if (isCaretContainerEmpty(getParentCaretContainer(selection.getStart()), nodes)) {\r
+ // Mark children\r
+ i = nodes.length;\r
+ while (i--) {\r
+ dom.setAttrib(nodes[i], 'data-mce-bogus', '1');\r
+ }\r
}\r
});\r
\r
- // Only register listeners once if we need to\r
- if (!pendingFormats.isListening && hasPending()) {\r
- pendingFormats.isListening = true;\r
- function performPendingFormat(node, textNode) {\r
- var rng = dom.createRng();\r
- perform(node);\r
+ // Remove caret container on mouse up and on key up\r
+ tinymce.each('onMouseUp onKeyUp'.split(' '), function(name) {\r
+ ed[name].addToTop(function() {\r
+ removeCaretContainer();\r
+ });\r
+ });\r
\r
- rng.setStart(textNode, textNode.nodeValue.length);\r
- rng.setEnd(textNode, textNode.nodeValue.length);\r
- selection.setRng(rng);\r
- ed.nodeChanged();\r
+ // Remove caret container on keydown and it's a backspace, enter or left/right arrow keys\r
+ ed.onKeyDown.addToTop(function(ed, e) {\r
+ var keyCode = e.keyCode;\r
+\r
+ if (keyCode == 8 || keyCode == 37 || keyCode == 39) {\r
+ removeCaretContainer(getParentCaretContainer(selection.getStart()));\r
}\r
- var enterKeyPressed = false;\r
+ });\r
\r
- each('onKeyDown,onKeyUp,onKeyPress,onMouseUp'.split(','), function(event) {\r
- ed[event].addToTop(function(ed, e) {\r
- if (e.keyCode==13 && !e.shiftKey) {\r
- enterKeyPressed = true;\r
- return;\r
- }\r
- // Do we have pending formats and is the selection moved has moved\r
- if (hasPending() && !tinymce.dom.RangeUtils.compareRanges(pendingFormats.lastRng, selection.getRng())) {\r
- var foundCaret = false;\r
- each(dom.select('font,span'), function(node) {\r
- var textNode, rng;\r
-\r
- // Look for marker\r
- if (isCaretNode(node)) {\r
- foundCaret = true;\r
- textNode = node.firstChild;\r
-\r
- // Find the first text node within node\r
- while (textNode && textNode.nodeType != 3)\r
- textNode = textNode.firstChild;\r
-\r
- if (textNode) \r
- performPendingFormat(node, textNode);\r
- else\r
- dom.remove(node);\r
- }\r
- });\r
- \r
- // no caret - so we are \r
- if (enterKeyPressed && !foundCaret) {\r
- var node = selection.getNode();\r
- var textNode = node;\r
-\r
- // Find the first text node within node\r
- while (textNode && textNode.nodeType != 3)\r
- textNode = textNode.firstChild;\r
- if (textNode) {\r
- node=textNode.parentNode;\r
- while (!isBlock(node)){\r
- node=node.parentNode;\r
- }\r
- performPendingFormat(node, textNode);\r
- }\r
- }\r
+ self._hasCaretEvents = true;\r
+ }\r
\r
- // Always unbind and clear pending styles on keyup\r
- if (e.type == 'keyup' || e.type == 'mouseup') {\r
- resetPending();\r
- enterKeyPressed=false;\r
- }\r
- }\r
- });\r
- });\r
+ // Do apply or remove caret format\r
+ if (type == "apply") {\r
+ applyCaretFormat();\r
+ } else {\r
+ removeCaretFormat();\r
+ }\r
+ };\r
+\r
+ function moveStart(rng) {\r
+ var container = rng.startContainer,\r
+ offset = rng.startOffset,\r
+ walker, node, nodes, tmpNode;\r
+\r
+ // Convert text node into index if possible\r
+ if (container.nodeType == 3 && offset >= container.nodeValue.length - 1) {\r
+ container = container.parentNode;\r
+ offset = nodeIndex(container) + 1;\r
+ }\r
+\r
+ // Move startContainer/startOffset in to a suitable node\r
+ if (container.nodeType == 1) {\r
+ nodes = container.childNodes;\r
+ container = nodes[Math.min(offset, nodes.length - 1)];\r
+ walker = new TreeWalker(container);\r
+\r
+ // If offset is at end of the parent node walk to the next one\r
+ if (offset > nodes.length - 1)\r
+ walker.next();\r
+\r
+ for (node = walker.current(); node; node = walker.next()) {\r
+ if (node.nodeType == 3 && !isWhiteSpaceNode(node)) {\r
+ // IE has a "neat" feature where it moves the start node into the closest element\r
+ // we can avoid this by inserting an element before it and then remove it after we set the selection\r
+ tmpNode = dom.create('a', null, INVISIBLE_CHAR);\r
+ node.parentNode.insertBefore(tmpNode, node);\r
+\r
+ // Set selection and remove tmpNode\r
+ rng.setStart(node, 0);\r
+ selection.setRng(rng);\r
+ dom.remove(tmpNode);\r
+\r
+ return;\r
+ }\r
}\r
}\r
};\r
+\r
};\r
})(tinymce);\r
\r
var filters, fontSizes, dom, settings = ed.settings;\r
\r
if (settings.inline_styles) {\r
- fontSizes = tinymce.explode(settings.font_size_style_values);\r
+ fontSizes = tinymce.explode(settings.font_size_legacy_values);\r
\r
function replaceWithSpan(node, styles) {\r
tinymce.each(styles, function(value, name) {\r