var whiteSpaceRe = /^\s*|\s*$/g,\r
undefined;\r
\r
- win.tinymce = win.tinyMCE = {\r
+ var tinymce = {\r
majorVersion : '3',\r
\r
- minorVersion : '3rc1',\r
+ minorVersion : '3.8',\r
\r
- releaseDate : '2010-02-23',\r
+ releaseDate : '2010-06-30',\r
\r
_init : function() {\r
var t = this, d = document, na = navigator, ua = na.userAgent, i, nl, n, base, p, v;\r
\r
t.isAir = /adobeair/i.test(ua);\r
\r
+ t.isIDevice = /(iPad|iPhone)/.test(ua);\r
+\r
// TinyMCE .NET webcontrol might be setting the values for TinyMCE\r
if (win.tinyMCEPreInit) {\r
t.suffix = tinyMCEPreInit.suffix;\r
createNS : function(n, o) {\r
var i, v;\r
\r
- o = o || window;\r
+ o = o || win;\r
\r
n = n.split('.');\r
for (i=0; i<n.length; i++) {\r
\r
// Initialize the API\r
tinymce._init();\r
+\r
+ // Expose tinymce namespace to the global namespace (window)\r
+ win.tinymce = win.tinyMCE = tinymce;\r
})(window);\r
\r
(function($, tinymce) {\r
tinymce.onCreate = function(ty, c, p) {\r
tinymce.extend(p, patches[c]);\r
};\r
-})(jQuery, tinymce);\r
+})(window.jQuery, tinymce);\r
+\r
+\r
\r
tinymce.create('tinymce.util.Dispatcher', {\r
scope : null,\r
}\r
\r
});\r
+\r
(function() {\r
var each = tinymce.each;\r
\r
}\r
});\r
})();\r
+\r
(function() {\r
var each = tinymce.each;\r
\r
}\r
});\r
})();\r
+\r
tinymce.create('static tinymce.util.JSON', {\r
serialize : function(o) {\r
var i, v, s = tinymce.util.JSON.serialize, t;\r
}\r
\r
});\r
+\r
tinymce.create('static tinymce.util.XHR', {\r
send : function(o) {\r
var x, t, w = window, c = 0;\r
}\r
}\r
});\r
+\r
(function() {\r
var extend = tinymce.extend, JSON = tinymce.util.JSON, XHR = tinymce.util.XHR;\r
\r
}\r
}\r
});\r
-}());(function(tinymce) {\r
+}());\r
+(function(tinymce) {\r
// Shorten names\r
var each = tinymce.each,\r
is = tinymce.is,\r
return o + ' />';\r
},\r
\r
- remove : function(n, k) {\r
- var t = this;\r
-\r
- return this.run(n, function(n) {\r
- var p, g, i;\r
+ remove : function(node, keep_children) {\r
+ return this.run(node, function(node) {\r
+ var parent, child;\r
\r
- p = n.parentNode;\r
+ parent = node.parentNode;\r
\r
- if (!p)\r
+ if (!parent)\r
return null;\r
\r
- if (k) {\r
- for (i = n.childNodes.length - 1; i >= 0; i--)\r
- t.insertAfter(n.childNodes[i], n);\r
-\r
- //each(n.childNodes, function(c) {\r
- // p.insertBefore(c.cloneNode(true), n);\r
- //});\r
- }\r
-\r
- // Fix IE psuedo leak\r
- if (t.fixPsuedoLeaks) {\r
- p = n.cloneNode(true);\r
- k = 'IELeakGarbageBin';\r
- g = t.get(k) || t.add(t.doc.body, 'div', {id : k, style : 'display:none'});\r
- g.appendChild(n);\r
- g.innerHTML = '';\r
-\r
- return p;\r
+ if (keep_children) {\r
+ while (child = node.firstChild) {\r
+ // IE 8 will crash if you don't remove completely empty text nodes\r
+ if (!tinymce.isIE || child.nodeType !== 3 || child.nodeValue)\r
+ parent.insertBefore(child, node);\r
+ else\r
+ node.removeChild(child);\r
+ }\r
}\r
\r
- return p.removeChild(n);\r
+ return parent.removeChild(node);\r
});\r
},\r
\r
e = t.boxModel ? d.documentElement : d.body;\r
x = t.getStyle(t.select('html')[0], 'borderWidth'); // Remove border\r
x = (x == 'medium' || t.boxModel && !t.isIE6) && 2 || x;\r
- n.top += t.win.self != t.win.top ? 2 : 0; // IE adds some strange extra cord if used in a frameset\r
\r
return {x : n.left + e.scrollLeft - x, y : n.top + e.scrollTop - x};\r
}\r
e.className = v;\r
\r
// Empty class attr\r
- if (!v)\r
+ if (!v) {\r
e.removeAttribute('class');\r
+ e.removeAttribute('className');\r
+ }\r
\r
return v;\r
}\r
// So if we replace the p elements with divs and mark them and then replace them back to paragraphs\r
// after we use innerHTML we can fix the DOM tree\r
h = h.replace(/<p ([^>]+)>|<p>/ig, '<div $1 _mce_tmp="1">');\r
- h = h.replace(/<\/p>/g, '</div>');\r
+ h = h.replace(/<\/p>/gi, '</div>');\r
\r
// Set the new HTML with DIVs\r
set();\r
});\r
},\r
\r
- insertAfter : function(n, r) {\r
- var t = this;\r
-\r
- r = t.get(r);\r
+ insertAfter : function(node, reference_node) {\r
+ reference_node = this.get(reference_node);\r
\r
- return this.run(n, function(n) {\r
- var p, ns;\r
+ return this.run(node, function(node) {\r
+ var parent, nextSibling;\r
\r
- p = r.parentNode;\r
- ns = r.nextSibling;\r
+ parent = reference_node.parentNode;\r
+ nextSibling = reference_node.nextSibling;\r
\r
- if (ns)\r
- p.insertBefore(n, ns);\r
+ if (nextSibling)\r
+ parent.insertBefore(node, nextSibling);\r
else\r
- p.appendChild(n);\r
+ parent.appendChild(node);\r
\r
- return n;\r
+ return node;\r
});\r
},\r
\r
});\r
}\r
\r
- // Fix IE psuedo leak for elements since replacing elements if fairly common\r
- // Will break parentNode for some unknown reason\r
- if (t.fixPsuedoLeaks && o.nodeType === 1) {\r
- o.parentNode.insertBefore(n, o);\r
- t.remove(o);\r
- return n;\r
- }\r
-\r
return o.parentNode.replaceChild(n, o);\r
});\r
},\r
},\r
\r
nodeIndex : function(node, normalized) {\r
- var idx = 0, lastNode, nodeType;\r
+ var idx = 0, lastNodeType, lastNode, nodeType;\r
\r
if (node) {\r
- for (node = node.previousSibling, lastNode = node; node; node = node.previousSibling) {\r
+ for (lastNodeType = node.nodeType, node = node.previousSibling, lastNode = node; node; node = node.previousSibling) {\r
nodeType = node.nodeType;\r
\r
- // Text nodes needs special treatment if the normalized argument is specified\r
+ // Normalize text nodes\r
if (normalized && nodeType == 3) {\r
- // Checks if the current node has contents and that the last node is a non text node or empty\r
- if (node.nodeValue.length > 0 && (lastNode.nodeType != nodeType || lastNode.nodeValue.length === 0))\r
- idx++;\r
- } else\r
- idx++;\r
+ if (nodeType == lastNodeType || !node.nodeValue.length)\r
+ continue;\r
+ }\r
\r
- lastNode = node;\r
+ idx++;\r
+ lastNodeType = nodeType;\r
}\r
}\r
\r
\r
tinymce.DOM = new tinymce.dom.DOMUtils(document, {process_html : 0});\r
})(tinymce);\r
+\r
(function(ns) {\r
// Range constructor\r
function Range(dom) {\r
\r
ns.Range = Range;\r
})(tinymce.dom);\r
+\r
(function() {\r
function Selection(selection) {\r
var t = this, invisibleChar = '\uFEFF', range, lastIERng, dom = selection.dom, TRUE = true, FALSE = false;\r
\r
- // Compares two IE specific ranges to see if they are different\r
- // this method is useful when invalidating the cached selection range\r
- function compareRanges(rng1, rng2) {\r
- if (rng1 && rng2) {\r
- // Both are control ranges and the selected element matches\r
- if (rng1.item && rng2.item && rng1.item(0) === rng2.item(0))\r
- return TRUE;\r
-\r
- // Both are text ranges and the range matches\r
- if (rng1.isEqual && rng2.isEqual && rng2.isEqual(rng1)) {\r
- // IE will say that the range is equal then produce an invalid argument exception\r
- // if you perform specific operations in a keyup event. For example Ctrl+Del.\r
- // This hack will invalidate the range cache if the exception occurs\r
- try {\r
- // Try accessing nextSibling will producer an invalid argument some times\r
- range.startContainer.nextSibling;\r
- return TRUE;\r
- } catch (ex) {\r
- // Ignore\r
- }\r
- }\r
- }\r
-\r
- return FALSE;\r
- };\r
-\r
// Returns a W3C DOM compatible range object by using the IE Range API\r
function getRange() {\r
- var ieRange = selection.getRng(), domRange = dom.createRng(), ieRange2, element, collapsed, isMerged;\r
+ var ieRange = selection.getRng(), domRange = dom.createRng(), element, collapsed;\r
\r
// If selection is outside the current document just return an empty range\r
element = ieRange.item ? ieRange.item(0) : ieRange.parentElement();\r
return domRange;\r
}\r
\r
- // Duplicare IE selection range and check if the range is collapsed\r
- ieRange2 = ieRange.duplicate();\r
collapsed = selection.isCollapsed();\r
\r
- // Insert invisible start marker\r
- ieRange.collapse();\r
- ieRange.pasteHTML('<span id="_mce_start" style="display:none;line-height:0">' + invisibleChar + '</span>');\r
+ function findEndPoint(start) {\r
+ var marker, container, offset, nodes, startIndex = 0, endIndex, index, parent, checkRng, position;\r
\r
- // Insert invisible end marker\r
- if (!collapsed) {\r
- ieRange2.collapse(FALSE);\r
- ieRange2.pasteHTML('<span id="_mce_end" style="display:none;line-height:0">' + invisibleChar + '</span>');\r
- }\r
+ // Setup temp range and collapse it\r
+ checkRng = ieRange.duplicate();\r
+ checkRng.collapse(start);\r
\r
- // Sets the end point of the range by looking for the marker\r
- // This method also merges the text nodes it splits so that\r
- // the DOM doesn't get fragmented.\r
- function setEndPoint(start) {\r
- var container, offset, marker, sibling;\r
+ // Create marker and insert it at the end of the endpoints parent\r
+ marker = dom.create('a');\r
+ parent = checkRng.parentElement();\r
\r
- // Look for endpoint marker\r
- marker = dom.get('_mce_' + (start ? 'start' : 'end'));\r
- sibling = marker.previousSibling;\r
+ // If parent doesn't have any children then set the container to that parent and the index to 0\r
+ if (!parent.hasChildNodes()) {\r
+ domRange[start ? 'setStart' : 'setEnd'](parent, 0);\r
+ return;\r
+ }\r
\r
- // Is marker after a text node\r
- if (sibling && sibling.nodeType == 3) {\r
- // Get container node and calc offset\r
- container = sibling;\r
- offset = container.nodeValue.length;\r
+ parent.appendChild(marker);\r
+ checkRng.moveToElementText(marker);\r
+ position = ieRange.compareEndPoints(start ? 'StartToStart' : 'EndToEnd', checkRng);\r
+ if (position > 0) {\r
+ // The position is after the end of the parent element.\r
+ // This is the case where IE puts the caret to the left edge of a table.\r
+ domRange[start ? 'setStartAfter' : 'setEndAfter'](parent);\r
dom.remove(marker);\r
+ return;\r
+ }\r
\r
- // Merge text nodes to reduce DOM fragmentation\r
- sibling = container.nextSibling;\r
- if (sibling && sibling.nodeType == 3) {\r
- isMerged = TRUE;\r
- container.appendData(sibling.nodeValue);\r
- dom.remove(sibling);\r
+ // Setup node list and endIndex\r
+ nodes = tinymce.grep(parent.childNodes);\r
+ endIndex = nodes.length - 1;\r
+ // Perform a binary search for the position\r
+ while (startIndex <= endIndex) {\r
+ index = Math.floor((startIndex + endIndex) / 2);\r
+\r
+ // Insert marker and check it's position relative to the selection\r
+ parent.insertBefore(marker, nodes[index]);\r
+ checkRng.moveToElementText(marker);\r
+ position = ieRange.compareEndPoints(start ? 'StartToStart' : 'EndToEnd', checkRng);\r
+ if (position > 0) {\r
+ // Marker is to the right\r
+ startIndex = index + 1;\r
+ } else if (position < 0) {\r
+ // Marker is to the left\r
+ endIndex = index - 1;\r
+ } else {\r
+ // Maker is where we are\r
+ found = true;\r
+ break;\r
}\r
- } else {\r
- sibling = marker.nextSibling;\r
+ }\r
\r
- // Is marker before a text node\r
- if (sibling && sibling.nodeType == 3) {\r
- container = sibling;\r
- offset = 0;\r
- } else {\r
- // Is marker before an element\r
- if (sibling)\r
- offset = dom.nodeIndex(sibling) - 1;\r
- else\r
- offset = dom.nodeIndex(marker);\r
+ // Setup container\r
+ container = position > 0 || index == 0 ? marker.nextSibling : marker.previousSibling;\r
+\r
+ // Handle element selection\r
+ if (container.nodeType == 1) {\r
+ dom.remove(marker);\r
+\r
+ // Find offset and container\r
+ offset = dom.nodeIndex(container);\r
+ container = container.parentNode;\r
\r
- container = marker.parentNode;\r
+ // Move the offset if we are setting the end or the position is after an element\r
+ if (!start || index > 0)\r
+ offset++;\r
+ } else {\r
+ // Calculate offset within text node\r
+ if (position > 0 || index == 0) {\r
+ checkRng.setEndPoint(start ? 'StartToStart' : 'EndToEnd', ieRange);\r
+ offset = checkRng.text.length;\r
+ } else {\r
+ checkRng.setEndPoint(start ? 'StartToStart' : 'EndToEnd', ieRange);\r
+ offset = container.nodeValue.length - checkRng.text.length;\r
}\r
\r
dom.remove(marker);\r
}\r
\r
- // Set start of range\r
- if (start)\r
- domRange.setStart(container, offset);\r
-\r
- // Set end of range or automatically if it's collapsed to increase performance\r
- if (!start || collapsed)\r
- domRange.setEnd(container, offset);\r
+ domRange[start ? 'setStart' : 'setEnd'](container, offset);\r
};\r
\r
- // Set start of range\r
- setEndPoint(TRUE);\r
+ // Find start point\r
+ findEndPoint(true);\r
\r
- // Set end of range if needed\r
+ // Find end point if needed\r
if (!collapsed)\r
- setEndPoint(FALSE);\r
-\r
- // Restore selection if the range contents was merged\r
- // since the selection was then moved since the text nodes got changed\r
- if (isMerged)\r
- t.addRange(domRange);\r
+ findEndPoint();\r
\r
return domRange;\r
};\r
\r
this.addRange = function(rng) {\r
- var ieRng, ieRng2, doc = selection.dom.doc, body = doc.body, startPos, endPos, sc, so, ec, eo, marker, lastIndex, skipStart, skipEnd;\r
-\r
- this.destroy();\r
-\r
- // Setup some shorter versions\r
- sc = rng.startContainer;\r
- so = rng.startOffset;\r
- ec = rng.endContainer;\r
- eo = rng.endOffset;\r
- ieRng = body.createTextRange();\r
-\r
- // If document selection move caret to first node in document\r
- if (sc == doc || ec == doc) {\r
- ieRng = body.createTextRange();\r
- ieRng.collapse();\r
- ieRng.select();\r
- return;\r
- }\r
-\r
- // If child index resolve it\r
- if (sc.nodeType == 1 && sc.hasChildNodes()) {\r
- lastIndex = sc.childNodes.length - 1;\r
+ var ieRng, ctrlRng, startContainer, startOffset, endContainer, endOffset, doc = selection.dom.doc, body = doc.body;\r
\r
- // Index is higher that the child count then we need to jump over the start container\r
- if (so > lastIndex) {\r
- skipStart = 1;\r
- sc = sc.childNodes[lastIndex];\r
- } else\r
- sc = sc.childNodes[so];\r
-\r
- // Child was text node then move offset to start of it\r
- if (sc.nodeType == 3)\r
- so = 0;\r
- }\r
-\r
- // If child index resolve it\r
- if (ec.nodeType == 1 && ec.hasChildNodes()) {\r
- lastIndex = ec.childNodes.length - 1;\r
+ function setEndPoint(start) {\r
+ var container, offset, marker, tmpRng, nodes;\r
\r
- if (eo == 0) {\r
- skipEnd = 1;\r
- ec = ec.childNodes[0];\r
- } else {\r
- ec = ec.childNodes[Math.min(lastIndex, eo - 1)];\r
+ marker = dom.create('a');\r
+ container = start ? startContainer : endContainer;\r
+ offset = start ? startOffset : endOffset;\r
+ tmpRng = ieRng.duplicate();\r
\r
- // Child was text node then move offset to end of text node\r
- if (ec.nodeType == 3)\r
- eo = ec.nodeValue.length;\r
+ if (container == doc) {\r
+ container = body;\r
+ offset = 0;\r
}\r
- }\r
\r
- // Single element selection\r
- if (sc == ec && sc.nodeType == 1) {\r
- // Make control selection for some elements\r
- if (/^(IMG|TABLE)$/.test(sc.nodeName) && so != eo) {\r
- ieRng = body.createControlRange();\r
- ieRng.addElement(sc);\r
+ if (container.nodeType == 3) {\r
+ container.parentNode.insertBefore(marker, container);\r
+ tmpRng.moveToElementText(marker);\r
+ tmpRng.moveStart('character', offset);\r
+ dom.remove(marker);\r
+ ieRng.setEndPoint(start ? 'StartToStart' : 'EndToEnd', tmpRng);\r
} else {\r
- ieRng = body.createTextRange();\r
+ nodes = container.childNodes;\r
\r
- // Padd empty elements with invisible character\r
- if (!sc.hasChildNodes() && sc.canHaveHTML)\r
- sc.innerHTML = invisibleChar;\r
-\r
- // Select element contents\r
- ieRng.moveToElementText(sc);\r
+ if (nodes.length) {\r
+ if (offset >= nodes.length) {\r
+ dom.insertAfter(marker, nodes[nodes.length - 1]);\r
+ } else {\r
+ container.insertBefore(marker, nodes[offset]);\r
+ }\r
\r
- // If it's only containing a padding remove it so the caret remains\r
- if (sc.innerHTML == invisibleChar) {\r
- ieRng.collapse(TRUE);\r
- sc.removeChild(sc.firstChild);\r
+ tmpRng.moveToElementText(marker);\r
+ } else {\r
+ // Empty node selection for example <div>|</div>\r
+ marker = doc.createTextNode(invisibleChar);\r
+ container.appendChild(marker);\r
+ tmpRng.moveToElementText(marker.parentNode);\r
+ tmpRng.collapse(TRUE);\r
}\r
- }\r
-\r
- if (so == eo)\r
- ieRng.collapse(eo <= rng.endContainer.childNodes.length - 1);\r
\r
- ieRng.select();\r
- ieRng.scrollIntoView();\r
- return;\r
+ ieRng.setEndPoint(start ? 'StartToStart' : 'EndToEnd', tmpRng);\r
+ dom.remove(marker);\r
+ }\r
}\r
\r
- // Create range and marker\r
- ieRng = body.createTextRange();\r
- marker = doc.createElement('span');\r
- marker.innerHTML = ' ';\r
-\r
- // Set start of range to startContainer/startOffset\r
- if (sc.nodeType == 3) {\r
- // Insert marker after/before startContainer\r
- if (skipStart)\r
- dom.insertAfter(marker, sc);\r
- else\r
- sc.parentNode.insertBefore(marker, sc);\r
-\r
- // Select marker the caret to offset position\r
- ieRng.moveToElementText(marker);\r
- marker.parentNode.removeChild(marker);\r
- ieRng.move('character', so);\r
- } else {\r
- ieRng.moveToElementText(sc);\r
+ // Destroy cached range\r
+ this.destroy();\r
\r
- if (skipStart)\r
- ieRng.collapse(FALSE);\r
- }\r
+ // Setup some shorter versions\r
+ startContainer = rng.startContainer;\r
+ startOffset = rng.startOffset;\r
+ endContainer = rng.endContainer;\r
+ endOffset = rng.endOffset;\r
+ ieRng = body.createTextRange();\r
\r
- // If same text container then we can do a more simple move\r
- if (sc == ec && sc.nodeType == 3) {\r
- ieRng.moveEnd('character', eo - so);\r
- ieRng.select();\r
- ieRng.scrollIntoView();\r
- return;\r
+ // If single element selection then try making a control selection out of it\r
+ if (startContainer == endContainer && startContainer.nodeType == 1 && startOffset == endOffset - 1) {\r
+ if (startOffset == endOffset - 1) {\r
+ try {\r
+ ctrlRng = body.createControlRange();\r
+ ctrlRng.addElement(startContainer.childNodes[startOffset]);\r
+ ctrlRng.select();\r
+ ctrlRng.scrollIntoView();\r
+ return;\r
+ } catch (ex) {\r
+ // Ignore\r
+ }\r
+ }\r
}\r
\r
- // Set end of range to endContainer/endOffset\r
- ieRng2 = body.createTextRange();\r
- if (ec.nodeType == 3) {\r
- // Insert marker after/before startContainer\r
- ec.parentNode.insertBefore(marker, ec);\r
-\r
- // Move selection to end marker and move caret to end offset\r
- ieRng2.moveToElementText(marker);\r
- marker.parentNode.removeChild(marker);\r
- ieRng2.move('character', eo);\r
- ieRng.setEndPoint('EndToStart', ieRng2);\r
- } else {\r
- ieRng2.moveToElementText(ec);\r
- ieRng2.collapse(!!skipEnd);\r
- ieRng.setEndPoint('EndToEnd', ieRng2);\r
- }\r
+ // Set start/end point of selection\r
+ setEndPoint(true);\r
+ setEndPoint();\r
\r
+ // Select the new range and scroll it into view\r
ieRng.select();\r
ieRng.scrollIntoView();\r
};\r
\r
this.getRangeAt = function() {\r
// Setup new range if the cache is empty\r
- if (!range || !compareRanges(lastIERng, selection.getRng())) {\r
+ if (!range || !tinymce.dom.RangeUtils.compareRanges(lastIERng, selection.getRng())) {\r
range = getRange();\r
\r
// Store away text range for next call\r
lastIERng = selection.getRng();\r
}\r
\r
+ // IE will say that the range is equal then produce an invalid argument exception\r
+ // if you perform specific operations in a keyup event. For example Ctrl+Del.\r
+ // This hack will invalidate the range cache if the exception occurs\r
+ try {\r
+ range.startContainer.nextSibling;\r
+ } catch (ex) {\r
+ range = getRange();\r
+ lastIERng = null;\r
+ }\r
+\r
// Return cached range\r
return range;\r
};\r
// Expose the selection object\r
tinymce.dom.TridentSelection = Selection;\r
})();\r
+\r
+\r
(function(tinymce) {\r
// Shorten names\r
var each = tinymce.each, DOM = tinymce.DOM, isIE = tinymce.isIE, isWebKit = tinymce.isWebKit, Event;\r
Event.destroy();\r
});\r
})(tinymce);\r
+\r
(function(tinymce) {\r
tinymce.dom.Element = function(id, settings) {\r
var t = this, dom, el;\r
});\r
};\r
})(tinymce);\r
+\r
(function(tinymce) {\r
function trimNl(s) {\r
return s.replace(/[\n\r]+/g, '');\r
h += '<span id="__caret">_</span>';\r
\r
// Delete and insert new node\r
- if (r.startContainer == d && r.endContainer == d) {\r
+ \r
+ if (r.startContainer == d && r.endContainer == d) {\r
// WebKit will fail if the body is empty since the range is then invalid and it can't insert contents\r
d.body.innerHTML = h;\r
} else {\r
r.deleteContents();\r
- r.insertNode(t.getRng().createContextualFragment(h));\r
+ if (d.body.childNodes.length == 0) {\r
+ d.body.innerHTML = h;\r
+ } else {\r
+ r.insertNode(r.createContextualFragment(h));\r
+ }\r
}\r
\r
// Move to caret marker\r
c = t.dom.get('__caret');\r
-\r
// Make sure we wrap it compleatly, Opera fails with a simple select call\r
r = d.createRange();\r
r.setStartBefore(c);\r
},\r
\r
getStart : function() {\r
- var t = this, r = t.getRng(), e;\r
-\r
- if (isIE) {\r
- if (r.item)\r
- return r.item(0);\r
+ var rng = this.getRng(), startElement, parentElement, checkRng, node;\r
\r
- r = r.duplicate();\r
- r.collapse(1);\r
- e = r.parentElement();\r
+ if (rng.duplicate || rng.item) {\r
+ // Control selection, return first item\r
+ if (rng.item)\r
+ return rng.item(0);\r
+\r
+ // Get start element\r
+ checkRng = rng.duplicate();\r
+ checkRng.collapse(1);\r
+ startElement = checkRng.parentElement();\r
+\r
+ // Check if range parent is inside the start element, then return the inner parent element\r
+ // This will fix issues when a single element is selected, IE would otherwise return the wrong start element\r
+ parentElement = node = rng.parentElement();\r
+ while (node = node.parentNode) {\r
+ if (node == startElement) {\r
+ startElement = parentElement;\r
+ break;\r
+ }\r
+ }\r
\r
- if (e && e.nodeName == 'BODY')\r
- return e.firstChild || e;\r
+ // If start element is body element try to move to the first child if it exists\r
+ if (startElement && startElement.nodeName == 'BODY')\r
+ return startElement.firstChild || startElement;\r
\r
- return e;\r
+ return startElement;\r
} else {\r
- e = r.startContainer;\r
+ startElement = rng.startContainer;\r
\r
- if (e.nodeType == 1 && e.hasChildNodes())\r
- e = e.childNodes[Math.min(e.childNodes.length - 1, r.startOffset)];\r
+ if (startElement.nodeType == 1 && startElement.hasChildNodes())\r
+ startElement = startElement.childNodes[Math.min(startElement.childNodes.length - 1, rng.startOffset)];\r
\r
- if (e && e.nodeType == 3)\r
- return e.parentNode;\r
+ if (startElement && startElement.nodeType == 3)\r
+ return startElement.parentNode;\r
\r
- return e;\r
+ return startElement;\r
}\r
},\r
\r
getEnd : function() {\r
var t = this, r = t.getRng(), e, eo;\r
\r
- if (isIE) {\r
+ if (r.duplicate || r.item) {\r
if (r.item)\r
return r.item(0);\r
\r
var rng = t.getRng(true), root = dom.getRoot(), bookmark = {};\r
\r
function getPoint(rng, start) {\r
- var indexes = [], node, lastIdx,\r
- container = rng[start ? 'startContainer' : 'endContainer'],\r
- offset = rng[start ? 'startOffset' : 'endOffset'], exclude, point = {};\r
-\r
- // Resolve element index\r
- if (container.nodeType == 1 && container.hasChildNodes()) {\r
- lastIdx = container.childNodes.length - 1;\r
- point.exclude = (start && offset > lastIdx) || (!start && offset == 0);\r
-\r
- if (!start && offset)\r
- offset--;\r
-\r
- container = container.childNodes[offset > lastIdx ? lastIdx : offset];\r
-\r
- if (container.nodeType == 3)\r
- offset = start ? 0 : container.nodeValue.length;\r
- }\r
+ var container = rng[start ? 'startContainer' : 'endContainer'],\r
+ offset = rng[start ? 'startOffset' : 'endOffset'], point = [], node, childNodes, after = 0;\r
\r
if (container.nodeType == 3) {\r
if (normalized) {\r
offset += node.nodeValue.length;\r
}\r
\r
- point.offset = offset;\r
+ point.push(offset);\r
+ } else {\r
+ childNodes = container.childNodes;\r
+\r
+ if (offset >= childNodes.length && childNodes.length) {\r
+ after = 1;\r
+ offset = Math.max(0, childNodes.length - 1);\r
+ }\r
+\r
+ point.push(t.dom.nodeIndex(childNodes[offset], normalized) + after);\r
}\r
\r
for (; container && container != root; container = container.parentNode)\r
- indexes.push(t.dom.nodeIndex(container, normalized));\r
-\r
- point.indexes = indexes;\r
+ point.push(t.dom.nodeIndex(container, normalized));\r
\r
return point;\r
};\r
},\r
\r
moveToBookmark : function(bookmark) {\r
- var t = this, dom = t.dom, marker1, marker2, rng, root;\r
+ var t = this, dom = t.dom, marker1, marker2, rng, root, startContainer, endContainer, startOffset, endOffset;\r
\r
// Clear selection cache\r
if (t.tridentSel)\r
root = dom.getRoot();\r
\r
function setEndPoint(start) {\r
- var point = bookmark[start ? 'start' : 'end'], i, node, offset;\r
+ var point = bookmark[start ? 'start' : 'end'], i, node, offset, children;\r
\r
if (point) {\r
- for (node = root, i = point.indexes.length - 1; i >= 0; i--)\r
- node = node.childNodes[point.indexes[i]] || node;\r
-\r
- if (start) {\r
- if (node.nodeType == 3 && point.offset)\r
- rng.setStart(node, point.offset);\r
- else {\r
- if (point.exclude)\r
- rng.setStartAfter(node);\r
- else\r
- rng.setStartBefore(node);\r
- }\r
- } else {\r
- if (node.nodeType == 3 && point.offset)\r
- rng.setEnd(node, point.offset);\r
- else {\r
- if (point.exclude)\r
- rng.setEndBefore(node);\r
- else\r
- rng.setEndAfter(node);\r
- }\r
+ // Find container node\r
+ for (node = root, i = point.length - 1; i >= 1; i--) {\r
+ children = node.childNodes;\r
+\r
+ if (children.length)\r
+ node = children[point[i]];\r
}\r
+\r
+ // Set offset within container node\r
+ if (start)\r
+ rng.setStart(node, point[0]);\r
+ else\r
+ rng.setEnd(node, point[0]);\r
}\r
};\r
\r
\r
t.setRng(rng);\r
} else if (bookmark.id) {\r
- rng = dom.createRng();\r
-\r
function restoreEndPoint(suffix) {\r
var marker = dom.get(bookmark.id + '_' + suffix), node, idx, next, prev, keep = bookmark.keep;\r
\r
if (suffix == 'start') {\r
if (!keep) {\r
idx = dom.nodeIndex(marker);\r
-\r
- if (idx > 0)\r
- idx++;\r
} else {\r
- node = marker;\r
+ node = marker.firstChild;\r
idx = 1;\r
}\r
\r
- rng.setStart(node, idx);\r
- rng.setEnd(node, idx);\r
+ startContainer = endContainer = node;\r
+ startOffset = endOffset = idx;\r
} else {\r
if (!keep) {\r
idx = dom.nodeIndex(marker);\r
} else {\r
- node = marker;\r
+ node = marker.firstChild;\r
idx = 1;\r
}\r
\r
- rng.setEnd(node, idx);\r
+ endContainer = node;\r
+ endOffset = idx;\r
}\r
\r
if (!keep) {\r
dom.remove(next);\r
\r
if (suffix == 'start') {\r
- rng.setStart(prev, idx);\r
- rng.setEnd(prev, idx);\r
- } else\r
- rng.setEnd(prev, idx);\r
+ startContainer = endContainer = prev;\r
+ startOffset = endOffset = idx;\r
+ } else {\r
+ endContainer = prev;\r
+ endOffset = idx;\r
+ }\r
}\r
}\r
}\r
};\r
\r
+ function addBogus(node) {\r
+ // Adds a bogus BR element for empty block elements\r
+ // on non IE browsers just to have a place to put the caret\r
+ if (!isIE && dom.isBlock(node) && !node.innerHTML)\r
+ node.innerHTML = '<br _mce_bogus="1" />';\r
+\r
+ return node;\r
+ };\r
+\r
// Restore start/end points\r
restoreEndPoint('start');\r
restoreEndPoint('end');\r
\r
+ rng = dom.createRng();\r
+ rng.setStart(addBogus(startContainer), startOffset);\r
+ rng.setEnd(addBogus(endContainer), endOffset);\r
t.setRng(rng);\r
} else if (bookmark.name) {\r
t.select(dom.select(bookmark.name)[bookmark.index]);\r
// This can occur when the editor is placed in a hidden container element on Gecko\r
// Or on IE when there was an exception\r
if (!r)\r
- r = isIE ? t.win.document.body.createTextRange() : t.win.document.createRange();\r
+ r = t.win.document.createRange ? t.win.document.createRange() : t.win.document.body.createTextRange();\r
\r
+ if (t.selectedRange && t.explicitRange) {\r
+ if (r.compareBoundaryPoints(r.START_TO_START, t.selectedRange) === 0 && r.compareBoundaryPoints(r.END_TO_END, t.selectedRange) === 0) {\r
+ // Safari, Opera and Chrome only ever select text which causes the range to change.\r
+ // This lets us use the originally set range if the selection hasn't been changed by the user.\r
+ r = t.explicitRange;\r
+ } else {\r
+ t.selectedRange = null;\r
+ t.explicitRange = null;\r
+ }\r
+ }\r
return r;\r
},\r
\r
setRng : function(r) {\r
var s, t = this;\r
-\r
+ \r
if (!t.tridentSel) {\r
s = t.getSel();\r
\r
if (s) {\r
+ t.explicitRange = r;\r
s.removeAllRanges();\r
s.addRange(r);\r
+ t.selectedRange = s.getRangeAt(0);\r
}\r
} else {\r
// Is W3C Range\r
getNode : function() {\r
var t = this, rng = t.getRng(), sel = t.getSel(), elm;\r
\r
- if (!isIE) {\r
+ if (rng.setStart) {\r
// Range maybe lost after the editor is made visible again\r
if (!rng)\r
return t.dom.getRoot();\r
}\r
});\r
})(tinymce);\r
+\r
(function(tinymce) {\r
tinymce.create('tinymce.dom.XMLWriter', {\r
node : null,\r
}\r
});\r
})(tinymce);\r
+\r
(function(tinymce) {\r
tinymce.create('tinymce.dom.StringWriter', {\r
str : null,\r
}\r
});\r
})(tinymce);\r
+\r
(function(tinymce) {\r
// Shorten names\r
var extend = tinymce.extend, each = tinymce.each, Dispatcher = tinymce.util.Dispatcher, isIE = tinymce.isIE, isGecko = tinymce.isGecko;\r
},\r
\r
_serializeNode : function(n, inner) {\r
- 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;\r
+ 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;\r
\r
if (!s.node_filter || s.node_filter(n)) {\r
switch (n.nodeType) {\r
\r
// Add correct prefix on IE\r
if (isIE) {\r
- if (n.scopeName !== 'HTML' && n.scopeName !== 'html')\r
- nn = n.scopeName + ':' + nn;\r
+ scopeName = n.scopeName;\r
+ if (scopeName && scopeName !== 'HTML' && scopeName !== 'html')\r
+ nn = scopeName + ':' + nn;\r
}\r
\r
// Remove mce prefix on IE needed for the abbr element\r
}\r
\r
ru = t.findRule(nn);\r
+ \r
+ // No valid rule for this element could be found then skip it\r
+ if (!ru) {\r
+ iv = true;\r
+ break;\r
+ }\r
+\r
nn = ru.name || nn;\r
closed = s.closed.test(nn);\r
\r
}\r
});\r
})(tinymce);\r
+\r
(function(tinymce) {\r
tinymce.dom.ScriptLoader = function(settings) {\r
var QUEUED = 0,\r
// Global script loader\r
tinymce.ScriptLoader = new tinymce.dom.ScriptLoader();\r
})(tinymce);\r
+\r
tinymce.dom.TreeWalker = function(start_node, root_node) {\r
var node = start_node;\r
\r
return (node = findSibling(node, 'lastChild', 'lastSibling', shallow));\r
};\r
};\r
+\r
(function() {\r
var transitional = {};\r
\r
return !!(element && (!child_name || element[child_name]));\r
};\r
};\r
-})();(function(tinymce) {\r
+})();\r
+(function(tinymce) {\r
tinymce.dom.RangeUtils = function(dom) {\r
var INVISIBLE_CHAR = '\uFEFF';\r
\r
};\r
*/\r
};\r
+\r
+ tinymce.dom.RangeUtils.compareRanges = function(rng1, rng2) {\r
+ if (rng1 && rng2) {\r
+ // Compare native IE ranges\r
+ if (rng1.item || rng1.duplicate) {\r
+ // Both are control ranges and the selected element matches\r
+ if (rng1.item && rng2.item && rng1.item(0) === rng2.item(0))\r
+ return true;\r
+\r
+ // Both are text ranges and the range matches\r
+ if (rng1.isEqual && rng2.isEqual && rng2.isEqual(rng1))\r
+ return true;\r
+ } else {\r
+ // Compare w3c ranges\r
+ return rng1.startContainer == rng2.startContainer && rng1.startOffset == rng2.startOffset;\r
+ }\r
+ }\r
+\r
+ return false;\r
+ };\r
})(tinymce);\r
+\r
(function(tinymce) {\r
// Shorten class names\r
var DOM = tinymce.DOM, is = tinymce.is;\r
tinymce.dom.Event.clear(this.id);\r
}\r
});\r
-})(tinymce);tinymce.create('tinymce.ui.Container:tinymce.ui.Control', {\r
+})(tinymce);\r
+tinymce.create('tinymce.ui.Container:tinymce.ui.Control', {\r
Container : function(id, s) {\r
this.parent(id, s);\r
\r
}\r
});\r
\r
+\r
tinymce.create('tinymce.ui.Separator:tinymce.ui.Control', {\r
Separator : function(id, s) {\r
this.parent(id, s);\r
return tinymce.DOM.createHTML('span', {'class' : this.classPrefix});\r
}\r
});\r
+\r
(function(tinymce) {\r
var is = tinymce.is, DOM = tinymce.DOM, each = tinymce.each, walk = tinymce.walk;\r
\r
}\r
});\r
})(tinymce);\r
+\r
(function(tinymce) {\r
var is = tinymce.is, DOM = tinymce.DOM, each = tinymce.each, walk = tinymce.walk;\r
\r
return m;\r
}\r
});\r
-})(tinymce);(function(tinymce) {\r
+})(tinymce);\r
+(function(tinymce) {\r
var is = tinymce.is, DOM = tinymce.DOM, each = tinymce.each, Event = tinymce.dom.Event, Element = tinymce.dom.Element;\r
\r
tinymce.create('tinymce.ui.DropMenu:tinymce.ui.Menu', {\r
DOM.addClass(ro, 'mceLast');\r
}\r
});\r
-})(tinymce);(function(tinymce) {\r
+})(tinymce);\r
+(function(tinymce) {\r
var DOM = tinymce.DOM;\r
\r
tinymce.create('tinymce.ui.Button:tinymce.ui.Control', {\r
}\r
});\r
})(tinymce);\r
+\r
(function(tinymce) {\r
var DOM = tinymce.DOM, Event = tinymce.dom.Event, each = tinymce.each, Dispatcher = tinymce.util.Dispatcher;\r
\r
var t = this, cp = t.classPrefix;\r
\r
Event.add(t.id, 'click', t.showMenu, t);\r
- Event.add(t.id + '_text', 'focus', function(e) {\r
+ Event.add(t.id + '_text', 'focus', function() {\r
if (!t._focused) {\r
t.keyDownHandler = Event.add(t.id + '_text', 'keydown', function(e) {\r
var idx = -1, v, kc = e.keyCode;\r
Event.clear(this.id + '_open');\r
}\r
});\r
-})(tinymce);(function(tinymce) {\r
+})(tinymce);\r
+(function(tinymce) {\r
var DOM = tinymce.DOM, Event = tinymce.dom.Event, each = tinymce.each, Dispatcher = tinymce.util.Dispatcher;\r
\r
tinymce.create('tinymce.ui.NativeListBox:tinymce.ui.ListBox', {\r
},\r
\r
getLength : function() {\r
- return DOM.get(this.id).options.length - 1;\r
+ return this.items.length;\r
},\r
\r
renderHTML : function() {\r
t.onPostRender.dispatch(t, DOM.get(t.id));\r
}\r
});\r
-})(tinymce);(function(tinymce) {\r
+})(tinymce);\r
+(function(tinymce) {\r
var DOM = tinymce.DOM, Event = tinymce.dom.Event, each = tinymce.each;\r
\r
tinymce.create('tinymce.ui.MenuButton:tinymce.ui.Button', {\r
}\r
});\r
})(tinymce);\r
+\r
(function(tinymce) {\r
var DOM = tinymce.DOM, Event = tinymce.dom.Event, each = tinymce.each;\r
\r
}\r
});\r
})(tinymce);\r
+\r
(function(tinymce) {\r
var DOM = tinymce.DOM, Event = tinymce.dom.Event, is = tinymce.is, each = tinymce.each;\r
\r
}\r
});\r
})(tinymce);\r
+\r
tinymce.create('tinymce.ui.Toolbar:tinymce.ui.Container', {\r
renderHTML : function() {\r
var t = this, h = '', c, co, dom = tinymce.DOM, s = t.settings, i, pr, nx, cl;\r
return dom.createHTML('table', {id : t.id, 'class' : 'mceToolbar' + (s['class'] ? ' ' + s['class'] : ''), cellpadding : '0', cellspacing : '0', align : t.settings.align || ''}, '<tbody><tr>' + h + '</tr></tbody>');\r
}\r
});\r
+\r
(function(tinymce) {\r
var Dispatcher = tinymce.util.Dispatcher, each = tinymce.each;\r
\r
if (!t.getElement())\r
return;\r
\r
+ // Is a iPad/iPhone, then skip initialization. We need to sniff here since the\r
+ // browser says it has contentEditable support but there is no visible caret\r
+ // We will remove this check ones Apple implements full contentEditable support\r
+ if (tinymce.isIDevice)\r
+ return;\r
+\r
// Add hidden input for non input elements inside form elements\r
if (!/TEXTAREA|INPUT/i.test(t.getElement().nodeName) && s.hidden_input && DOM.getParent(id, 'form'))\r
DOM.insertAfter(DOM.create('input', {type : 'hidden', name : id}), id);\r
hilitecolor : {inline : 'span', styles : {backgroundColor : '%value'}},\r
fontname : {inline : 'span', styles : {fontFamily : '%value'}},\r
fontsize : {inline : 'span', styles : {fontSize : '%value'}},\r
- blockquote : {block : 'blockquote', wrapper : 1},\r
+ fontsize_class : {inline : 'span', attributes : {'class' : '%value'}},\r
+ blockquote : {block : 'blockquote', wrapper : 1, remove : 'all'},\r
\r
removeformat : [\r
{selector : 'b,strong,em,i,font,u,strike', remove : 'all', split : true, expand : false, block_expand : true, deep : true},\r
{selector : 'span', attributes : ['style', 'class'], remove : 'empty', split : true, expand : false, deep : true},\r
- {selector : '*', attributes : ['style', 'class'], expand : false, deep : true}\r
+ {selector : '*', attributes : ['style', 'class'], split : false, expand : false, deep : true}\r
]\r
});\r
\r
// Register default block formats\r
each('p h1 h2 h3 h4 h5 h6 div address pre div code dt dd samp'.split(/\s/), function(name) {\r
- t.formatter.register(name, {block : name});\r
+ t.formatter.register(name, {block : name, remove : 'all'});\r
});\r
\r
// Register user defined formats\r
\r
\r
focus : function(sf) {\r
- var oed, t = this, ce = t.settings.content_editable;\r
+ var oed, t = this, ce = t.settings.content_editable, ieRng, controlElm, doc = t.getDoc();\r
\r
if (!sf) {\r
- // Is not content editable or the selection is outside the area in IE\r
- // the IE statement is needed to avoid bluring if element selections inside layers since\r
- // the layer is like it's own document in IE\r
- if (!ce && (!isIE || t.selection.getNode().ownerDocument != t.getDoc()))\r
+ // Get selected control element\r
+ ieRng = t.selection.getRng();\r
+ if (ieRng.item) {\r
+ controlElm = ieRng.item(0);\r
+ }\r
+\r
+ // Is not content editable\r
+ if (!ce)\r
t.getWin().focus();\r
\r
+ // Restore selected control element\r
+ // This is needed when for example an image is selected within a\r
+ // layer a call to focus will then remove the control selection\r
+ if (controlElm && controlElm.ownerDocument == doc) {\r
+ ieRng = doc.body.createControlRange();\r
+ ieRng.addElement(controlElm);\r
+ ieRng.select();\r
+ }\r
+\r
}\r
\r
if (tinymce.activeEditor != t) {\r
},\r
\r
nodeChanged : function(o) {\r
- var t = this, s = t.selection, n = s.getNode() || t.getBody();\r
+ var t = this, s = t.selection, n = (isIE ? s.getNode() : s.getStart()) || t.getBody();\r
\r
// Fix for bug #1896577 it seems that this can not be fired while the editor is loading\r
if (t.initialized) {\r
\r
// Add node change handlers\r
t.onMouseUp.add(t.nodeChanged);\r
- t.onClick.add(t.nodeChanged);\r
+ //t.onClick.add(t.nodeChanged);\r
t.onKeyUp.add(function(ed, e) {\r
var c = e.keyCode;\r
\r
}\r
\r
// Add default shortcuts for gecko\r
- if (isGecko) {\r
- t.addShortcut('ctrl+b', t.getLang('bold_desc'), 'Bold');\r
- t.addShortcut('ctrl+i', t.getLang('italic_desc'), 'Italic');\r
- t.addShortcut('ctrl+u', t.getLang('underline_desc'), 'Underline');\r
- }\r
+ t.addShortcut('ctrl+b', t.getLang('bold_desc'), 'Bold');\r
+ t.addShortcut('ctrl+i', t.getLang('italic_desc'), 'Italic');\r
+ t.addShortcut('ctrl+u', t.getLang('underline_desc'), 'Underline');\r
\r
// BlockFormat shortcuts keys\r
for (i=1; i<=6; i++)\r
case 8:\r
// Fix IE control + backspace browser bug\r
if (t.selection.getRng().item) {\r
- t.selection.getRng().item(0).removeNode();\r
+ ed.dom.remove(t.selection.getRng().item(0));\r
return Event.cancel(e);\r
}\r
}\r
});\r
\r
t.onKeyDown.add(function(ed, e) {\r
+ var rng, parent, bookmark;\r
+\r
+ // IE has a really odd bug where the DOM might include an node that doesn't have\r
+ // a proper structure. If you try to access nodeValue it would throw an illegal value exception.\r
+ // This seems to only happen when you delete contents and it seems to be avoidable if you refresh the element\r
+ // after you delete contents from it. See: #3008923\r
+ if (isIE && e.keyCode == 46) {\r
+ rng = t.selection.getRng();\r
+\r
+ if (rng.parentElement) {\r
+ parent = rng.parentElement();\r
+\r
+ // Select next word when ctrl key is used in combo with delete\r
+ if (e.ctrlKey) {\r
+ rng.moveEnd('word', 1);\r
+ rng.select();\r
+ }\r
+\r
+ // Delete contents\r
+ t.selection.getSel().clear();\r
+\r
+ // Check if we are within the same parent\r
+ if (rng.parentElement() == parent) {\r
+ bookmark = t.selection.getBookmark();\r
+\r
+ try {\r
+ // Update the HTML and hopefully it will remove the artifacts\r
+ parent.innerHTML = parent.innerHTML;\r
+ } catch (ex) {\r
+ // And since it's IE it can sometimes produce an unknown runtime error\r
+ }\r
+\r
+ // Restore the caret position\r
+ t.selection.moveToBookmark(bookmark);\r
+ }\r
+\r
+ // Block the default delete behavior since it might be broken\r
+ e.preventDefault();\r
+ return;\r
+ }\r
+ }\r
+\r
// Is caracter positon keys\r
if ((e.keyCode >= 33 && e.keyCode <= 36) || (e.keyCode >= 37 && e.keyCode <= 40) || e.keyCode == 13 || e.keyCode == 45) {\r
if (t.undoManager.typing)\r
}\r
});\r
})(tinymce);\r
+\r
(function(tinymce) {\r
// Added for compression purposes\r
var each = tinymce.each, undefined, TRUE = true, FALSE = false;\r
}\r
\r
// Present alert message about clipboard access not being available\r
- if (failed || !doc.queryCommandEnabled(command)) {\r
+ if (failed || !doc.queryCommandSupported(command)) {\r
if (tinymce.isGecko) {\r
editor.windowManager.confirm(editor.getLang('clipboard_msg'), function(state) {\r
if (state)\r
},\r
\r
FormatBlock : function(command, ui, value) {\r
- return toggleFormat(value);\r
+ return toggleFormat(value || 'p');\r
},\r
\r
mceCleanup : function() {\r
- storeSelection();\r
+ var bookmark = selection.getBookmark();\r
+\r
editor.setContent(editor.getContent({cleanup : TRUE}), {cleanup : TRUE});\r
- restoreSelection();\r
+\r
+ selection.moveToBookmark(bookmark);\r
},\r
\r
mceRemoveNode : function(command, ui, value) {\r
var node = value || selection.getNode();\r
\r
// Make sure that the body node isn't removed\r
- if (node != ed.getBody()) {\r
+ if (node != editor.getBody()) {\r
storeSelection();\r
editor.dom.remove(node, TRUE);\r
restoreSelection();\r
}\r
},\r
\r
+ mceToggleFormat : function(command, ui, value) {\r
+ editor.formatter.toggle(value);\r
+ },\r
+\r
InsertHorizontalRule : function() {\r
selection.setContent('<hr />');\r
},\r
if (value.href)\r
dom.setAttribs(link, value);\r
else\r
- ed.dom.remove(link, TRUE);\r
+ editor.dom.remove(link, TRUE);\r
}\r
+ },\r
+ \r
+ selectAll : function() {\r
+ var root = dom.getRoot(), rng = dom.createRng();\r
+\r
+ rng.setStart(root, 0);\r
+ rng.setEnd(root, root.childNodes.length);\r
+\r
+ editor.selection.setRng(rng);\r
}\r
});\r
\r
});\r
}\r
};\r
-})(tinymce);(function(tinymce) {\r
- tinymce.create('tinymce.UndoManager', {\r
- index : 0,\r
- data : null,\r
- typing : 0,\r
+})(tinymce);\r
+(function(tinymce) {\r
+ var Dispatcher = tinymce.util.Dispatcher;\r
\r
- UndoManager : function(ed) {\r
- var t = this, Dispatcher = tinymce.util.Dispatcher;\r
+ tinymce.UndoManager = function(editor) {\r
+ var self, index = 0, data = [];\r
\r
- t.editor = ed;\r
- t.data = [];\r
- t.onAdd = new Dispatcher(this);\r
- t.onUndo = new Dispatcher(this);\r
- t.onRedo = new Dispatcher(this);\r
- },\r
+ function getContent() {\r
+ return tinymce.trim(editor.getContent({format : 'raw', no_events : 1}));\r
+ };\r
\r
- add : function(l) {\r
- var t = this, i, ed = t.editor, b, s = ed.settings, la;\r
+ return self = {\r
+ typing : 0,\r
\r
- l = l || {};\r
- l.content = l.content || ed.getContent({format : 'raw', no_events : 1});\r
- l.content = l.content.replace(/^\s*|\s*$/g, '');\r
+ onAdd : new Dispatcher(self),\r
+ onUndo : new Dispatcher(self),\r
+ onRedo : new Dispatcher(self),\r
\r
- // Add undo level if needed\r
- la = t.data[t.index];\r
- if (la && la.content == l.content) {\r
- if (t.index > 0 || t.data.length == 1)\r
- return null;\r
- }\r
+ add : function(level) {\r
+ var i, settings = editor.settings, lastLevel;\r
\r
- // Time to compress\r
- if (s.custom_undo_redo_levels) {\r
- if (t.data.length > s.custom_undo_redo_levels) {\r
- for (i = 0; i < t.data.length - 1; i++)\r
- t.data[i] = t.data[i + 1];\r
+ level = level || {};\r
+ level.content = getContent();\r
\r
- t.data.length--;\r
- t.index = t.data.length;\r
+ // Add undo level if needed\r
+ lastLevel = data[index];\r
+ if (lastLevel && lastLevel.content == level.content) {\r
+ if (index > 0 || data.length == 1)\r
+ return null;\r
}\r
- }\r
\r
- if (s.custom_undo_redo_restore_selection)\r
- l.bookmark = b = l.bookmark || ed.selection.getBookmark(2, true);\r
+ // Time to compress\r
+ if (settings.custom_undo_redo_levels) {\r
+ if (data.length > settings.custom_undo_redo_levels) {\r
+ for (i = 0; i < data.length - 1; i++)\r
+ data[i] = data[i + 1];\r
\r
- // Crop array if needed\r
- if (t.index < t.data.length - 1) {\r
- // Treat first level as initial\r
- if (t.index == 0)\r
- t.data = [];\r
- else\r
- t.data.length = t.index + 1;\r
- }\r
+ data.length--;\r
+ index = data.length;\r
+ }\r
+ }\r
\r
- t.data.push(l);\r
- t.index = t.data.length - 1;\r
+ // Get a non intrusive normalized bookmark\r
+ level.bookmark = editor.selection.getBookmark(2, true);\r
\r
- t.onAdd.dispatch(t, l);\r
- ed.isNotDirty = 0;\r
+ // Crop array if needed\r
+ if (index < data.length - 1) {\r
+ // Treat first level as initial\r
+ if (index == 0)\r
+ data = [];\r
+ else\r
+ data.length = index + 1;\r
+ }\r
\r
- //console.log(t.index);\r
- //console.dir(t.data);\r
+ data.push(level);\r
+ index = data.length - 1;\r
\r
- return l;\r
- },\r
+ self.onAdd.dispatch(self, level);\r
+ editor.isNotDirty = 0;\r
\r
- undo : function() {\r
- var t = this, ed = t.editor, l = l, i;\r
+ return level;\r
+ },\r
\r
- if (t.typing) {\r
- t.add();\r
- t.typing = 0;\r
- }\r
+ undo : function() {\r
+ var level, i;\r
\r
- if (t.index > 0) {\r
- l = t.data[--t.index];\r
+ if (self.typing) {\r
+ self.add();\r
+ self.typing = 0;\r
+ }\r
\r
- ed.setContent(l.content, {format : 'raw'});\r
- ed.selection.moveToBookmark(l.bookmark);\r
+ if (index > 0) {\r
+ level = data[--index];\r
\r
- t.onUndo.dispatch(t, l);\r
- }\r
+ editor.setContent(level.content, {format : 'raw'});\r
+ editor.selection.moveToBookmark(level.bookmark);\r
\r
- return l;\r
- },\r
+ self.onUndo.dispatch(self, level);\r
+ }\r
\r
- redo : function() {\r
- var t = this, ed = t.editor, l = null;\r
+ return level;\r
+ },\r
\r
- if (t.index < t.data.length - 1) {\r
- l = t.data[++t.index];\r
- ed.setContent(l.content, {format : 'raw'});\r
- ed.selection.moveToBookmark(l.bookmark);\r
+ redo : function() {\r
+ var level;\r
\r
- t.onRedo.dispatch(t, l);\r
- }\r
+ if (index < data.length - 1) {\r
+ level = data[++index];\r
\r
- return l;\r
- },\r
+ editor.setContent(level.content, {format : 'raw'});\r
+ editor.selection.moveToBookmark(level.bookmark);\r
\r
- clear : function() {\r
- var t = this;\r
+ self.onRedo.dispatch(self, level);\r
+ }\r
\r
- t.data = [];\r
- t.index = 0;\r
- t.typing = 0;\r
- },\r
+ return level;\r
+ },\r
\r
- hasUndo : function() {\r
- return this.index > 0 || this.typing;\r
- },\r
+ clear : function() {\r
+ data = [];\r
+ index = self.typing = 0;\r
+ },\r
\r
- hasRedo : function() {\r
- return this.index < this.data.length - 1;\r
- }\r
- });\r
+ hasUndo : function() {\r
+ return index > 0 || self.typing;\r
+ },\r
+\r
+ hasRedo : function() {\r
+ return index < data.length - 1;\r
+ }\r
+ };\r
+ };\r
})(tinymce);\r
+\r
(function(tinymce) {\r
// Shorten names\r
var Event = tinymce.dom.Event,\r
TRUE = true,\r
FALSE = false;\r
\r
+ function cloneFormats(node) {\r
+ var clone, temp, inner;\r
+\r
+ do {\r
+ if (/^(SPAN|STRONG|B|EM|I|FONT|STRIKE|U)$/.test(node.nodeName)) {\r
+ if (clone) {\r
+ temp = node.cloneNode(false);\r
+ temp.appendChild(clone);\r
+ clone = temp;\r
+ } else {\r
+ clone = inner = node.cloneNode(false);\r
+ }\r
+\r
+ clone.removeAttribute('id');\r
+ }\r
+ } while (node = node.parentNode);\r
+\r
+ if (clone)\r
+ return {wrapper : clone, inner : inner};\r
+ };\r
+\r
// Checks if the selection/caret is at the end of the specified block element\r
function isAtEnd(rng, par) {\r
var rng2 = par.ownerDocument.createRange();\r
}\r
}\r
\r
- if (!isIE && s.force_p_newlines) {\r
- ed.onKeyPress.add(function(ed, e) {\r
- if (e.keyCode == 13 && !e.shiftKey && !t.insertPara(e))\r
- Event.cancel(e);\r
- });\r
+ if (s.force_p_newlines) {\r
+ if (!isIE) {\r
+ ed.onKeyPress.add(function(ed, e) {\r
+ if (e.keyCode == 13 && !e.shiftKey && !t.insertPara(e))\r
+ Event.cancel(e);\r
+ });\r
+ } else {\r
+ // Ungly hack to for IE to preserve the formatting when you press\r
+ // enter at the end of a block element with formatted contents\r
+ // This logic overrides the browsers default logic with\r
+ // custom logic that enables us to control the output\r
+ tinymce.addUnload(function() {\r
+ t._previousFormats = 0; // Fix IE leak\r
+ });\r
+\r
+ ed.onKeyPress.add(function(ed, e) {\r
+ t._previousFormats = 0;\r
+\r
+ // Clone the current formats, this will later be applied to the new block contents\r
+ if (e.keyCode == 13 && !e.shiftKey && ed.selection.isCollapsed() && s.keep_styles)\r
+ t._previousFormats = cloneFormats(ed.selection.getStart());\r
+ });\r
+\r
+ ed.onKeyUp.add(function(ed, e) {\r
+ // Let IE break the element and the wrap the new caret location in the previous formats\r
+ if (e.keyCode == 13 && !e.shiftKey) {\r
+ var parent = ed.selection.getStart(), fmt = t._previousFormats;\r
+\r
+ // Parent is an empty block\r
+ if (!parent.hasChildNodes()) {\r
+ parent = dom.getParent(parent, dom.isBlock);\r
+\r
+ if (parent) {\r
+ parent.innerHTML = '';\r
+ \r
+ if (t._previousFormats) {\r
+ parent.appendChild(fmt.wrapper);\r
+ fmt.inner.innerHTML = '\uFEFF';\r
+ } else\r
+ parent.innerHTML = '\uFEFF';\r
+\r
+ selection.select(parent, 1);\r
+ ed.getDoc().execCommand('Delete', false, null);\r
+ }\r
+ }\r
+ }\r
+ });\r
+ }\r
\r
if (isGecko) {\r
ed.onKeyDown.add(function(ed, e) {\r
// Workaround for missing shift+enter support, http://bugs.webkit.org/show_bug.cgi?id=16973\r
if (tinymce.isWebKit) {\r
function insertBr(ed) {\r
- var rng = selection.getRng(), br;\r
+ var rng = selection.getRng(), br, div = dom.create('div', null, ' '), divYPos, vpHeight = dom.getViewPort(ed.getWin()).h;\r
\r
// Insert BR element\r
rng.insertNode(br = dom.create('br'));\r
selection.collapse(TRUE);\r
}\r
\r
+ // Create a temporary DIV after the BR and get the position as it\r
+ // seems like getPos() returns 0 for text nodes and BR elements.\r
+ dom.insertAfter(div, br);\r
+ divYPos = dom.getPos(div).y;\r
+ dom.remove(div);\r
+\r
// Scroll to new position, scrollIntoView can't be used due to bug: http://bugs.webkit.org/show_bug.cgi?id=16117\r
- ed.getWin().scrollTo(0, dom.getPos(selection.getRng().startContainer).y);\r
+ if (divYPos > vpHeight) // It is not necessary to scroll if the DIV is inside the view port.\r
+ ed.getWin().scrollTo(0, divYPos);\r
};\r
\r
ed.onKeyPress.add(function(ed, e) {\r
- if (e.keyCode == 13 && (e.shiftKey || s.force_br_newlines)) {\r
+ if (e.keyCode == 13 && (e.shiftKey || (s.force_br_newlines && !dom.getParent(selection.getNode(), 'h1,h2,h3,h4,h5,h6,ol,ul')))) {\r
insertBr(ed);\r
Event.cancel(e);\r
}\r
}\r
}\r
} else {\r
+ // Force control range into text range\r
+ if (r.item) {\r
+ tr = d.body.createTextRange();\r
+ tr.moveToElementText(r.item(0));\r
+ r = tr;\r
+ }\r
+\r
tr = d.body.createTextRange();\r
tr.moveToElementText(b);\r
tr.collapse(1);\r
},\r
\r
backspaceDelete : function(e, bs) {\r
- 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;\r
+ 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;\r
+\r
+ // Delete when caret is behind a element doesn't work correctly on Gecko see #3011651\r
+ if (!bs && r.collapsed && sc.nodeType == 1 && r.startOffset == sc.childNodes.length) {\r
+ walker = new tinymce.dom.TreeWalker(sc.lastChild, sc);\r
+\r
+ // Walk the dom backwards until we find a text node\r
+ for (n = sc.lastChild; n; n = walker.prev()) {\r
+ if (n.nodeType == 3) {\r
+ r.setStart(n, n.nodeValue.length);\r
+ r.collapse(true);\r
+ se.setRng(r);\r
+ return;\r
+ }\r
+ }\r
+ }\r
\r
// The caret sometimes gets stuck in Gecko if you delete empty paragraphs\r
// This workaround removes the element by hand and moves the caret to the previous element\r
}\r
}\r
}\r
-\r
- // Gecko generates BR elements here and there, we don't like those so lets remove them\r
- function handler(e) {\r
- var pr;\r
-\r
- e = e.target;\r
-\r
- // A new BR was created in a block element, remove it\r
- if (e && e.parentNode && e.nodeName == 'BR' && (n = t.getParentBlock(e))) {\r
- pr = e.previousSibling;\r
-\r
- Event.remove(b, 'DOMNodeInserted', handler);\r
-\r
- // Is there whitespace at the end of the node before then we might need the pesky BR\r
- // to place the caret at a correct location see bug: #2013943\r
- if (pr && pr.nodeType == 3 && /\s+$/.test(pr.nodeValue))\r
- return;\r
-\r
- // Only remove BR elements that got inserted in the middle of the text\r
- if (e.previousSibling || e.nextSibling)\r
- ed.dom.remove(e);\r
- }\r
- };\r
-\r
- // Listen for new nodes\r
- Event._add(b, 'DOMNodeInserted', handler);\r
-\r
- // Remove listener\r
- window.setTimeout(function() {\r
- Event._remove(b, 'DOMNodeInserted', handler);\r
- }, 1);\r
}\r
});\r
})(tinymce);\r
+\r
(function(tinymce) {\r
// Shorten names\r
var DOM = tinymce.DOM, Event = tinymce.dom.Event, each = tinymce.each, extend = tinymce.extend;\r
}\r
});\r
})(tinymce);\r
+\r
(function(tinymce) {\r
var Dispatcher = tinymce.util.Dispatcher, each = tinymce.each, isIE = tinymce.isIE, isOpera = tinymce.isOpera;\r
\r
return tinymce.DOM.decode(s).replace(/\\n/g, '\n');\r
}\r
});\r
-}(tinymce));(function(tinymce) {\r
+}(tinymce));\r
+(function(tinymce) {\r
function CommandManager() {\r
var execCommands = {}, queryStateCommands = {}, queryValueCommands = {};\r
\r
};\r
\r
tinymce.GlobalCommands = new CommandManager();\r
-})(tinymce);(function(tinymce) {\r
+})(tinymce);\r
+(function(tinymce) {\r
tinymce.Formatter = function(ed) {\r
var formats = {},\r
each = tinymce.each,\r
FALSE = false,\r
TRUE = true,\r
undefined,\r
- caretHandler,\r
- pendingFormats;\r
+ pendingFormats = {apply : [], remove : []};\r
+\r
+ function isArray(obj) {\r
+ return obj instanceof Array;\r
+ };\r
\r
function getParents(node, selector) {\r
return dom.getParents(node, selector, dom.getRoot());\r
};\r
\r
- function resetPending() {\r
- // Needs reset\r
- if (!pendingFormats || pendingFormats.apply.length || pendingFormats.remove.length)\r
- pendingFormats = {apply : [], remove : []};\r
+ function isCaretNode(node) {\r
+ return node.nodeType === 1 && (node.face === 'mceinline' || node.style.fontFamily === 'mceinline');\r
};\r
\r
- ed.onMouseUp.add(resetPending);\r
- resetPending();\r
-\r
// Public functions\r
\r
function get(name) {\r
\r
// Default to true\r
if (format.split === undefined)\r
- format.split = !format.selector;\r
+ format.split = !format.selector || format.inline;\r
\r
// Default to true\r
- if (format.remove === undefined && format.selector)\r
+ if (format.remove === undefined && format.selector && !format.inline)\r
format.remove = 'none';\r
\r
+ // Mark format as a mixed format inline + block level\r
+ if (format.selector && format.inline) {\r
+ format.mixed = true;\r
+ format.block_expand = true;\r
+ }\r
+\r
// Split classes if needed\r
if (typeof(format.classes) === 'string')\r
format.classes = format.classes.split(/\s+/);\r
\r
// Move startContainer/startOffset in to a suitable node\r
if (container.nodeType == 1 || container.nodeValue === "") {\r
- walker = new TreeWalker(container.childNodes[offset]);\r
- for (node = walker.current(); node; node = walker.next()) {\r
- if (node.nodeType == 3 && !isBlock(node.parentNode) && !isWhiteSpaceNode(node)) {\r
- rng.setStart(node, 0);\r
- break;\r
+ container = container.nodeType == 1 ? container.childNodes[offset] : container;\r
+\r
+ // Might fail if the offset is behind the last element in it's container\r
+ if (container) {\r
+ walker = new TreeWalker(container, container.parentNode);\r
+ for (node = walker.current(); node; node = walker.next()) {\r
+ if (node.nodeType == 3 && !isWhiteSpaceNode(node)) {\r
+ rng.setStart(node, 0);\r
+ break;\r
+ }\r
}\r
}\r
}\r
var currentWrapElm;\r
\r
function process(node) {\r
- var nodeName = node.nodeName.toLowerCase(), parentName = node.parentNode.nodeName.toLowerCase();\r
+ var nodeName = node.nodeName.toLowerCase(), parentName = node.parentNode.nodeName.toLowerCase(), found;\r
\r
// Stop wrapping on br elements\r
if (isEq(nodeName, 'br')) {\r
if (format.selector) {\r
// Look for matching formats\r
each(formatList, function(format) {\r
- if (dom.is(node, format.selector))\r
+ if (dom.is(node, format.selector) && !isCaretNode(node)) {\r
setElementFormat(node, format);\r
+ found = true;\r
+ }\r
});\r
\r
- return;\r
+ // Continue processing if a selector match wasn't found and a inline element is defined\r
+ if (!format.inline || found) {\r
+ currentWrapElm = 0;\r
+ return;\r
+ }\r
}\r
\r
// Is it valid to wrap this item\r
var child, clone;\r
\r
each(node.childNodes, function(node) {\r
- if (node.nodeType == 1 && !isBookmarkNode(node)) {\r
+ if (node.nodeType == 1 && !isBookmarkNode(node) && !isCaretNode(node)) {\r
child = node;\r
return FALSE; // break loop\r
}\r
\r
dom.replace(clone, node, TRUE);\r
dom.remove(child, 1);\r
-\r
- return TRUE;\r
}\r
+\r
+ return clone || node;\r
};\r
\r
childCount = getChildCount(node);\r
\r
if (format.inline || format.wrapper) {\r
// Merges the current node with it's children of similar type to reduce the number of elements\r
- if (!format.exact && childCount === 1) {\r
- if (mergeStyles(node))\r
- return;\r
- }\r
+ if (!format.exact && childCount === 1)\r
+ node = mergeStyles(node);\r
\r
// Remove/merge children\r
each(formatList, function(format) {\r
});\r
});\r
\r
+ // Remove child if direct parent is of same type\r
+ if (matchNode(node.parentNode, name, vars)) {\r
+ dom.remove(node, 1);\r
+ node = 0;\r
+ return TRUE;\r
+ }\r
+\r
// Look for parent with similar style format\r
- dom.getParent(node.parentNode, function(parent) {\r
- if (matchNode(parent, name, vars)) {\r
- dom.remove(node, 1);\r
- node = 0;\r
- return TRUE;\r
- }\r
- });\r
+ if (format.merge_with_parents) {\r
+ dom.getParent(node.parentNode, function(parent) {\r
+ if (matchNode(parent, name, vars)) {\r
+ dom.remove(node, 1);\r
+ node = 0;\r
+ return TRUE;\r
+ }\r
+ });\r
+ }\r
\r
// Merge next and previous siblings if they are similar <b>text</b><b>text</b> becomes <b>texttext</b>\r
if (node) {\r
rng.setStartBefore(node);\r
rng.setEndAfter(node);\r
\r
- applyRngStyle(rng);\r
+ applyRngStyle(expandRng(rng, formatList));\r
} else {\r
if (!selection.isCollapsed() || !format.inline) {\r
// Apply formatting to selection\r
function remove(name, vars, node) {\r
var formatList = get(name), format = formatList[0], bookmark, i, rng;\r
\r
+ function moveStart(rng) {\r
+ var container = rng.startContainer,\r
+ offset = rng.startOffset,\r
+ walker, node, nodes, tmpNode;\r
+\r
+ // Convert text node into index if possible\r
+ if (container.nodeType == 3 && offset >= container.nodeValue.length - 1) {\r
+ container = container.parentNode;\r
+ offset = nodeIndex(container) + 1;\r
+ }\r
+\r
+ // Move startContainer/startOffset in to a suitable node\r
+ if (container.nodeType == 1) {\r
+ nodes = container.childNodes;\r
+ container = nodes[Math.min(offset, nodes.length - 1)];\r
+ walker = new TreeWalker(container);\r
+\r
+ // If offset is at end of the parent node walk to the next one\r
+ if (offset > nodes.length - 1)\r
+ walker.next();\r
+\r
+ for (node = walker.current(); node; node = walker.next()) {\r
+ if (node.nodeType == 3 && !isWhiteSpaceNode(node)) {\r
+ // IE has a "neat" feature where it moves the start node into the closest element\r
+ // we can avoid this by inserting an element before it and then remove it after we set the selection\r
+ tmpNode = dom.create('a', null, INVISIBLE_CHAR);\r
+ node.parentNode.insertBefore(tmpNode, node);\r
+\r
+ // Set selection and remove tmpNode\r
+ rng.setStart(node, 0);\r
+ selection.setRng(rng);\r
+ dom.remove(tmpNode);\r
+\r
+ return;\r
+ }\r
+ }\r
+ }\r
+ };\r
+\r
// Merges the styles for each node\r
function process(node) {\r
var children, i, l;\r
\r
// Find format root\r
each(getParents(container.parentNode).reverse(), function(parent) {\r
+ var format;\r
+\r
// Find format root element\r
if (!formatRoot && parent.id != '_start' && parent.id != '_end') {\r
- // If the matched format has a remove none flag we shouldn't split it\r
- if (!isBlock(parent) && matchNode(parent, name, vars))\r
+ // Is the node matching the format we are looking for\r
+ format = matchNode(parent, name, vars);\r
+ if (format && format.split !== false)\r
formatRoot = parent;\r
}\r
});\r
}\r
}\r
\r
- if (split)\r
+ // Never split block elements if the format is mixed\r
+ if (split && (!format.mixed || !isBlock(format_root)))\r
container = dom.split(format_root, container);\r
\r
// Wrap container in cloned formats\r
var node = dom.get(start ? '_start' : '_end'),\r
out = node[start ? 'firstChild' : 'lastChild'];\r
\r
- dom.remove(node, 1);\r
+ // If the end is placed within the start the result will be removed\r
+ // So this checks if the out node is a bookmark node if it is it\r
+ // checks for another more suitable node\r
+ if (isBookmarkNode(out))\r
+ out = out[start ? 'firstChild' : 'lastChild'];\r
+\r
+ dom.remove(node, true);\r
\r
return out;\r
};\r
bookmark = selection.getBookmark();\r
removeRngStyle(selection.getRng(TRUE));\r
selection.moveToBookmark(bookmark);\r
+\r
+ // Check if start element still has formatting then we are at: "<b>text|</b>text" and need to move the start into the next text node\r
+ if (match(name, vars, selection.getStart())) {\r
+ moveStart(selection.getRng(true));\r
+ }\r
+\r
ed.nodeChanged();\r
} else\r
performCaretAction('remove', name, vars);\r
apply(name, vars, node);\r
};\r
\r
- function matchNode(node, name, vars) {\r
+ function matchNode(node, name, vars, similar) {\r
var formatList = get(name), format, i, classes;\r
\r
function matchItems(node, format, item_name) {\r
else\r
value = getStyle(node, key);\r
\r
- if (!isEq(value, replaceVars(items[key], vars)))\r
+ if (similar && !value && !format.exact)\r
+ return;\r
+\r
+ if ((!similar || format.exact) && !isEq(value, replaceVars(items[key], vars)))\r
return;\r
}\r
}\r
// Only one match needed for indexed arrays\r
for (i = 0; i < items.length; i++) {\r
if (item_name === 'attributes' ? dom.getAttrib(node, items[i]) : getStyle(node, items[i]))\r
- return TRUE;\r
+ return format;\r
}\r
}\r
}\r
\r
- return TRUE;\r
+ return format;\r
};\r
\r
if (formatList && node) {\r
}\r
}\r
\r
- return TRUE;\r
+ return format;\r
}\r
}\r
}\r
function matchParents(node) {\r
// Find first node with similar format settings\r
node = dom.getParent(node, function(node) {\r
- return !!matchNode(node, name, vars);\r
+ return !!matchNode(node, name, vars, true);\r
});\r
\r
// Do an exact check on the similar format element\r
return FALSE;\r
};\r
\r
+ function matchAll(names, vars) {\r
+ var startElement, matchedFormatNames = [], checkedMap = {}, i, ni, name;\r
+\r
+ // If the selection is collapsed then check pending formats\r
+ if (selection.isCollapsed()) {\r
+ for (ni = 0; ni < names.length; ni++) {\r
+ // If the name is to be removed, then stop it from being added\r
+ for (i = pendingFormats.remove.length - 1; i >= 0; i--) {\r
+ name = names[ni];\r
+\r
+ if (pendingFormats.remove[i].name == name) {\r
+ checkedMap[name] = true;\r
+ break;\r
+ }\r
+ }\r
+ }\r
+\r
+ // If the format is to be applied\r
+ for (i = pendingFormats.apply.length - 1; i >= 0; i--) {\r
+ for (ni = 0; ni < names.length; ni++) {\r
+ name = names[ni];\r
+\r
+ if (!checkedMap[name] && pendingFormats.apply[i].name == name) {\r
+ checkedMap[name] = true;\r
+ matchedFormatNames.push(name);\r
+ }\r
+ }\r
+ }\r
+ }\r
+\r
+ // Check start of selection for formats\r
+ startElement = selection.getStart();\r
+ dom.getParent(startElement, function(node) {\r
+ var i, name;\r
+\r
+ for (i = 0; i < names.length; i++) {\r
+ name = names[i];\r
+\r
+ if (!checkedMap[name] && matchNode(node, name, vars)) {\r
+ checkedMap[name] = true;\r
+ matchedFormatNames.push(name);\r
+ }\r
+ }\r
+ });\r
+\r
+ return matchedFormatNames;\r
+ };\r
+\r
function canApply(name) {\r
var formatList = get(name), startNode, parents, i, x, selector;\r
\r
remove : remove,\r
toggle : toggle,\r
match : match,\r
+ matchAll : matchAll,\r
matchNode : matchNode,\r
canApply : canApply\r
});\r
str1 = str1 || '';\r
str2 = str2 || '';\r
\r
- str1 = str1.nodeName || str1;\r
- str2 = str2.nodeName || str2;\r
+ str1 = '' + (str1.nodeName || str1);\r
+ str2 = '' + (str2.nodeName || str2);\r
\r
return str1.toLowerCase() == str2.toLowerCase();\r
};\r
};\r
\r
function isWhiteSpaceNode(node) {\r
- return node && node.nodeType === 3 && /^\s*$/.test(node.nodeValue);\r
+ return node && node.nodeType === 3 && /^([\s\r\n]+|)$/.test(node.nodeValue);\r
};\r
\r
function wrap(node, name, attrs) {\r
}\r
\r
// Expand start/end container to matching selector\r
- if (format[0].selector && format[0].expand !== FALSE) {\r
+ if (format[0].selector && format[0].expand !== FALSE && !format[0].inline) {\r
function findSelectorEndPoint(container, sibling_name) {\r
var parents, i, y;\r
\r
}\r
}\r
\r
+ // Never remove nodes that isn't the specified inline element if a selector is specified too\r
+ if (format.selector && format.inline && !isEq(format.inline, node))\r
+ return;\r
+\r
dom.remove(node, 1);\r
};\r
\r
each(dom.getAttribs(node), function(attr) {\r
var name = attr.nodeName.toLowerCase();\r
\r
- // Don't compare internal attributes or style/class\r
- if (name.indexOf('_') !== 0 && name !== 'class' && name !== 'style')\r
+ // Don't compare internal attributes or style\r
+ if (name.indexOf('_') !== 0 && name !== 'style')\r
attribs[name] = dom.getAttrib(node, name);\r
});\r
\r
};\r
\r
function isTextBlock(name) {\r
- return /^(h[1-6]|p|div|pre|address)$/.test(name);\r
+ return /^(h[1-6]|p|div|pre|address|dl|dt|dd)$/.test(name);\r
};\r
\r
function getContainer(rng, start) {\r
};\r
\r
function performCaretAction(type, name, vars) {\r
- var i, rng, selectedNode = selection.getNode().parentNode,\r
- doc = ed.getDoc(), marker = 'mceinline',\r
- events = ['onKeyDown', 'onKeyUp', 'onKeyPress'],\r
- currentPendingFormats = pendingFormats[type],\r
+ var i, currentPendingFormats = pendingFormats[type],\r
otherPendingFormats = pendingFormats[type == 'apply' ? 'remove' : 'apply'];\r
\r
- // Check if it already exists\r
- for (i = currentPendingFormats.length - 1; i >= 0; i--) {\r
- if (currentPendingFormats[i].name == name)\r
- return;\r
- }\r
-\r
- currentPendingFormats.push({name : name, vars : vars});\r
-\r
- // Check if it's in the oter type\r
- for (i = otherPendingFormats.length - 1; i >= 0; i--) {\r
- if (otherPendingFormats[i].name == name)\r
- otherPendingFormats.splice(i, 1);\r
- }\r
-\r
- function unbind() {\r
- if (caretHandler) {\r
- each(events, function(event) {\r
- ed[event].remove(caretHandler);\r
- });\r
+ function hasPending() {\r
+ return pendingFormats.apply.length || pendingFormats.remove.length;\r
+ };\r
\r
- caretHandler = 0;\r
- }\r
+ function resetPending() {\r
+ pendingFormats.apply = [];\r
+ pendingFormats.remove = [];\r
};\r
\r
function perform(caret_node) {\r
resetPending();\r
};\r
\r
- function isMarker(node) {\r
- return node.face == marker || node.style.fontFamily == marker;\r
- };\r
-\r
- unbind();\r
-\r
- doc.execCommand('FontName', false, marker);\r
+ // Check if it already exists then ignore it\r
+ for (i = currentPendingFormats.length - 1; i >= 0; i--) {\r
+ if (currentPendingFormats[i].name == name)\r
+ return;\r
+ }\r
\r
- // IE will convert the current word\r
- each(dom.select('font,span', selectedNode), function(node) {\r
- var bookmark;\r
+ currentPendingFormats.push({name : name, vars : vars});\r
\r
- if (isMarker(node)) {\r
- bookmark = selection.getBookmark();\r
- perform(node);\r
- selection.moveToBookmark(bookmark);\r
- ed.nodeChanged();\r
- selectedNode = 0;\r
- }\r
- });\r
+ // Check if it's in the other type, then remove it\r
+ for (i = otherPendingFormats.length - 1; i >= 0; i--) {\r
+ if (otherPendingFormats[i].name == name)\r
+ otherPendingFormats.splice(i, 1);\r
+ }\r
\r
- if (selectedNode) {\r
- caretHandler = function(ed, e) {\r
- each(dom.select('font,span', selectedNode), function(node) {\r
- var bookmark, textNode;\r
+ // Pending apply or remove formats\r
+ if (hasPending()) {\r
+ ed.getDoc().execCommand('FontName', false, 'mceinline');\r
+ pendingFormats.lastRng = selection.getRng();\r
\r
- // Look for marker\r
- if (node.face == marker || node.style.fontFamily == marker) {\r
- textNode = node.firstChild;\r
+ // IE will convert the current word\r
+ each(dom.select('font,span'), function(node) {\r
+ var bookmark;\r
\r
- perform(node);\r
+ if (isCaretNode(node)) {\r
+ bookmark = selection.getBookmark();\r
+ perform(node);\r
+ selection.moveToBookmark(bookmark);\r
+ ed.nodeChanged();\r
+ }\r
+ });\r
\r
- rng = dom.createRng();\r
- rng.setStart(textNode, textNode.nodeValue.length);\r
- rng.setEnd(textNode, textNode.nodeValue.length);\r
- selection.setRng(rng);\r
- ed.nodeChanged();\r
+ // Only register listeners once if we need to\r
+ if (!pendingFormats.isListening && hasPending()) {\r
+ pendingFormats.isListening = true;\r
+\r
+ each('onKeyDown,onKeyUp,onKeyPress,onMouseUp'.split(','), function(event) {\r
+ ed[event].addToTop(function(ed, e) {\r
+ // Do we have pending formats and is the selection moved has moved\r
+ if (hasPending() && !tinymce.dom.RangeUtils.compareRanges(pendingFormats.lastRng, selection.getRng())) {\r
+ each(dom.select('font,span'), function(node) {\r
+ var textNode, rng;\r
+\r
+ // Look for marker\r
+ if (isCaretNode(node)) {\r
+ textNode = node.firstChild;\r
+\r
+ if (textNode) {\r
+ perform(node);\r
+\r
+ rng = dom.createRng();\r
+ rng.setStart(textNode, textNode.nodeValue.length);\r
+ rng.setEnd(textNode, textNode.nodeValue.length);\r
+ selection.setRng(rng);\r
+ ed.nodeChanged();\r
+ } else\r
+ dom.remove(node);\r
+ }\r
+ });\r
\r
- unbind();\r
- }\r
+ // Always unbind and clear pending styles on keyup\r
+ if (e.type == 'keyup' || e.type == 'mouseup')\r
+ resetPending();\r
+ }\r
+ });\r
});\r
-\r
- // Always unbind and clear pending styles on keyup\r
- if (e.type == 'keyup') {\r
- unbind();\r
- resetPending();\r
- }\r
- };\r
-\r
- each(events, function(event) {\r
- ed[event].addToTop(caretHandler);\r
- });\r
+ }\r
}\r
- }\r
+ };\r
};\r
})(tinymce);\r
+\r
tinymce.onAddEditor.add(function(tinymce, ed) {\r
var filters, fontSizes, dom, settings = ed.settings;\r
\r
});\r
}\r
});\r
+\r