X-Git-Url: https://git.mxchange.org/?a=blobdiff_plain;f=plugins%2FTinyMCE%2Fjs%2Ftiny_mce_src.js;h=acc1db528ebe57464f843a1ab93db38f3b3ae74a;hb=7cd0706aefd3d3eb4d70213d915651958faaadbc;hp=a26e4ea0047fc8ed431fadb917ac2d6f54ad6625;hpb=7c7d42b5f1c3c9cea62b4a73a97e07cea25f8d30;p=quix0rs-gnu-social.git diff --git a/plugins/TinyMCE/js/tiny_mce_src.js b/plugins/TinyMCE/js/tiny_mce_src.js index a26e4ea004..acc1db528e 100644 --- a/plugins/TinyMCE/js/tiny_mce_src.js +++ b/plugins/TinyMCE/js/tiny_mce_src.js @@ -2,12 +2,12 @@ var whiteSpaceRe = /^\s*|\s*$/g, undefined; - win.tinymce = win.tinyMCE = { + var tinymce = { majorVersion : '3', - minorVersion : '3rc1', + minorVersion : '3.8', - releaseDate : '2010-02-23', + releaseDate : '2010-06-30', _init : function() { var t = this, d = document, na = navigator, ua = na.userAgent, i, nl, n, base, p, v; @@ -26,6 +26,8 @@ t.isAir = /adobeair/i.test(ua); + t.isIDevice = /(iPad|iPhone)/.test(ua); + // TinyMCE .NET webcontrol might be setting the values for TinyMCE if (win.tinyMCEPreInit) { t.suffix = tinyMCEPreInit.suffix; @@ -236,7 +238,7 @@ createNS : function(n, o) { var i, v; - o = o || window; + o = o || win; n = n.split('.'); for (i=0; i'; }, - remove : function(n, k) { - var t = this; - - return this.run(n, function(n) { - var p, g, i; + remove : function(node, keep_children) { + return this.run(node, function(node) { + var parent, child; - p = n.parentNode; + parent = node.parentNode; - if (!p) + if (!parent) return null; - if (k) { - for (i = n.childNodes.length - 1; i >= 0; i--) - t.insertAfter(n.childNodes[i], n); - - //each(n.childNodes, function(c) { - // p.insertBefore(c.cloneNode(true), n); - //}); - } - - // Fix IE psuedo leak - if (t.fixPsuedoLeaks) { - p = n.cloneNode(true); - k = 'IELeakGarbageBin'; - g = t.get(k) || t.add(t.doc.body, 'div', {id : k, style : 'display:none'}); - g.appendChild(n); - g.innerHTML = ''; - - return p; + if (keep_children) { + while (child = node.firstChild) { + // IE 8 will crash if you don't remove completely empty text nodes + if (!tinymce.isIE || child.nodeType !== 3 || child.nodeValue) + parent.insertBefore(child, node); + else + node.removeChild(child); + } } - return p.removeChild(n); + return parent.removeChild(node); }); }, @@ -1818,7 +1819,6 @@ tinymce.create('static tinymce.util.XHR', { e = t.boxModel ? d.documentElement : d.body; x = t.getStyle(t.select('html')[0], 'borderWidth'); // Remove border x = (x == 'medium' || t.boxModel && !t.isIE6) && 2 || x; - n.top += t.win.self != t.win.top ? 2 : 0; // IE adds some strange extra cord if used in a frameset return {x : n.left + e.scrollLeft - x, y : n.top + e.scrollTop - x}; } @@ -2045,8 +2045,10 @@ tinymce.create('static tinymce.util.XHR', { e.className = v; // Empty class attr - if (!v) + if (!v) { e.removeAttribute('class'); + e.removeAttribute('className'); + } return v; } @@ -2148,7 +2150,7 @@ tinymce.create('static tinymce.util.XHR', { // So if we replace the p elements with divs and mark them and then replace them back to paragraphs // after we use innerHTML we can fix the DOM tree h = h.replace(/

]+)>|

/ig, '

'); - h = h.replace(/<\/p>/g, '
'); + h = h.replace(/<\/p>/gi, ''); // Set the new HTML with DIVs set(); @@ -2405,23 +2407,21 @@ tinymce.create('static tinymce.util.XHR', { }); }, - insertAfter : function(n, r) { - var t = this; - - r = t.get(r); + insertAfter : function(node, reference_node) { + reference_node = this.get(reference_node); - return this.run(n, function(n) { - var p, ns; + return this.run(node, function(node) { + var parent, nextSibling; - p = r.parentNode; - ns = r.nextSibling; + parent = reference_node.parentNode; + nextSibling = reference_node.nextSibling; - if (ns) - p.insertBefore(n, ns); + if (nextSibling) + parent.insertBefore(node, nextSibling); else - p.appendChild(n); + parent.appendChild(node); - return n; + return node; }); }, @@ -2447,14 +2447,6 @@ tinymce.create('static tinymce.util.XHR', { }); } - // Fix IE psuedo leak for elements since replacing elements if fairly common - // Will break parentNode for some unknown reason - if (t.fixPsuedoLeaks && o.nodeType === 1) { - o.parentNode.insertBefore(n, o); - t.remove(o); - return n; - } - return o.parentNode.replaceChild(n, o); }); }, @@ -2656,21 +2648,20 @@ tinymce.create('static tinymce.util.XHR', { }, nodeIndex : function(node, normalized) { - var idx = 0, lastNode, nodeType; + var idx = 0, lastNodeType, lastNode, nodeType; if (node) { - for (node = node.previousSibling, lastNode = node; node; node = node.previousSibling) { + for (lastNodeType = node.nodeType, node = node.previousSibling, lastNode = node; node; node = node.previousSibling) { nodeType = node.nodeType; - // Text nodes needs special treatment if the normalized argument is specified + // Normalize text nodes if (normalized && nodeType == 3) { - // Checks if the current node has contents and that the last node is a non text node or empty - if (node.nodeValue.length > 0 && (lastNode.nodeType != nodeType || lastNode.nodeValue.length === 0)) - idx++; - } else - idx++; + if (nodeType == lastNodeType || !node.nodeValue.length) + continue; + } - lastNode = node; + idx++; + lastNodeType = nodeType; } } @@ -2827,6 +2818,7 @@ tinymce.create('static tinymce.util.XHR', { tinymce.DOM = new tinymce.dom.DOMUtils(document, {process_html : 0}); })(tinymce); + (function(ns) { // Range constructor function Range(dom) { @@ -3503,39 +3495,14 @@ tinymce.create('static tinymce.util.XHR', { ns.Range = Range; })(tinymce.dom); + (function() { function Selection(selection) { var t = this, invisibleChar = '\uFEFF', range, lastIERng, dom = selection.dom, TRUE = true, FALSE = false; - // Compares two IE specific ranges to see if they are different - // this method is useful when invalidating the cached selection range - function compareRanges(rng1, rng2) { - if (rng1 && rng2) { - // Both are control ranges and the selected element matches - if (rng1.item && rng2.item && rng1.item(0) === rng2.item(0)) - return TRUE; - - // Both are text ranges and the range matches - if (rng1.isEqual && rng2.isEqual && rng2.isEqual(rng1)) { - // IE will say that the range is equal then produce an invalid argument exception - // if you perform specific operations in a keyup event. For example Ctrl+Del. - // This hack will invalidate the range cache if the exception occurs - try { - // Try accessing nextSibling will producer an invalid argument some times - range.startContainer.nextSibling; - return TRUE; - } catch (ex) { - // Ignore - } - } - } - - return FALSE; - }; - // Returns a W3C DOM compatible range object by using the IE Range API function getRange() { - var ieRange = selection.getRng(), domRange = dom.createRng(), ieRange2, element, collapsed, isMerged; + var ieRange = selection.getRng(), domRange = dom.createRng(), element, collapsed; // If selection is outside the current document just return an empty range element = ieRange.item ? ieRange.item(0) : ieRange.parentElement(); @@ -3550,233 +3517,199 @@ tinymce.create('static tinymce.util.XHR', { return domRange; } - // Duplicare IE selection range and check if the range is collapsed - ieRange2 = ieRange.duplicate(); collapsed = selection.isCollapsed(); - // Insert invisible start marker - ieRange.collapse(); - ieRange.pasteHTML(''); + function findEndPoint(start) { + var marker, container, offset, nodes, startIndex = 0, endIndex, index, parent, checkRng, position; - // Insert invisible end marker - if (!collapsed) { - ieRange2.collapse(FALSE); - ieRange2.pasteHTML(''); - } + // Setup temp range and collapse it + checkRng = ieRange.duplicate(); + checkRng.collapse(start); - // Sets the end point of the range by looking for the marker - // This method also merges the text nodes it splits so that - // the DOM doesn't get fragmented. - function setEndPoint(start) { - var container, offset, marker, sibling; + // Create marker and insert it at the end of the endpoints parent + marker = dom.create('a'); + parent = checkRng.parentElement(); - // Look for endpoint marker - marker = dom.get('_mce_' + (start ? 'start' : 'end')); - sibling = marker.previousSibling; + // If parent doesn't have any children then set the container to that parent and the index to 0 + if (!parent.hasChildNodes()) { + domRange[start ? 'setStart' : 'setEnd'](parent, 0); + return; + } - // Is marker after a text node - if (sibling && sibling.nodeType == 3) { - // Get container node and calc offset - container = sibling; - offset = container.nodeValue.length; + parent.appendChild(marker); + checkRng.moveToElementText(marker); + position = ieRange.compareEndPoints(start ? 'StartToStart' : 'EndToEnd', checkRng); + if (position > 0) { + // The position is after the end of the parent element. + // This is the case where IE puts the caret to the left edge of a table. + domRange[start ? 'setStartAfter' : 'setEndAfter'](parent); dom.remove(marker); + return; + } - // Merge text nodes to reduce DOM fragmentation - sibling = container.nextSibling; - if (sibling && sibling.nodeType == 3) { - isMerged = TRUE; - container.appendData(sibling.nodeValue); - dom.remove(sibling); + // Setup node list and endIndex + nodes = tinymce.grep(parent.childNodes); + endIndex = nodes.length - 1; + // Perform a binary search for the position + while (startIndex <= endIndex) { + index = Math.floor((startIndex + endIndex) / 2); + + // Insert marker and check it's position relative to the selection + parent.insertBefore(marker, nodes[index]); + checkRng.moveToElementText(marker); + position = ieRange.compareEndPoints(start ? 'StartToStart' : 'EndToEnd', checkRng); + if (position > 0) { + // Marker is to the right + startIndex = index + 1; + } else if (position < 0) { + // Marker is to the left + endIndex = index - 1; + } else { + // Maker is where we are + found = true; + break; } - } else { - sibling = marker.nextSibling; + } - // Is marker before a text node - if (sibling && sibling.nodeType == 3) { - container = sibling; - offset = 0; - } else { - // Is marker before an element - if (sibling) - offset = dom.nodeIndex(sibling) - 1; - else - offset = dom.nodeIndex(marker); + // Setup container + container = position > 0 || index == 0 ? marker.nextSibling : marker.previousSibling; + + // Handle element selection + if (container.nodeType == 1) { + dom.remove(marker); + + // Find offset and container + offset = dom.nodeIndex(container); + container = container.parentNode; - container = marker.parentNode; + // Move the offset if we are setting the end or the position is after an element + if (!start || index > 0) + offset++; + } else { + // Calculate offset within text node + if (position > 0 || index == 0) { + checkRng.setEndPoint(start ? 'StartToStart' : 'EndToEnd', ieRange); + offset = checkRng.text.length; + } else { + checkRng.setEndPoint(start ? 'StartToStart' : 'EndToEnd', ieRange); + offset = container.nodeValue.length - checkRng.text.length; } dom.remove(marker); } - // Set start of range - if (start) - domRange.setStart(container, offset); - - // Set end of range or automatically if it's collapsed to increase performance - if (!start || collapsed) - domRange.setEnd(container, offset); + domRange[start ? 'setStart' : 'setEnd'](container, offset); }; - // Set start of range - setEndPoint(TRUE); + // Find start point + findEndPoint(true); - // Set end of range if needed + // Find end point if needed if (!collapsed) - setEndPoint(FALSE); - - // Restore selection if the range contents was merged - // since the selection was then moved since the text nodes got changed - if (isMerged) - t.addRange(domRange); + findEndPoint(); return domRange; }; this.addRange = function(rng) { - var ieRng, ieRng2, doc = selection.dom.doc, body = doc.body, startPos, endPos, sc, so, ec, eo, marker, lastIndex, skipStart, skipEnd; - - this.destroy(); - - // Setup some shorter versions - sc = rng.startContainer; - so = rng.startOffset; - ec = rng.endContainer; - eo = rng.endOffset; - ieRng = body.createTextRange(); - - // If document selection move caret to first node in document - if (sc == doc || ec == doc) { - ieRng = body.createTextRange(); - ieRng.collapse(); - ieRng.select(); - return; - } - - // If child index resolve it - if (sc.nodeType == 1 && sc.hasChildNodes()) { - lastIndex = sc.childNodes.length - 1; + var ieRng, ctrlRng, startContainer, startOffset, endContainer, endOffset, doc = selection.dom.doc, body = doc.body; - // Index is higher that the child count then we need to jump over the start container - if (so > lastIndex) { - skipStart = 1; - sc = sc.childNodes[lastIndex]; - } else - sc = sc.childNodes[so]; - - // Child was text node then move offset to start of it - if (sc.nodeType == 3) - so = 0; - } - - // If child index resolve it - if (ec.nodeType == 1 && ec.hasChildNodes()) { - lastIndex = ec.childNodes.length - 1; + function setEndPoint(start) { + var container, offset, marker, tmpRng, nodes; - if (eo == 0) { - skipEnd = 1; - ec = ec.childNodes[0]; - } else { - ec = ec.childNodes[Math.min(lastIndex, eo - 1)]; + marker = dom.create('a'); + container = start ? startContainer : endContainer; + offset = start ? startOffset : endOffset; + tmpRng = ieRng.duplicate(); - // Child was text node then move offset to end of text node - if (ec.nodeType == 3) - eo = ec.nodeValue.length; + if (container == doc) { + container = body; + offset = 0; } - } - // Single element selection - if (sc == ec && sc.nodeType == 1) { - // Make control selection for some elements - if (/^(IMG|TABLE)$/.test(sc.nodeName) && so != eo) { - ieRng = body.createControlRange(); - ieRng.addElement(sc); + if (container.nodeType == 3) { + container.parentNode.insertBefore(marker, container); + tmpRng.moveToElementText(marker); + tmpRng.moveStart('character', offset); + dom.remove(marker); + ieRng.setEndPoint(start ? 'StartToStart' : 'EndToEnd', tmpRng); } else { - ieRng = body.createTextRange(); + nodes = container.childNodes; - // Padd empty elements with invisible character - if (!sc.hasChildNodes() && sc.canHaveHTML) - sc.innerHTML = invisibleChar; - - // Select element contents - ieRng.moveToElementText(sc); + if (nodes.length) { + if (offset >= nodes.length) { + dom.insertAfter(marker, nodes[nodes.length - 1]); + } else { + container.insertBefore(marker, nodes[offset]); + } - // If it's only containing a padding remove it so the caret remains - if (sc.innerHTML == invisibleChar) { - ieRng.collapse(TRUE); - sc.removeChild(sc.firstChild); + tmpRng.moveToElementText(marker); + } else { + // Empty node selection for example
|
+ marker = doc.createTextNode(invisibleChar); + container.appendChild(marker); + tmpRng.moveToElementText(marker.parentNode); + tmpRng.collapse(TRUE); } - } - - if (so == eo) - ieRng.collapse(eo <= rng.endContainer.childNodes.length - 1); - ieRng.select(); - ieRng.scrollIntoView(); - return; + ieRng.setEndPoint(start ? 'StartToStart' : 'EndToEnd', tmpRng); + dom.remove(marker); + } } - // Create range and marker - ieRng = body.createTextRange(); - marker = doc.createElement('span'); - marker.innerHTML = ' '; - - // Set start of range to startContainer/startOffset - if (sc.nodeType == 3) { - // Insert marker after/before startContainer - if (skipStart) - dom.insertAfter(marker, sc); - else - sc.parentNode.insertBefore(marker, sc); - - // Select marker the caret to offset position - ieRng.moveToElementText(marker); - marker.parentNode.removeChild(marker); - ieRng.move('character', so); - } else { - ieRng.moveToElementText(sc); + // Destroy cached range + this.destroy(); - if (skipStart) - ieRng.collapse(FALSE); - } + // Setup some shorter versions + startContainer = rng.startContainer; + startOffset = rng.startOffset; + endContainer = rng.endContainer; + endOffset = rng.endOffset; + ieRng = body.createTextRange(); - // If same text container then we can do a more simple move - if (sc == ec && sc.nodeType == 3) { - ieRng.moveEnd('character', eo - so); - ieRng.select(); - ieRng.scrollIntoView(); - return; + // If single element selection then try making a control selection out of it + if (startContainer == endContainer && startContainer.nodeType == 1 && startOffset == endOffset - 1) { + if (startOffset == endOffset - 1) { + try { + ctrlRng = body.createControlRange(); + ctrlRng.addElement(startContainer.childNodes[startOffset]); + ctrlRng.select(); + ctrlRng.scrollIntoView(); + return; + } catch (ex) { + // Ignore + } + } } - // Set end of range to endContainer/endOffset - ieRng2 = body.createTextRange(); - if (ec.nodeType == 3) { - // Insert marker after/before startContainer - ec.parentNode.insertBefore(marker, ec); - - // Move selection to end marker and move caret to end offset - ieRng2.moveToElementText(marker); - marker.parentNode.removeChild(marker); - ieRng2.move('character', eo); - ieRng.setEndPoint('EndToStart', ieRng2); - } else { - ieRng2.moveToElementText(ec); - ieRng2.collapse(!!skipEnd); - ieRng.setEndPoint('EndToEnd', ieRng2); - } + // Set start/end point of selection + setEndPoint(true); + setEndPoint(); + // Select the new range and scroll it into view ieRng.select(); ieRng.scrollIntoView(); }; this.getRangeAt = function() { // Setup new range if the cache is empty - if (!range || !compareRanges(lastIERng, selection.getRng())) { + if (!range || !tinymce.dom.RangeUtils.compareRanges(lastIERng, selection.getRng())) { range = getRange(); // Store away text range for next call lastIERng = selection.getRng(); } + // IE will say that the range is equal then produce an invalid argument exception + // if you perform specific operations in a keyup event. For example Ctrl+Del. + // This hack will invalidate the range cache if the exception occurs + try { + range.startContainer.nextSibling; + } catch (ex) { + range = getRange(); + lastIERng = null; + } + // Return cached range return range; }; @@ -3863,6 +3796,8 @@ tinymce.create('static tinymce.util.XHR', { // Expose the selection object tinymce.dom.TridentSelection = Selection; })(); + + (function(tinymce) { // Shorten names var each = tinymce.each, DOM = tinymce.DOM, isIE = tinymce.isIE, isWebKit = tinymce.isWebKit, Event; @@ -4146,6 +4081,7 @@ tinymce.create('static tinymce.util.XHR', { Event.destroy(); }); })(tinymce); + (function(tinymce) { tinymce.dom.Element = function(id, settings) { var t = this, dom, el; @@ -4254,6 +4190,7 @@ tinymce.create('static tinymce.util.XHR', { }); }; })(tinymce); + (function(tinymce) { function trimNl(s) { return s.replace(/[\n\r]+/g, ''); @@ -4341,17 +4278,21 @@ tinymce.create('static tinymce.util.XHR', { h += '_'; // Delete and insert new node - if (r.startContainer == d && r.endContainer == d) { + + if (r.startContainer == d && r.endContainer == d) { // WebKit will fail if the body is empty since the range is then invalid and it can't insert contents d.body.innerHTML = h; } else { r.deleteContents(); - r.insertNode(t.getRng().createContextualFragment(h)); + if (d.body.childNodes.length == 0) { + d.body.innerHTML = h; + } else { + r.insertNode(r.createContextualFragment(h)); + } } // Move to caret marker c = t.dom.get('__caret'); - // Make sure we wrap it compleatly, Opera fails with a simple select call r = d.createRange(); r.setStartBefore(c); @@ -4375,37 +4316,50 @@ tinymce.create('static tinymce.util.XHR', { }, getStart : function() { - var t = this, r = t.getRng(), e; - - if (isIE) { - if (r.item) - return r.item(0); + var rng = this.getRng(), startElement, parentElement, checkRng, node; - r = r.duplicate(); - r.collapse(1); - e = r.parentElement(); + if (rng.duplicate || rng.item) { + // Control selection, return first item + if (rng.item) + return rng.item(0); + + // Get start element + checkRng = rng.duplicate(); + checkRng.collapse(1); + startElement = checkRng.parentElement(); + + // Check if range parent is inside the start element, then return the inner parent element + // This will fix issues when a single element is selected, IE would otherwise return the wrong start element + parentElement = node = rng.parentElement(); + while (node = node.parentNode) { + if (node == startElement) { + startElement = parentElement; + break; + } + } - if (e && e.nodeName == 'BODY') - return e.firstChild || e; + // If start element is body element try to move to the first child if it exists + if (startElement && startElement.nodeName == 'BODY') + return startElement.firstChild || startElement; - return e; + return startElement; } else { - e = r.startContainer; + startElement = rng.startContainer; - if (e.nodeType == 1 && e.hasChildNodes()) - e = e.childNodes[Math.min(e.childNodes.length - 1, r.startOffset)]; + if (startElement.nodeType == 1 && startElement.hasChildNodes()) + startElement = startElement.childNodes[Math.min(startElement.childNodes.length - 1, rng.startOffset)]; - if (e && e.nodeType == 3) - return e.parentNode; + if (startElement && startElement.nodeType == 3) + return startElement.parentNode; - return e; + return startElement; } }, getEnd : function() { var t = this, r = t.getRng(), e, eo; - if (isIE) { + if (r.duplicate || r.item) { if (r.item) return r.item(0); @@ -4450,23 +4404,8 @@ tinymce.create('static tinymce.util.XHR', { var rng = t.getRng(true), root = dom.getRoot(), bookmark = {}; function getPoint(rng, start) { - var indexes = [], node, lastIdx, - container = rng[start ? 'startContainer' : 'endContainer'], - offset = rng[start ? 'startOffset' : 'endOffset'], exclude, point = {}; - - // Resolve element index - if (container.nodeType == 1 && container.hasChildNodes()) { - lastIdx = container.childNodes.length - 1; - point.exclude = (start && offset > lastIdx) || (!start && offset == 0); - - if (!start && offset) - offset--; - - container = container.childNodes[offset > lastIdx ? lastIdx : offset]; - - if (container.nodeType == 3) - offset = start ? 0 : container.nodeValue.length; - } + var container = rng[start ? 'startContainer' : 'endContainer'], + offset = rng[start ? 'startOffset' : 'endOffset'], point = [], node, childNodes, after = 0; if (container.nodeType == 3) { if (normalized) { @@ -4474,13 +4413,20 @@ tinymce.create('static tinymce.util.XHR', { offset += node.nodeValue.length; } - point.offset = offset; + point.push(offset); + } else { + childNodes = container.childNodes; + + if (offset >= childNodes.length && childNodes.length) { + after = 1; + offset = Math.max(0, childNodes.length - 1); + } + + point.push(t.dom.nodeIndex(childNodes[offset], normalized) + after); } for (; container && container != root; container = container.parentNode) - indexes.push(t.dom.nodeIndex(container, normalized)); - - point.indexes = indexes; + point.push(t.dom.nodeIndex(container, normalized)); return point; }; @@ -4552,7 +4498,7 @@ tinymce.create('static tinymce.util.XHR', { }, moveToBookmark : function(bookmark) { - var t = this, dom = t.dom, marker1, marker2, rng, root; + var t = this, dom = t.dom, marker1, marker2, rng, root, startContainer, endContainer, startOffset, endOffset; // Clear selection cache if (t.tridentSel) @@ -4564,31 +4510,22 @@ tinymce.create('static tinymce.util.XHR', { root = dom.getRoot(); function setEndPoint(start) { - var point = bookmark[start ? 'start' : 'end'], i, node, offset; + var point = bookmark[start ? 'start' : 'end'], i, node, offset, children; if (point) { - for (node = root, i = point.indexes.length - 1; i >= 0; i--) - node = node.childNodes[point.indexes[i]] || node; - - if (start) { - if (node.nodeType == 3 && point.offset) - rng.setStart(node, point.offset); - else { - if (point.exclude) - rng.setStartAfter(node); - else - rng.setStartBefore(node); - } - } else { - if (node.nodeType == 3 && point.offset) - rng.setEnd(node, point.offset); - else { - if (point.exclude) - rng.setEndBefore(node); - else - rng.setEndAfter(node); - } + // Find container node + for (node = root, i = point.length - 1; i >= 1; i--) { + children = node.childNodes; + + if (children.length) + node = children[point[i]]; } + + // Set offset within container node + if (start) + rng.setStart(node, point[0]); + else + rng.setEnd(node, point[0]); } }; @@ -4597,8 +4534,6 @@ tinymce.create('static tinymce.util.XHR', { t.setRng(rng); } else if (bookmark.id) { - rng = dom.createRng(); - function restoreEndPoint(suffix) { var marker = dom.get(bookmark.id + '_' + suffix), node, idx, next, prev, keep = bookmark.keep; @@ -4608,25 +4543,23 @@ tinymce.create('static tinymce.util.XHR', { if (suffix == 'start') { if (!keep) { idx = dom.nodeIndex(marker); - - if (idx > 0) - idx++; } else { - node = marker; + node = marker.firstChild; idx = 1; } - rng.setStart(node, idx); - rng.setEnd(node, idx); + startContainer = endContainer = node; + startOffset = endOffset = idx; } else { if (!keep) { idx = dom.nodeIndex(marker); } else { - node = marker; + node = marker.firstChild; idx = 1; } - rng.setEnd(node, idx); + endContainer = node; + endOffset = idx; } if (!keep) { @@ -4651,19 +4584,33 @@ tinymce.create('static tinymce.util.XHR', { dom.remove(next); if (suffix == 'start') { - rng.setStart(prev, idx); - rng.setEnd(prev, idx); - } else - rng.setEnd(prev, idx); + startContainer = endContainer = prev; + startOffset = endOffset = idx; + } else { + endContainer = prev; + endOffset = idx; + } } } } }; + function addBogus(node) { + // Adds a bogus BR element for empty block elements + // on non IE browsers just to have a place to put the caret + if (!isIE && dom.isBlock(node) && !node.innerHTML) + node.innerHTML = '
'; + + return node; + }; + // Restore start/end points restoreEndPoint('start'); restoreEndPoint('end'); + rng = dom.createRng(); + rng.setStart(addBogus(startContainer), startOffset); + rng.setEnd(addBogus(endContainer), endOffset); t.setRng(rng); } else if (bookmark.name) { t.select(dom.select(bookmark.name)[bookmark.index]); @@ -4766,20 +4713,32 @@ tinymce.create('static tinymce.util.XHR', { // This can occur when the editor is placed in a hidden container element on Gecko // Or on IE when there was an exception if (!r) - r = isIE ? t.win.document.body.createTextRange() : t.win.document.createRange(); + r = t.win.document.createRange ? t.win.document.createRange() : t.win.document.body.createTextRange(); + if (t.selectedRange && t.explicitRange) { + if (r.compareBoundaryPoints(r.START_TO_START, t.selectedRange) === 0 && r.compareBoundaryPoints(r.END_TO_END, t.selectedRange) === 0) { + // Safari, Opera and Chrome only ever select text which causes the range to change. + // This lets us use the originally set range if the selection hasn't been changed by the user. + r = t.explicitRange; + } else { + t.selectedRange = null; + t.explicitRange = null; + } + } return r; }, setRng : function(r) { var s, t = this; - + if (!t.tridentSel) { s = t.getSel(); if (s) { + t.explicitRange = r; s.removeAllRanges(); s.addRange(r); + t.selectedRange = s.getRangeAt(0); } } else { // Is W3C Range @@ -4808,7 +4767,7 @@ tinymce.create('static tinymce.util.XHR', { getNode : function() { var t = this, rng = t.getRng(), sel = t.getSel(), elm; - if (!isIE) { + if (rng.setStart) { // Range maybe lost after the editor is made visible again if (!rng) return t.dom.getRoot(); @@ -4876,6 +4835,7 @@ tinymce.create('static tinymce.util.XHR', { } }); })(tinymce); + (function(tinymce) { tinymce.create('tinymce.dom.XMLWriter', { node : null, @@ -4967,6 +4927,7 @@ tinymce.create('static tinymce.util.XHR', { } }); })(tinymce); + (function(tinymce) { tinymce.create('tinymce.dom.StringWriter', { str : null, @@ -5093,6 +5054,7 @@ tinymce.create('static tinymce.util.XHR', { } }); })(tinymce); + (function(tinymce) { // Shorten names var extend = tinymce.extend, each = tinymce.each, Dispatcher = tinymce.util.Dispatcher, isIE = tinymce.isIE, isGecko = tinymce.isGecko; @@ -5615,7 +5577,7 @@ tinymce.create('static tinymce.util.XHR', { }, _serializeNode : function(n, inner) { - var t = this, s = t.settings, w = t.writer, hc, el, cn, i, l, a, at, no, v, nn, ru, ar, iv, closed, keep, type; + var t = this, s = t.settings, w = t.writer, hc, el, cn, i, l, a, at, no, v, nn, ru, ar, iv, closed, keep, type, scopeName; if (!s.node_filter || s.node_filter(n)) { switch (n.nodeType) { @@ -5639,8 +5601,9 @@ tinymce.create('static tinymce.util.XHR', { // Add correct prefix on IE if (isIE) { - if (n.scopeName !== 'HTML' && n.scopeName !== 'html') - nn = n.scopeName + ':' + nn; + scopeName = n.scopeName; + if (scopeName && scopeName !== 'HTML' && scopeName !== 'html') + nn = scopeName + ':' + nn; } // Remove mce prefix on IE needed for the abbr element @@ -5684,6 +5647,13 @@ tinymce.create('static tinymce.util.XHR', { } ru = t.findRule(nn); + + // No valid rule for this element could be found then skip it + if (!ru) { + iv = true; + break; + } + nn = ru.name || nn; closed = s.closed.test(nn); @@ -5959,6 +5929,7 @@ tinymce.create('static tinymce.util.XHR', { } }); })(tinymce); + (function(tinymce) { tinymce.dom.ScriptLoader = function(settings) { var QUEUED = 0, @@ -6142,6 +6113,7 @@ tinymce.create('static tinymce.util.XHR', { // Global script loader tinymce.ScriptLoader = new tinymce.dom.ScriptLoader(); })(tinymce); + tinymce.dom.TreeWalker = function(start_node, root_node) { var node = start_node; @@ -6181,6 +6153,7 @@ tinymce.dom.TreeWalker = function(start_node, root_node) { return (node = findSibling(node, 'lastChild', 'lastSibling', shallow)); }; }; + (function() { var transitional = {}; @@ -6334,7 +6307,8 @@ tinymce.dom.TreeWalker = function(start_node, root_node) { return !!(element && (!child_name || element[child_name])); }; }; -})();(function(tinymce) { +})(); +(function(tinymce) { tinymce.dom.RangeUtils = function(dom) { var INVISIBLE_CHAR = '\uFEFF'; @@ -6495,7 +6469,28 @@ tinymce.dom.TreeWalker = function(start_node, root_node) { }; */ }; + + tinymce.dom.RangeUtils.compareRanges = function(rng1, rng2) { + if (rng1 && rng2) { + // Compare native IE ranges + if (rng1.item || rng1.duplicate) { + // Both are control ranges and the selected element matches + if (rng1.item && rng2.item && rng1.item(0) === rng2.item(0)) + return true; + + // Both are text ranges and the range matches + if (rng1.isEqual && rng2.isEqual && rng2.isEqual(rng1)) + return true; + } else { + // Compare w3c ranges + return rng1.startContainer == rng2.startContainer && rng1.startOffset == rng2.startOffset; + } + } + + return false; + }; })(tinymce); + (function(tinymce) { // Shorten class names var DOM = tinymce.DOM, is = tinymce.is; @@ -6596,7 +6591,8 @@ tinymce.dom.TreeWalker = function(start_node, root_node) { tinymce.dom.Event.clear(this.id); } }); -})(tinymce);tinymce.create('tinymce.ui.Container:tinymce.ui.Control', { +})(tinymce); +tinymce.create('tinymce.ui.Container:tinymce.ui.Control', { Container : function(id, s) { this.parent(id, s); @@ -6617,6 +6613,7 @@ tinymce.dom.TreeWalker = function(start_node, root_node) { } }); + tinymce.create('tinymce.ui.Separator:tinymce.ui.Control', { Separator : function(id, s) { this.parent(id, s); @@ -6627,6 +6624,7 @@ tinymce.create('tinymce.ui.Separator:tinymce.ui.Control', { return tinymce.DOM.createHTML('span', {'class' : this.classPrefix}); } }); + (function(tinymce) { var is = tinymce.is, DOM = tinymce.DOM, each = tinymce.each, walk = tinymce.walk; @@ -6656,6 +6654,7 @@ tinymce.create('tinymce.ui.Separator:tinymce.ui.Control', { } }); })(tinymce); + (function(tinymce) { var is = tinymce.is, DOM = tinymce.DOM, each = tinymce.each, walk = tinymce.walk; @@ -6753,7 +6752,8 @@ tinymce.create('tinymce.ui.Separator:tinymce.ui.Control', { return m; } }); -})(tinymce);(function(tinymce) { +})(tinymce); +(function(tinymce) { var is = tinymce.is, DOM = tinymce.DOM, each = tinymce.each, Event = tinymce.dom.Event, Element = tinymce.dom.Element; tinymce.create('tinymce.ui.DropMenu:tinymce.ui.Menu', { @@ -7080,7 +7080,8 @@ tinymce.create('tinymce.ui.Separator:tinymce.ui.Control', { DOM.addClass(ro, 'mceLast'); } }); -})(tinymce);(function(tinymce) { +})(tinymce); +(function(tinymce) { var DOM = tinymce.DOM; tinymce.create('tinymce.ui.Button:tinymce.ui.Control', { @@ -7113,6 +7114,7 @@ tinymce.create('tinymce.ui.Separator:tinymce.ui.Control', { } }); })(tinymce); + (function(tinymce) { var DOM = tinymce.DOM, Event = tinymce.dom.Event, each = tinymce.each, Dispatcher = tinymce.util.Dispatcher; @@ -7324,7 +7326,7 @@ tinymce.create('tinymce.ui.Separator:tinymce.ui.Control', { var t = this, cp = t.classPrefix; Event.add(t.id, 'click', t.showMenu, t); - Event.add(t.id + '_text', 'focus', function(e) { + Event.add(t.id + '_text', 'focus', function() { if (!t._focused) { t.keyDownHandler = Event.add(t.id + '_text', 'keydown', function(e) { var idx = -1, v, kc = e.keyCode; @@ -7382,7 +7384,8 @@ tinymce.create('tinymce.ui.Separator:tinymce.ui.Control', { Event.clear(this.id + '_open'); } }); -})(tinymce);(function(tinymce) { +})(tinymce); +(function(tinymce) { var DOM = tinymce.DOM, Event = tinymce.dom.Event, each = tinymce.each, Dispatcher = tinymce.util.Dispatcher; tinymce.create('tinymce.ui.NativeListBox:tinymce.ui.ListBox', { @@ -7455,7 +7458,7 @@ tinymce.create('tinymce.ui.Separator:tinymce.ui.Control', { }, getLength : function() { - return DOM.get(this.id).options.length - 1; + return this.items.length; }, renderHTML : function() { @@ -7510,7 +7513,8 @@ tinymce.create('tinymce.ui.Separator:tinymce.ui.Control', { t.onPostRender.dispatch(t, DOM.get(t.id)); } }); -})(tinymce);(function(tinymce) { +})(tinymce); +(function(tinymce) { var DOM = tinymce.DOM, Event = tinymce.dom.Event, each = tinymce.each; tinymce.create('tinymce.ui.MenuButton:tinymce.ui.Button', { @@ -7599,6 +7603,7 @@ tinymce.create('tinymce.ui.Separator:tinymce.ui.Control', { } }); })(tinymce); + (function(tinymce) { var DOM = tinymce.DOM, Event = tinymce.dom.Event, each = tinymce.each; @@ -7664,6 +7669,7 @@ tinymce.create('tinymce.ui.Separator:tinymce.ui.Control', { } }); })(tinymce); + (function(tinymce) { var DOM = tinymce.DOM, Event = tinymce.dom.Event, is = tinymce.is, each = tinymce.each; @@ -7830,6 +7836,7 @@ tinymce.create('tinymce.ui.Separator:tinymce.ui.Control', { } }); })(tinymce); + tinymce.create('tinymce.ui.Toolbar:tinymce.ui.Container', { renderHTML : function() { var t = this, h = '', c, co, dom = tinymce.DOM, s = t.settings, i, pr, nx, cl; @@ -7892,6 +7899,7 @@ tinymce.create('tinymce.ui.Toolbar:tinymce.ui.Container', { return dom.createHTML('table', {id : t.id, 'class' : 'mceToolbar' + (s['class'] ? ' ' + s['class'] : ''), cellpadding : '0', cellspacing : '0', align : t.settings.align || ''}, '' + h + ''); } }); + (function(tinymce) { var Dispatcher = tinymce.util.Dispatcher, each = tinymce.each; @@ -8433,6 +8441,12 @@ tinymce.create('tinymce.ui.Toolbar:tinymce.ui.Container', { if (!t.getElement()) return; + // Is a iPad/iPhone, then skip initialization. We need to sniff here since the + // browser says it has contentEditable support but there is no visible caret + // We will remove this check ones Apple implements full contentEditable support + if (tinymce.isIDevice) + return; + // Add hidden input for non input elements inside form elements if (!/TEXTAREA|INPUT/i.test(t.getElement().nodeName) && s.hidden_input && DOM.getParent(id, 'form')) DOM.insertAfter(DOM.create('input', {type : 'hidden', name : id}), id); @@ -8798,18 +8812,19 @@ tinymce.create('tinymce.ui.Toolbar:tinymce.ui.Container', { hilitecolor : {inline : 'span', styles : {backgroundColor : '%value'}}, fontname : {inline : 'span', styles : {fontFamily : '%value'}}, fontsize : {inline : 'span', styles : {fontSize : '%value'}}, - blockquote : {block : 'blockquote', wrapper : 1}, + fontsize_class : {inline : 'span', attributes : {'class' : '%value'}}, + blockquote : {block : 'blockquote', wrapper : 1, remove : 'all'}, removeformat : [ {selector : 'b,strong,em,i,font,u,strike', remove : 'all', split : true, expand : false, block_expand : true, deep : true}, {selector : 'span', attributes : ['style', 'class'], remove : 'empty', split : true, expand : false, deep : true}, - {selector : '*', attributes : ['style', 'class'], expand : false, deep : true} + {selector : '*', attributes : ['style', 'class'], split : false, expand : false, deep : true} ] }); // Register default block formats each('p h1 h2 h3 h4 h5 h6 div address pre div code dt dd samp'.split(/\s/), function(name) { - t.formatter.register(name, {block : name}); + t.formatter.register(name, {block : name, remove : 'all'}); }); // Register user defined formats @@ -9076,15 +9091,28 @@ tinymce.create('tinymce.ui.Toolbar:tinymce.ui.Container', { focus : function(sf) { - var oed, t = this, ce = t.settings.content_editable; + var oed, t = this, ce = t.settings.content_editable, ieRng, controlElm, doc = t.getDoc(); if (!sf) { - // Is not content editable or the selection is outside the area in IE - // the IE statement is needed to avoid bluring if element selections inside layers since - // the layer is like it's own document in IE - if (!ce && (!isIE || t.selection.getNode().ownerDocument != t.getDoc())) + // Get selected control element + ieRng = t.selection.getRng(); + if (ieRng.item) { + controlElm = ieRng.item(0); + } + + // Is not content editable + if (!ce) t.getWin().focus(); + // Restore selected control element + // This is needed when for example an image is selected within a + // layer a call to focus will then remove the control selection + if (controlElm && controlElm.ownerDocument == doc) { + ieRng = doc.body.createControlRange(); + ieRng.addElement(controlElm); + ieRng.select(); + } + } if (tinymce.activeEditor != t) { @@ -9160,7 +9188,7 @@ tinymce.create('tinymce.ui.Toolbar:tinymce.ui.Container', { }, nodeChanged : function(o) { - var t = this, s = t.selection, n = s.getNode() || t.getBody(); + var t = this, s = t.selection, n = (isIE ? s.getNode() : s.getStart()) || t.getBody(); // Fix for bug #1896577 it seems that this can not be fired while the editor is loading if (t.initialized) { @@ -9850,7 +9878,7 @@ tinymce.create('tinymce.ui.Toolbar:tinymce.ui.Container', { // Add node change handlers t.onMouseUp.add(t.nodeChanged); - t.onClick.add(t.nodeChanged); + //t.onClick.add(t.nodeChanged); t.onKeyUp.add(function(ed, e) { var c = e.keyCode; @@ -9871,11 +9899,9 @@ tinymce.create('tinymce.ui.Toolbar:tinymce.ui.Container', { } // Add default shortcuts for gecko - if (isGecko) { - t.addShortcut('ctrl+b', t.getLang('bold_desc'), 'Bold'); - t.addShortcut('ctrl+i', t.getLang('italic_desc'), 'Italic'); - t.addShortcut('ctrl+u', t.getLang('underline_desc'), 'Underline'); - } + t.addShortcut('ctrl+b', t.getLang('bold_desc'), 'Bold'); + t.addShortcut('ctrl+i', t.getLang('italic_desc'), 'Italic'); + t.addShortcut('ctrl+u', t.getLang('underline_desc'), 'Underline'); // BlockFormat shortcuts keys for (i=1; i<=6; i++) @@ -9985,7 +10011,7 @@ tinymce.create('tinymce.ui.Toolbar:tinymce.ui.Container', { case 8: // Fix IE control + backspace browser bug if (t.selection.getRng().item) { - t.selection.getRng().item(0).removeNode(); + ed.dom.remove(t.selection.getRng().item(0)); return Event.cancel(e); } } @@ -10026,6 +10052,48 @@ tinymce.create('tinymce.ui.Toolbar:tinymce.ui.Container', { }); t.onKeyDown.add(function(ed, e) { + var rng, parent, bookmark; + + // IE has a really odd bug where the DOM might include an node that doesn't have + // a proper structure. If you try to access nodeValue it would throw an illegal value exception. + // This seems to only happen when you delete contents and it seems to be avoidable if you refresh the element + // after you delete contents from it. See: #3008923 + if (isIE && e.keyCode == 46) { + rng = t.selection.getRng(); + + if (rng.parentElement) { + parent = rng.parentElement(); + + // Select next word when ctrl key is used in combo with delete + if (e.ctrlKey) { + rng.moveEnd('word', 1); + rng.select(); + } + + // Delete contents + t.selection.getSel().clear(); + + // Check if we are within the same parent + if (rng.parentElement() == parent) { + bookmark = t.selection.getBookmark(); + + try { + // Update the HTML and hopefully it will remove the artifacts + parent.innerHTML = parent.innerHTML; + } catch (ex) { + // And since it's IE it can sometimes produce an unknown runtime error + } + + // Restore the caret position + t.selection.moveToBookmark(bookmark); + } + + // Block the default delete behavior since it might be broken + e.preventDefault(); + return; + } + } + // Is caracter positon keys if ((e.keyCode >= 33 && e.keyCode <= 36) || (e.keyCode >= 37 && e.keyCode <= 40) || e.keyCode == 13 || e.keyCode == 45) { if (t.undoManager.typing) @@ -10110,6 +10178,7 @@ tinymce.create('tinymce.ui.Toolbar:tinymce.ui.Container', { } }); })(tinymce); + (function(tinymce) { // Added for compression purposes var each = tinymce.each, undefined, TRUE = true, FALSE = false; @@ -10221,7 +10290,7 @@ tinymce.create('tinymce.ui.Toolbar:tinymce.ui.Container', { } // Present alert message about clipboard access not being available - if (failed || !doc.queryCommandEnabled(command)) { + if (failed || !doc.queryCommandSupported(command)) { if (tinymce.isGecko) { editor.windowManager.confirm(editor.getLang('clipboard_msg'), function(state) { if (state) @@ -10312,20 +10381,22 @@ tinymce.create('tinymce.ui.Toolbar:tinymce.ui.Container', { }, FormatBlock : function(command, ui, value) { - return toggleFormat(value); + return toggleFormat(value || 'p'); }, mceCleanup : function() { - storeSelection(); + var bookmark = selection.getBookmark(); + editor.setContent(editor.getContent({cleanup : TRUE}), {cleanup : TRUE}); - restoreSelection(); + + selection.moveToBookmark(bookmark); }, mceRemoveNode : function(command, ui, value) { var node = value || selection.getNode(); // Make sure that the body node isn't removed - if (node != ed.getBody()) { + if (node != editor.getBody()) { storeSelection(); editor.dom.remove(node, TRUE); restoreSelection(); @@ -10398,6 +10469,10 @@ tinymce.create('tinymce.ui.Toolbar:tinymce.ui.Container', { } }, + mceToggleFormat : function(command, ui, value) { + editor.formatter.toggle(value); + }, + InsertHorizontalRule : function() { selection.setContent('
'); }, @@ -10426,8 +10501,17 @@ tinymce.create('tinymce.ui.Toolbar:tinymce.ui.Container', { if (value.href) dom.setAttribs(link, value); else - ed.dom.remove(link, TRUE); + editor.dom.remove(link, TRUE); } + }, + + selectAll : function() { + var root = dom.getRoot(), rng = dom.createRng(); + + rng.setStart(root, 0); + rng.setEnd(root, root.childNodes.length); + + editor.selection.setRng(rng); } }); @@ -10494,122 +10578,120 @@ tinymce.create('tinymce.ui.Toolbar:tinymce.ui.Container', { }); } }; -})(tinymce);(function(tinymce) { - tinymce.create('tinymce.UndoManager', { - index : 0, - data : null, - typing : 0, +})(tinymce); +(function(tinymce) { + var Dispatcher = tinymce.util.Dispatcher; - UndoManager : function(ed) { - var t = this, Dispatcher = tinymce.util.Dispatcher; + tinymce.UndoManager = function(editor) { + var self, index = 0, data = []; - t.editor = ed; - t.data = []; - t.onAdd = new Dispatcher(this); - t.onUndo = new Dispatcher(this); - t.onRedo = new Dispatcher(this); - }, + function getContent() { + return tinymce.trim(editor.getContent({format : 'raw', no_events : 1})); + }; - add : function(l) { - var t = this, i, ed = t.editor, b, s = ed.settings, la; + return self = { + typing : 0, - l = l || {}; - l.content = l.content || ed.getContent({format : 'raw', no_events : 1}); - l.content = l.content.replace(/^\s*|\s*$/g, ''); + onAdd : new Dispatcher(self), + onUndo : new Dispatcher(self), + onRedo : new Dispatcher(self), - // Add undo level if needed - la = t.data[t.index]; - if (la && la.content == l.content) { - if (t.index > 0 || t.data.length == 1) - return null; - } + add : function(level) { + var i, settings = editor.settings, lastLevel; - // Time to compress - if (s.custom_undo_redo_levels) { - if (t.data.length > s.custom_undo_redo_levels) { - for (i = 0; i < t.data.length - 1; i++) - t.data[i] = t.data[i + 1]; + level = level || {}; + level.content = getContent(); - t.data.length--; - t.index = t.data.length; + // Add undo level if needed + lastLevel = data[index]; + if (lastLevel && lastLevel.content == level.content) { + if (index > 0 || data.length == 1) + return null; } - } - if (s.custom_undo_redo_restore_selection) - l.bookmark = b = l.bookmark || ed.selection.getBookmark(2, true); + // Time to compress + if (settings.custom_undo_redo_levels) { + if (data.length > settings.custom_undo_redo_levels) { + for (i = 0; i < data.length - 1; i++) + data[i] = data[i + 1]; - // Crop array if needed - if (t.index < t.data.length - 1) { - // Treat first level as initial - if (t.index == 0) - t.data = []; - else - t.data.length = t.index + 1; - } + data.length--; + index = data.length; + } + } - t.data.push(l); - t.index = t.data.length - 1; + // Get a non intrusive normalized bookmark + level.bookmark = editor.selection.getBookmark(2, true); - t.onAdd.dispatch(t, l); - ed.isNotDirty = 0; + // Crop array if needed + if (index < data.length - 1) { + // Treat first level as initial + if (index == 0) + data = []; + else + data.length = index + 1; + } - //console.log(t.index); - //console.dir(t.data); + data.push(level); + index = data.length - 1; - return l; - }, + self.onAdd.dispatch(self, level); + editor.isNotDirty = 0; - undo : function() { - var t = this, ed = t.editor, l = l, i; + return level; + }, - if (t.typing) { - t.add(); - t.typing = 0; - } + undo : function() { + var level, i; - if (t.index > 0) { - l = t.data[--t.index]; + if (self.typing) { + self.add(); + self.typing = 0; + } - ed.setContent(l.content, {format : 'raw'}); - ed.selection.moveToBookmark(l.bookmark); + if (index > 0) { + level = data[--index]; - t.onUndo.dispatch(t, l); - } + editor.setContent(level.content, {format : 'raw'}); + editor.selection.moveToBookmark(level.bookmark); - return l; - }, + self.onUndo.dispatch(self, level); + } - redo : function() { - var t = this, ed = t.editor, l = null; + return level; + }, - if (t.index < t.data.length - 1) { - l = t.data[++t.index]; - ed.setContent(l.content, {format : 'raw'}); - ed.selection.moveToBookmark(l.bookmark); + redo : function() { + var level; - t.onRedo.dispatch(t, l); - } + if (index < data.length - 1) { + level = data[++index]; - return l; - }, + editor.setContent(level.content, {format : 'raw'}); + editor.selection.moveToBookmark(level.bookmark); - clear : function() { - var t = this; + self.onRedo.dispatch(self, level); + } - t.data = []; - t.index = 0; - t.typing = 0; - }, + return level; + }, - hasUndo : function() { - return this.index > 0 || this.typing; - }, + clear : function() { + data = []; + index = self.typing = 0; + }, - hasRedo : function() { - return this.index < this.data.length - 1; - } - }); + hasUndo : function() { + return index > 0 || self.typing; + }, + + hasRedo : function() { + return index < data.length - 1; + } + }; + }; })(tinymce); + (function(tinymce) { // Shorten names var Event = tinymce.dom.Event, @@ -10621,6 +10703,27 @@ tinymce.create('tinymce.ui.Toolbar:tinymce.ui.Container', { TRUE = true, FALSE = false; + function cloneFormats(node) { + var clone, temp, inner; + + do { + if (/^(SPAN|STRONG|B|EM|I|FONT|STRIKE|U)$/.test(node.nodeName)) { + if (clone) { + temp = node.cloneNode(false); + temp.appendChild(clone); + clone = temp; + } else { + clone = inner = node.cloneNode(false); + } + + clone.removeAttribute('id'); + } + } while (node = node.parentNode); + + if (clone) + return {wrapper : clone, inner : inner}; + }; + // Checks if the selection/caret is at the end of the specified block element function isAtEnd(rng, par) { var rng2 = par.ownerDocument.createRange(); @@ -10729,11 +10832,54 @@ tinymce.create('tinymce.ui.Toolbar:tinymce.ui.Container', { } } - if (!isIE && s.force_p_newlines) { - ed.onKeyPress.add(function(ed, e) { - if (e.keyCode == 13 && !e.shiftKey && !t.insertPara(e)) - Event.cancel(e); - }); + if (s.force_p_newlines) { + if (!isIE) { + ed.onKeyPress.add(function(ed, e) { + if (e.keyCode == 13 && !e.shiftKey && !t.insertPara(e)) + Event.cancel(e); + }); + } else { + // Ungly hack to for IE to preserve the formatting when you press + // enter at the end of a block element with formatted contents + // This logic overrides the browsers default logic with + // custom logic that enables us to control the output + tinymce.addUnload(function() { + t._previousFormats = 0; // Fix IE leak + }); + + ed.onKeyPress.add(function(ed, e) { + t._previousFormats = 0; + + // Clone the current formats, this will later be applied to the new block contents + if (e.keyCode == 13 && !e.shiftKey && ed.selection.isCollapsed() && s.keep_styles) + t._previousFormats = cloneFormats(ed.selection.getStart()); + }); + + ed.onKeyUp.add(function(ed, e) { + // Let IE break the element and the wrap the new caret location in the previous formats + if (e.keyCode == 13 && !e.shiftKey) { + var parent = ed.selection.getStart(), fmt = t._previousFormats; + + // Parent is an empty block + if (!parent.hasChildNodes()) { + parent = dom.getParent(parent, dom.isBlock); + + if (parent) { + parent.innerHTML = ''; + + if (t._previousFormats) { + parent.appendChild(fmt.wrapper); + fmt.inner.innerHTML = '\uFEFF'; + } else + parent.innerHTML = '\uFEFF'; + + selection.select(parent, 1); + ed.getDoc().execCommand('Delete', false, null); + } + } + } + }); + } if (isGecko) { ed.onKeyDown.add(function(ed, e) { @@ -10746,7 +10892,7 @@ tinymce.create('tinymce.ui.Toolbar:tinymce.ui.Container', { // Workaround for missing shift+enter support, http://bugs.webkit.org/show_bug.cgi?id=16973 if (tinymce.isWebKit) { function insertBr(ed) { - var rng = selection.getRng(), br; + var rng = selection.getRng(), br, div = dom.create('div', null, ' '), divYPos, vpHeight = dom.getViewPort(ed.getWin()).h; // Insert BR element rng.insertNode(br = dom.create('br')); @@ -10762,12 +10908,19 @@ tinymce.create('tinymce.ui.Toolbar:tinymce.ui.Container', { selection.collapse(TRUE); } + // Create a temporary DIV after the BR and get the position as it + // seems like getPos() returns 0 for text nodes and BR elements. + dom.insertAfter(div, br); + divYPos = dom.getPos(div).y; + dom.remove(div); + // Scroll to new position, scrollIntoView can't be used due to bug: http://bugs.webkit.org/show_bug.cgi?id=16117 - ed.getWin().scrollTo(0, dom.getPos(selection.getRng().startContainer).y); + if (divYPos > vpHeight) // It is not necessary to scroll if the DIV is inside the view port. + ed.getWin().scrollTo(0, divYPos); }; ed.onKeyPress.add(function(ed, e) { - if (e.keyCode == 13 && (e.shiftKey || s.force_br_newlines)) { + if (e.keyCode == 13 && (e.shiftKey || (s.force_br_newlines && !dom.getParent(selection.getNode(), 'h1,h2,h3,h4,h5,h6,ol,ul')))) { insertBr(ed); Event.cancel(e); } @@ -10877,6 +11030,13 @@ tinymce.create('tinymce.ui.Toolbar:tinymce.ui.Container', { } } } else { + // Force control range into text range + if (r.item) { + tr = d.body.createTextRange(); + tr.moveToElementText(r.item(0)); + r = tr; + } + tr = d.body.createTextRange(); tr.moveToElementText(b); tr.collapse(1); @@ -11221,7 +11381,22 @@ tinymce.create('tinymce.ui.Toolbar:tinymce.ui.Container', { }, backspaceDelete : function(e, bs) { - var t = this, ed = t.editor, b = ed.getBody(), dom = ed.dom, n, se = ed.selection, r = se.getRng(), sc = r.startContainer, n, w, tn; + var t = this, ed = t.editor, b = ed.getBody(), dom = ed.dom, n, se = ed.selection, r = se.getRng(), sc = r.startContainer, n, w, tn, walker; + + // Delete when caret is behind a element doesn't work correctly on Gecko see #3011651 + if (!bs && r.collapsed && sc.nodeType == 1 && r.startOffset == sc.childNodes.length) { + walker = new tinymce.dom.TreeWalker(sc.lastChild, sc); + + // Walk the dom backwards until we find a text node + for (n = sc.lastChild; n; n = walker.prev()) { + if (n.nodeType == 3) { + r.setStart(n, n.nodeValue.length); + r.collapse(true); + se.setRng(r); + return; + } + } + } // The caret sometimes gets stuck in Gecko if you delete empty paragraphs // This workaround removes the element by hand and moves the caret to the previous element @@ -11252,40 +11427,10 @@ tinymce.create('tinymce.ui.Toolbar:tinymce.ui.Container', { } } } - - // Gecko generates BR elements here and there, we don't like those so lets remove them - function handler(e) { - var pr; - - e = e.target; - - // A new BR was created in a block element, remove it - if (e && e.parentNode && e.nodeName == 'BR' && (n = t.getParentBlock(e))) { - pr = e.previousSibling; - - Event.remove(b, 'DOMNodeInserted', handler); - - // Is there whitespace at the end of the node before then we might need the pesky BR - // to place the caret at a correct location see bug: #2013943 - if (pr && pr.nodeType == 3 && /\s+$/.test(pr.nodeValue)) - return; - - // Only remove BR elements that got inserted in the middle of the text - if (e.previousSibling || e.nextSibling) - ed.dom.remove(e); - } - }; - - // Listen for new nodes - Event._add(b, 'DOMNodeInserted', handler); - - // Remove listener - window.setTimeout(function() { - Event._remove(b, 'DOMNodeInserted', handler); - }, 1); } }); })(tinymce); + (function(tinymce) { // Shorten names var DOM = tinymce.DOM, Event = tinymce.dom.Event, each = tinymce.each, extend = tinymce.extend; @@ -11647,6 +11792,7 @@ tinymce.create('tinymce.ui.Toolbar:tinymce.ui.Container', { } }); })(tinymce); + (function(tinymce) { var Dispatcher = tinymce.util.Dispatcher, each = tinymce.each, isIE = tinymce.isIE, isOpera = tinymce.isOpera; @@ -11761,7 +11907,8 @@ tinymce.create('tinymce.ui.Toolbar:tinymce.ui.Container', { return tinymce.DOM.decode(s).replace(/\\n/g, '\n'); } }); -}(tinymce));(function(tinymce) { +}(tinymce)); +(function(tinymce) { function CommandManager() { var execCommands = {}, queryStateCommands = {}, queryValueCommands = {}; @@ -11807,7 +11954,8 @@ tinymce.create('tinymce.ui.Toolbar:tinymce.ui.Container', { }; tinymce.GlobalCommands = new CommandManager(); -})(tinymce);(function(tinymce) { +})(tinymce); +(function(tinymce) { tinymce.Formatter = function(ed) { var formats = {}, each = tinymce.each, @@ -11824,22 +11972,20 @@ tinymce.create('tinymce.ui.Toolbar:tinymce.ui.Container', { FALSE = false, TRUE = true, undefined, - caretHandler, - pendingFormats; + pendingFormats = {apply : [], remove : []}; + + function isArray(obj) { + return obj instanceof Array; + }; function getParents(node, selector) { return dom.getParents(node, selector, dom.getRoot()); }; - function resetPending() { - // Needs reset - if (!pendingFormats || pendingFormats.apply.length || pendingFormats.remove.length) - pendingFormats = {apply : [], remove : []}; + function isCaretNode(node) { + return node.nodeType === 1 && (node.face === 'mceinline' || node.style.fontFamily === 'mceinline'); }; - ed.onMouseUp.add(resetPending); - resetPending(); - // Public functions function get(name) { @@ -11864,12 +12010,18 @@ tinymce.create('tinymce.ui.Toolbar:tinymce.ui.Container', { // Default to true if (format.split === undefined) - format.split = !format.selector; + format.split = !format.selector || format.inline; // Default to true - if (format.remove === undefined && format.selector) + if (format.remove === undefined && format.selector && !format.inline) format.remove = 'none'; + // Mark format as a mixed format inline + block level + if (format.selector && format.inline) { + format.mixed = true; + format.block_expand = true; + } + // Split classes if needed if (typeof(format.classes) === 'string') format.classes = format.classes.split(/\s+/); @@ -11890,11 +12042,16 @@ tinymce.create('tinymce.ui.Toolbar:tinymce.ui.Container', { // Move startContainer/startOffset in to a suitable node if (container.nodeType == 1 || container.nodeValue === "") { - walker = new TreeWalker(container.childNodes[offset]); - for (node = walker.current(); node; node = walker.next()) { - if (node.nodeType == 3 && !isBlock(node.parentNode) && !isWhiteSpaceNode(node)) { - rng.setStart(node, 0); - break; + container = container.nodeType == 1 ? container.childNodes[offset] : container; + + // Might fail if the offset is behind the last element in it's container + if (container) { + walker = new TreeWalker(container, container.parentNode); + for (node = walker.current(); node; node = walker.next()) { + if (node.nodeType == 3 && !isWhiteSpaceNode(node)) { + rng.setStart(node, 0); + break; + } } } } @@ -11935,7 +12092,7 @@ tinymce.create('tinymce.ui.Toolbar:tinymce.ui.Container', { var currentWrapElm; function process(node) { - var nodeName = node.nodeName.toLowerCase(), parentName = node.parentNode.nodeName.toLowerCase(); + var nodeName = node.nodeName.toLowerCase(), parentName = node.parentNode.nodeName.toLowerCase(), found; // Stop wrapping on br elements if (isEq(nodeName, 'br')) { @@ -11967,11 +12124,17 @@ tinymce.create('tinymce.ui.Toolbar:tinymce.ui.Container', { if (format.selector) { // Look for matching formats each(formatList, function(format) { - if (dom.is(node, format.selector)) + if (dom.is(node, format.selector) && !isCaretNode(node)) { setElementFormat(node, format); + found = true; + } }); - return; + // Continue processing if a selector match wasn't found and a inline element is defined + if (!format.inline || found) { + currentWrapElm = 0; + return; + } } // Is it valid to wrap this item @@ -12019,7 +12182,7 @@ tinymce.create('tinymce.ui.Toolbar:tinymce.ui.Container', { var child, clone; each(node.childNodes, function(node) { - if (node.nodeType == 1 && !isBookmarkNode(node)) { + if (node.nodeType == 1 && !isBookmarkNode(node) && !isCaretNode(node)) { child = node; return FALSE; // break loop } @@ -12032,9 +12195,9 @@ tinymce.create('tinymce.ui.Toolbar:tinymce.ui.Container', { dom.replace(clone, node, TRUE); dom.remove(child, 1); - - return TRUE; } + + return clone || node; }; childCount = getChildCount(node); @@ -12047,10 +12210,8 @@ tinymce.create('tinymce.ui.Toolbar:tinymce.ui.Container', { if (format.inline || format.wrapper) { // Merges the current node with it's children of similar type to reduce the number of elements - if (!format.exact && childCount === 1) { - if (mergeStyles(node)) - return; - } + if (!format.exact && childCount === 1) + node = mergeStyles(node); // Remove/merge children each(formatList, function(format) { @@ -12062,14 +12223,23 @@ tinymce.create('tinymce.ui.Toolbar:tinymce.ui.Container', { }); }); + // Remove child if direct parent is of same type + if (matchNode(node.parentNode, name, vars)) { + dom.remove(node, 1); + node = 0; + return TRUE; + } + // Look for parent with similar style format - dom.getParent(node.parentNode, function(parent) { - if (matchNode(parent, name, vars)) { - dom.remove(node, 1); - node = 0; - return TRUE; - } - }); + if (format.merge_with_parents) { + dom.getParent(node.parentNode, function(parent) { + if (matchNode(parent, name, vars)) { + dom.remove(node, 1); + node = 0; + return TRUE; + } + }); + } // Merge next and previous siblings if they are similar texttext becomes texttext if (node) { @@ -12087,7 +12257,7 @@ tinymce.create('tinymce.ui.Toolbar:tinymce.ui.Container', { rng.setStartBefore(node); rng.setEndAfter(node); - applyRngStyle(rng); + applyRngStyle(expandRng(rng, formatList)); } else { if (!selection.isCollapsed() || !format.inline) { // Apply formatting to selection @@ -12106,6 +12276,45 @@ tinymce.create('tinymce.ui.Toolbar:tinymce.ui.Container', { function remove(name, vars, node) { var formatList = get(name), format = formatList[0], bookmark, i, rng; + function moveStart(rng) { + var container = rng.startContainer, + offset = rng.startOffset, + walker, node, nodes, tmpNode; + + // Convert text node into index if possible + if (container.nodeType == 3 && offset >= container.nodeValue.length - 1) { + container = container.parentNode; + offset = nodeIndex(container) + 1; + } + + // Move startContainer/startOffset in to a suitable node + if (container.nodeType == 1) { + nodes = container.childNodes; + container = nodes[Math.min(offset, nodes.length - 1)]; + walker = new TreeWalker(container); + + // If offset is at end of the parent node walk to the next one + if (offset > nodes.length - 1) + walker.next(); + + for (node = walker.current(); node; node = walker.next()) { + if (node.nodeType == 3 && !isWhiteSpaceNode(node)) { + // IE has a "neat" feature where it moves the start node into the closest element + // we can avoid this by inserting an element before it and then remove it after we set the selection + tmpNode = dom.create('a', null, INVISIBLE_CHAR); + node.parentNode.insertBefore(tmpNode, node); + + // Set selection and remove tmpNode + rng.setStart(node, 0); + selection.setRng(rng); + dom.remove(tmpNode); + + return; + } + } + } + }; + // Merges the styles for each node function process(node) { var children, i, l; @@ -12131,10 +12340,13 @@ tinymce.create('tinymce.ui.Toolbar:tinymce.ui.Container', { // Find format root each(getParents(container.parentNode).reverse(), function(parent) { + var format; + // Find format root element if (!formatRoot && parent.id != '_start' && parent.id != '_end') { - // If the matched format has a remove none flag we shouldn't split it - if (!isBlock(parent) && matchNode(parent, name, vars)) + // Is the node matching the format we are looking for + format = matchNode(parent, name, vars); + if (format && format.split !== false) formatRoot = parent; } }); @@ -12171,7 +12383,8 @@ tinymce.create('tinymce.ui.Toolbar:tinymce.ui.Container', { } } - if (split) + // Never split block elements if the format is mixed + if (split && (!format.mixed || !isBlock(format_root))) container = dom.split(format_root, container); // Wrap container in cloned formats @@ -12192,7 +12405,13 @@ tinymce.create('tinymce.ui.Toolbar:tinymce.ui.Container', { var node = dom.get(start ? '_start' : '_end'), out = node[start ? 'firstChild' : 'lastChild']; - dom.remove(node, 1); + // If the end is placed within the start the result will be removed + // So this checks if the out node is a bookmark node if it is it + // checks for another more suitable node + if (isBookmarkNode(out)) + out = out[start ? 'firstChild' : 'lastChild']; + + dom.remove(node, true); return out; }; @@ -12249,6 +12468,12 @@ tinymce.create('tinymce.ui.Toolbar:tinymce.ui.Container', { bookmark = selection.getBookmark(); removeRngStyle(selection.getRng(TRUE)); selection.moveToBookmark(bookmark); + + // Check if start element still has formatting then we are at: "text|text" and need to move the start into the next text node + if (match(name, vars, selection.getStart())) { + moveStart(selection.getRng(true)); + } + ed.nodeChanged(); } else performCaretAction('remove', name, vars); @@ -12261,7 +12486,7 @@ tinymce.create('tinymce.ui.Toolbar:tinymce.ui.Container', { apply(name, vars, node); }; - function matchNode(node, name, vars) { + function matchNode(node, name, vars, similar) { var formatList = get(name), format, i, classes; function matchItems(node, format, item_name) { @@ -12278,7 +12503,10 @@ tinymce.create('tinymce.ui.Toolbar:tinymce.ui.Container', { else value = getStyle(node, key); - if (!isEq(value, replaceVars(items[key], vars))) + if (similar && !value && !format.exact) + return; + + if ((!similar || format.exact) && !isEq(value, replaceVars(items[key], vars))) return; } } @@ -12286,12 +12514,12 @@ tinymce.create('tinymce.ui.Toolbar:tinymce.ui.Container', { // Only one match needed for indexed arrays for (i = 0; i < items.length; i++) { if (item_name === 'attributes' ? dom.getAttrib(node, items[i]) : getStyle(node, items[i])) - return TRUE; + return format; } } } - return TRUE; + return format; }; if (formatList && node) { @@ -12309,7 +12537,7 @@ tinymce.create('tinymce.ui.Toolbar:tinymce.ui.Container', { } } - return TRUE; + return format; } } } @@ -12321,7 +12549,7 @@ tinymce.create('tinymce.ui.Toolbar:tinymce.ui.Container', { function matchParents(node) { // Find first node with similar format settings node = dom.getParent(node, function(node) { - return !!matchNode(node, name, vars); + return !!matchNode(node, name, vars, true); }); // Do an exact check on the similar format element @@ -12362,6 +12590,54 @@ tinymce.create('tinymce.ui.Toolbar:tinymce.ui.Container', { return FALSE; }; + 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; + + for (i = 0; i < names.length; i++) { + name = names[i]; + + if (!checkedMap[name] && matchNode(node, name, vars)) { + checkedMap[name] = true; + matchedFormatNames.push(name); + } + } + }); + + return matchedFormatNames; + }; + function canApply(name) { var formatList = get(name), startNode, parents, i, x, selector; @@ -12394,6 +12670,7 @@ tinymce.create('tinymce.ui.Toolbar:tinymce.ui.Container', { remove : remove, toggle : toggle, match : match, + matchAll : matchAll, matchNode : matchNode, canApply : canApply }); @@ -12418,8 +12695,8 @@ tinymce.create('tinymce.ui.Toolbar:tinymce.ui.Container', { str1 = str1 || ''; str2 = str2 || ''; - str1 = str1.nodeName || str1; - str2 = str2.nodeName || str2; + str1 = '' + (str1.nodeName || str1); + str2 = '' + (str2.nodeName || str2); return str1.toLowerCase() == str2.toLowerCase(); }; @@ -12451,7 +12728,7 @@ tinymce.create('tinymce.ui.Toolbar:tinymce.ui.Container', { }; function isWhiteSpaceNode(node) { - return node && node.nodeType === 3 && /^\s*$/.test(node.nodeValue); + return node && node.nodeType === 3 && /^([\s\r\n]+|)$/.test(node.nodeValue); }; function wrap(node, name, attrs) { @@ -12538,7 +12815,7 @@ tinymce.create('tinymce.ui.Toolbar:tinymce.ui.Container', { } // Expand start/end container to matching selector - if (format[0].selector && format[0].expand !== FALSE) { + if (format[0].selector && format[0].expand !== FALSE && !format[0].inline) { function findSelectorEndPoint(container, sibling_name) { var parents, i, y; @@ -12765,6 +13042,10 @@ tinymce.create('tinymce.ui.Toolbar:tinymce.ui.Container', { } } + // Never remove nodes that isn't the specified inline element if a selector is specified too + if (format.selector && format.inline && !isEq(format.inline, node)) + return; + dom.remove(node, 1); }; @@ -12797,8 +13078,8 @@ tinymce.create('tinymce.ui.Toolbar:tinymce.ui.Container', { each(dom.getAttribs(node), function(attr) { var name = attr.nodeName.toLowerCase(); - // Don't compare internal attributes or style/class - if (name.indexOf('_') !== 0 && name !== 'class' && name !== 'style') + // Don't compare internal attributes or style + if (name.indexOf('_') !== 0 && name !== 'style') attribs[name] = dom.getAttrib(node, name); }); @@ -12890,7 +13171,7 @@ tinymce.create('tinymce.ui.Toolbar:tinymce.ui.Container', { }; function isTextBlock(name) { - return /^(h[1-6]|p|div|pre|address)$/.test(name); + return /^(h[1-6]|p|div|pre|address|dl|dt|dd)$/.test(name); }; function getContainer(rng, start) { @@ -12912,34 +13193,16 @@ tinymce.create('tinymce.ui.Toolbar:tinymce.ui.Container', { }; function performCaretAction(type, name, vars) { - var i, rng, selectedNode = selection.getNode().parentNode, - doc = ed.getDoc(), marker = 'mceinline', - events = ['onKeyDown', 'onKeyUp', 'onKeyPress'], - currentPendingFormats = pendingFormats[type], + var i, currentPendingFormats = pendingFormats[type], otherPendingFormats = pendingFormats[type == 'apply' ? 'remove' : 'apply']; - // Check if it already exists - for (i = currentPendingFormats.length - 1; i >= 0; i--) { - if (currentPendingFormats[i].name == name) - return; - } - - currentPendingFormats.push({name : name, vars : vars}); - - // Check if it's in the oter type - for (i = otherPendingFormats.length - 1; i >= 0; i--) { - if (otherPendingFormats[i].name == name) - otherPendingFormats.splice(i, 1); - } - - function unbind() { - if (caretHandler) { - each(events, function(event) { - ed[event].remove(caretHandler); - }); + function hasPending() { + return pendingFormats.apply.length || pendingFormats.remove.length; + }; - caretHandler = 0; - } + function resetPending() { + pendingFormats.apply = []; + pendingFormats.remove = []; }; function perform(caret_node) { @@ -12957,62 +13220,77 @@ tinymce.create('tinymce.ui.Toolbar:tinymce.ui.Container', { resetPending(); }; - function isMarker(node) { - return node.face == marker || node.style.fontFamily == marker; - }; - - unbind(); - - doc.execCommand('FontName', false, marker); + // Check if it already exists then ignore it + for (i = currentPendingFormats.length - 1; i >= 0; i--) { + if (currentPendingFormats[i].name == name) + return; + } - // IE will convert the current word - each(dom.select('font,span', selectedNode), function(node) { - var bookmark; + currentPendingFormats.push({name : name, vars : vars}); - if (isMarker(node)) { - bookmark = selection.getBookmark(); - perform(node); - selection.moveToBookmark(bookmark); - ed.nodeChanged(); - selectedNode = 0; - } - }); + // 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 (selectedNode) { - caretHandler = function(ed, e) { - each(dom.select('font,span', selectedNode), function(node) { - var bookmark, textNode; + // Pending apply or remove formats + if (hasPending()) { + ed.getDoc().execCommand('FontName', false, 'mceinline'); + pendingFormats.lastRng = selection.getRng(); - // Look for marker - if (node.face == marker || node.style.fontFamily == marker) { - textNode = node.firstChild; + // IE will convert the current word + each(dom.select('font,span'), function(node) { + var bookmark; - perform(node); + if (isCaretNode(node)) { + bookmark = selection.getBookmark(); + perform(node); + selection.moveToBookmark(bookmark); + ed.nodeChanged(); + } + }); - rng = dom.createRng(); - rng.setStart(textNode, textNode.nodeValue.length); - rng.setEnd(textNode, textNode.nodeValue.length); - selection.setRng(rng); - ed.nodeChanged(); + // Only register listeners once if we need to + if (!pendingFormats.isListening && hasPending()) { + pendingFormats.isListening = true; + + each('onKeyDown,onKeyUp,onKeyPress,onMouseUp'.split(','), function(event) { + ed[event].addToTop(function(ed, e) { + // Do we have pending formats and is the selection moved has moved + if (hasPending() && !tinymce.dom.RangeUtils.compareRanges(pendingFormats.lastRng, selection.getRng())) { + each(dom.select('font,span'), function(node) { + var textNode, rng; + + // Look for marker + if (isCaretNode(node)) { + textNode = node.firstChild; + + if (textNode) { + perform(node); + + rng = dom.createRng(); + rng.setStart(textNode, textNode.nodeValue.length); + rng.setEnd(textNode, textNode.nodeValue.length); + selection.setRng(rng); + ed.nodeChanged(); + } else + dom.remove(node); + } + }); - unbind(); - } + // Always unbind and clear pending styles on keyup + if (e.type == 'keyup' || e.type == 'mouseup') + resetPending(); + } + }); }); - - // Always unbind and clear pending styles on keyup - if (e.type == 'keyup') { - unbind(); - resetPending(); - } - }; - - each(events, function(event) { - ed[event].addToTop(caretHandler); - }); + } } - } + }; }; })(tinymce); + tinymce.onAddEditor.add(function(tinymce, ed) { var filters, fontSizes, dom, settings = ed.settings; @@ -13065,3 +13343,4 @@ tinymce.onAddEditor.add(function(tinymce, ed) { }); } }); +