assets/precompiled/tinymce/tiny_mce_src.js in tinymce-rails-3.4.6 vs assets/precompiled/tinymce/tiny_mce_src.js in tinymce-rails-3.4.7

- old
+ new

@@ -3,13 +3,13 @@ undefined, isRegExpBroken = 'B'.replace(/A(.)|B/, '$1') === '$1'; var tinymce = { majorVersion : '3', - minorVersion : '4.6', + minorVersion : '4.7', - releaseDate : '2011-09-29', + releaseDate : '2011-11-03', _init : function() { var t = this, d = document, na = navigator, ua = na.userAgent, i, nl, n, base, p, v; t.isOpera = win.opera && opera.buildNumber; @@ -899,12 +899,15 @@ return v + ']'; } v = '{'; - for (i in o) - v += typeof o[i] != 'function' ? (v.length > 1 ? ',' + quote : quote) + i + quote +':' + serialize(o[i], quote) : ''; + for (i in o) { + if (o.hasOwnProperty(i)) { + v += typeof o[i] != 'function' ? (v.length > 1 ? ',' + quote : quote) + i + quote +':' + serialize(o[i], quote) : ''; + } + } return v + '}'; } return '' + o; @@ -921,10 +924,11 @@ } } }; })(); + tinymce.create('static tinymce.util.XHR', { send : function(o) { var x, t, w = window, c = 0; // Default settings @@ -1036,15 +1040,18 @@ } }); }()); (function(tinymce){ tinymce.VK = { - DELETE:46, - BACKSPACE:8 - + DELETE: 46, + BACKSPACE: 8, + ENTER: 13, + TAB: 9, + SPACEBAR: 32, + UP: 38, + DOWN: 40 } - })(tinymce); (function(tinymce) { var VK = tinymce.VK, BACKSPACE = VK.BACKSPACE, DELETE = VK.DELETE; @@ -1069,11 +1076,11 @@ // Locate root span element and clone it since it would otherwise get merged by the "apple-style-span" on delete/backspace if (blockElm) { node = blockElm.firstChild; // Ignore empty text nodes - while (node.nodeType == 3 && node.nodeValue.length == 0) + while (node && node.nodeType == 3 && node.nodeValue.length == 0) node = node.nextSibling; if (node && node.nodeName === 'SPAN') { clonedSpan = node.cloneNode(false); } @@ -1118,10 +1125,25 @@ ed.dom.bind(ed.getDoc(), 'focusin', function() { ed.selection.setRng(ed.selection.getRng()); }); }; + function removeHrOnBackspace(ed) { + ed.onKeyDown.add(function(ed, e) { + if (e.keyCode === BACKSPACE) { + if (ed.selection.isCollapsed() && ed.selection.getRng(true).startOffset === 0) { + var node = ed.selection.getNode(); + var previousSibling = node.previousSibling; + if (previousSibling && previousSibling.nodeName && previousSibling.nodeName.toLowerCase() === "hr") { + ed.dom.remove(previousSibling); + tinymce.dom.Event.cancel(e); + } + } + } + }) + } + function focusBody(ed) { // Fix for a focus bug in FF 3.x where the body element // wouldn't get proper focus if the user clicked on the HTML element if (!Range.prototype.getClientRects) { // Detect getClientRects got introduced in FF 4 ed.onMouseDown.add(function(ed, e) { @@ -1155,36 +1177,70 @@ ed.nodeChanged(); }); }; + function selectionChangeNodeChanged(ed) { + var lastRng, selectionTimer; + + ed.dom.bind(ed.getDoc(), 'selectionchange', function() { + if (selectionTimer) { + clearTimeout(selectionTimer); + selectionTimer = 0; + } + + selectionTimer = window.setTimeout(function() { + var rng = ed.selection.getRng(); + + // Compare the ranges to see if it was a real change or not + if (!lastRng || !tinymce.dom.RangeUtils.compareRanges(rng, lastRng)) { + ed.nodeChanged(); + lastRng = rng; + } + }, 50); + }); + } + + function ensureBodyHasRoleApplication(ed) { + document.body.setAttribute("role", "application"); + } + tinymce.create('tinymce.util.Quirks', { Quirks: function(ed) { // WebKit if (tinymce.isWebKit) { cleanupStylesWhenDeleting(ed); emptyEditorWhenDeleting(ed); inputMethodFocus(ed); selectControlElements(ed); + + // iOS + if (tinymce.isIDevice) { + selectionChangeNodeChanged(ed); + } } // IE if (tinymce.isIE) { + removeHrOnBackspace(ed); emptyEditorWhenDeleting(ed); + ensureBodyHasRoleApplication(ed); } // Gecko if (tinymce.isGecko) { + removeHrOnBackspace(ed); focusBody(ed); } } }); })(tinymce); + (function(tinymce) { var namedEntities, baseEntities, reverseEntities, - attrsCharsRegExp = /[&<>\"\u007E-\uD7FF]|[\uD800-\uDBFF][\uDC00-\uDFFF]/g, - textCharsRegExp = /[<>&\u007E-\uD7FF]|[\uD800-\uDBFF][\uDC00-\uDFFF]/g, + attrsCharsRegExp = /[&<>\"\u007E-\uD7FF\uE000-\uFFEF]|[\uD800-\uDBFF][\uDC00-\uDFFF]/g, + textCharsRegExp = /[<>&\u007E-\uD7FF\uE000-\uFFEF]|[\uD800-\uDBFF][\uDC00-\uDFFF]/g, rawCharsRegExp = /[<>&\"\']/g, entityRegExp = /&(#x|#)?([\w]+);/g, asciiMap = { 128 : "\u20AC", 130 : "\u201A", 131 : "\u0192", 132 : "\u201E", 133 : "\u2026", 134 : "\u2020", 135 : "\u2021", 136 : "\u02C6", 137 : "\u2030", 138 : "\u0160", 139 : "\u2039", 140 : "\u0152", @@ -2166,11 +2222,11 @@ '(?:!--([\\w\\W]*?)-->)|' + // Comment '(?:!\\[CDATA\\[([\\w\\W]*?)\\]\\]>)|' + // CDATA '(?:!DOCTYPE([\\w\\W]*?)>)|' + // DOCTYPE '(?:\\?([^\\s\\/<>]+) ?([\\w\\W]*?)[?/]>)|' + // PI '(?:\\/([^>]+)>)|' + // End element - '(?:([^\\s\\/<>]+)\\s*((?:[^"\'>]+(?:(?:"[^"]*")|(?:\'[^\']*\')|[^>]*))*)>)' + // Start element + '(?:([^\\s\\/<>]+)((?:\\s+[^"\'>]+(?:(?:"[^"]*")|(?:\'[^\']*\')|[^>]*))*|\\/)>)' + // Start element ')', 'g'); attrRegExp = /([\w:\-]+)(?:\s*=\s*(?:(?:\"((?:\\.|[^\"])*)\")|(?:\'((?:\\.|[^\'])*)\')|([^>\s]+)))?/g; specialElements = { 'script' : /<\/script[^>]*>/gi, @@ -3871,52 +3927,52 @@ if (t.settings.strict) n = n.toLowerCase(); return this.run(e, function(e) { var s = t.settings; + if (v !== null) { + switch (n) { + case "style": + if (!is(v, 'string')) { + each(v, function(v, n) { + t.setStyle(e, n, v); + }); - switch (n) { - case "style": - if (!is(v, 'string')) { - each(v, function(v, n) { - t.setStyle(e, n, v); - }); + return; + } - return; - } + // No mce_style for elements with these since they might get resized by the user + if (s.keep_values) { + if (v && !t._isRes(v)) + e.setAttribute('data-mce-style', v, 2); + else + e.removeAttribute('data-mce-style', 2); + } - // No mce_style for elements with these since they might get resized by the user - if (s.keep_values) { - if (v && !t._isRes(v)) - e.setAttribute('data-mce-style', v, 2); - else - e.removeAttribute('data-mce-style', 2); - } + e.style.cssText = v; + break; - e.style.cssText = v; - break; + case "class": + e.className = v || ''; // Fix IE null bug + break; - case "class": - e.className = v || ''; // Fix IE null bug - break; + case "src": + case "href": + if (s.keep_values) { + if (s.url_converter) + v = s.url_converter.call(s.url_converter_scope || t, v, n, e); - case "src": - case "href": - if (s.keep_values) { - if (s.url_converter) - v = s.url_converter.call(s.url_converter_scope || t, v, n, e); + t.setAttrib(e, 'data-mce-' + n, v, 2); + } - t.setAttrib(e, 'data-mce-' + n, v, 2); - } + break; - break; - - case "shape": - e.setAttribute('data-mce-style', v); - break; + case "shape": + e.setAttribute('data-mce-style', v); + break; + } } - if (is(v) && v !== null && v.length !== 0) e.setAttribute(n, '' + v, 2); else e.removeAttribute(n, 2); }); @@ -5688,11 +5744,11 @@ var node, parent, root, children, i, indexes = []; parent = node.parentNode; root = dom.getRoot().parentNode; - while (parent != root) { + while (parent != root && parent.nodeType !== 9) { children = parent.children; i = children.length; while (i--) { if (node === children[i]) { @@ -8067,11 +8123,12 @@ bl.push(sb); if (sb && eb && sb != eb) { n = sb; - while ((n = n.nextSibling) && n != eb) { + var walker = new tinymce.dom.TreeWalker(sb, dom.getRoot()); + while ((n = walker.next()) && n != eb) { if (dom.isBlock(n)) bl.push(n); } } @@ -8473,11 +8530,11 @@ htmlParser.parse(args.getInner ? node.innerHTML : tinymce.trim(dom.getOuterHTML(node), args), args) ); // Replace all BOM characters for now until we can find a better solution if (!args.cleanup) - args.content = args.content.replace(/\uFEFF/g, ''); + args.content = args.content.replace(/\uFEFF|\u200B/g, ''); // Post process if (!args.no_events) onPostProcess.dispatch(self, args); @@ -8767,10 +8824,28 @@ }); return; } + function exclude(nodes) { + var node; + + // First node is excluded + node = nodes[0]; + if (node.nodeType === 3 && node === startContainer && startOffset >= node.nodeValue.length) { + nodes.splice(0, 1); + } + + // Last node is excluded + node = nodes[nodes.length - 1]; + if (endOffset === 0 && nodes.length > 0 && node === endContainer && node.nodeType === 3) { + nodes.splice(nodes.length - 1, 1); + } + + return nodes; + }; + function collectSiblings(node, name, end_node) { var siblings = []; for (; node && node != end_node; node = node[name]) siblings.push(node); @@ -8796,11 +8871,11 @@ if (siblings.length) { if (!next) siblings.reverse(); - callback(siblings); + callback(exclude(siblings)); } } }; // If index based start position then resolve it @@ -8809,32 +8884,32 @@ // If index based end position then resolve it if (endContainer.nodeType == 1 && endContainer.hasChildNodes()) endContainer = endContainer.childNodes[Math.min(endOffset - 1, endContainer.childNodes.length - 1)]; - // Find common ancestor and end points - ancestor = dom.findCommonAncestor(startContainer, endContainer); - // Same container if (startContainer == endContainer) - return callback([startContainer]); + return callback(exclude([startContainer])); + // Find common ancestor and end points + ancestor = dom.findCommonAncestor(startContainer, endContainer); + // Process left side for (node = startContainer; node; node = node.parentNode) { - if (node == endContainer) + if (node === endContainer) return walkBoundary(startContainer, ancestor, true); - if (node == ancestor) + if (node === ancestor) break; } // Process right side for (node = endContainer; node; node = node.parentNode) { - if (node == startContainer) + if (node === startContainer) return walkBoundary(endContainer, ancestor); - if (node == ancestor) + if (node === ancestor) break; } // Find start/end point startPoint = findEndPoint(startContainer, ancestor) || startContainer; @@ -8849,52 +8924,50 @@ 'nextSibling', endPoint == endContainer ? endPoint.nextSibling : endPoint ); if (siblings.length) - callback(siblings); + callback(exclude(siblings)); // Walk right leaf walkBoundary(endContainer, endPoint); }; - /* this.split = function(rng) { + this.split = function(rng) { var startContainer = rng.startContainer, startOffset = rng.startOffset, endContainer = rng.endContainer, endOffset = rng.endOffset; function splitText(node, offset) { - if (offset == node.nodeValue.length) - node.appendData(INVISIBLE_CHAR); - - node = node.splitText(offset); - - if (node.nodeValue === INVISIBLE_CHAR) - node.nodeValue = ''; - - return node; + return node.splitText(offset); }; // Handle single text node - if (startContainer == endContainer) { - if (startContainer.nodeType == 3) { - if (startOffset != 0) - startContainer = endContainer = splitText(startContainer, startOffset); + if (startContainer == endContainer && startContainer.nodeType == 3) { + if (startOffset > 0 && startOffset < startContainer.nodeValue.length) { + endContainer = splitText(startContainer, startOffset); + startContainer = endContainer.previousSibling; - if (endOffset - startOffset != startContainer.nodeValue.length) - splitText(startContainer, endOffset - startOffset); + if (endOffset > startOffset) { + endOffset = endOffset - startOffset; + startContainer = endContainer = splitText(endContainer, endOffset).previousSibling; + endOffset = endContainer.nodeValue.length; + startOffset = 0; + } else { + endOffset = 0; + } } } else { // Split startContainer text node if needed - if (startContainer.nodeType == 3 && startOffset != 0) { + if (startContainer.nodeType == 3 && startOffset > 0 && startOffset < startContainer.nodeValue.length) { startContainer = splitText(startContainer, startOffset); startOffset = 0; } // Split endContainer text node if needed - if (endContainer.nodeType == 3 && endOffset != endContainer.nodeValue.length) { + if (endContainer.nodeType == 3 && endOffset > 0 && endOffset < endContainer.nodeValue.length) { endContainer = splitText(endContainer, endOffset).previousSibling; endOffset = endContainer.nodeValue.length; } } @@ -8903,11 +8976,11 @@ startOffset : startOffset, endContainer : endContainer, endOffset : endOffset }; }; -*/ + }; tinymce.dom.RangeUtils.compareRanges = function(rng1, rng2) { if (rng1 && rng2) { // Compare native IE ranges @@ -11190,10 +11263,11 @@ custom_undo_redo : 1, doctype : tinymce.isIE6 ? '<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN">' : '<!DOCTYPE>', // Use old doctype on IE 6 to avoid horizontal scroll visual_table_class : 'mceItemTable', visual : 1, font_size_style_values : 'xx-small,x-small,small,medium,large,x-large,xx-large', + 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 apply_source_formatting : 1, directionality : 'ltr', forced_root_block : 'p', hidden_input : 1, padd_empty_editor : 1, @@ -11621,14 +11695,16 @@ } }); // Keep scripts from executing t.parser.addNodeFilter('script', function(nodes, name) { - var i = nodes.length; + var i = nodes.length, node; - while (i--) - nodes[i].attr('type', 'mce-text/javascript'); + while (i--) { + node = nodes[i]; + node.attr('type', 'mce-' + (node.attr('type') || 'text/javascript')); + } }); t.parser.addNodeFilter('#cdata', function(nodes, name) { var i = nodes.length, node; @@ -12965,35 +13041,20 @@ if (t.undoManager.typing) addUndo(); }); } - // Fire a nodeChanged when the selection is changed on WebKit this fixes selection issues on iOS5 - // It only fires the nodeChange event every 50ms since it would other wise update the UI when you type and it hogs the CPU - if (tinymce.isWebKit) { - dom.bind(t.getDoc(), 'selectionchange', function() { - if (t.selectionTimer) { - clearTimeout(t.selectionTimer); - t.selectionTimer = 0; - } - - t.selectionTimer = window.setTimeout(function() { - t.nodeChanged(); - }, 50); - }); - } - // Bug fix for FireFox keeping styles from end of selection instead of start. if (tinymce.isGecko) { function getAttributeApplyFunction() { var template = t.dom.getAttribs(t.selection.getStart().cloneNode(false)); return function() { var target = t.selection.getStart(); if (target !== t.getBody()) { - t.dom.removeAllAttribs(target); + t.dom.setAttrib(target, "style", null); each(template, function(attr) { target.setAttributeNode(attr.cloneNode(true)); }); } @@ -14842,12 +14903,11 @@ nodeIndex = dom.nodeIndex, INVISIBLE_CHAR = '\uFEFF', MCE_ATTR_RE = /^(src|href|style)$/, FALSE = false, TRUE = true, - undefined, - pendingFormats = {apply : [], remove : []}; + undefined; function isArray(obj) { return obj instanceof Array; }; @@ -15059,11 +15119,11 @@ return currentWrapElm; } }; - function applyRngStyle(rng, bookmark) { + function applyRngStyle(rng, bookmark, node_specific) { var newWrappers = [], wrapName, wrapElm; // Setup wrapper element wrapName = format.inline || format.block; wrapElm = dom.create(wrapName); @@ -15123,11 +15183,11 @@ } } // Is it valid to wrap this item if (isValid(wrapName, nodeName) && isValid(parentName, wrapName) && - !(node.nodeType === 3 && node.nodeValue.length === 1 && node.nodeValue.charCodeAt(0) === 65279)) { + !(!node_specific && node.nodeType === 3 && node.nodeValue.length === 1 && node.nodeValue.charCodeAt(0) === 65279) && node.id !== '_mce_caret') { // Start wrapping if (!currentWrapElm) { // Wrap the node currentWrapElm = wrapElm.cloneNode(FALSE); node.parentNode.insertBefore(currentWrapElm, node); @@ -15278,16 +15338,18 @@ }); }; if (format) { if (node) { - rng = dom.createRng(); - - rng.setStartBefore(node); - rng.setEndAfter(node); - - applyRngStyle(expandRng(rng, formatList)); + if (node.nodeType) { + rng = dom.createRng(); + rng.setStartBefore(node); + rng.setEndAfter(node); + applyRngStyle(expandRng(rng, formatList), null, true); + } else { + applyRngStyle(node, null, true); + } } else { if (!isCollapsed || !format.inline || dom.select('td.mceSelected,th.mceSelected').length) { // Obtain selection node before selection is unselected by applyRngStyle() var curSelNode = ed.selection.getNode(); @@ -15497,14 +15559,19 @@ }); }; // Handle node if (node) { - rng = dom.createRng(); - rng.setStartBefore(node); - rng.setEndAfter(node); - removeRngStyle(rng); + if (node.nodeType) { + rng = dom.createRng(); + rng.setStartBefore(node); + rng.setEndAfter(node); + removeRngStyle(rng); + } else { + removeRngStyle(node); + } + return; } if (!selection.isCollapsed() || !format.inline || dom.select('td.mceSelected,th.mceSelected').length) { bookmark = selection.getBookmark(); @@ -15517,10 +15584,15 @@ } ed.nodeChanged(); } else performCaretAction('remove', name, vars); + + // 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 + if (tinymce.isWebKit) { + ed.execCommand('mceCleanup'); + } }; function toggle(name, vars, node) { var fmt = get(name); @@ -15591,11 +15663,11 @@ } } }; function match(name, vars, node) { - var startNode, i; + var startNode; function matchParents(node) { // Find first node with similar format settings node = dom.getParent(node, function(node) { return !!matchNode(node, name, vars, true); @@ -15607,25 +15679,10 @@ // Check specified node if (node) return matchParents(node); - // Check pending formats - if (selection.isCollapsed()) { - for (i = pendingFormats.apply.length - 1; i >= 0; i--) { - if (pendingFormats.apply[i].name == name) - return true; - } - - for (i = pendingFormats.remove.length - 1; i >= 0; i--) { - if (pendingFormats.remove[i].name == name) - return false; - } - - return matchParents(selection.getNode()); - } - // Check selected node node = selection.getNode(); if (matchParents(node)) return TRUE; @@ -15640,37 +15697,10 @@ }; function matchAll(names, vars) { var startElement, matchedFormatNames = [], checkedMap = {}, i, ni, name; - // If the selection is collapsed then check pending formats - if (selection.isCollapsed()) { - for (ni = 0; ni < names.length; ni++) { - // If the name is to be removed, then stop it from being added - for (i = pendingFormats.remove.length - 1; i >= 0; i--) { - name = names[ni]; - - if (pendingFormats.remove[i].name == name) { - checkedMap[name] = true; - break; - } - } - } - - // If the format is to be applied - for (i = pendingFormats.apply.length - 1; i >= 0; i--) { - for (ni = 0; ni < names.length; ni++) { - name = names[ni]; - - if (!checkedMap[name] && pendingFormats.apply[i].name == name) { - checkedMap[name] = true; - matchedFormatNames.push(name); - } - } - } - } - // Check start of selection for formats startElement = selection.getStart(); dom.getParent(startElement, function(node) { var i, name; @@ -15775,11 +15805,11 @@ return value; }; function isWhiteSpaceNode(node) { - return node && node.nodeType === 3 && /^([\s\r\n]+|)$/.test(node.nodeValue); + return node && node.nodeType === 3 && /^([\t \r\n]+|)$/.test(node.nodeValue); }; function wrap(node, name, attrs) { var wrapper = dom.create(name, attrs); @@ -15791,35 +15821,41 @@ function expandRng(rng, format, remove) { var startContainer = rng.startContainer, startOffset = rng.startOffset, endContainer = rng.endContainer, - endOffset = rng.endOffset, sibling, lastIdx, leaf; + endOffset = rng.endOffset, sibling, lastIdx, leaf, endPoint; // This function walks up the tree if there is no siblings before/after the node - function findParentContainer(container, child_name, sibling_name, root) { - var parent, child; + function findParentContainer(start) { + var container, parent, child, sibling, siblingName; - root = root || dom.getRoot(); + container = parent = start ? startContainer : endContainer; + siblingName = start ? 'previousSibling' : 'nextSibling'; + root = dom.getRoot(); - for (;;) { - // Check if we can move up are we at root level or body level - parent = container.parentNode; + // If it's a text node and the offset is inside the text + if (container.nodeType == 3 && !isWhiteSpaceNode(container)) { + if (start ? startOffset > 0 : endOffset < container.nodeValue.length) { + return container; + } + } + for (;;) { // Stop expanding on block elements or root depending on format if (parent == root || (!format[0].block_expand && isBlock(parent))) - return container; + return parent; - for (sibling = parent[child_name]; sibling && sibling != container; sibling = sibling[sibling_name]) { - if (sibling.nodeType == 1 && !isBookmarkNode(sibling)) - return container; - - if (sibling.nodeType == 3 && !isWhiteSpaceNode(sibling)) - return container; + // Walk left/right + for (sibling = parent[siblingName]; sibling; sibling = sibling[siblingName]) { + if (!isBookmarkNode(sibling) && !isWhiteSpaceNode(sibling)) { + return parent; + } } - container = container.parentNode; + // Check if we can move up are we at root level or body level + parent = parent.parentNode; } return container; }; @@ -15853,27 +15889,107 @@ if (endContainer.nodeType == 3) endOffset = endContainer.nodeValue.length; } // Exclude bookmark nodes if possible - if (isBookmarkNode(startContainer.parentNode)) - startContainer = startContainer.parentNode; - - if (isBookmarkNode(startContainer)) + if (isBookmarkNode(startContainer.parentNode) || isBookmarkNode(startContainer)) { + startContainer = isBookmarkNode(startContainer) ? startContainer : startContainer.parentNode; startContainer = startContainer.nextSibling || startContainer; - if (isBookmarkNode(endContainer.parentNode)) { - endOffset = dom.nodeIndex(endContainer); - endContainer = endContainer.parentNode; + if (startContainer.nodeType == 3) + startOffset = 0; } - if (isBookmarkNode(endContainer) && endContainer.previousSibling) { - endContainer = endContainer.previousSibling; - endOffset = endContainer.length; + if (isBookmarkNode(endContainer.parentNode) || isBookmarkNode(endContainer)) { + endContainer = isBookmarkNode(endContainer) ? endContainer : endContainer.parentNode; + endContainer = endContainer.previousSibling || endContainer; + + if (endContainer.nodeType == 3) + endOffset = endContainer.length; } if (format[0].inline) { + if (rng.collapsed) { + function findWordEndPoint(container, offset, start) { + var walker, node, pos, lastTextNode; + + function findSpace(node, offset) { + var pos, pos2, str = node.nodeValue; + + if (typeof(offset) == "undefined") { + offset = start ? str.length : 0; + } + + if (start) { + pos = str.lastIndexOf(' ', offset); + pos2 = str.lastIndexOf('\u00a0', offset); + pos = pos > pos2 ? pos : pos2; + + // Include the space on remove to avoid tag soup + if (pos !== -1 && !remove) { + pos++; + } + } else { + pos = str.indexOf(' ', offset); + pos2 = str.indexOf('\u00a0', offset); + pos = pos !== -1 && (pos2 === -1 || pos < pos2) ? pos : pos2; + } + + return pos; + }; + + if (container.nodeType === 3) { + pos = findSpace(container, offset); + + if (pos !== -1) { + return {container : container, offset : pos}; + } + + lastTextNode = container; + } + + // Walk the nodes inside the block + walker = new TreeWalker(container, dom.getParent(container, isBlock) || ed.getBody()); + while (node = walker[start ? 'prev' : 'next']()) { + if (node.nodeType === 3) { + lastTextNode = node; + pos = findSpace(node); + + if (pos !== -1) { + return {container : node, offset : pos}; + } + } else if (isBlock(node)) { + break; + } + } + + if (lastTextNode) { + if (start) { + offset = 0; + } else { + offset = lastTextNode.length; + } + + return {container: lastTextNode, offset: offset}; + } + } + + // Expand left to closest word boundery + endPoint = findWordEndPoint(startContainer, startOffset, true); + if (endPoint) { + startContainer = endPoint.container; + startOffset = endPoint.offset; + } + + // Expand right to closest word boundery + endPoint = findWordEndPoint(endContainer, endOffset); + if (endPoint) { + endContainer = endPoint.container; + endOffset = endPoint.offset; + } + } + // Avoid applying formatting to a trailing space. leaf = findLeaf(endContainer, endOffset); if (leaf.node) { while (leaf.node && leaf.offset === 0 && leaf.node.previousSibling) leaf = findLeaf(leaf.node.previousSibling); @@ -15883,23 +15999,29 @@ if (leaf.offset > 1) { endContainer = leaf.node; endContainer.splitText(leaf.offset - 1); } else if (leaf.node.previousSibling) { - endContainer = leaf.node.previousSibling; + // TODO: Figure out why this is in here + //endContainer = leaf.node.previousSibling; } } } } - + // Move start/end point up the tree if the leaves are sharp and if we are in different containers // Example * becomes !: !<p><b><i>*text</i><i>text*</i></b></p>! // This will reduce the number of wrapper elements that needs to be created // Move start point up the tree if (format[0].inline || format[0].block_expand) { - startContainer = findParentContainer(startContainer, 'firstChild', 'nextSibling'); - endContainer = findParentContainer(endContainer, 'lastChild', 'previousSibling'); + if (!format[0].inline || (startContainer.nodeType != 3 || startOffset === 0)) { + startContainer = findParentContainer(true); + } + + if (!format[0].inline || (endContainer.nodeType != 3 || endOffset === endContainer.nodeValue.length)) { + endContainer = findParentContainer(); + } } // Expand start/end container to matching selector if (format[0].selector && format[0].expand !== FALSE && !format[0].inline) { function findSelectorEndPoint(container, sibling_name) { @@ -15969,14 +16091,14 @@ endContainer = findBlockEndPoint(endContainer, 'nextSibling'); // Non block element then try to expand up the leaf if (format[0].block) { if (!isBlock(startContainer)) - startContainer = findParentContainer(startContainer, 'firstChild', 'nextSibling'); + startContainer = findParentContainer(true); if (!isBlock(endContainer)) - endContainer = findParentContainer(endContainer, 'lastChild', 'previousSibling'); + endContainer = findParentContainer(); } } // Setup index for startContainer if (startContainer.nodeType == 1) { @@ -16265,11 +16387,11 @@ function isTextBlock(name) { return /^(h[1-6]|p|div|pre|address|dl|dt|dd)$/.test(name); }; function getContainer(rng, start) { - var container, offset, lastIdx; + var container, offset, lastIdx, walker; container = rng[start ? 'startContainer' : 'endContainer']; offset = rng[start ? 'startOffset' : 'endOffset']; if (container.nodeType == 1) { @@ -16279,153 +16401,280 @@ offset--; container = container.childNodes[offset > lastIdx ? lastIdx : offset]; } + // If start text node is excluded then walk to the next node + if (container.nodeType === 3 && start && offset >= container.nodeValue.length) { + container = new TreeWalker(container, ed.getBody()).next() || container; + } + + // If end text node is excluded then walk to the previous node + if (container.nodeType === 3 && !start && offset == 0) { + container = new TreeWalker(container, ed.getBody()).prev() || container; + } + return container; }; function performCaretAction(type, name, vars) { - var i, currentPendingFormats = pendingFormats[type], - otherPendingFormats = pendingFormats[type == 'apply' ? 'remove' : 'apply']; + var invisibleChar, caretContainerId = '_mce_caret', debug = ed.settings.caret_debug; - function hasPending() { - return pendingFormats.apply.length || pendingFormats.remove.length; + // Setup invisible character use zero width space on Gecko since it doesn't change the heigt of the container + invisibleChar = tinymce.isGecko ? '\u200B' : INVISIBLE_CHAR; + + // Creates a caret container bogus element + function createCaretContainer(fill) { + var caretContainer = dom.create('span', {id: caretContainerId, 'data-mce-bogus': true, style: debug ? 'color:red' : ''}); + + if (fill) { + caretContainer.appendChild(ed.getDoc().createTextNode(invisibleChar)); + } + + return caretContainer; }; - function resetPending() { - pendingFormats.apply = []; - pendingFormats.remove = []; + function isCaretContainerEmpty(node, nodes) { + while (node) { + if ((node.nodeType === 3 && node.nodeValue !== invisibleChar) || node.childNodes.length > 1) { + return false; + } + + // Collect nodes + if (nodes && node.nodeType === 1) { + nodes.push(node); + } + + node = node.firstChild; + } + + return true; }; + + // Returns any parent caret container element + function getParentCaretContainer(node) { + while (node) { + if (node.id === caretContainerId) { + return node; + } - function perform(caret_node) { - // Apply pending formats - each(pendingFormats.apply.reverse(), function(item) { - apply(item.name, item.vars, caret_node); + node = node.parentNode; + } + }; - // Colored nodes should be underlined so that the color of the underline matches the text color. - if (item.name === 'forecolor' && item.vars.value) - processUnderlineAndColor(caret_node.parentNode); - }); + // Finds the first text node in the specified node + function findFirstTextNode(node) { + var walker; - // Remove pending formats - each(pendingFormats.remove.reverse(), function(item) { - remove(item.name, item.vars, caret_node); - }); + if (node) { + walker = new TreeWalker(node, node); - dom.remove(caret_node, 1); - resetPending(); + for (node = walker.current(); node; node = walker.next()) { + if (node.nodeType === 3) { + return node; + } + } + } }; - // Check if it already exists then ignore it - for (i = currentPendingFormats.length - 1; i >= 0; i--) { - if (currentPendingFormats[i].name == name) - return; - } + // Removes the caret container for the specified node or all on the current document + function removeCaretContainer(node, move_caret) { + var child, rng; - currentPendingFormats.push({name : name, vars : vars}); + if (!node) { + node = getParentCaretContainer(selection.getStart()); - // Check if it's in the other type, then remove it - for (i = otherPendingFormats.length - 1; i >= 0; i--) { - if (otherPendingFormats[i].name == name) - otherPendingFormats.splice(i, 1); - } + if (!node) { + while (node = dom.get(caretContainerId)) { + removeCaretContainer(node, false); + } + } + } else { + rng = selection.getRng(true); - // Pending apply or remove formats - if (hasPending()) { - ed.getDoc().execCommand('FontName', false, 'mceinline'); - pendingFormats.lastRng = selection.getRng(); + if (isCaretContainerEmpty(node)) { + if (move_caret !== false) { + rng.setStartBefore(node); + rng.setEndBefore(node); + } - // IE will convert the current word - each(dom.select('font,span'), function(node) { - var bookmark; + dom.remove(node); + } else { + child = findFirstTextNode(node); + child = child.deleteData(0, 1); + dom.remove(node, 1); + } - if (isCaretNode(node)) { - bookmark = selection.getBookmark(); - perform(node); - selection.moveToBookmark(bookmark); - ed.nodeChanged(); + selection.setRng(rng); + } + }; + + // Applies formatting to the caret postion + function applyCaretFormat() { + var rng, caretContainer, textNode, offset, bookmark, container, text; + + rng = selection.getRng(true); + offset = rng.startOffset; + container = rng.startContainer; + text = container.nodeValue; + + caretContainer = getParentCaretContainer(selection.getStart()); + if (caretContainer) { + textNode = findFirstTextNode(caretContainer); + } + + // Expand to word is caret is in the middle of a text node and the char before/after is a alpha numeric character + if (text && offset > 0 && offset < text.length && /\w/.test(text.charAt(offset)) && /\w/.test(text.charAt(offset - 1))) { + // Get bookmark of caret position + bookmark = selection.getBookmark(); + + // Collapse bookmark range (WebKit) + rng.collapse(true); + + // Expand the range to the closest word and split it at those points + rng = expandRng(rng, get(name)); + rng = rangeUtils.split(rng); + + // Apply the format to the range + apply(name, vars, rng); + + // Move selection back to caret position + selection.moveToBookmark(bookmark); + } else { + if (!caretContainer || textNode.nodeValue !== invisibleChar) { + caretContainer = createCaretContainer(true); + textNode = caretContainer.firstChild; + + rng.insertNode(caretContainer); + offset = 1; + + apply(name, vars, caretContainer); + } else { + apply(name, vars, caretContainer); } - }); - // Only register listeners once if we need to - if (!pendingFormats.isListening && hasPending()) { - pendingFormats.isListening = true; - function performPendingFormat(node, textNode) { - var rng = dom.createRng(); - perform(node); + // Move selection to text node + selection.setCursorLocation(textNode, offset); + } + }; - rng.setStart(textNode, textNode.nodeValue.length); - rng.setEnd(textNode, textNode.nodeValue.length); - selection.setRng(rng); - ed.nodeChanged(); + function removeCaretFormat() { + var rng = selection.getRng(true), container, offset, bookmark, + hasContentAfter, node, formatNode, parents = [], i, caretContainer; + + container = rng.startContainer; + offset = rng.startOffset; + node = container; + + if (container.nodeType == 3) { + if (offset != container.nodeValue.length || container.nodeValue === invisibleChar) { + hasContentAfter = true; } - var enterKeyPressed = false; - each('onKeyDown,onKeyUp,onKeyPress,onMouseUp'.split(','), function(event) { - ed[event].addToTop(function(ed, e) { - if (e.keyCode==13 && !e.shiftKey) { - enterKeyPressed = true; - return; - } - // Do we have pending formats and is the selection moved has moved - if (hasPending() && !tinymce.dom.RangeUtils.compareRanges(pendingFormats.lastRng, selection.getRng())) { - var foundCaret = false; - each(dom.select('font,span'), function(node) { - var textNode, rng; + node = node.parentNode; + } - // Look for marker - if (isCaretNode(node)) { - foundCaret = true; - textNode = node.firstChild; + while (node) { + if (matchNode(node, name, vars)) { + formatNode = node; + break; + } - // Find the first text node within node - while (textNode && textNode.nodeType != 3) - textNode = textNode.firstChild; + if (node.nextSibling) { + hasContentAfter = true; + } - if (textNode) - performPendingFormat(node, textNode); - else - dom.remove(node); - } - }); - - // no caret - so we are - if (enterKeyPressed && !foundCaret) { - var node = selection.getNode(); - var textNode = node; + parents.push(node); + node = node.parentNode; + } - // Find the first text node within node - while (textNode && textNode.nodeType != 3) - textNode = textNode.firstChild; - if (textNode) { - node=textNode.parentNode; - while (!isBlock(node)){ - node=node.parentNode; - } - performPendingFormat(node, textNode); - } - } + // Node doesn't have the specified format + if (!formatNode) { + return; + } - // Always unbind and clear pending styles on keyup - if (e.type == 'keyup' || e.type == 'mouseup') { - resetPending(); - enterKeyPressed=false; - } - } - }); - }); + // Is there contents after the caret then remove the format on the element + if (hasContentAfter) { + // Get bookmark of caret position + bookmark = selection.getBookmark(); + + // Collapse bookmark range (WebKit) + rng.collapse(true); + + // Expand the range to the closest word and split it at those points + rng = expandRng(rng, get(name), true); + rng = rangeUtils.split(rng); + + // Remove the format from the range + remove(name, vars, rng); + + // Move selection back to caret position + selection.moveToBookmark(bookmark); + } else { + caretContainer = createCaretContainer(); + + node = caretContainer; + for (i = parents.length - 1; i >= 0; i--) { + node.appendChild(parents[i].cloneNode(false)); + node = node.firstChild; + } + + // Insert invisible character into inner most format element + node.appendChild(dom.doc.createTextNode(invisibleChar)); + node = node.firstChild; + + // Insert caret container after the formated node + dom.insertAfter(caretContainer, formatNode); + + // Move selection to text node + selection.setCursorLocation(node, 1); } + }; + + // Mark current caret container elements as bogus when getting the contents so we don't end up with empty elements + ed.onBeforeGetContent.addToTop(function() { + var nodes = [], i; + + if (isCaretContainerEmpty(getParentCaretContainer(selection.getStart()), nodes)) { + // Mark children + i = nodes.length; + while (i--) { + dom.setAttrib(nodes[i], 'data-mce-bogus', '1'); + } + } + }); + + // Remove caret container on mouse up and on key up + tinymce.each('onMouseUp onKeyUp'.split(' '), function(name) { + ed[name].addToTop(function() { + removeCaretContainer(); + }); + }); + + // Remove caret container on keydown and it's a backspace, enter or left/right arrow keys + ed.onKeyDown.addToTop(function(ed, e) { + var keyCode = e.keyCode; + + if (keyCode == 8 || keyCode == 37 || keyCode == 39) { + removeCaretContainer(getParentCaretContainer(selection.getStart())); + } + }); + + // Do apply or remove caret format + if (type == "apply") { + applyCaretFormat(); + } else { + removeCaretFormat(); } }; }; })(tinymce); tinymce.onAddEditor.add(function(tinymce, ed) { var filters, fontSizes, dom, settings = ed.settings; if (settings.inline_styles) { - fontSizes = tinymce.explode(settings.font_size_style_values); + fontSizes = tinymce.explode(settings.font_size_legacy_values); function replaceWithSpan(node, styles) { tinymce.each(styles, function(value, name) { if (value) dom.setStyle(node, name, value);