From 4cd91acc35bd1923b9c2bfd5c377c0ac2e157d23 Mon Sep 17 00:00:00 2001 From: iionly Date: Tue, 19 Feb 2013 20:15:34 +0100 Subject: Tinymce plugin: update to version 3.5.8 of Tinymce editor --- .../tinymce/jscripts/tiny_mce/tiny_mce_src.js | 16206 +++++++++++-------- 1 file changed, 9254 insertions(+), 6952 deletions(-) (limited to 'mod/tinymce/vendor/tinymce/jscripts/tiny_mce/tiny_mce_src.js') diff --git a/mod/tinymce/vendor/tinymce/jscripts/tiny_mce/tiny_mce_src.js b/mod/tinymce/vendor/tinymce/jscripts/tiny_mce/tiny_mce_src.js index 0866d617b..3bbe9ce95 100644 --- a/mod/tinymce/vendor/tinymce/jscripts/tiny_mce/tiny_mce_src.js +++ b/mod/tinymce/vendor/tinymce/jscripts/tiny_mce/tiny_mce_src.js @@ -1,13 +1,14 @@ +// FILE IS GENERATED BY COMBINING THE SOURCES IN THE "classes" DIRECTORY SO DON'T MODIFY THIS FILE DIRECTLY (function(win) { var whiteSpaceRe = /^\s*|\s*$/g, - undefined, isRegExpBroken = 'B'.replace(/A(.)|B/, '$1') === '$1'; + undef, isRegExpBroken = 'B'.replace(/A(.)|B/, '$1') === '$1'; var tinymce = { majorVersion : '3', - minorVersion : '4.7', + minorVersion : '5.8', - releaseDate : '2011-11-03', + releaseDate : '2012-11-20', _init : function() { var t = this, d = document, na = navigator, ua = na.userAgent, i, nl, n, base, p, v; @@ -50,7 +51,8 @@ // If base element found, add that infront of baseURL nl = d.getElementsByTagName('base'); for (i=0; i 0 ? args : [listener.scope]); - if (s === false) + if (returnValue === false) break; } - return s; + self.inDispatch = false; + + return returnValue; } }); @@ -571,14 +597,14 @@ tinymce.create('tinymce.util.Dispatcher', { u = (s.base_uri ? s.base_uri.protocol || 'http' : 'http') + '://mce_host' + u; // Relative path http:// or protocol relative //path - if (!/^[\w-]*:?\/\//.test(u)) { + if (!/^[\w\-]*:?\/\//.test(u)) { base_url = s.base_uri ? s.base_uri.path : new tinymce.util.URI(location.href).directory; u = ((s.base_uri && s.base_uri.protocol) || 'http') + '://mce_host' + t.toAbsPath(base_url, u); } // Parse URL (Credits goes to Steave, http://blog.stevenlevithan.com/archives/parseuri) u = u.replace(/@@/g, '(mce_at)'); // Zope 3 workaround, they use @@something - u = /^(?:(?![^:@]+:[^:@\/]*@)([^:\/?#.]+):)?(?:\/\/)?((?:(([^:@]*):?([^:@]*))?@)?([^:\/?#]*)(?::(\d*))?)(((\/(?:[^?#](?![^?#\/]*\.[^?#\/.]+(?:[?#]|$)))*\/?)?([^?#\/]*))(?:\?([^#]*))?(?:#(.*))?)/.exec(u); + u = /^(?:(?![^:@]+:[^:@\/]*@)([^:\/?#.]+):)?(?:\/\/)?((?:(([^:@\/]*):?([^:@\/]*))?@)?([^:\/?#]*)(?::(\d*))?)(((\/(?:[^?#](?![^?#\/]*\.[^?#\/.]+(?:[?#]|$)))*\/?)?([^?#\/]*))(?:\?([^#]*))?(?:#(.*))?)/.exec(u); each(["source","protocol","authority","userInfo","user","password","host","port","relative","path","directory","file","query","anchor"], function(v, i) { var s = u[i]; @@ -589,17 +615,18 @@ tinymce.create('tinymce.util.Dispatcher', { t[v] = s; }); - if (b = s.base_uri) { + b = s.base_uri; + if (b) { if (!t.protocol) t.protocol = b.protocol; if (!t.userInfo) t.userInfo = b.userInfo; - if (!t.port && t.host == 'mce_host') + if (!t.port && t.host === 'mce_host') t.port = b.port; - if (!t.host || t.host == 'mce_host') + if (!t.host || t.host === 'mce_host') t.host = b.host; t.source = ''; @@ -635,6 +662,12 @@ tinymce.create('tinymce.util.Dispatcher', { if ((u.host != 'mce_host' && t.host != u.host && u.host) || t.port != u.port || t.protocol != u.protocol) return u.getURI(); + var tu = t.getURI(), uu = u.getURI(); + + // Allow usage of the base_uri when relative_urls = true + if(tu == uu || (tu.charAt(tu.length - 1) == "/" && tu.substr(0, tu.length - 1) == uu)) + return tu; + o = t.toRelPath(t.path, u.path); // Add query @@ -649,7 +682,7 @@ tinymce.create('tinymce.util.Dispatcher', { }, toAbsolute : function(u, nh) { - var u = new tinymce.util.URI(u, {base_uri : this}); + u = new tinymce.util.URI(u, {base_uri : this}); return u.getURI(this.host == u.host && this.protocol == u.protocol ? nh : 0); }, @@ -680,7 +713,7 @@ tinymce.create('tinymce.util.Dispatcher', { } } - if (bp == 1) + if (bp === 1) return path; for (i = 0, l = base.length - (bp - 1); i < l; i++) @@ -715,11 +748,11 @@ tinymce.create('tinymce.util.Dispatcher', { // Merge relURLParts chunks for (i = path.length - 1, o = []; i >= 0; i--) { // Ignore empty or . - if (path[i].length == 0 || path[i] == ".") + if (path[i].length === 0 || path[i] === ".") continue; // Is parent - if (path[i] == '..') { + if (path[i] === '..') { nb++; continue; } @@ -830,7 +863,7 @@ tinymce.create('tinymce.util.Dispatcher', { if (b == -1) { b = c.indexOf(p); - if (b != 0) + if (b !== 0) return null; } else b += 2; @@ -851,19 +884,19 @@ tinymce.create('tinymce.util.Dispatcher', { ((s) ? "; secure" : ""); }, - remove : function(n, p) { - var d = new Date(); + remove : function(name, path, domain) { + var date = new Date(); - d.setTime(d.getTime() - 1000); + date.setTime(date.getTime() - 1000); - this.set(n, '', d, p, d); + this.set(name, '', date, path, domain); } }); })(); (function() { function serialize(o, quote) { - var i, v, t; + var i, v, t, name; quote = quote || '"'; @@ -892,7 +925,7 @@ tinymce.create('tinymce.util.Dispatcher', { } if (t == 'object') { - if (o.hasOwnProperty && o instanceof Array) { + if (o.hasOwnProperty && Object.prototype.toString.call(o) === '[object Array]') { for (i=0, v = '['; i 0 ? ',' : '') + serialize(o[i], quote); @@ -901,9 +934,9 @@ tinymce.create('tinymce.util.Dispatcher', { v = '{'; - for (i in o) { - if (o.hasOwnProperty(i)) { - v += typeof o[i] != 'function' ? (v.length > 1 ? ',' + quote : quote) + i + quote +':' + serialize(o[i], quote) : ''; + for (name in o) { + if (o.hasOwnProperty(name)) { + v += typeof o[name] != 'function' ? (v.length > 1 ? ',' + quote : quote) + name + quote +':' + serialize(o[name], quote) : ''; } } @@ -931,6 +964,18 @@ tinymce.create('static tinymce.util.XHR', { send : function(o) { var x, t, w = window, c = 0; + function ready() { + if (!o.async || x.readyState == 4 || c++ > 10000) { + if (o.success && c < 10000 && x.status == 200) + o.success.call(o.success_scope, '' + x.responseText, x, o); + else if (o.error) + o.error.call(o.error_scope, c > 10000 ? 'TIMED_OUT' : 'GENERAL', x, o); + + x = null; + } else + w.setTimeout(ready, 10); + }; + // Default settings o.scope = o.scope || this; o.success_scope = o.success_scope || o.scope; @@ -964,18 +1009,6 @@ tinymce.create('static tinymce.util.XHR', { x.send(o.data); - function ready() { - if (!o.async || x.readyState == 4 || c++ > 10000) { - if (o.success && c < 10000 && x.status == 200) - o.success.call(o.success_scope, '' + x.responseText, x, o); - else if (o.error) - o.error.call(o.error_scope, c > 10000 ? 'TIMED_OUT' : 'GENERAL', x, o); - - x = null; - } else - w.setTimeout(ready, 10); - }; - // Syncronous request if (!o.async) return ready(); @@ -1042,99 +1075,198 @@ tinymce.create('static tinymce.util.XHR', { }()); (function(tinymce){ tinymce.VK = { - DELETE: 46, BACKSPACE: 8, + DELETE: 46, + DOWN: 40, ENTER: 13, + LEFT: 37, + RIGHT: 39, + SPACEBAR: 32, TAB: 9, - SPACEBAR: 32, UP: 38, - DOWN: 40 - } + + modifierPressed: function (e) { + return e.shiftKey || e.ctrlKey || e.altKey; + }, + + metaKeyPressed: function(e) { + // Check if ctrl or meta key is pressed also check if alt is false for Polish users + return tinymce.isMac ? e.metaKey : e.ctrlKey && !e.altKey; + } + }; })(tinymce); -(function(tinymce) { - var VK = tinymce.VK, BACKSPACE = VK.BACKSPACE, DELETE = VK.DELETE; +tinymce.util.Quirks = function(editor) { + var VK = tinymce.VK, BACKSPACE = VK.BACKSPACE, DELETE = VK.DELETE, dom = editor.dom, selection = editor.selection, + settings = editor.settings, parser = editor.parser, serializer = editor.serializer, each = tinymce.each; - function cleanupStylesWhenDeleting(ed) { - var dom = ed.dom, selection = ed.selection; + function setEditorCommandState(cmd, state) { + try { + editor.getDoc().execCommand(cmd, false, state); + } catch (ex) { + // Ignore + } + } - ed.onKeyDown.add(function(ed, e) { - var rng, blockElm, node, clonedSpan, isDelete; + function getDocumentMode() { + var documentMode = editor.getDoc().documentMode; - isDelete = e.keyCode == DELETE; - if (isDelete || e.keyCode == BACKSPACE) { - e.preventDefault(); - rng = selection.getRng(); + return documentMode ? documentMode : 6; + }; - // Find root block - blockElm = dom.getParent(rng.startContainer, dom.isBlock); + function isDefaultPrevented(e) { + return e.isDefaultPrevented(); + }; - // On delete clone the root span of the next block element - if (isDelete) - blockElm = dom.getNext(blockElm, dom.isBlock); + function cleanupStylesWhenDeleting() { + function removeMergedFormatSpans(isDelete) { + var rng, blockElm, node, clonedSpan; - // Locate root span element and clone it since it would otherwise get merged by the "apple-style-span" on delete/backspace - if (blockElm) { - node = blockElm.firstChild; + rng = selection.getRng(); - // Ignore empty text nodes - while (node && node.nodeType == 3 && node.nodeValue.length == 0) - node = node.nextSibling; + // Find root block + blockElm = dom.getParent(rng.startContainer, dom.isBlock); - if (node && node.nodeName === 'SPAN') { - clonedSpan = node.cloneNode(false); - } + // On delete clone the root span of the next block element + if (isDelete) { + blockElm = dom.getNext(blockElm, dom.isBlock); + } + + // Locate root span element and clone it since it would otherwise get merged by the "apple-style-span" on delete/backspace + if (blockElm) { + node = blockElm.firstChild; + + // Ignore empty text nodes + while (node && node.nodeType == 3 && node.nodeValue.length === 0) { + node = node.nextSibling; } - // Do the backspace/delete actiopn - ed.getDoc().execCommand(isDelete ? 'ForwardDelete' : 'Delete', false, null); + if (node && node.nodeName === 'SPAN') { + clonedSpan = node.cloneNode(false); + } + } - // Find all odd apple-style-spans - blockElm = dom.getParent(rng.startContainer, dom.isBlock); - tinymce.each(dom.select('span.Apple-style-span,font.Apple-style-span', blockElm), function(span) { - var bm = selection.getBookmark(); + each(dom.select('span', blockElm), function(span) { + span.setAttribute('data-mce-mark', '1'); + }); - if (clonedSpan) { - dom.replace(clonedSpan.cloneNode(false), span, true); - } else { - dom.remove(span, true); - } + // Do the backspace/delete action + editor.getDoc().execCommand(isDelete ? 'ForwardDelete' : 'Delete', false, null); - // Restore the selection - selection.moveToBookmark(bm); - }); + // Find all odd apple-style-spans + blockElm = dom.getParent(rng.startContainer, dom.isBlock); + each(dom.select('span', blockElm), function(span) { + var bm = selection.getBookmark(); + + if (clonedSpan) { + dom.replace(clonedSpan.cloneNode(false), span, true); + } else if (!span.getAttribute('data-mce-mark')) { + dom.remove(span, true); + } else { + span.removeAttribute('data-mce-mark'); + } + + // Restore the selection + selection.moveToBookmark(bm); + }); + } + + editor.onKeyDown.add(function(editor, e) { + var isDelete; + + isDelete = e.keyCode == DELETE; + if (!isDefaultPrevented(e) && (isDelete || e.keyCode == BACKSPACE) && !VK.modifierPressed(e)) { + e.preventDefault(); + removeMergedFormatSpans(isDelete); } }); + + editor.addCommand('Delete', function() {removeMergedFormatSpans();}); }; + + function emptyEditorWhenDeleting() { + function serializeRng(rng) { + var body = dom.create("body"); + var contents = rng.cloneContents(); + body.appendChild(contents); + return selection.serializer.serialize(body, {format: 'html'}); + } - function emptyEditorWhenDeleting(ed) { - ed.onKeyUp.add(function(ed, e) { - var keyCode = e.keyCode; + function allContentsSelected(rng) { + var selection = serializeRng(rng); + + var allRng = dom.createRng(); + allRng.selectNode(editor.getBody()); + + var allSelection = serializeRng(allRng); + return selection === allSelection; + } + + editor.onKeyDown.add(function(editor, e) { + var keyCode = e.keyCode, isCollapsed; + + // Empty the editor if it's needed for example backspace at

|

+ if (!isDefaultPrevented(e) && (keyCode == DELETE || keyCode == BACKSPACE)) { + isCollapsed = editor.selection.isCollapsed(); + + // Selection is collapsed but the editor isn't empty + if (isCollapsed && !dom.isEmpty(editor.getBody())) { + return; + } - if (keyCode == DELETE || keyCode == BACKSPACE) { - if (ed.dom.isEmpty(ed.getBody())) { - ed.setContent('', {format : 'raw'}); - ed.nodeChanged(); + // IE deletes all contents correctly when everything is selected + if (tinymce.isIE && !isCollapsed) { return; } + + // Selection isn't collapsed but not all the contents is selected + if (!isCollapsed && !allContentsSelected(editor.selection.getRng())) { + return; + } + + // Manually empty the editor + editor.setContent(''); + editor.selection.setCursorLocation(editor.getBody(), 0); + editor.nodeChanged(); } }); }; - function inputMethodFocus(ed) { - ed.dom.bind(ed.getDoc(), 'focusin', function() { - ed.selection.setRng(ed.selection.getRng()); + function selectAll() { + editor.onKeyDown.add(function(editor, e) { + if (!isDefaultPrevented(e) && e.keyCode == 65 && VK.metaKeyPressed(e)) { + e.preventDefault(); + editor.execCommand('SelectAll'); + } }); }; - function removeHrOnBackspace(ed) { - ed.onKeyDown.add(function(ed, e) { - if (e.keyCode === BACKSPACE) { - if (ed.selection.isCollapsed() && ed.selection.getRng(true).startOffset === 0) { - var node = ed.selection.getNode(); + function inputMethodFocus() { + if (!editor.settings.content_editable) { + // Case 1 IME doesn't initialize if you focus the document + dom.bind(editor.getDoc(), 'focusin', function(e) { + selection.setRng(selection.getRng()); + }); + + // Case 2 IME doesn't initialize if you click the documentElement it also doesn't properly fire the focusin event + dom.bind(editor.getDoc(), 'mousedown', function(e) { + if (e.target == editor.getDoc().documentElement) { + editor.getWin().focus(); + selection.setRng(selection.getRng()); + } + }); + } + }; + + function removeHrOnBackspace() { + editor.onKeyDown.add(function(editor, e) { + if (!isDefaultPrevented(e) && e.keyCode === BACKSPACE) { + if (selection.isCollapsed() && selection.getRng(true).startOffset === 0) { + var node = selection.getNode(); var previousSibling = node.previousSibling; + if (previousSibling && previousSibling.nodeName && previousSibling.nodeName.toLowerCase() === "hr") { - ed.dom.remove(previousSibling); + dom.remove(previousSibling); tinymce.dom.Event.cancel(e); } } @@ -1142,13 +1274,13 @@ tinymce.create('static tinymce.util.XHR', { }) } - function focusBody(ed) { + function focusBody() { // Fix for a focus bug in FF 3.x where the body element // wouldn't get proper focus if the user clicked on the HTML element if (!Range.prototype.getClientRects) { // Detect getClientRects got introduced in FF 4 - ed.onMouseDown.add(function(ed, e) { - if (e.target.nodeName === "HTML") { - var body = ed.getBody(); + editor.onMouseDown.add(function(editor, e) { + if (!isDefaultPrevented(e) && e.target.nodeName === "HTML") { + var body = editor.getBody(); // Blur the body it's focused but not correctly focused body.blur(); @@ -1162,149 +1294,752 @@ tinymce.create('static tinymce.util.XHR', { } }; - function selectControlElements(ed) { - ed.onClick.add(function(ed, e) { + function selectControlElements() { + editor.onClick.add(function(editor, e) { e = e.target; // Workaround for bug, http://bugs.webkit.org/show_bug.cgi?id=12250 // WebKit can't even do simple things like selecting an image // Needs tobe the setBaseAndExtend or it will fail to select floated images - if (/^(IMG|HR)$/.test(e.nodeName)) - ed.selection.getSel().setBaseAndExtent(e, 0, e, 1); + if (/^(IMG|HR)$/.test(e.nodeName)) { + selection.getSel().setBaseAndExtent(e, 0, e, 1); + } - if (e.nodeName == 'A' && ed.dom.hasClass(e, 'mceItemAnchor')) - ed.selection.select(e); + if (e.nodeName == 'A' && dom.hasClass(e, 'mceItemAnchor')) { + selection.select(e); + } - ed.nodeChanged(); + editor.nodeChanged(); }); }; - function selectionChangeNodeChanged(ed) { + function removeStylesWhenDeletingAccrossBlockElements() { + function getAttributeApplyFunction() { + var template = dom.getAttribs(selection.getStart().cloneNode(false)); + + return function() { + var target = selection.getStart(); + + if (target !== editor.getBody()) { + dom.setAttrib(target, "style", null); + + each(template, function(attr) { + target.setAttributeNode(attr.cloneNode(true)); + }); + } + }; + } + + function isSelectionAcrossElements() { + return !selection.isCollapsed() && dom.getParent(selection.getStart(), dom.isBlock) != dom.getParent(selection.getEnd(), dom.isBlock); + } + + function blockEvent(editor, e) { + e.preventDefault(); + return false; + } + + editor.onKeyPress.add(function(editor, e) { + var applyAttributes; + + if (!isDefaultPrevented(e) && (e.keyCode == 8 || e.keyCode == 46) && isSelectionAcrossElements()) { + applyAttributes = getAttributeApplyFunction(); + editor.getDoc().execCommand('delete', false, null); + applyAttributes(); + e.preventDefault(); + return false; + } + }); + + dom.bind(editor.getDoc(), 'cut', function(e) { + var applyAttributes; + + if (!isDefaultPrevented(e) && isSelectionAcrossElements()) { + applyAttributes = getAttributeApplyFunction(); + editor.onKeyUp.addToTop(blockEvent); + + setTimeout(function() { + applyAttributes(); + editor.onKeyUp.remove(blockEvent); + }, 0); + } + }); + } + + function selectionChangeNodeChanged() { var lastRng, selectionTimer; - ed.dom.bind(ed.getDoc(), 'selectionchange', function() { + dom.bind(editor.getDoc(), 'selectionchange', function() { if (selectionTimer) { clearTimeout(selectionTimer); selectionTimer = 0; } selectionTimer = window.setTimeout(function() { - var rng = ed.selection.getRng(); + var rng = selection.getRng(); // Compare the ranges to see if it was a real change or not if (!lastRng || !tinymce.dom.RangeUtils.compareRanges(rng, lastRng)) { - ed.nodeChanged(); + editor.nodeChanged(); lastRng = rng; } }, 50); }); } - function ensureBodyHasRoleApplication(ed) { + function ensureBodyHasRoleApplication() { document.body.setAttribute("role", "application"); } - tinymce.create('tinymce.util.Quirks', { - Quirks: function(ed) { - // WebKit - if (tinymce.isWebKit) { - cleanupStylesWhenDeleting(ed); - emptyEditorWhenDeleting(ed); - inputMethodFocus(ed); - selectControlElements(ed); - - // iOS - if (tinymce.isIDevice) { - selectionChangeNodeChanged(ed); + function disableBackspaceIntoATable() { + editor.onKeyDown.add(function(editor, e) { + if (!isDefaultPrevented(e) && e.keyCode === BACKSPACE) { + if (selection.isCollapsed() && selection.getRng(true).startOffset === 0) { + var previousSibling = selection.getNode().previousSibling; + if (previousSibling && previousSibling.nodeName && previousSibling.nodeName.toLowerCase() === "table") { + return tinymce.dom.Event.cancel(e); + } } } + }) + } - // IE - if (tinymce.isIE) { - removeHrOnBackspace(ed); - emptyEditorWhenDeleting(ed); - ensureBodyHasRoleApplication(ed); + function addNewLinesBeforeBrInPre() { + // IE8+ rendering mode does the right thing with BR in PRE + if (getDocumentMode() > 7) { + return; + } + + // Enable display: none in area and add a specific class that hides all BR elements in PRE to + // avoid the caret from getting stuck at the BR elements while pressing the right arrow key + setEditorCommandState('RespectVisibilityInDesign', true); + editor.contentStyles.push('.mceHideBrInPre pre br {display: none}'); + dom.addClass(editor.getBody(), 'mceHideBrInPre'); + + // Adds a \n before all BR elements in PRE to get them visual + parser.addNodeFilter('pre', function(nodes, name) { + var i = nodes.length, brNodes, j, brElm, sibling; + + while (i--) { + brNodes = nodes[i].getAll('br'); + j = brNodes.length; + while (j--) { + brElm = brNodes[j]; + + // Add \n before BR in PRE elements on older IE:s so the new lines get rendered + sibling = brElm.prev; + if (sibling && sibling.type === 3 && sibling.value.charAt(sibling.value - 1) != '\n') { + sibling.value += '\n'; + } else { + brElm.parent.insert(new tinymce.html.Node('#text', 3), brElm, true).value = '\n'; + } + } } + }); - // Gecko - if (tinymce.isGecko) { - removeHrOnBackspace(ed); - focusBody(ed); + // Removes any \n before BR elements in PRE since other browsers and in contentEditable=false mode they will be visible + serializer.addNodeFilter('pre', function(nodes, name) { + var i = nodes.length, brNodes, j, brElm, sibling; + + while (i--) { + brNodes = nodes[i].getAll('br'); + j = brNodes.length; + while (j--) { + brElm = brNodes[j]; + sibling = brElm.prev; + if (sibling && sibling.type == 3) { + sibling.value = sibling.value.replace(/\r?\n$/, ''); + } + } } - } - }); -})(tinymce); + }); + } -(function(tinymce) { - var namedEntities, baseEntities, reverseEntities, - attrsCharsRegExp = /[&<>\"\u007E-\uD7FF\uE000-\uFFEF]|[\uD800-\uDBFF][\uDC00-\uDFFF]/g, - textCharsRegExp = /[<>&\u007E-\uD7FF\uE000-\uFFEF]|[\uD800-\uDBFF][\uDC00-\uDFFF]/g, - rawCharsRegExp = /[<>&\"\']/g, - entityRegExp = /&(#x|#)?([\w]+);/g, - asciiMap = { - 128 : "\u20AC", 130 : "\u201A", 131 : "\u0192", 132 : "\u201E", 133 : "\u2026", 134 : "\u2020", - 135 : "\u2021", 136 : "\u02C6", 137 : "\u2030", 138 : "\u0160", 139 : "\u2039", 140 : "\u0152", - 142 : "\u017D", 145 : "\u2018", 146 : "\u2019", 147 : "\u201C", 148 : "\u201D", 149 : "\u2022", - 150 : "\u2013", 151 : "\u2014", 152 : "\u02DC", 153 : "\u2122", 154 : "\u0161", 155 : "\u203A", - 156 : "\u0153", 158 : "\u017E", 159 : "\u0178" - }; + function removePreSerializedStylesWhenSelectingControls() { + dom.bind(editor.getBody(), 'mouseup', function(e) { + var value, node = selection.getNode(); - // Raw entities - baseEntities = { - '\"' : '"', // Needs to be escaped since the YUI compressor would otherwise break the code - "'" : ''', - '<' : '<', - '>' : '>', - '&' : '&' - }; + // Moved styles to attributes on IMG eements + if (node.nodeName == 'IMG') { + // Convert style width to width attribute + if (value = dom.getStyle(node, 'width')) { + dom.setAttrib(node, 'width', value.replace(/[^0-9%]+/g, '')); + dom.setStyle(node, 'width', ''); + } - // Reverse lookup table for raw entities - reverseEntities = { - '<' : '<', - '>' : '>', - '&' : '&', - '"' : '"', - ''' : "'" - }; + // Convert style height to height attribute + if (value = dom.getStyle(node, 'height')) { + dom.setAttrib(node, 'height', value.replace(/[^0-9%]+/g, '')); + dom.setStyle(node, 'height', ''); + } + } + }); + } - // Decodes text by using the browser - function nativeDecode(text) { - var elm; + function keepInlineElementOnDeleteBackspace() { + editor.onKeyDown.add(function(editor, e) { + var isDelete, rng, container, offset, brElm, sibling, collapsed; - elm = document.createElement("div"); - elm.innerHTML = text; + isDelete = e.keyCode == DELETE; + if (!isDefaultPrevented(e) && (isDelete || e.keyCode == BACKSPACE) && !VK.modifierPressed(e)) { + rng = selection.getRng(); + container = rng.startContainer; + offset = rng.startOffset; + collapsed = rng.collapsed; - return elm.textContent || elm.innerText || text; - }; + // Override delete if the start container is a text node and is at the beginning of text or + // just before/after the last character to be deleted in collapsed mode + if (container.nodeType == 3 && container.nodeValue.length > 0 && ((offset === 0 && !collapsed) || (collapsed && offset === (isDelete ? 0 : 1)))) { + nonEmptyElements = editor.schema.getNonEmptyElements(); - // Build a two way lookup table for the entities - function buildEntitiesLookup(items, radix) { - var i, chr, entity, lookup = {}; + // Prevent default logic since it's broken + e.preventDefault(); - if (items) { - items = items.split(','); - radix = radix || 10; + // Insert a BR before the text node this will prevent the containing element from being deleted/converted + brElm = dom.create('br', {id: '__tmp'}); + container.parentNode.insertBefore(brElm, container); - // Build entities lookup table - for (i = 0; i < items.length; i += 2) { - chr = String.fromCharCode(parseInt(items[i], radix)); + // Do the browser delete + editor.getDoc().execCommand(isDelete ? 'ForwardDelete' : 'Delete', false, null); - // Only add non base entities - if (!baseEntities[chr]) { - entity = '&' + items[i + 1] + ';'; - lookup[chr] = entity; - lookup[entity] = chr; + // Check if the previous sibling is empty after deleting for example:

|

+ container = selection.getRng().startContainer; + sibling = container.previousSibling; + if (sibling && sibling.nodeType == 1 && !dom.isBlock(sibling) && dom.isEmpty(sibling) && !nonEmptyElements[sibling.nodeName.toLowerCase()]) { + dom.remove(sibling); + } + + // Remove the temp element we inserted + dom.remove('__tmp'); } } + }); + } - return lookup; + function removeBlockQuoteOnBackSpace() { + // Add block quote deletion handler + editor.onKeyDown.add(function(editor, e) { + var rng, container, offset, root, parent; + + if (isDefaultPrevented(e) || e.keyCode != VK.BACKSPACE) { + return; + } + + rng = selection.getRng(); + container = rng.startContainer; + offset = rng.startOffset; + root = dom.getRoot(); + parent = container; + + if (!rng.collapsed || offset !== 0) { + return; + } + + while (parent && parent.parentNode && parent.parentNode.firstChild == parent && parent.parentNode != root) { + parent = parent.parentNode; + } + + // Is the cursor at the beginning of a blockquote? + if (parent.tagName === 'BLOCKQUOTE') { + // Remove the blockquote + editor.formatter.toggle('blockquote', null, parent); + + // Move the caret to the beginning of container + rng = dom.createRng(); + rng.setStart(container, 0); + rng.setEnd(container, 0); + selection.setRng(rng); + } + }); + }; + + function setGeckoEditingOptions() { + function setOpts() { + editor._refreshContentEditable(); + + setEditorCommandState("StyleWithCSS", false); + setEditorCommandState("enableInlineTableEditing", false); + + if (!settings.object_resizing) { + setEditorCommandState("enableObjectResizing", false); + } + }; + + if (!settings.readonly) { + editor.onBeforeExecCommand.add(setOpts); + editor.onMouseDown.add(setOpts); } }; - // Unpack entities lookup where the numbers are in radix 32 to reduce the size - namedEntities = buildEntitiesLookup( - '50,nbsp,51,iexcl,52,cent,53,pound,54,curren,55,yen,56,brvbar,57,sect,58,uml,59,copy,' + - '5a,ordf,5b,laquo,5c,not,5d,shy,5e,reg,5f,macr,5g,deg,5h,plusmn,5i,sup2,5j,sup3,5k,acute,' + + function addBrAfterLastLinks() { + function fixLinks(editor, o) { + each(dom.select('a'), function(node) { + var parentNode = node.parentNode, root = dom.getRoot(); + + if (parentNode.lastChild === node) { + while (parentNode && !dom.isBlock(parentNode)) { + if (parentNode.parentNode.lastChild !== parentNode || parentNode === root) { + return; + } + + parentNode = parentNode.parentNode; + } + + dom.add(parentNode, 'br', {'data-mce-bogus' : 1}); + } + }); + }; + + editor.onExecCommand.add(function(editor, cmd) { + if (cmd === 'CreateLink') { + fixLinks(editor); + } + }); + + editor.onSetContent.add(selection.onSetContent.add(fixLinks)); + }; + + function setDefaultBlockType() { + if (settings.forced_root_block) { + editor.onInit.add(function() { + setEditorCommandState('DefaultParagraphSeparator', settings.forced_root_block); + }); + } + } + + function removeGhostSelection() { + function repaint(sender, args) { + if (!sender || !args.initial) { + editor.execCommand('mceRepaint'); + } + }; + + editor.onUndo.add(repaint); + editor.onRedo.add(repaint); + editor.onSetContent.add(repaint); + }; + + function deleteControlItemOnBackSpace() { + editor.onKeyDown.add(function(editor, e) { + var rng; + + if (!isDefaultPrevented(e) && e.keyCode == BACKSPACE) { + rng = editor.getDoc().selection.createRange(); + if (rng && rng.item) { + e.preventDefault(); + editor.undoManager.beforeChange(); + dom.remove(rng.item(0)); + editor.undoManager.add(); + } + } + }); + }; + + function renderEmptyBlocksFix() { + var emptyBlocksCSS; + + // IE10+ + if (getDocumentMode() >= 10) { + emptyBlocksCSS = ''; + each('p div h1 h2 h3 h4 h5 h6'.split(' '), function(name, i) { + emptyBlocksCSS += (i > 0 ? ',' : '') + name + ':empty'; + }); + + editor.contentStyles.push(emptyBlocksCSS + '{padding-right: 1px !important}'); + } + }; + + function fakeImageResize() { + var selectedElmX, selectedElmY, selectedElm, selectedElmGhost, selectedHandle, startX, startY, startW, startH, ratio, + resizeHandles, width, height, rootDocument = document, editableDoc = editor.getDoc(); + + if (!settings.object_resizing || settings.webkit_fake_resize === false) { + return; + } + + // Try disabling object resizing if WebKit implements resizing in the future + setEditorCommandState("enableObjectResizing", false); + + // Details about each resize handle how to scale etc + resizeHandles = { + // Name: x multiplier, y multiplier, delta size x, delta size y + n: [.5, 0, 0, -1], + e: [1, .5, 1, 0], + s: [.5, 1, 0, 1], + w: [0, .5, -1, 0], + nw: [0, 0, -1, -1], + ne: [1, 0, 1, -1], + se: [1, 1, 1, 1], + sw : [0, 1, -1, 1] + }; + + function resizeElement(e) { + var deltaX, deltaY; + + // Calc new width/height + deltaX = e.screenX - startX; + deltaY = e.screenY - startY; + + // Calc new size + width = deltaX * selectedHandle[2] + startW; + height = deltaY * selectedHandle[3] + startH; + + // Never scale down lower than 5 pixels + width = width < 5 ? 5 : width; + height = height < 5 ? 5 : height; + + // Constrain proportions when modifier key is pressed or if the nw, ne, sw, se corners are moved on an image + if (VK.modifierPressed(e) || (selectedElm.nodeName == "IMG" && selectedHandle[2] * selectedHandle[3] !== 0)) { + width = Math.round(height / ratio); + height = Math.round(width * ratio); + } + + // Update ghost size + dom.setStyles(selectedElmGhost, { + width: width, + height: height + }); + + // Update ghost X position if needed + if (selectedHandle[2] < 0 && selectedElmGhost.clientWidth <= width) { + dom.setStyle(selectedElmGhost, 'left', selectedElmX + (startW - width)); + } + + // Update ghost Y position if needed + if (selectedHandle[3] < 0 && selectedElmGhost.clientHeight <= height) { + dom.setStyle(selectedElmGhost, 'top', selectedElmY + (startH - height)); + } + } + + function endResize() { + function setSizeProp(name, value) { + if (value) { + // Resize by using style or attribute + if (selectedElm.style[name] || !editor.schema.isValid(selectedElm.nodeName.toLowerCase(), name)) { + dom.setStyle(selectedElm, name, value); + } else { + dom.setAttrib(selectedElm, name, value); + } + } + } + + // Set width/height properties + setSizeProp('width', width); + setSizeProp('height', height); + + dom.unbind(editableDoc, 'mousemove', resizeElement); + dom.unbind(editableDoc, 'mouseup', endResize); + + if (rootDocument != editableDoc) { + dom.unbind(rootDocument, 'mousemove', resizeElement); + dom.unbind(rootDocument, 'mouseup', endResize); + } + + // Remove ghost and update resize handle positions + dom.remove(selectedElmGhost); + showResizeRect(selectedElm); + } + + function showResizeRect(targetElm) { + var position, targetWidth, targetHeight; + + hideResizeRect(); + + // Get position and size of target + position = dom.getPos(targetElm); + selectedElmX = position.x; + selectedElmY = position.y; + targetWidth = targetElm.offsetWidth; + targetHeight = targetElm.offsetHeight; + + // Reset width/height if user selects a new image/table + if (selectedElm != targetElm) { + selectedElm = targetElm; + width = height = 0; + } + + each(resizeHandles, function(handle, name) { + var handleElm; + + // Get existing or render resize handle + handleElm = dom.get('mceResizeHandle' + name); + if (!handleElm) { + handleElm = dom.add(editableDoc.documentElement, 'div', { + id: 'mceResizeHandle' + name, + 'class': 'mceResizeHandle', + style: 'cursor:' + name + '-resize; margin:0; padding:0' + }); + + dom.bind(handleElm, 'mousedown', function(e) { + e.preventDefault(); + + endResize(); + + startX = e.screenX; + startY = e.screenY; + startW = selectedElm.clientWidth; + startH = selectedElm.clientHeight; + ratio = startH / startW; + selectedHandle = handle; + + selectedElmGhost = selectedElm.cloneNode(true); + dom.addClass(selectedElmGhost, 'mceClonedResizable'); + dom.setStyles(selectedElmGhost, { + left: selectedElmX, + top: selectedElmY, + margin: 0 + }); + + editableDoc.documentElement.appendChild(selectedElmGhost); + + dom.bind(editableDoc, 'mousemove', resizeElement); + dom.bind(editableDoc, 'mouseup', endResize); + + if (rootDocument != editableDoc) { + dom.bind(rootDocument, 'mousemove', resizeElement); + dom.bind(rootDocument, 'mouseup', endResize); + } + }); + } else { + dom.show(handleElm); + } + + // Position element + dom.setStyles(handleElm, { + left: (targetWidth * handle[0] + selectedElmX) - (handleElm.offsetWidth / 2), + top: (targetHeight * handle[1] + selectedElmY) - (handleElm.offsetHeight / 2) + }); + }); + + // Only add resize rectangle on WebKit and only on images + if (!tinymce.isOpera && selectedElm.nodeName == "IMG") { + selectedElm.setAttribute('data-mce-selected', '1'); + } + } + + function hideResizeRect() { + if (selectedElm) { + selectedElm.removeAttribute('data-mce-selected'); + } + + for (var name in resizeHandles) { + dom.hide('mceResizeHandle' + name); + } + } + + // Add CSS for resize handles, cloned element and selected + editor.contentStyles.push( + '.mceResizeHandle {' + + 'position: absolute;' + + 'border: 1px solid black;' + + 'background: #FFF;' + + 'width: 5px;' + + 'height: 5px;' + + 'z-index: 10000' + + '}' + + '.mceResizeHandle:hover {' + + 'background: #000' + + '}' + + 'img[data-mce-selected] {' + + 'outline: 1px solid black' + + '}' + + 'img.mceClonedResizable, table.mceClonedResizable {' + + 'position: absolute;' + + 'outline: 1px dashed black;' + + 'opacity: .5;' + + 'z-index: 10000' + + '}' + ); + + function updateResizeRect() { + var controlElm = dom.getParent(selection.getNode(), 'table,img'); + + // Remove data-mce-selected from all elements since they might have been copied using Ctrl+c/v + each(dom.select('img[data-mce-selected]'), function(img) { + img.removeAttribute('data-mce-selected'); + }); + + if (controlElm) { + showResizeRect(controlElm); + } else { + hideResizeRect(); + } + } + + // Show/hide resize rect when image is selected + editor.onNodeChange.add(updateResizeRect); + + // Fixes WebKit quirk where it returns IMG on getNode if caret is after last image in container + dom.bind(editableDoc, 'selectionchange', updateResizeRect); + + // Remove the internal attribute when serializing the DOM + editor.serializer.addAttributeFilter('data-mce-selected', function(nodes, name) { + var i = nodes.length; + + while (i--) { + nodes[i].attr(name, null); + } + }); + } + + function keepNoScriptContents() { + if (getDocumentMode() < 9) { + parser.addNodeFilter('noscript', function(nodes) { + var i = nodes.length, node, textNode; + + while (i--) { + node = nodes[i]; + textNode = node.firstChild; + + if (textNode) { + node.attr('data-mce-innertext', textNode.value); + } + } + }); + + serializer.addNodeFilter('noscript', function(nodes) { + var i = nodes.length, node, textNode, value; + + while (i--) { + node = nodes[i]; + textNode = nodes[i].firstChild; + + if (textNode) { + textNode.value = tinymce.html.Entities.decode(textNode.value); + } else { + // Old IE can't retain noscript value so an attribute is used to store it + value = node.attributes.map['data-mce-innertext']; + if (value) { + node.attr('data-mce-innertext', null); + textNode = new tinymce.html.Node('#text', 3); + textNode.value = value; + textNode.raw = true; + node.append(textNode); + } + } + } + }); + } + } + + // All browsers + disableBackspaceIntoATable(); + removeBlockQuoteOnBackSpace(); + emptyEditorWhenDeleting(); + + // WebKit + if (tinymce.isWebKit) { + keepInlineElementOnDeleteBackspace(); + cleanupStylesWhenDeleting(); + inputMethodFocus(); + selectControlElements(); + setDefaultBlockType(); + + // iOS + if (tinymce.isIDevice) { + selectionChangeNodeChanged(); + } else { + fakeImageResize(); + selectAll(); + } + } + + // IE + if (tinymce.isIE) { + removeHrOnBackspace(); + ensureBodyHasRoleApplication(); + addNewLinesBeforeBrInPre(); + removePreSerializedStylesWhenSelectingControls(); + deleteControlItemOnBackSpace(); + renderEmptyBlocksFix(); + keepNoScriptContents(); + } + + // Gecko + if (tinymce.isGecko) { + removeHrOnBackspace(); + focusBody(); + removeStylesWhenDeletingAccrossBlockElements(); + setGeckoEditingOptions(); + addBrAfterLastLinks(); + removeGhostSelection(); + } + + // Opera + if (tinymce.isOpera) { + fakeImageResize(); + } +}; +(function(tinymce) { + var namedEntities, baseEntities, reverseEntities, + attrsCharsRegExp = /[&<>\"\u007E-\uD7FF\uE000-\uFFEF]|[\uD800-\uDBFF][\uDC00-\uDFFF]/g, + textCharsRegExp = /[<>&\u007E-\uD7FF\uE000-\uFFEF]|[\uD800-\uDBFF][\uDC00-\uDFFF]/g, + rawCharsRegExp = /[<>&\"\']/g, + entityRegExp = /&(#x|#)?([\w]+);/g, + asciiMap = { + 128 : "\u20AC", 130 : "\u201A", 131 : "\u0192", 132 : "\u201E", 133 : "\u2026", 134 : "\u2020", + 135 : "\u2021", 136 : "\u02C6", 137 : "\u2030", 138 : "\u0160", 139 : "\u2039", 140 : "\u0152", + 142 : "\u017D", 145 : "\u2018", 146 : "\u2019", 147 : "\u201C", 148 : "\u201D", 149 : "\u2022", + 150 : "\u2013", 151 : "\u2014", 152 : "\u02DC", 153 : "\u2122", 154 : "\u0161", 155 : "\u203A", + 156 : "\u0153", 158 : "\u017E", 159 : "\u0178" + }; + + // Raw entities + baseEntities = { + '\"' : '"', // Needs to be escaped since the YUI compressor would otherwise break the code + "'" : ''', + '<' : '<', + '>' : '>', + '&' : '&' + }; + + // Reverse lookup table for raw entities + reverseEntities = { + '<' : '<', + '>' : '>', + '&' : '&', + '"' : '"', + ''' : "'" + }; + + // Decodes text by using the browser + function nativeDecode(text) { + var elm; + + elm = document.createElement("div"); + elm.innerHTML = text; + + return elm.textContent || elm.innerText || text; + }; + + // Build a two way lookup table for the entities + function buildEntitiesLookup(items, radix) { + var i, chr, entity, lookup = {}; + + if (items) { + items = items.split(','); + radix = radix || 10; + + // Build entities lookup table + for (i = 0; i < items.length; i += 2) { + chr = String.fromCharCode(parseInt(items[i], radix)); + + // Only add non base entities + if (!baseEntities[chr]) { + entity = '&' + items[i + 1] + ';'; + lookup[chr] = entity; + lookup[entity] = chr; + } + } + + return lookup; + } + }; + + // Unpack entities lookup where the numbers are in radix 32 to reduce the size + namedEntities = buildEntitiesLookup( + '50,nbsp,51,iexcl,52,cent,53,pound,54,curren,55,yen,56,brvbar,57,sect,58,uml,59,copy,' + + '5a,ordf,5b,laquo,5c,not,5d,shy,5e,reg,5f,macr,5g,deg,5h,plusmn,5i,sup2,5j,sup3,5k,acute,' + '5l,micro,5m,para,5n,middot,5o,cedil,5p,sup1,5q,ordm,5r,raquo,5s,frac14,5t,frac12,5u,frac34,' + '5v,iquest,60,Agrave,61,Aacute,62,Acirc,63,Atilde,64,Auml,65,Aring,66,AElig,67,Ccedil,' + '68,Egrave,69,Eacute,6a,Ecirc,6b,Euml,6c,Igrave,6d,Iacute,6e,Icirc,6f,Iuml,6g,ETH,6h,Ntilde,' + @@ -1327,8 +2062,7 @@ tinymce.create('static tinymce.util.XHR', { 'rfloor,8p9,lang,8pa,rang,9ea,loz,9j0,spades,9j3,clubs,9j5,hearts,9j6,diams,ai,OElig,aj,oelig,b0,' + 'Scaron,b1,scaron,bo,Yuml,m6,circ,ms,tilde,802,ensp,803,emsp,809,thinsp,80c,zwnj,80d,zwj,80e,lrm,' + '80f,rlm,80j,ndash,80k,mdash,80o,lsquo,80p,rsquo,80q,sbquo,80s,ldquo,80t,rdquo,80u,bdquo,810,dagger,' + - '811,Dagger,81g,permil,81p,lsaquo,81q,rsaquo,85c,euro' - , 32); + '811,Dagger,81g,permil,81p,lsaquo,81q,rsaquo,85c,euro', 32); tinymce.html = tinymce.html || {}; @@ -1539,7 +2273,27 @@ tinymce.html.Styles = function(settings, schema) { str = str.replace(/\\([\'\";:])/g, "$1"); return str; - } + }; + + function processUrl(match, url, url2, url3, str, str2) { + str = str || str2; + + if (str) { + str = decode(str); + + // Force strings into single quote format + return "'" + str.replace(/\'/g, "\\'") + "'"; + } + + url = decode(url || url2 || url3); + + // Convert the URL to relative/absolute depending on config + if (urlConverter) + url = urlConverter.call(urlConverterScope, url, 'style'); + + // Output new URL format + return "url('" + url.replace(/\'/g, "\\'") + "')"; + }; if (css) { // Encode \" \' % and ; and : inside strings so they don't interfere with the style parsing @@ -1563,26 +2317,7 @@ tinymce.html.Styles = function(settings, schema) { value = value.replace(rgbRegExp, toHex); // Convert URLs and force them into url('value') format - value = value.replace(urlOrStrRegExp, function(match, url, url2, url3, str, str2) { - str = str || str2; - - if (str) { - str = decode(str); - - // Force strings into single quote format - return "'" + str.replace(/\'/g, "\\'") + "'"; - } - - url = decode(url || url2 || url3); - - // Convert the URL to relative/absolute depending on config - if (urlConverter) - url = urlConverter.call(urlConverterScope, url, 'style'); - - // Output new URL format - return "url('" + url.replace(/\'/g, "\\'") + "')"; - }); - + value = value.replace(urlOrStrRegExp, processUrl); styles[name] = isEncoded ? decode(value, true) : value; } @@ -1645,8 +2380,7 @@ tinymce.html.Styles = function(settings, schema) { }; (function(tinymce) { - var transitional = {}, boolAttrMap, blockElementsMap, shortEndedElementsMap, nonEmptyElementsMap, customElementsMap = {}, - defaultWhiteSpaceElementsMap, selfClosingElementsMap, makeMap = tinymce.makeMap, each = tinymce.each; + var mapCache = {}, makeMap = tinymce.makeMap, each = tinymce.each; function split(str, delim) { return str.split(delim || ','); @@ -1681,142 +2415,286 @@ tinymce.html.Styles = function(settings, schema) { return elements; }; - // Build a lookup table for block elements both lowercase and uppercase - blockElementsMap = 'h1,h2,h3,h4,h5,h6,hr,p,div,address,pre,form,table,tbody,thead,tfoot,' + - 'th,tr,td,li,ol,ul,caption,blockquote,center,dl,dt,dd,dir,fieldset,' + - 'noscript,menu,isindex,samp,header,footer,article,section,hgroup'; - blockElementsMap = makeMap(blockElementsMap, ',', makeMap(blockElementsMap.toUpperCase())); - - // This is the XHTML 1.0 transitional elements with it's attributes and children packed to reduce it's size - transitional = unpack({ - Z : 'H|K|N|O|P', - Y : 'X|form|R|Q', - ZG : 'E|span|width|align|char|charoff|valign', - X : 'p|T|div|U|W|isindex|fieldset|table', - ZF : 'E|align|char|charoff|valign', - W : 'pre|hr|blockquote|address|center|noframes', - ZE : 'abbr|axis|headers|scope|rowspan|colspan|align|char|charoff|valign|nowrap|bgcolor|width|height', - ZD : '[E][S]', - U : 'ul|ol|dl|menu|dir', - ZC : 'p|Y|div|U|W|table|br|span|bdo|object|applet|img|map|K|N|Q', - T : 'h1|h2|h3|h4|h5|h6', - ZB : 'X|S|Q', - S : 'R|P', - ZA : 'a|G|J|M|O|P', - R : 'a|H|K|N|O', - Q : 'noscript|P', - P : 'ins|del|script', - O : 'input|select|textarea|label|button', - N : 'M|L', - M : 'em|strong|dfn|code|q|samp|kbd|var|cite|abbr|acronym', - L : 'sub|sup', - K : 'J|I', - J : 'tt|i|b|u|s|strike', - I : 'big|small|font|basefont', - H : 'G|F', - G : 'br|span|bdo', - F : 'object|applet|img|map|iframe', - E : 'A|B|C', - D : 'accesskey|tabindex|onfocus|onblur', - C : 'onclick|ondblclick|onmousedown|onmouseup|onmouseover|onmousemove|onmouseout|onkeypress|onkeydown|onkeyup', - B : 'lang|xml:lang|dir', - A : 'id|class|style|title' - }, 'script[id|charset|type|language|src|defer|xml:space][]' + - 'style[B|id|type|media|title|xml:space][]' + - 'object[E|declare|classid|codebase|data|type|codetype|archive|standby|width|height|usemap|name|tabindex|align|border|hspace|vspace][#|param|Y]' + - 'param[id|name|value|valuetype|type][]' + - 'p[E|align][#|S]' + - 'a[E|D|charset|type|name|href|hreflang|rel|rev|shape|coords|target][#|Z]' + - 'br[A|clear][]' + - 'span[E][#|S]' + - 'bdo[A|C|B][#|S]' + - 'applet[A|codebase|archive|code|object|alt|name|width|height|align|hspace|vspace][#|param|Y]' + - 'h1[E|align][#|S]' + - 'img[E|src|alt|name|longdesc|width|height|usemap|ismap|align|border|hspace|vspace][]' + - 'map[B|C|A|name][X|form|Q|area]' + - 'h2[E|align][#|S]' + - 'iframe[A|longdesc|name|src|frameborder|marginwidth|marginheight|scrolling|align|width|height][#|Y]' + - 'h3[E|align][#|S]' + - 'tt[E][#|S]' + - 'i[E][#|S]' + - 'b[E][#|S]' + - 'u[E][#|S]' + - 's[E][#|S]' + - 'strike[E][#|S]' + - 'big[E][#|S]' + - 'small[E][#|S]' + - 'font[A|B|size|color|face][#|S]' + - 'basefont[id|size|color|face][]' + - 'em[E][#|S]' + - 'strong[E][#|S]' + - 'dfn[E][#|S]' + - 'code[E][#|S]' + - 'q[E|cite][#|S]' + - 'samp[E][#|S]' + - 'kbd[E][#|S]' + - 'var[E][#|S]' + - 'cite[E][#|S]' + - 'abbr[E][#|S]' + - 'acronym[E][#|S]' + - 'sub[E][#|S]' + - 'sup[E][#|S]' + - 'input[E|D|type|name|value|checked|disabled|readonly|size|maxlength|src|alt|usemap|onselect|onchange|accept|align][]' + - 'select[E|name|size|multiple|disabled|tabindex|onfocus|onblur|onchange][optgroup|option]' + - 'optgroup[E|disabled|label][option]' + - 'option[E|selected|disabled|label|value][]' + - 'textarea[E|D|name|rows|cols|disabled|readonly|onselect|onchange][]' + - 'label[E|for|accesskey|onfocus|onblur][#|S]' + - 'button[E|D|name|value|type|disabled][#|p|T|div|U|W|table|G|object|applet|img|map|K|N|Q]' + - 'h4[E|align][#|S]' + - 'ins[E|cite|datetime][#|Y]' + - 'h5[E|align][#|S]' + - 'del[E|cite|datetime][#|Y]' + - 'h6[E|align][#|S]' + - 'div[E|align][#|Y]' + - 'ul[E|type|compact][li]' + - 'li[E|type|value][#|Y]' + - 'ol[E|type|compact|start][li]' + - 'dl[E|compact][dt|dd]' + - 'dt[E][#|S]' + - 'dd[E][#|Y]' + - 'menu[E|compact][li]' + - 'dir[E|compact][li]' + - 'pre[E|width|xml:space][#|ZA]' + - 'hr[E|align|noshade|size|width][]' + - 'blockquote[E|cite][#|Y]' + - 'address[E][#|S|p]' + - 'center[E][#|Y]' + - 'noframes[E][#|Y]' + - 'isindex[A|B|prompt][]' + - 'fieldset[E][#|legend|Y]' + - 'legend[E|accesskey|align][#|S]' + - 'table[E|summary|width|border|frame|rules|cellspacing|cellpadding|align|bgcolor][caption|col|colgroup|thead|tfoot|tbody|tr]' + - 'caption[E|align][#|S]' + - 'col[ZG][]' + - 'colgroup[ZG][col]' + - 'thead[ZF][tr]' + - 'tr[ZF|bgcolor][th|td]' + - 'th[E|ZE][#|Y]' + - 'form[E|action|method|name|enctype|onsubmit|onreset|accept|accept-charset|target][#|X|R|Q]' + - 'noscript[E][#|Y]' + - 'td[E|ZE][#|Y]' + - 'tfoot[ZF][tr]' + - 'tbody[ZF][tr]' + - 'area[E|D|shape|coords|href|nohref|alt|target][]' + - 'base[id|href|target][]' + - 'body[E|onload|onunload|background|bgcolor|text|link|vlink|alink][#|Y]' - ); - - boolAttrMap = makeMap('checked,compact,declare,defer,disabled,ismap,multiple,nohref,noresize,noshade,nowrap,readonly,selected,autoplay,loop,controls'); - shortEndedElementsMap = makeMap('area,base,basefont,br,col,frame,hr,img,input,isindex,link,meta,param,embed,source'); - nonEmptyElementsMap = tinymce.extend(makeMap('td,th,iframe,video,audio,object'), shortEndedElementsMap); - defaultWhiteSpaceElementsMap = makeMap('pre,script,style,textarea'); - selfClosingElementsMap = makeMap('colgroup,dd,dt,li,options,p,td,tfoot,th,thead,tr'); + function getHTML5() { + var html5 = mapCache.html5; + + if (!html5) { + html5 = mapCache.html5 = unpack({ + A : 'id|accesskey|class|dir|draggable|item|hidden|itemprop|role|spellcheck|style|subject|title|onclick|ondblclick|onmousedown|onmouseup|onmouseover|onmousemove|onmouseout|onkeypress|onkeydown|onkeyup', + B : '#|a|abbr|area|audio|b|bdo|br|button|canvas|cite|code|command|datalist|del|dfn|em|embed|i|iframe|img|input|ins|kbd|keygen|label|link|map|mark|meta|' + + 'meter|noscript|object|output|progress|q|ruby|samp|script|select|small|span|strong|sub|sup|svg|textarea|time|var|video|wbr', + C : '#|a|abbr|area|address|article|aside|audio|b|bdo|blockquote|br|button|canvas|cite|code|command|datalist|del|details|dfn|dialog|div|dl|em|embed|fieldset|' + + 'figure|footer|form|h1|h2|h3|h4|h5|h6|header|hgroup|hr|i|iframe|img|input|ins|kbd|keygen|label|link|map|mark|menu|meta|meter|nav|noscript|ol|object|output|' + + 'p|pre|progress|q|ruby|samp|script|section|select|small|span|strong|style|sub|sup|svg|table|textarea|time|ul|var|video' + }, 'html[A|manifest][body|head]' + + 'head[A][base|command|link|meta|noscript|script|style|title]' + + 'title[A][#]' + + 'base[A|href|target][]' + + 'link[A|href|rel|media|type|sizes][]' + + 'meta[A|http-equiv|name|content|charset][]' + + 'style[A|type|media|scoped][#]' + + 'script[A|charset|type|src|defer|async][#]' + + 'noscript[A][C]' + + 'body[A][C]' + + 'section[A][C]' + + 'nav[A][C]' + + 'article[A][C]' + + 'aside[A][C]' + + 'h1[A][B]' + + 'h2[A][B]' + + 'h3[A][B]' + + 'h4[A][B]' + + 'h5[A][B]' + + 'h6[A][B]' + + 'hgroup[A][h1|h2|h3|h4|h5|h6]' + + 'header[A][C]' + + 'footer[A][C]' + + 'address[A][C]' + + 'p[A][B]' + + 'br[A][]' + + 'pre[A][B]' + + 'dialog[A][dd|dt]' + + 'blockquote[A|cite][C]' + + 'ol[A|start|reversed][li]' + + 'ul[A][li]' + + 'li[A|value][C]' + + 'dl[A][dd|dt]' + + 'dt[A][B]' + + 'dd[A][C]' + + 'a[A|href|target|ping|rel|media|type][B]' + + 'em[A][B]' + + 'strong[A][B]' + + 'small[A][B]' + + 'cite[A][B]' + + 'q[A|cite][B]' + + 'dfn[A][B]' + + 'abbr[A][B]' + + 'code[A][B]' + + 'var[A][B]' + + 'samp[A][B]' + + 'kbd[A][B]' + + 'sub[A][B]' + + 'sup[A][B]' + + 'i[A][B]' + + 'b[A][B]' + + 'mark[A][B]' + + 'progress[A|value|max][B]' + + 'meter[A|value|min|max|low|high|optimum][B]' + + 'time[A|datetime][B]' + + 'ruby[A][B|rt|rp]' + + 'rt[A][B]' + + 'rp[A][B]' + + 'bdo[A][B]' + + 'span[A][B]' + + 'ins[A|cite|datetime][B]' + + 'del[A|cite|datetime][B]' + + 'figure[A][C|legend|figcaption]' + + 'figcaption[A][C]' + + 'img[A|alt|src|height|width|usemap|ismap][]' + + 'iframe[A|name|src|height|width|sandbox|seamless][]' + + 'embed[A|src|height|width|type][]' + + 'object[A|data|type|height|width|usemap|name|form|classid][param]' + + 'param[A|name|value][]' + + 'details[A|open][C|legend]' + + 'command[A|type|label|icon|disabled|checked|radiogroup][]' + + 'menu[A|type|label][C|li]' + + 'legend[A][C|B]' + + 'div[A][C]' + + 'source[A|src|type|media][]' + + 'audio[A|src|autobuffer|autoplay|loop|controls][source]' + + 'video[A|src|autobuffer|autoplay|loop|controls|width|height|poster][source]' + + 'hr[A][]' + + 'form[A|accept-charset|action|autocomplete|enctype|method|name|novalidate|target][C]' + + 'fieldset[A|disabled|form|name][C|legend]' + + 'label[A|form|for][B]' + + 'input[A|type|accept|alt|autocomplete|autofocus|checked|disabled|form|formaction|formenctype|formmethod|formnovalidate|formtarget|height|list|max|maxlength|min|' + + 'multiple|pattern|placeholder|readonly|required|size|src|step|width|files|value|name][]' + + 'button[A|autofocus|disabled|form|formaction|formenctype|formmethod|formnovalidate|formtarget|name|value|type][B]' + + 'select[A|autofocus|disabled|form|multiple|name|size][option|optgroup]' + + 'datalist[A][B|option]' + + 'optgroup[A|disabled|label][option]' + + 'option[A|disabled|selected|label|value][]' + + 'textarea[A|autofocus|disabled|form|maxlength|name|placeholder|readonly|required|rows|cols|wrap][]' + + 'keygen[A|autofocus|challenge|disabled|form|keytype|name][]' + + 'output[A|for|form|name][B]' + + 'canvas[A|width|height][]' + + 'map[A|name][B|C]' + + 'area[A|shape|coords|href|alt|target|media|rel|ping|type][]' + + 'mathml[A][]' + + 'svg[A][]' + + 'table[A|border][caption|colgroup|thead|tfoot|tbody|tr]' + + 'caption[A][C]' + + 'colgroup[A|span][col]' + + 'col[A|span][]' + + 'thead[A][tr]' + + 'tfoot[A][tr]' + + 'tbody[A][tr]' + + 'tr[A][th|td]' + + 'th[A|headers|rowspan|colspan|scope][B]' + + 'td[A|headers|rowspan|colspan][C]' + + 'wbr[A][]' + ); + } + + return html5; + }; + + function getHTML4() { + var html4 = mapCache.html4; + + if (!html4) { + // This is the XHTML 1.0 transitional elements with it's attributes and children packed to reduce it's size + html4 = mapCache.html4 = unpack({ + Z : 'H|K|N|O|P', + Y : 'X|form|R|Q', + ZG : 'E|span|width|align|char|charoff|valign', + X : 'p|T|div|U|W|isindex|fieldset|table', + ZF : 'E|align|char|charoff|valign', + W : 'pre|hr|blockquote|address|center|noframes', + ZE : 'abbr|axis|headers|scope|rowspan|colspan|align|char|charoff|valign|nowrap|bgcolor|width|height', + ZD : '[E][S]', + U : 'ul|ol|dl|menu|dir', + ZC : 'p|Y|div|U|W|table|br|span|bdo|object|applet|img|map|K|N|Q', + T : 'h1|h2|h3|h4|h5|h6', + ZB : 'X|S|Q', + S : 'R|P', + ZA : 'a|G|J|M|O|P', + R : 'a|H|K|N|O', + Q : 'noscript|P', + P : 'ins|del|script', + O : 'input|select|textarea|label|button', + N : 'M|L', + M : 'em|strong|dfn|code|q|samp|kbd|var|cite|abbr|acronym', + L : 'sub|sup', + K : 'J|I', + J : 'tt|i|b|u|s|strike', + I : 'big|small|font|basefont', + H : 'G|F', + G : 'br|span|bdo', + F : 'object|applet|img|map|iframe', + E : 'A|B|C', + D : 'accesskey|tabindex|onfocus|onblur', + C : 'onclick|ondblclick|onmousedown|onmouseup|onmouseover|onmousemove|onmouseout|onkeypress|onkeydown|onkeyup', + B : 'lang|xml:lang|dir', + A : 'id|class|style|title' + }, 'script[id|charset|type|language|src|defer|xml:space][]' + + 'style[B|id|type|media|title|xml:space][]' + + 'object[E|declare|classid|codebase|data|type|codetype|archive|standby|width|height|usemap|name|tabindex|align|border|hspace|vspace][#|param|Y]' + + 'param[id|name|value|valuetype|type][]' + + 'p[E|align][#|S]' + + 'a[E|D|charset|type|name|href|hreflang|rel|rev|shape|coords|target][#|Z]' + + 'br[A|clear][]' + + 'span[E][#|S]' + + 'bdo[A|C|B][#|S]' + + 'applet[A|codebase|archive|code|object|alt|name|width|height|align|hspace|vspace][#|param|Y]' + + 'h1[E|align][#|S]' + + 'img[E|src|alt|name|longdesc|width|height|usemap|ismap|align|border|hspace|vspace][]' + + 'map[B|C|A|name][X|form|Q|area]' + + 'h2[E|align][#|S]' + + 'iframe[A|longdesc|name|src|frameborder|marginwidth|marginheight|scrolling|align|width|height][#|Y]' + + 'h3[E|align][#|S]' + + 'tt[E][#|S]' + + 'i[E][#|S]' + + 'b[E][#|S]' + + 'u[E][#|S]' + + 's[E][#|S]' + + 'strike[E][#|S]' + + 'big[E][#|S]' + + 'small[E][#|S]' + + 'font[A|B|size|color|face][#|S]' + + 'basefont[id|size|color|face][]' + + 'em[E][#|S]' + + 'strong[E][#|S]' + + 'dfn[E][#|S]' + + 'code[E][#|S]' + + 'q[E|cite][#|S]' + + 'samp[E][#|S]' + + 'kbd[E][#|S]' + + 'var[E][#|S]' + + 'cite[E][#|S]' + + 'abbr[E][#|S]' + + 'acronym[E][#|S]' + + 'sub[E][#|S]' + + 'sup[E][#|S]' + + 'input[E|D|type|name|value|checked|disabled|readonly|size|maxlength|src|alt|usemap|onselect|onchange|accept|align][]' + + 'select[E|name|size|multiple|disabled|tabindex|onfocus|onblur|onchange][optgroup|option]' + + 'optgroup[E|disabled|label][option]' + + 'option[E|selected|disabled|label|value][]' + + 'textarea[E|D|name|rows|cols|disabled|readonly|onselect|onchange][]' + + 'label[E|for|accesskey|onfocus|onblur][#|S]' + + 'button[E|D|name|value|type|disabled][#|p|T|div|U|W|table|G|object|applet|img|map|K|N|Q]' + + 'h4[E|align][#|S]' + + 'ins[E|cite|datetime][#|Y]' + + 'h5[E|align][#|S]' + + 'del[E|cite|datetime][#|Y]' + + 'h6[E|align][#|S]' + + 'div[E|align][#|Y]' + + 'ul[E|type|compact][li]' + + 'li[E|type|value][#|Y]' + + 'ol[E|type|compact|start][li]' + + 'dl[E|compact][dt|dd]' + + 'dt[E][#|S]' + + 'dd[E][#|Y]' + + 'menu[E|compact][li]' + + 'dir[E|compact][li]' + + 'pre[E|width|xml:space][#|ZA]' + + 'hr[E|align|noshade|size|width][]' + + 'blockquote[E|cite][#|Y]' + + 'address[E][#|S|p]' + + 'center[E][#|Y]' + + 'noframes[E][#|Y]' + + 'isindex[A|B|prompt][]' + + 'fieldset[E][#|legend|Y]' + + 'legend[E|accesskey|align][#|S]' + + 'table[E|summary|width|border|frame|rules|cellspacing|cellpadding|align|bgcolor][caption|col|colgroup|thead|tfoot|tbody|tr]' + + 'caption[E|align][#|S]' + + 'col[ZG][]' + + 'colgroup[ZG][col]' + + 'thead[ZF][tr]' + + 'tr[ZF|bgcolor][th|td]' + + 'th[E|ZE][#|Y]' + + 'form[E|action|method|name|enctype|onsubmit|onreset|accept|accept-charset|target][#|X|R|Q]' + + 'noscript[E][#|Y]' + + 'td[E|ZE][#|Y]' + + 'tfoot[ZF][tr]' + + 'tbody[ZF][tr]' + + 'area[E|D|shape|coords|href|nohref|alt|target][]' + + 'base[id|href|target][]' + + 'body[E|onload|onunload|background|bgcolor|text|link|vlink|alink][#|Y]' + ); + } + + return html4; + }; tinymce.html.Schema = function(settings) { - var self = this, elements = {}, children = {}, patternElements = [], validStyles, whiteSpaceElementsMap; + var self = this, elements = {}, children = {}, patternElements = [], validStyles, schemaItems; + var whiteSpaceElementsMap, selfClosingElementsMap, shortEndedElementsMap, boolAttrMap, blockElementsMap, nonEmptyElementsMap, customElementsMap = {}; + + // Creates an lookup table map object for the specified option or the default value + function createLookupTable(option, default_value, extend) { + var value = settings[option]; + + if (!value) { + // Get cached default map or make it if needed + value = mapCache[option]; + + if (!value) { + value = makeMap(default_value, ' ', makeMap(default_value.toUpperCase(), ' ')); + value = tinymce.extend(value, extend); + + mapCache[option] = value; + } + } else { + // Create custom map + value = makeMap(value, ',', makeMap(value.toUpperCase(), ' ')); + } + + return value; + }; settings = settings || {}; + schemaItems = settings.schema == "html5" ? getHTML5() : getHTML4(); // Allow all elements and attributes if verify_html is set to false if (settings.verify_html === false) @@ -1832,7 +2710,16 @@ tinymce.html.Styles = function(settings, schema) { }); } - whiteSpaceElementsMap = settings.whitespace_elements ? makeMap(settings.whitespace_elements) : defaultWhiteSpaceElementsMap; + // Setup map objects + whiteSpaceElementsMap = createLookupTable('whitespace_elements', 'pre script noscript style textarea'); + selfClosingElementsMap = createLookupTable('self_closing_elements', 'colgroup dd dt li option p td tfoot th thead tr'); + shortEndedElementsMap = createLookupTable('short_ended_elements', 'area base basefont br col frame hr img input isindex link meta param embed source wbr'); + boolAttrMap = createLookupTable('boolean_attributes', 'checked compact declare defer disabled ismap multiple nohref noresize noshade nowrap readonly selected autoplay loop controls'); + nonEmptyElementsMap = createLookupTable('non_empty_elements', 'td th iframe video audio object', shortEndedElementsMap); + textBlockElementsMap = createLookupTable('text_block_elements', 'h1 h2 h3 h4 h5 h6 p div address pre form ' + + 'blockquote center dir fieldset header footer article section hgroup aside nav figure'); + blockElementsMap = createLookupTable('block_elements', 'hr table tbody thead tfoot ' + + 'th tr td li ol ul caption dl dt dd noscript menu isindex samp option datalist select optgroup', textBlockElementsMap); // Converts a wildcard expression string to a regexp for example *a will become /.*a/. function patternToRegExp(str) { @@ -1844,7 +2731,7 @@ tinymce.html.Styles = function(settings, schema) { function addValidElements(valid_elements) { var ei, el, ai, al, yl, matches, element, attr, attrData, elementName, attrName, attrType, attributes, attributesOrder, prefix, outputName, globalAttributes, globalAttributesOrder, transElement, key, childKey, value, - elementRuleRegExp = /^([#+-])?([^\[\/]+)(?:\/([^\[]+))?(?:\[([^\]]+)\])?$/, + elementRuleRegExp = /^([#+\-])?([^\[\/]+)(?:\/([^\[]+))?(?:\[([^\]]+)\])?$/, attrRuleRegExp = /^([!\-])?(\w+::\w+|[^=:<]+)?(?:([=:<])(.*))?$/, hasPatternsRegExp = /[*?+]/; @@ -1986,7 +2873,7 @@ tinymce.html.Styles = function(settings, schema) { addValidElements(valid_elements); - each(transitional, function(element, name) { + each(schemaItems, function(element, name) { children[name] = element.children; }); }; @@ -2006,8 +2893,15 @@ tinymce.html.Styles = function(settings, schema) { customElementsMap[name] = cloneName; // If it's not marked as inline then add it to valid block elements - if (!inline) + if (!inline) { + blockElementsMap[name.toUpperCase()] = {}; blockElementsMap[name] = {}; + } + + // Add elements clone if needed + if (!elements[name]) { + elements[name] = elements[cloneName]; + } // Add custom elements at span/div positions each(children, function(element, child) { @@ -2066,8 +2960,8 @@ tinymce.html.Styles = function(settings, schema) { }; if (!settings.valid_elements) { - // No valid elements defined then clone the elements from the transitional spec - each(transitional, function(element, name) { + // No valid elements defined then clone the elements from the schema spec + each(schemaItems, function(element, name) { elements[name] = { attributes : element.attributes, attributesOrder : element.attributesOrder @@ -2076,18 +2970,22 @@ tinymce.html.Styles = function(settings, schema) { children[name] = element.children; }); - // Switch these - each(split('strong/b,em/i'), function(item) { - item = split(item, '/'); - elements[item[1]].outputName = item[0]; - }); + // Switch these on HTML4 + if (settings.schema != "html5") { + each(split('strong/b,em/i'), function(item) { + item = split(item, '/'); + elements[item[1]].outputName = item[0]; + }); + } // Add default alt attribute for images elements.img.attributesDefault = [{name: 'alt', value: ''}]; // Remove these if they are empty by default - each(split('ol,ul,sub,sup,blockquote,span,font,a,table,tbody,tr'), function(name) { - elements[name].removeEmpty = true; + each(split('ol,ul,sub,sup,blockquote,span,font,a,table,tbody,tr,strong,em,b,i'), function(name) { + if (elements[name]) { + elements[name].removeEmpty = true; + } }); // Padd these by default @@ -2104,10 +3002,6 @@ tinymce.html.Styles = function(settings, schema) { // Todo: Remove this when we fix list handling to be valid addValidChildren('+ol[ul|ol],+ul[ul|ol]'); - // If the user didn't allow span only allow internal spans - if (!getElementRule('span')) - addValidElements('span[!data-mce-type|*]'); - // Delete invalid elements if (settings.invalid_elements) { tinymce.each(tinymce.explode(settings.invalid_elements), function(item) { @@ -2116,6 +3010,10 @@ tinymce.html.Styles = function(settings, schema) { }); } + // If the user didn't allow span only allow internal spans + if (!getElementRule('span')) + addValidElements('span[!data-mce-type|*]'); + self.children = children; self.styles = validStyles; @@ -2128,6 +3026,10 @@ tinymce.html.Styles = function(settings, schema) { return blockElementsMap; }; + self.getTextBlockElements = function() { + return textBlockElementsMap; + }; + self.getShortEndedElements = function() { return shortEndedElementsMap; }; @@ -2150,6 +3052,36 @@ tinymce.html.Styles = function(settings, schema) { return !!(parent && parent[child]); }; + self.isValid = function(name, attr) { + var attrPatterns, i, rule = getElementRule(name); + + // Check if it's a valid element + if (rule) { + if (attr) { + // Check if attribute name exists + if (rule.attributes[attr]) { + return true; + } + + // Check if attribute matches a regexp pattern + attrPatterns = rule.attributePatterns; + if (attrPatterns) { + i = attrPatterns.length; + while (i--) { + if (attrPatterns[i].pattern.test(name)) { + return true; + } + } + } + } else { + return true; + } + } + + // No match + return false; + }; + self.getElementRule = getElementRule; self.getCustomElements = function() { @@ -2163,11 +3095,9 @@ tinymce.html.Styles = function(settings, schema) { self.addCustomElements = addCustomElements; self.addValidChildren = addValidChildren; - }; - // Expose boolMap and blockElementMap as static properties for usage in DOMUtils - tinymce.html.Schema.boolAttrMap = boolAttrMap; - tinymce.html.Schema.blockElementsMap = blockElementsMap; + self.elements = elements; + }; })(tinymce); (function(tinymce) { @@ -2217,6 +3147,47 @@ tinymce.html.Styles = function(settings, schema) { } }; + function parseAttribute(match, name, value, val2, val3) { + var attrRule, i; + + name = name.toLowerCase(); + value = name in fillAttrsMap ? name : decode(value || val2 || val3 || ''); // Handle boolean attribute than value attribute + + // Validate name and value + if (validate && !isInternalElement && name.indexOf('data-') !== 0) { + attrRule = validAttributesMap[name]; + + // Find rule by pattern matching + if (!attrRule && validAttributePatterns) { + i = validAttributePatterns.length; + while (i--) { + attrRule = validAttributePatterns[i]; + if (attrRule.pattern.test(name)) + break; + } + + // No rule matched + if (i === -1) + attrRule = null; + } + + // No attribute rule found + if (!attrRule) + return; + + // Validate value + if (attrRule.validValues && !(value in attrRule.validValues)) + return; + } + + // Add attribute to list and map + attrList.map[name] = value; + attrList.push({ + name: name, + value: value + }); + }; + // Precompile RegExps and map objects tokenRegExp = new RegExp('<(?:' + '(?:!--([\\w\\W]*?)-->)|' + // Comment @@ -2224,10 +3195,10 @@ tinymce.html.Styles = function(settings, schema) { '(?:!DOCTYPE([\\w\\W]*?)>)|' + // DOCTYPE '(?:\\?([^\\s\\/<>]+) ?([\\w\\W]*?)[?/]>)|' + // PI '(?:\\/([^>]+)>)|' + // End element - '(?:([^\\s\\/<>]+)((?:\\s+[^"\'>]+(?:(?:"[^"]*")|(?:\'[^\']*\')|[^>]*))*|\\/)>)' + // Start element + '(?:([A-Za-z0-9\\-\\:\\.]+)((?:\\s+[^"\'>]+(?:(?:"[^"]*")|(?:\'[^\']*\')|[^>]*))*|\\/|\\s+)>)' + // Start element ')', 'g'); - attrRegExp = /([\w:\-]+)(?:\s*=\s*(?:(?:\"((?:\\.|[^\"])*)\")|(?:\'((?:\\.|[^\'])*)\')|([^>\s]+)))?/g; + attrRegExp = /([\w:\-]+)(?:\s*=\s*(?:(?:\"((?:[^\"])*)\")|(?:\'((?:[^\'])*)\')|([^>\s]+)))?/g; specialElements = { 'script' : /<\/script[^>]*>/gi, 'style' : /<\/style[^>]*>/gi, @@ -2236,7 +3207,7 @@ tinymce.html.Styles = function(settings, schema) { // Setup lookup tables for empty elements and boolean attributes shortEndedElements = schema.getShortEndedElements(); - selfClosing = schema.getSelfClosingElements(); + selfClosing = settings.self_closing_elements || schema.getSelfClosingElements(); fillAttrsMap = schema.getBoolAttrs(); validate = settings.validate; removeInternalElements = settings.remove_internals; @@ -2291,46 +3262,7 @@ tinymce.html.Styles = function(settings, schema) { attrList = []; attrList.map = {}; - attribsValue.replace(attrRegExp, function(match, name, value, val2, val3) { - var attrRule, i; - - name = name.toLowerCase(); - value = name in fillAttrsMap ? name : decode(value || val2 || val3 || ''); // Handle boolean attribute than value attribute - - // Validate name and value - if (validate && !isInternalElement && name.indexOf('data-') !== 0) { - attrRule = validAttributesMap[name]; - - // Find rule by pattern matching - if (!attrRule && validAttributePatterns) { - i = validAttributePatterns.length; - while (i--) { - attrRule = validAttributePatterns[i]; - if (attrRule.pattern.test(name)) - break; - } - - // No rule matched - if (i === -1) - attrRule = null; - } - - // No attribute rule found - if (!attrRule) - return; - - // Validate value - if (attrRule.validValues && !(value in attrRule.validValues)) - return; - } - - // Add attribute to list and map - attrList.map[name] = value; - attrList.push({ - name: name, - value: value - }); - }); + attribsValue.replace(attrRegExp, parseAttribute); } else { attrList = []; attrList.map = {}; @@ -2749,11 +3681,15 @@ tinymce.html.Styles = function(settings, schema) { i = node.attributes.length; while (i--) { name = node.attributes[i].name; - if (name === "name" || name.indexOf('data-') === 0) + if (name === "name" || name.indexOf('data-mce-') === 0) return false; } } + // Keep comments + if (node.type === 8) + return false; + // Keep non whitespace text nodes if ((node.type === 3 && !whiteSpaceRegExp.test(node.value))) return false; @@ -2801,17 +3737,40 @@ tinymce.html.Styles = function(settings, schema) { function fixInvalidChildren(nodes) { var ni, node, parent, parents, newParent, currentNode, tempNode, childNode, i, - childClone, nonEmptyElements, nonSplitableElements, sibling, nextNode; + childClone, nonEmptyElements, nonSplitableElements, textBlockElements, sibling, nextNode; nonSplitableElements = tinymce.makeMap('tr,td,th,tbody,thead,tfoot,table'); nonEmptyElements = schema.getNonEmptyElements(); + textBlockElements = schema.getTextBlockElements(); for (ni = 0; ni < nodes.length; ni++) { node = nodes[ni]; - // Already removed - if (!node.parent) + // Already removed or fixed + if (!node.parent || node.fixed) + continue; + + // If the invalid element is a text block and the text block is within a parent LI element + // Then unwrap the first text block and convert other sibling text blocks to LI elements similar to Word/Open Office + if (textBlockElements[node.name] && node.parent.name == 'li') { + // Move sibling text blocks after LI element + sibling = node.next; + while (sibling) { + if (textBlockElements[sibling.name]) { + sibling.name = 'li'; + sibling.fixed = true; + node.parent.insert(sibling, node.parent); + } else { + break; + } + + sibling = sibling.next; + } + + // Unwrap current text block + node.unwrap(node); continue; + } // Get list of all parent nodes until we find a valid parent to stick the child into parents = [node]; @@ -2947,8 +3906,8 @@ tinymce.html.Styles = function(settings, schema) { self.parse = function(html, args) { var parser, rootNode, node, nodes, i, l, fi, fl, list, name, validate, - blockElements, startWhiteSpaceRegExp, invalidChildren = [], - endWhiteSpaceRegExp, allWhiteSpaceRegExp, whiteSpaceElements, children, nonEmptyElements, rootBlockName; + blockElements, startWhiteSpaceRegExp, invalidChildren = [], isInWhiteSpacePreservedElement, + endWhiteSpaceRegExp, allWhiteSpaceRegExp, isAllWhiteSpaceRegExp, whiteSpaceElements, children, nonEmptyElements, rootBlockName; args = args || {}; matchedNodes = {}; @@ -2963,6 +3922,7 @@ tinymce.html.Styles = function(settings, schema) { startWhiteSpaceRegExp = /^[ \t\r\n]+/; endWhiteSpaceRegExp = /[ \t\r\n]+$/; allWhiteSpaceRegExp = /[ \t\r\n]+/g; + isAllWhiteSpaceRegExp = /^[ \t\r\n]+$/; function addRootBlocks() { var node = rootNode.firstChild, next, rootBlockNode; @@ -3018,9 +3978,23 @@ tinymce.html.Styles = function(settings, schema) { } }; + function cloneAndExcludeBlocks(input) { + var name, output = {}; + + for (name in input) { + if (name !== 'li' && name != 'p') { + output[name] = input[name]; + } + } + + return output; + }; + parser = new tinymce.html.SaxParser({ validate : validate, - fix_self_closing : !validate, // Let the DOM parser handle
  • in
  • or

    in

    for better results + + // Exclude P and LI from DOM parsing since it's treated better by the DOM parser + self_closing_elements: cloneAndExcludeBlocks(schema.getSelfClosingElements()), cdata: function(text) { node.append(createNode('#cdata', 4)).value = text; @@ -3030,7 +4004,7 @@ tinymce.html.Styles = function(settings, schema) { var textNode; // Trim all redundant whitespace on non white space elements - if (!whiteSpaceElements[node.name]) { + if (!isInWhiteSpacePreservedElement) { text = text.replace(allWhiteSpaceRegExp, ' '); if (node.lastChild && blockElements[node.lastChild.name]) @@ -3100,6 +4074,11 @@ tinymce.html.Styles = function(settings, schema) { // Change current node if the element wasn't empty i.e not
    or if (!empty) node = newNode; + + // Check if we are inside a whitespace preserved element + if (!isInWhiteSpacePreservedElement && whiteSpaceElements[name]) { + isInWhiteSpacePreservedElement = true; + } } }, @@ -3109,11 +4088,13 @@ tinymce.html.Styles = function(settings, schema) { elementRule = validate ? schema.getElementRule(name) : {}; if (elementRule) { if (blockElements[name]) { - if (!whiteSpaceElements[node.name]) { - // Trim whitespace at beginning of block - for (textNode = node.firstChild; textNode && textNode.type === 3; ) { + if (!isInWhiteSpacePreservedElement) { + // Trim whitespace of the first node in a block + textNode = node.firstChild; + if (textNode && textNode.type === 3) { text = textNode.value.replace(startWhiteSpaceRegExp, ''); + // Any characters left after trim or should we remove it if (text.length > 0) { textNode.value = text; textNode = textNode.next; @@ -3122,12 +4103,27 @@ tinymce.html.Styles = function(settings, schema) { textNode.remove(); textNode = sibling; } + + // Remove any pure whitespace siblings + while (textNode && textNode.type === 3) { + text = textNode.value; + sibling = textNode.next; + + if (text.length === 0 || isAllWhiteSpaceRegExp.test(text)) { + textNode.remove(); + textNode = sibling; + } + + textNode = sibling; + } } - // Trim whitespace at end of block - for (textNode = node.lastChild; textNode && textNode.type === 3; ) { + // Trim whitespace of the last node in a block + textNode = node.lastChild; + if (textNode && textNode.type === 3) { text = textNode.value.replace(endWhiteSpaceRegExp, ''); + // Any characters left after trim or should we remove it if (text.length > 0) { textNode.value = text; textNode = textNode.prev; @@ -3136,11 +4132,25 @@ tinymce.html.Styles = function(settings, schema) { textNode.remove(); textNode = sibling; } + + // Remove any pure whitespace siblings + while (textNode && textNode.type === 3) { + text = textNode.value; + sibling = textNode.prev; + + if (text.length === 0 || isAllWhiteSpaceRegExp.test(text)) { + textNode.remove(); + textNode = sibling; + } + + textNode = sibling; + } } } // Trim start white space - textNode = node.prev; + // Removed due to: #5424 + /*textNode = node.prev; if (textNode && textNode.type === 3) { text = textNode.value.replace(startWhiteSpaceRegExp, ''); @@ -3148,7 +4158,12 @@ tinymce.html.Styles = function(settings, schema) { textNode.value = text; else textNode.remove(); - } + }*/ + } + + // Check if we exited a whitespace preserved element + if (isInWhiteSpacePreservedElement && whiteSpaceElements[name]) { + isInWhiteSpacePreservedElement = false; } // Handle empty nodes @@ -3158,7 +4173,7 @@ tinymce.html.Styles = function(settings, schema) { node.empty().append(new Node('#text', '3')).value = '\u00a0'; else { // Leave nodes that have a name like - if (!node.attributes.map.name) { + if (!node.attributes.map.name && !node.attributes.map.id) { tempNode = node.parent; node.empty().remove(); node = tempNode; @@ -3235,8 +4250,8 @@ tinymce.html.Styles = function(settings, schema) { // these elements and keep br elements that where intended to be there intact if (settings.remove_trailing_brs) { self.addNodeFilter('br', function(nodes, name) { - var i, l = nodes.length, node, blockElements = schema.getBlockElements(), - nonEmptyElements = schema.getNonEmptyElements(), parent, prev, prevName; + var i, l = nodes.length, node, blockElements = tinymce.extend({}, schema.getBlockElements()), + nonEmptyElements = schema.getNonEmptyElements(), parent, lastParent, prev, prevName; // Remove brs from body element as well blockElements.body = 1; @@ -3247,7 +4262,7 @@ tinymce.html.Styles = function(settings, schema) { parent = node.parent; if (blockElements[node.parent.name] && node === parent.lastChild) { - // Loop all nodes to the right of the current node and check for other BR elements + // Loop all nodes to the left of the current node and check for other BR elements // excluding bookmarks since they are invisible prev = node.prev; while (prev) { @@ -3278,17 +4293,57 @@ tinymce.html.Styles = function(settings, schema) { // Remove or padd the element depending on schema rule if (elementRule) { - if (elementRule.removeEmpty) - parent.remove(); - else if (elementRule.paddEmpty) - parent.empty().append(new tinymce.html.Node('#text', 3)).value = '\u00a0'; - } - } + if (elementRule.removeEmpty) + parent.remove(); + else if (elementRule.paddEmpty) + parent.empty().append(new tinymce.html.Node('#text', 3)).value = '\u00a0'; + } + } + } + } else { + // Replaces BR elements inside inline elements like


    so they become

     

    + lastParent = node; + while (parent.firstChild === lastParent && parent.lastChild === lastParent) { + lastParent = parent; + + if (blockElements[parent.name]) { + break; + } + + parent = parent.parent; + } + + if (lastParent === parent) { + textNode = new tinymce.html.Node('#text', 3); + textNode.value = '\u00a0'; + node.replace(textNode); } } } }); } + + // Force anchor names closed, unless the setting "allow_html_in_named_anchor" is explicitly included. + if (!settings.allow_html_in_named_anchor) { + self.addAttributeFilter('id,name', function(nodes, name) { + var i = nodes.length, sibling, prevSibling, parent, node; + + while (i--) { + node = nodes[i]; + if (node.name === 'a' && node.firstChild && !node.attr('href')) { + parent = node.parent; + + // Move children after current node + sibling = node.lastChild; + do { + prevSibling = sibling.prev; + parent.insert(sibling, node); + sibling = prevSibling; + } while (sibling); + } + } + }); + } } })(tinymce); @@ -3433,7 +4488,7 @@ tinymce.html.Writer = function(settings) { writer.cdata(node.value); }, - // Document fragment + // Document fragment 11: function(node) { if ((node = node.firstChild)) { do { @@ -3508,2785 +4563,3628 @@ tinymce.html.Writer = function(settings) { } })(tinymce); -(function(tinymce) { - // Shorten names - var each = tinymce.each, - is = tinymce.is, - isWebKit = tinymce.isWebKit, - isIE = tinymce.isIE, - Entities = tinymce.html.Entities, - simpleSelectorRe = /^([a-z0-9],?)+$/i, - blockElementsMap = tinymce.html.Schema.blockElementsMap, - whiteSpaceRegExp = /^[ \t\r\n]*$/; +// JSLint defined globals +/*global tinymce:false, window:false */ - tinymce.create('tinymce.dom.DOMUtils', { - doc : null, - root : null, - files : null, - pixelStyles : /^(top|left|bottom|right|width|height|borderWidth)$/, - props : { - "for" : "htmlFor", - "class" : "className", - className : "className", - checked : "checked", - disabled : "disabled", - maxlength : "maxLength", - readonly : "readOnly", - selected : "selected", - value : "value", - id : "id", - name : "name", - type : "type" - }, +tinymce.dom = {}; - DOMUtils : function(d, s) { - var t = this, globalStyle, name; +(function(namespace, expando) { + var w3cEventModel = !!document.addEventListener; - t.doc = d; - t.win = window; - t.files = {}; - t.cssFlicker = false; - t.counter = 0; - t.stdMode = !tinymce.isIE || d.documentMode >= 8; - t.boxModel = !tinymce.isIE || d.compatMode == "CSS1Compat" || t.stdMode; - t.hasOuterHTML = "outerHTML" in d.createElement("a"); + function addEvent(target, name, callback, capture) { + if (target.addEventListener) { + target.addEventListener(name, callback, capture || false); + } else if (target.attachEvent) { + target.attachEvent('on' + name, callback); + } + } - t.settings = s = tinymce.extend({ - keep_values : false, - hex_colors : 1 - }, s); - - t.schema = s.schema; - t.styles = new tinymce.html.Styles({ - url_converter : s.url_converter, - url_converter_scope : s.url_converter_scope - }, s.schema); + function removeEvent(target, name, callback, capture) { + if (target.removeEventListener) { + target.removeEventListener(name, callback, capture || false); + } else if (target.detachEvent) { + target.detachEvent('on' + name, callback); + } + } - // Fix IE6SP2 flicker and check it failed for pre SP2 - if (tinymce.isIE6) { - try { - d.execCommand('BackgroundImageCache', false, true); - } catch (e) { - t.cssFlicker = true; - } + function fix(original_event, data) { + var name, event = data || {}; + + // Dummy function that gets replaced on the delegation state functions + function returnFalse() { + return false; + } + + // Dummy function that gets replaced on the delegation state functions + function returnTrue() { + return true; + } + + // Copy all properties from the original event + for (name in original_event) { + // layerX/layerY is deprecated in Chrome and produces a warning + if (name !== "layerX" && name !== "layerY") { + event[name] = original_event[name]; } + } - if (isIE && s.schema) { - // Add missing HTML 4/5 elements to IE - ('abbr article aside audio canvas ' + - 'details figcaption figure footer ' + - 'header hgroup mark menu meter nav ' + - 'output progress section summary ' + - 'time video').replace(/\w+/g, function(name) { - d.createElement(name); - }); + // Normalize target IE uses srcElement + if (!event.target) { + event.target = event.srcElement || document; + } - // Create all custom elements - for (name in s.schema.getCustomElements()) { - d.createElement(name); + // Add preventDefault method + event.preventDefault = function() { + event.isDefaultPrevented = returnTrue; + + // Execute preventDefault on the original event object + if (original_event) { + if (original_event.preventDefault) { + original_event.preventDefault(); + } else { + original_event.returnValue = false; // IE } } + }; - tinymce.addUnload(t.destroy, t); - }, + // Add stopPropagation + event.stopPropagation = function() { + event.isPropagationStopped = returnTrue; - getRoot : function() { - var t = this, s = t.settings; + // Execute stopPropagation on the original event object + if (original_event) { + if (original_event.stopPropagation) { + original_event.stopPropagation(); + } else { + original_event.cancelBubble = true; // IE + } + } + }; - return (s && t.get(s.root_element)) || t.doc.body; - }, + // Add stopImmediatePropagation + event.stopImmediatePropagation = function() { + event.isImmediatePropagationStopped = returnTrue; + event.stopPropagation(); + }; - getViewPort : function(w) { - var d, b; + // Add event delegation states + if (!event.isDefaultPrevented) { + event.isDefaultPrevented = returnFalse; + event.isPropagationStopped = returnFalse; + event.isImmediatePropagationStopped = returnFalse; + } - w = !w ? this.win : w; - d = w.document; - b = this.boxModel ? d.documentElement : d.body; + return event; + } - // Returns viewport size excluding scrollbars - return { - x : w.pageXOffset || b.scrollLeft, - y : w.pageYOffset || b.scrollTop, - w : w.innerWidth || b.clientWidth, - h : w.innerHeight || b.clientHeight - }; - }, + function bindOnReady(win, callback, event_utils) { + var doc = win.document, event = {type: 'ready'}; - getRect : function(e) { - var p, t = this, sr; + // Gets called when the DOM is ready + function readyHandler() { + if (!event_utils.domLoaded) { + event_utils.domLoaded = true; + callback(event); + } + } - e = t.get(e); - p = t.getPos(e); - sr = t.getSize(e); + // Page already loaded then fire it directly + if (doc.readyState == "complete") { + readyHandler(); + return; + } - return { - x : p.x, - y : p.y, - w : sr.w, - h : sr.h - }; - }, + // Use W3C method + if (w3cEventModel) { + addEvent(win, 'DOMContentLoaded', readyHandler); + } else { + // Use IE method + addEvent(doc, "readystatechange", function() { + if (doc.readyState === "complete") { + removeEvent(doc, "readystatechange", arguments.callee); + readyHandler(); + } + }); - getSize : function(e) { - var t = this, w, h; + // Wait until we can scroll, when we can the DOM is initialized + if (doc.documentElement.doScroll && win === win.top) { + (function() { + try { + // If IE is used, use the trick by Diego Perini licensed under MIT by request to the author. + // http://javascript.nwbox.com/IEContentLoaded/ + doc.documentElement.doScroll("left"); + } catch (ex) { + setTimeout(arguments.callee, 0); + return; + } - e = t.get(e); - w = t.getStyle(e, 'width'); - h = t.getStyle(e, 'height'); + readyHandler(); + })(); + } + } - // Non pixel value, then force offset/clientWidth - if (w.indexOf('px') === -1) - w = 0; + // Fallback if any of the above methods should fail for some odd reason + addEvent(win, 'load', readyHandler); + } - // Non pixel value, then force offset/clientWidth - if (h.indexOf('px') === -1) - h = 0; - - return { - w : parseInt(w) || e.offsetWidth || e.clientWidth, - h : parseInt(h) || e.offsetHeight || e.clientHeight - }; - }, - - getParent : function(n, f, r) { - return this.getParents(n, f, r, false); - }, + function EventUtils(proxy) { + var self = this, events = {}, count, isFocusBlurBound, hasFocusIn, hasMouseEnterLeave, mouseEnterLeave; - getParents : function(n, f, r, c) { - var t = this, na, se = t.settings, o = []; + hasMouseEnterLeave = "onmouseenter" in document.documentElement; + hasFocusIn = "onfocusin" in document.documentElement; + mouseEnterLeave = {mouseenter: 'mouseover', mouseleave: 'mouseout'}; + count = 1; - n = t.get(n); - c = c === undefined; + // State if the DOMContentLoaded was executed or not + self.domLoaded = false; + self.events = events; - if (se.strict_root) - r = r || t.getRoot(); + function executeHandlers(evt, id) { + var callbackList, i, l, callback; - // Wrap node name as func - if (is(f, 'string')) { - na = f; + callbackList = events[id][evt.type]; + if (callbackList) { + for (i = 0, l = callbackList.length; i < l; i++) { + callback = callbackList[i]; + + // Check if callback exists might be removed if a unbind is called inside the callback + if (callback && callback.func.call(callback.scope, evt) === false) { + evt.preventDefault(); + } - if (f === '*') { - f = function(n) {return n.nodeType == 1;}; - } else { - f = function(n) { - return t.is(n, na); - }; + // Should we stop propagation to immediate listeners + if (evt.isImmediatePropagationStopped()) { + return; + } } } + } - while (n) { - if (n == r || !n.nodeType || n.nodeType === 9) - break; + self.bind = function(target, names, callback, scope) { + var id, callbackList, i, name, fakeName, nativeHandler, capture, win = window; - if (!f || f(n)) { - if (c) - o.push(n); - else - return n; - } + // Native event handler function patches the event and executes the callbacks for the expando + function defaultNativeHandler(evt) { + executeHandlers(fix(evt || win.event), id); + } - n = n.parentNode; + // Don't bind to text nodes or comments + if (!target || target.nodeType === 3 || target.nodeType === 8) { + return; } - return c ? o : null; - }, + // Create or get events id for the target + if (!target[expando]) { + id = count++; + target[expando] = id; + events[id] = {}; + } else { + id = target[expando]; - get : function(e) { - var n; + if (!events[id]) { + events[id] = {}; + } + } - if (e && this.doc && typeof(e) == 'string') { - n = e; - e = this.doc.getElementById(e); + // Setup the specified scope or use the target as a default + scope = scope || target; - // IE and Opera returns meta elements when they match the specified input ID, but getElementsByName seems to do the trick - if (e && e.id !== n) - return this.doc.getElementsByName(n)[1]; - } + // Split names and bind each event, enables you to bind multiple events with one call + names = names.split(' '); + i = names.length; + while (i--) { + name = names[i]; + nativeHandler = defaultNativeHandler; + fakeName = capture = false; - return e; - }, + // Use ready instead of DOMContentLoaded + if (name === "DOMContentLoaded") { + name = "ready"; + } - getNext : function(node, selector) { - return this._findSib(node, selector, 'nextSibling'); - }, + // DOM is already ready + if ((self.domLoaded || target.readyState == 'complete') && name === "ready") { + self.domLoaded = true; + callback.call(scope, fix({type: name})); + continue; + } - getPrev : function(node, selector) { - return this._findSib(node, selector, 'previousSibling'); - }, + // Handle mouseenter/mouseleaver + if (!hasMouseEnterLeave) { + fakeName = mouseEnterLeave[name]; + if (fakeName) { + nativeHandler = function(evt) { + var current, related; - select : function(pa, s) { - var t = this; + current = evt.currentTarget; + related = evt.relatedTarget; - return tinymce.dom.Sizzle(pa, t.get(s) || t.get(t.settings.root_element) || t.doc, []); - }, + // Check if related is inside the current target if it's not then the event should be ignored since it's a mouseover/mouseout inside the element + if (related && current.contains) { + // Use contains for performance + related = current.contains(related); + } else { + while (related && related !== current) { + related = related.parentNode; + } + } - is : function(n, selector) { - var i; + // Fire fake event + if (!related) { + evt = fix(evt || win.event); + evt.type = evt.type === 'mouseout' ? 'mouseleave' : 'mouseenter'; + evt.target = current; + executeHandlers(evt, id); + } + }; + } + } - // If it isn't an array then try to do some simple selectors instead of Sizzle for to boost performance - if (n.length === undefined) { - // Simple all selector - if (selector === '*') - return n.nodeType == 1; + // Fake bubbeling of focusin/focusout + if (!hasFocusIn && (name === "focusin" || name === "focusout")) { + capture = true; + fakeName = name === "focusin" ? "focus" : "blur"; + nativeHandler = function(evt) { + evt = fix(evt || win.event); + evt.type = evt.type === 'focus' ? 'focusin' : 'focusout'; + executeHandlers(evt, id); + }; + } - // Simple selector just elements - if (simpleSelectorRe.test(selector)) { - selector = selector.toLowerCase().split(/,/); - n = n.nodeName.toLowerCase(); + // Setup callback list and bind native event + callbackList = events[id][name]; + if (!callbackList) { + events[id][name] = callbackList = [{func: callback, scope: scope}]; + callbackList.fakeName = fakeName; + callbackList.capture = capture; - for (i = selector.length - 1; i >= 0; i--) { - if (selector[i] == n) - return true; + // Add the nativeHandler to the callback list so that we can later unbind it + callbackList.nativeHandler = nativeHandler; + if (!w3cEventModel) { + callbackList.proxyHandler = proxy(id); } - return false; + // Check if the target has native events support + if (name === "ready") { + bindOnReady(target, nativeHandler, self); + } else { + addEvent(target, fakeName || name, w3cEventModel ? nativeHandler : callbackList.proxyHandler, capture); + } + } else { + // If it already has an native handler then just push the callback + callbackList.push({func: callback, scope: scope}); } } - return tinymce.dom.Sizzle.matches(selector, n.nodeType ? [n] : n).length > 0; - }, + target = callbackList = 0; // Clean memory for IE + return callback; + }; - add : function(p, n, a, h, c) { - var t = this; + self.unbind = function(target, names, callback) { + var id, callbackList, i, ci, name, eventMap; - return this.run(p, function(p) { - var e, k; + // Don't bind to text nodes or comments + if (!target || target.nodeType === 3 || target.nodeType === 8) { + return self; + } - e = is(n, 'string') ? t.doc.createElement(n) : n; - t.setAttribs(e, a); + // Unbind event or events if the target has the expando + id = target[expando]; + if (id) { + eventMap = events[id]; - if (h) { - if (h.nodeType) - e.appendChild(h); - else - t.setHTML(e, h); - } + // Specific callback + if (names) { + names = names.split(' '); + i = names.length; + while (i--) { + name = names[i]; + callbackList = eventMap[name]; + + // Unbind the event if it exists in the map + if (callbackList) { + // Remove specified callback + if (callback) { + ci = callbackList.length; + while (ci--) { + if (callbackList[ci].func === callback) { + callbackList.splice(ci, 1); + } + } + } - return !c ? p.appendChild(e) : e; - }); - }, + // Remove all callbacks if there isn't a specified callback or there is no callbacks left + if (!callback || callbackList.length === 0) { + delete eventMap[name]; + removeEvent(target, callbackList.fakeName || name, w3cEventModel ? callbackList.nativeHandler : callbackList.proxyHandler, callbackList.capture); + } + } + } + } else { + // All events for a specific element + for (name in eventMap) { + callbackList = eventMap[name]; + removeEvent(target, callbackList.fakeName || name, w3cEventModel ? callbackList.nativeHandler : callbackList.proxyHandler, callbackList.capture); + } - create : function(n, a, h) { - return this.add(this.doc.createElement(n), n, a, h, 1); - }, + eventMap = {}; + } - createHTML : function(n, a, h) { - var o = '', t = this, k; + // Check if object is empty, if it isn't then we won't remove the expando map + for (name in eventMap) { + return self; + } - o += '<' + n; + // Delete event object + delete events[id]; - for (k in a) { - if (a.hasOwnProperty(k)) - o += ' ' + k + '="' + t.encode(a[k]) + '"'; + // Remove expando from target + try { + // IE will fail here since it can't delete properties from window + delete target[expando]; + } catch (ex) { + // IE will set it to null + target[expando] = null; + } } - // A call to tinymce.is doesn't work for some odd reason on IE9 possible bug inside their JS runtime - if (typeof(h) != "undefined") - return o + '>' + h + ''; + return self; + }; - return o + ' />'; - }, + self.fire = function(target, name, args) { + var id, event; - remove : function(node, keep_children) { - return this.run(node, function(node) { - var child, parent = node.parentNode; + // Don't bind to text nodes or comments + if (!target || target.nodeType === 3 || target.nodeType === 8) { + return self; + } - if (!parent) - return null; + // Build event object by patching the args + event = fix(null, args); + event.type = name; - 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); - } + do { + // Found an expando that means there is listeners to execute + id = target[expando]; + if (id) { + executeHandlers(event, id); } - return parent.removeChild(node); - }); - }, + // Walk up the DOM + target = target.parentNode || target.ownerDocument || target.defaultView || target.parentWindow; + } while (target && !event.isPropagationStopped()); - setStyle : function(n, na, v) { - var t = this; + return self; + }; - return t.run(n, function(e) { - var s, i; + self.clean = function(target) { + var i, children, unbind = self.unbind; + + // Don't bind to text nodes or comments + if (!target || target.nodeType === 3 || target.nodeType === 8) { + return self; + } - s = e.style; + // Unbind any element on the specificed target + if (target[expando]) { + unbind(target); + } - // Camelcase it, if needed - na = na.replace(/-(\D)/g, function(a, b){ - return b.toUpperCase(); - }); + // Target doesn't have getElementsByTagName it's probably a window object then use it's document to find the children + if (!target.getElementsByTagName) { + target = target.document; + } - // Default px suffix on these - if (t.pixelStyles.test(na) && (tinymce.is(v, 'number') || /^[\-0-9\.]+$/.test(v))) - v += 'px'; + // Remove events from each child element + if (target && target.getElementsByTagName) { + unbind(target); - switch (na) { - case 'opacity': - // IE specific opacity - if (isIE) { - s.filter = v === '' ? '' : "alpha(opacity=" + (v * 100) + ")"; + children = target.getElementsByTagName('*'); + i = children.length; + while (i--) { + target = children[i]; - if (!n.currentStyle || !n.currentStyle.hasLayout) - s.display = 'inline-block'; - } + if (target[expando]) { + unbind(target); + } + } + } - // Fix for older browsers - s[na] = s['-moz-opacity'] = s['-khtml-opacity'] = v || ''; - break; + return self; + }; - case 'float': - isIE ? s.styleFloat = v : s.cssFloat = v; - break; - - default: - s[na] = v || ''; - } + self.callNativeHandler = function(id, evt) { + if (events) { + events[id][evt.type].nativeHandler(evt); + } + }; - // Force update of the style data - if (t.settings.update_styles) - t.setAttrib(e, 'data-mce-style'); - }); - }, + self.destory = function() { + events = {}; + }; - getStyle : function(n, na, c) { - n = this.get(n); + // Legacy function calls - if (!n) - return; + self.add = function(target, events, func, scope) { + // Old API supported direct ID assignment + if (typeof(target) === "string") { + target = document.getElementById(target); + } - // Gecko - if (this.doc.defaultView && c) { - // Remove camelcase - na = na.replace(/[A-Z]/g, function(a){ - return '-' + a; - }); + // Old API supported multiple targets + if (target && target instanceof Array) { + var i = target.length; - try { - return this.doc.defaultView.getComputedStyle(n, null).getPropertyValue(na); - } catch (ex) { - // Old safari might fail - return null; + while (i--) { + self.add(target[i], events, func, scope); } + + return; } - // Camelcase it, if needed - na = na.replace(/-(\D)/g, function(a, b){ - return b.toUpperCase(); - }); + // Old API called ready init + if (events === "init") { + events = "ready"; + } - if (na == 'float') - na = isIE ? 'styleFloat' : 'cssFloat'; + return self.bind(target, events instanceof Array ? events.join(' ') : events, func, scope); + }; - // IE & Opera - if (n.currentStyle && c) - return n.currentStyle[na]; + self.remove = function(target, events, func, scope) { + if (!target) { + return self; + } - return n.style ? n.style[na] : undefined; - }, + // Old API supported direct ID assignment + if (typeof(target) === "string") { + target = document.getElementById(target); + } - setStyles : function(e, o) { - var t = this, s = t.settings, ol; + // Old API supported multiple targets + if (target instanceof Array) { + var i = target.length; - ol = s.update_styles; - s.update_styles = 0; + while (i--) { + self.remove(target[i], events, func, scope); + } - each(o, function(v, n) { - t.setStyle(e, n, v); - }); + return self; + } - // Update style info - s.update_styles = ol; - if (s.update_styles) - t.setAttrib(e, s.cssText); - }, + return self.unbind(target, events instanceof Array ? events.join(' ') : events, func); + }; - removeAllAttribs: function(e) { - return this.run(e, function(e) { - var i, attrs = e.attributes; - for (i = attrs.length - 1; i >= 0; i--) { - e.removeAttributeNode(attrs.item(i)); - } - }); - }, + self.clear = function(target) { + // Old API supported direct ID assignment + if (typeof(target) === "string") { + target = document.getElementById(target); + } - setAttrib : function(e, n, v) { - var t = this; + return self.clean(target); + }; - // Whats the point - if (!e || !n) - return; + self.cancel = function(e) { + if (e) { + self.prevent(e); + self.stop(e); + } - // Strict XML mode - if (t.settings.strict) - n = n.toLowerCase(); + return false; + }; - return this.run(e, function(e) { - var s = t.settings; - if (v !== null) { - switch (n) { - case "style": - if (!is(v, 'string')) { - each(v, function(v, n) { - t.setStyle(e, n, v); - }); + self.prevent = function(e) { + if (!e.preventDefault) { + e = fix(e); + } - return; - } + e.preventDefault(); - // No mce_style for elements with these since they might get resized by the user - if (s.keep_values) { - if (v && !t._isRes(v)) - e.setAttribute('data-mce-style', v, 2); - else - e.removeAttribute('data-mce-style', 2); - } + return false; + }; - e.style.cssText = v; - break; + self.stop = function(e) { + if (!e.stopPropagation) { + e = fix(e); + } - case "class": - e.className = v || ''; // Fix IE null bug - break; + e.stopPropagation(); - case "src": - case "href": - if (s.keep_values) { - if (s.url_converter) - v = s.url_converter.call(s.url_converter_scope || t, v, n, e); + return false; + }; + } - t.setAttrib(e, 'data-mce-' + n, v, 2); - } + namespace.EventUtils = EventUtils; - break; + namespace.Event = new EventUtils(function(id) { + return function(evt) { + tinymce.dom.Event.callNativeHandler(id, evt); + }; + }); - case "shape": - e.setAttribute('data-mce-style', v); - break; - } + // Bind ready event when tinymce script is loaded + namespace.Event.bind(window, 'ready', function() {}); + + namespace = 0; +})(tinymce.dom, 'data-mce-expando'); // Namespace and expando + +tinymce.dom.TreeWalker = function(start_node, root_node) { + var node = start_node; + + function findSibling(node, start_name, sibling_name, shallow) { + var sibling, parent; + + if (node) { + // Walk into nodes if it has a start + if (!shallow && node[start_name]) + return node[start_name]; + + // Return the sibling if it has one + if (node != root_node) { + sibling = node[sibling_name]; + if (sibling) + return sibling; + + // Walk up the parents to look for siblings + for (parent = node.parentNode; parent && parent != root_node; parent = parent.parentNode) { + sibling = parent[sibling_name]; + if (sibling) + return sibling; } - if (is(v) && v !== null && v.length !== 0) - e.setAttribute(n, '' + v, 2); - else - e.removeAttribute(n, 2); - }); - }, + } + } + }; - setAttribs : function(e, o) { - var t = this; + this.current = function() { + return node; + }; - return this.run(e, function(e) { - each(o, function(v, n) { - t.setAttrib(e, n, v); - }); - }); - }, + this.next = function(shallow) { + return (node = findSibling(node, 'firstChild', 'nextSibling', shallow)); + }; - getAttrib : function(e, n, dv) { - var v, t = this, undef; + this.prev = function(shallow) { + return (node = findSibling(node, 'lastChild', 'previousSibling', shallow)); + }; +}; - e = t.get(e); +(function(tinymce) { + // Shorten names + var each = tinymce.each, + is = tinymce.is, + isWebKit = tinymce.isWebKit, + isIE = tinymce.isIE, + Entities = tinymce.html.Entities, + simpleSelectorRe = /^([a-z0-9],?)+$/i, + whiteSpaceRegExp = /^[ \t\r\n]*$/; - if (!e || e.nodeType !== 1) - return dv === undef ? false : dv; + tinymce.create('tinymce.dom.DOMUtils', { + doc : null, + root : null, + files : null, + pixelStyles : /^(top|left|bottom|right|width|height|borderWidth)$/, + props : { + "for" : "htmlFor", + "class" : "className", + className : "className", + checked : "checked", + disabled : "disabled", + maxlength : "maxLength", + readonly : "readOnly", + selected : "selected", + value : "value", + id : "id", + name : "name", + type : "type" + }, - if (!is(dv)) - dv = ''; + DOMUtils : function(d, s) { + var t = this, globalStyle, name, blockElementsMap; - // Try the mce variant for these - if (/^(src|href|style|coords|shape)$/.test(n)) { - v = e.getAttribute("data-mce-" + n); + t.doc = d; + t.win = window; + t.files = {}; + t.cssFlicker = false; + t.counter = 0; + t.stdMode = !tinymce.isIE || d.documentMode >= 8; + t.boxModel = !tinymce.isIE || d.compatMode == "CSS1Compat" || t.stdMode; + t.hasOuterHTML = "outerHTML" in d.createElement("a"); - if (v) - return v; - } + t.settings = s = tinymce.extend({ + keep_values : false, + hex_colors : 1 + }, s); + + t.schema = s.schema; + t.styles = new tinymce.html.Styles({ + url_converter : s.url_converter, + url_converter_scope : s.url_converter_scope + }, s.schema); - if (isIE && t.props[n]) { - v = e[t.props[n]]; - v = v && v.nodeValue ? v.nodeValue : v; + // Fix IE6SP2 flicker and check it failed for pre SP2 + if (tinymce.isIE6) { + try { + d.execCommand('BackgroundImageCache', false, true); + } catch (e) { + t.cssFlicker = true; + } } - if (!v) - v = e.getAttribute(n, 2); + t.fixDoc(d); + t.events = s.ownEvents ? new tinymce.dom.EventUtils(s.proxy) : tinymce.dom.Event; + tinymce.addUnload(t.destroy, t); + blockElementsMap = s.schema ? s.schema.getBlockElements() : {}; - // Check boolean attribs - if (/^(checked|compact|declare|defer|disabled|ismap|multiple|nohref|noshade|nowrap|readonly|selected)$/.test(n)) { - if (e[t.props[n]] === true && v === '') - return n; + t.isBlock = function(node) { + // Fix for #5446 + if (!node) { + return false; + } - return v ? n : ''; - } + // This function is called in module pattern style since it might be executed with the wrong this scope + var type = node.nodeType; - // Inner input elements will override attributes on form elements - if (e.nodeName === "FORM" && e.getAttributeNode(n)) - return e.getAttributeNode(n).nodeValue; + // If it's a node then check the type and use the nodeName + if (type) + return !!(type === 1 && blockElementsMap[node.nodeName]); - if (n === 'style') { - v = v || e.style.cssText; + return !!blockElementsMap[node]; + }; + }, - if (v) { - v = t.serializeStyle(t.parseStyle(v), e.nodeName); + fixDoc: function(doc) { + var settings = this.settings, name; - if (t.settings.keep_values && !t._isRes(v)) - e.setAttribute('data-mce-style', v); + if (isIE && settings.schema) { + // Add missing HTML 4/5 elements to IE + ('abbr article aside audio canvas ' + + 'details figcaption figure footer ' + + 'header hgroup mark menu meter nav ' + + 'output progress section summary ' + + 'time video').replace(/\w+/g, function(name) { + doc.createElement(name); + }); + + // Create all custom elements + for (name in settings.schema.getCustomElements()) { + doc.createElement(name); } } + }, - // Remove Apple and WebKit stuff - if (isWebKit && n === "class" && v) - v = v.replace(/(apple|webkit)\-[a-z\-]+/gi, ''); + clone: function(node, deep) { + var self = this, clone, doc; - // Handle IE issues - if (isIE) { - switch (n) { - case 'rowspan': - case 'colspan': - // IE returns 1 as default value - if (v === 1) - v = ''; + // TODO: Add feature detection here in the future + if (!isIE || node.nodeType !== 1 || deep) { + return node.cloneNode(deep); + } - break; + doc = self.doc; - case 'size': - // IE returns +0 as default value for size - if (v === '+0' || v === 20 || v === 0) - v = ''; + // Make a HTML5 safe shallow copy + if (!deep) { + clone = doc.createElement(node.nodeName); - break; + // Copy attribs + each(self.getAttribs(node), function(attr) { + self.setAttrib(clone, attr.nodeName, self.getAttrib(node, attr.nodeName)); + }); - case 'width': - case 'height': - case 'vspace': - case 'checked': - case 'disabled': - case 'readonly': - if (v === 0) - v = ''; + return clone; + } +/* + // Setup HTML5 patched document fragment + if (!self.frag) { + self.frag = doc.createDocumentFragment(); + self.fixDoc(self.frag); + } - break; + // Make a deep copy by adding it to the document fragment then removing it this removed the :section + clone = doc.createElement('div'); + self.frag.appendChild(clone); + clone.innerHTML = node.outerHTML; + self.frag.removeChild(clone); +*/ + return clone.firstChild; + }, - case 'hspace': - // IE returns -1 as default value - if (v === -1) - v = ''; + getRoot : function() { + var t = this, s = t.settings; - break; + return (s && t.get(s.root_element)) || t.doc.body; + }, - case 'maxlength': - case 'tabindex': - // IE returns default value - if (v === 32768 || v === 2147483647 || v === '32768') - v = ''; + getViewPort : function(w) { + var d, b; - break; + w = !w ? this.win : w; + d = w.document; + b = this.boxModel ? d.documentElement : d.body; - case 'multiple': - case 'compact': - case 'noshade': - case 'nowrap': - if (v === 65535) - return n; + // Returns viewport size excluding scrollbars + return { + x : w.pageXOffset || b.scrollLeft, + y : w.pageYOffset || b.scrollTop, + w : w.innerWidth || b.clientWidth, + h : w.innerHeight || b.clientHeight + }; + }, - return dv; + getRect : function(e) { + var p, t = this, sr; - case 'shape': - v = v.toLowerCase(); - break; + e = t.get(e); + p = t.getPos(e); + sr = t.getSize(e); - default: - // IE has odd anonymous function for event attributes - if (n.indexOf('on') === 0 && v) - v = tinymce._replace(/^function\s+\w+\(\)\s+\{\s+(.*)\s+\}$/, '$1', '' + v); - } - } + return { + x : p.x, + y : p.y, + w : sr.w, + h : sr.h + }; + }, - return (v !== undef && v !== null && v !== '') ? '' + v : dv; + getSize : function(e) { + var t = this, w, h; + + e = t.get(e); + w = t.getStyle(e, 'width'); + h = t.getStyle(e, 'height'); + + // Non pixel value, then force offset/clientWidth + if (w.indexOf('px') === -1) + w = 0; + + // Non pixel value, then force offset/clientWidth + if (h.indexOf('px') === -1) + h = 0; + + return { + w : parseInt(w, 10) || e.offsetWidth || e.clientWidth, + h : parseInt(h, 10) || e.offsetHeight || e.clientHeight + }; }, - getPos : function(n, ro) { - var t = this, x = 0, y = 0, e, d = t.doc, r; + getParent : function(n, f, r) { + return this.getParents(n, f, r, false); + }, + + getParents : function(n, f, r, c) { + var t = this, na, se = t.settings, o = []; n = t.get(n); - ro = ro || d.body; + c = c === undefined; - if (n) { - // Use getBoundingClientRect if it exists since it's faster than looping offset nodes - if (n.getBoundingClientRect) { - n = n.getBoundingClientRect(); - e = t.boxModel ? d.documentElement : d.body; + if (se.strict_root) + r = r || t.getRoot(); - // Add scroll offsets from documentElement or body since IE with the wrong box model will use d.body and so do WebKit - // Also remove the body/documentelement clientTop/clientLeft on IE 6, 7 since they offset the position - x = n.left + (d.documentElement.scrollLeft || d.body.scrollLeft) - e.clientTop; - y = n.top + (d.documentElement.scrollTop || d.body.scrollTop) - e.clientLeft; + // Wrap node name as func + if (is(f, 'string')) { + na = f; - return {x : x, y : y}; + if (f === '*') { + f = function(n) {return n.nodeType == 1;}; + } else { + f = function(n) { + return t.is(n, na); + }; } + } - r = n; - while (r && r != ro && r.nodeType) { - x += r.offsetLeft || 0; - y += r.offsetTop || 0; - r = r.offsetParent; - } + while (n) { + if (n == r || !n.nodeType || n.nodeType === 9) + break; - r = n.parentNode; - while (r && r != ro && r.nodeType) { - x -= r.scrollLeft || 0; - y -= r.scrollTop || 0; - r = r.parentNode; + if (!f || f(n)) { + if (c) + o.push(n); + else + return n; } + + n = n.parentNode; } - return {x : x, y : y}; + return c ? o : null; }, - parseStyle : function(st) { - return this.styles.parse(st); + get : function(e) { + var n; + + if (e && this.doc && typeof(e) == 'string') { + n = e; + e = this.doc.getElementById(e); + + // IE and Opera returns meta elements when they match the specified input ID, but getElementsByName seems to do the trick + if (e && e.id !== n) + return this.doc.getElementsByName(n)[1]; + } + + return e; }, - serializeStyle : function(o, name) { - return this.styles.serialize(o, name); + getNext : function(node, selector) { + return this._findSib(node, selector, 'nextSibling'); }, - loadCSS : function(u) { - var t = this, d = t.doc, head; + getPrev : function(node, selector) { + return this._findSib(node, selector, 'previousSibling'); + }, - if (!u) - u = ''; - head = t.select('head')[0]; + select : function(pa, s) { + var t = this; - each(u.split(','), function(u) { - var link; + return tinymce.dom.Sizzle(pa, t.get(s) || t.get(t.settings.root_element) || t.doc, []); + }, - if (t.files[u]) - return; + is : function(n, selector) { + var i; - t.files[u] = true; - link = t.create('link', {rel : 'stylesheet', href : tinymce._addVer(u)}); + // If it isn't an array then try to do some simple selectors instead of Sizzle for to boost performance + if (n.length === undefined) { + // Simple all selector + if (selector === '*') + return n.nodeType == 1; - // IE 8 has a bug where dynamically loading stylesheets would produce a 1 item remaining bug - // This fix seems to resolve that issue by realcing the document ones a stylesheet finishes loading - // It's ugly but it seems to work fine. - if (isIE && d.documentMode && d.recalc) { - link.onload = function() { - if (d.recalc) - d.recalc(); + // Simple selector just elements + if (simpleSelectorRe.test(selector)) { + selector = selector.toLowerCase().split(/,/); + n = n.nodeName.toLowerCase(); - link.onload = null; - }; + for (i = selector.length - 1; i >= 0; i--) { + if (selector[i] == n) + return true; + } + + return false; } + } - head.appendChild(link); - }); + return tinymce.dom.Sizzle.matches(selector, n.nodeType ? [n] : n).length > 0; }, - addClass : function(e, c) { - return this.run(e, function(e) { - var o; - if (!c) - return 0; + add : function(p, n, a, h, c) { + var t = this; - if (this.hasClass(e, c)) - return e.className; + return this.run(p, function(p) { + var e, k; - o = this.removeClass(e, c); + e = is(n, 'string') ? t.doc.createElement(n) : n; + t.setAttribs(e, a); - return e.className = (o != '' ? (o + ' ') : '') + c; + if (h) { + if (h.nodeType) + e.appendChild(h); + else + t.setHTML(e, h); + } + + return !c ? p.appendChild(e) : e; }); }, - removeClass : function(e, c) { - var t = this, re; + create : function(n, a, h) { + return this.add(this.doc.createElement(n), n, a, h, 1); + }, - return t.run(e, function(e) { - var v; + createHTML : function(n, a, h) { + var o = '', t = this, k; - if (t.hasClass(e, c)) { - if (!re) - re = new RegExp("(^|\\s+)" + c + "(\\s+|$)", "g"); + o += '<' + n; - v = e.className.replace(re, ' '); - v = tinymce.trim(v != ' ' ? v : ''); + for (k in a) { + if (a.hasOwnProperty(k)) + o += ' ' + k + '="' + t.encode(a[k]) + '"'; + } - e.className = v; + // A call to tinymce.is doesn't work for some odd reason on IE9 possible bug inside their JS runtime + if (typeof(h) != "undefined") + return o + '>' + h + ''; - // Empty class attr - if (!v) { - e.removeAttribute('class'); - e.removeAttribute('className'); - } - - return v; - } - - return e.className; - }); + return o + ' />'; }, - hasClass : function(n, c) { - n = this.get(n); + remove : function(node, keep_children) { + return this.run(node, function(node) { + var child, parent = node.parentNode; - if (!n || !c) - return false; + if (!parent) + return null; - return (' ' + n.className + ' ').indexOf(' ' + c + ' ') !== -1; - }, + 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); + } + } - show : function(e) { - return this.setStyle(e, 'display', 'block'); + return parent.removeChild(node); + }); }, - hide : function(e) { - return this.setStyle(e, 'display', 'none'); - }, + setStyle : function(n, na, v) { + var t = this; - isHidden : function(e) { - e = this.get(e); + return t.run(n, function(e) { + var s, i; - return !e || e.style.display == 'none' || this.getStyle(e, 'display') == 'none'; - }, + s = e.style; - uniqueId : function(p) { - return (!p ? 'mce_' : p) + (this.counter++); - }, + // Camelcase it, if needed + na = na.replace(/-(\D)/g, function(a, b){ + return b.toUpperCase(); + }); - setHTML : function(element, html) { - var self = this; + // Default px suffix on these + if (t.pixelStyles.test(na) && (tinymce.is(v, 'number') || /^[\-0-9\.]+$/.test(v))) + v += 'px'; - return self.run(element, function(element) { - if (isIE) { - // Remove all child nodes, IE keeps empty text nodes in DOM - while (element.firstChild) - element.removeChild(element.firstChild); + switch (na) { + case 'opacity': + // IE specific opacity + if (isIE) { + s.filter = v === '' ? '' : "alpha(opacity=" + (v * 100) + ")"; - try { - // IE will remove comments from the beginning - // unless you padd the contents with something - element.innerHTML = '
    ' + html; - element.removeChild(element.firstChild); - } catch (ex) { - // IE sometimes produces an unknown runtime error on innerHTML if it's an block element within a block element for example a div inside a p - // This seems to fix this problem + if (!n.currentStyle || !n.currentStyle.hasLayout) + s.display = 'inline-block'; + } - // Create new div with HTML contents and a BR infront to keep comments - element = self.create('div'); - element.innerHTML = '
    ' + html; + // Fix for older browsers + s[na] = s['-moz-opacity'] = s['-khtml-opacity'] = v || ''; + break; - // Add all children from div to target - each (element.childNodes, function(node, i) { - // Skip br element - if (i) - element.appendChild(node); - }); - } - } else - element.innerHTML = html; + case 'float': + isIE ? s.styleFloat = v : s.cssFloat = v; + break; + + default: + s[na] = v || ''; + } - return html; + // Force update of the style data + if (t.settings.update_styles) + t.setAttrib(e, 'data-mce-style'); }); }, - getOuterHTML : function(elm) { - var doc, self = this; + getStyle : function(n, na, c) { + n = this.get(n); - elm = self.get(elm); + if (!n) + return; - if (!elm) - return null; + // Gecko + if (this.doc.defaultView && c) { + // Remove camelcase + na = na.replace(/[A-Z]/g, function(a){ + return '-' + a; + }); - if (elm.nodeType === 1 && self.hasOuterHTML) - return elm.outerHTML; + try { + return this.doc.defaultView.getComputedStyle(n, null).getPropertyValue(na); + } catch (ex) { + // Old safari might fail + return null; + } + } - doc = (elm.ownerDocument || self.doc).createElement("body"); - doc.appendChild(elm.cloneNode(true)); + // Camelcase it, if needed + na = na.replace(/-(\D)/g, function(a, b){ + return b.toUpperCase(); + }); - return doc.innerHTML; + if (na == 'float') + na = isIE ? 'styleFloat' : 'cssFloat'; + + // IE & Opera + if (n.currentStyle && c) + return n.currentStyle[na]; + + return n.style ? n.style[na] : undefined; }, - setOuterHTML : function(e, h, d) { - var t = this; + setStyles : function(e, o) { + var t = this, s = t.settings, ol; - function setHTML(e, h, d) { - var n, tp; + ol = s.update_styles; + s.update_styles = 0; - tp = d.createElement("body"); - tp.innerHTML = h; + each(o, function(v, n) { + t.setStyle(e, n, v); + }); - n = tp.lastChild; - while (n) { - t.insertAfter(n.cloneNode(true), e); - n = n.previousSibling; + // Update style info + s.update_styles = ol; + if (s.update_styles) + t.setAttrib(e, s.cssText); + }, + + removeAllAttribs: function(e) { + return this.run(e, function(e) { + var i, attrs = e.attributes; + for (i = attrs.length - 1; i >= 0; i--) { + e.removeAttributeNode(attrs.item(i)); } + }); + }, - t.remove(e); - }; + setAttrib : function(e, n, v) { + var t = this; + + // Whats the point + if (!e || !n) + return; + + // Strict XML mode + if (t.settings.strict) + n = n.toLowerCase(); return this.run(e, function(e) { - e = t.get(e); + var s = t.settings; + var originalValue = e.getAttribute(n); + if (v !== null) { + switch (n) { + case "style": + if (!is(v, 'string')) { + each(v, function(v, n) { + t.setStyle(e, n, v); + }); - // Only set HTML on elements - if (e.nodeType == 1) { - d = d || e.ownerDocument || t.doc; + return; + } - if (isIE) { - try { - // Try outerHTML for IE it sometimes produces an unknown runtime error - if (isIE && e.nodeType == 1) - e.outerHTML = h; - else - setHTML(e, h, d); - } catch (ex) { - // Fix for unknown runtime error - setHTML(e, h, d); - } - } else - setHTML(e, h, d); - } - }); - }, + // No mce_style for elements with these since they might get resized by the user + if (s.keep_values) { + if (v && !t._isRes(v)) + e.setAttribute('data-mce-style', v, 2); + else + e.removeAttribute('data-mce-style', 2); + } - decode : Entities.decode, + e.style.cssText = v; + break; - encode : Entities.encodeAllRaw, + case "class": + e.className = v || ''; // Fix IE null bug + break; - insertAfter : function(node, reference_node) { - reference_node = this.get(reference_node); + case "src": + case "href": + if (s.keep_values) { + if (s.url_converter) + v = s.url_converter.call(s.url_converter_scope || t, v, n, e); - return this.run(node, function(node) { - var parent, nextSibling; + t.setAttrib(e, 'data-mce-' + n, v, 2); + } - parent = reference_node.parentNode; - nextSibling = reference_node.nextSibling; + break; - if (nextSibling) - parent.insertBefore(node, nextSibling); + case "shape": + e.setAttribute('data-mce-style', v); + break; + } + } + if (is(v) && v !== null && v.length !== 0) + e.setAttribute(n, '' + v, 2); else - parent.appendChild(node); + e.removeAttribute(n, 2); - return node; + // fire onChangeAttrib event for attributes that have changed + if (tinyMCE.activeEditor && originalValue != v) { + var ed = tinyMCE.activeEditor; + ed.onSetAttrib.dispatch(ed, e, n, v); + } }); }, - isBlock : function(node) { - var type = node.nodeType; + setAttribs : function(e, o) { + var t = this; - // If it's a node then check the type and use the nodeName - if (type) - return !!(type === 1 && blockElementsMap[node.nodeName]); - - return !!blockElementsMap[node]; - }, - - replace : function(n, o, k) { - var t = this; - - if (is(o, 'array')) - n = n.cloneNode(true); - - return t.run(o, function(o) { - if (k) { - each(tinymce.grep(o.childNodes), function(c) { - n.appendChild(c); - }); - } - - return o.parentNode.replaceChild(n, o); + return this.run(e, function(e) { + each(o, function(v, n) { + t.setAttrib(e, n, v); + }); }); }, - rename : function(elm, name) { - var t = this, newElm; + getAttrib : function(e, n, dv) { + var v, t = this, undef; - if (elm.nodeName != name.toUpperCase()) { - // Rename block element - newElm = t.create(name); + e = t.get(e); - // Copy attribs to new block - each(t.getAttribs(elm), function(attr_node) { - t.setAttrib(newElm, attr_node.nodeName, t.getAttrib(elm, attr_node.nodeName)); - }); + if (!e || e.nodeType !== 1) + return dv === undef ? false : dv; - // Replace block - t.replace(newElm, elm, 1); - } + if (!is(dv)) + dv = ''; - return newElm || elm; - }, + // Try the mce variant for these + if (/^(src|href|style|coords|shape)$/.test(n)) { + v = e.getAttribute("data-mce-" + n); - findCommonAncestor : function(a, b) { - var ps = a, pe; + if (v) + return v; + } - while (ps) { - pe = b; + if (isIE && t.props[n]) { + v = e[t.props[n]]; + v = v && v.nodeValue ? v.nodeValue : v; + } - while (pe && ps != pe) - pe = pe.parentNode; + if (!v) + v = e.getAttribute(n, 2); - if (ps == pe) - break; + // Check boolean attribs + if (/^(checked|compact|declare|defer|disabled|ismap|multiple|nohref|noshade|nowrap|readonly|selected)$/.test(n)) { + if (e[t.props[n]] === true && v === '') + return n; - ps = ps.parentNode; + return v ? n : ''; } - if (!ps && a.ownerDocument) - return a.ownerDocument.documentElement; + // Inner input elements will override attributes on form elements + if (e.nodeName === "FORM" && e.getAttributeNode(n)) + return e.getAttributeNode(n).nodeValue; - return ps; - }, + if (n === 'style') { + v = v || e.style.cssText; - toHex : function(s) { - var c = /^\s*rgb\s*?\(\s*?([0-9]+)\s*?,\s*?([0-9]+)\s*?,\s*?([0-9]+)\s*?\)\s*$/i.exec(s); + if (v) { + v = t.serializeStyle(t.parseStyle(v), e.nodeName); - function hex(s) { - s = parseInt(s).toString(16); + if (t.settings.keep_values && !t._isRes(v)) + e.setAttribute('data-mce-style', v); + } + } - return s.length > 1 ? s : '0' + s; // 0 -> 00 - }; + // Remove Apple and WebKit stuff + if (isWebKit && n === "class" && v) + v = v.replace(/(apple|webkit)\-[a-z\-]+/gi, ''); - if (c) { - s = '#' + hex(c[1]) + hex(c[2]) + hex(c[3]); + // Handle IE issues + if (isIE) { + switch (n) { + case 'rowspan': + case 'colspan': + // IE returns 1 as default value + if (v === 1) + v = ''; - return s; - } + break; - return s; - }, + case 'size': + // IE returns +0 as default value for size + if (v === '+0' || v === 20 || v === 0) + v = ''; - getClasses : function() { - var t = this, cl = [], i, lo = {}, f = t.settings.class_filter, ov; + break; - if (t.classes) - return t.classes; + case 'width': + case 'height': + case 'vspace': + case 'checked': + case 'disabled': + case 'readonly': + if (v === 0) + v = ''; - function addClasses(s) { - // IE style imports - each(s.imports, function(r) { - addClasses(r); - }); + break; - each(s.cssRules || s.rules, function(r) { - // Real type or fake it on IE - switch (r.type || 1) { - // Rule - case 1: - if (r.selectorText) { - each(r.selectorText.split(','), function(v) { - v = v.replace(/^\s*|\s*$|^\s\./g, ""); + case 'hspace': + // IE returns -1 as default value + if (v === -1) + v = ''; - // Is internal or it doesn't contain a class - if (/\.mce/.test(v) || !/\.[\w\-]+$/.test(v)) - return; + break; - // Remove everything but class name - ov = v; - v = tinymce._replace(/.*\.([a-z0-9_\-]+).*/i, '$1', v); + case 'maxlength': + case 'tabindex': + // IE returns default value + if (v === 32768 || v === 2147483647 || v === '32768') + v = ''; - // Filter classes - if (f && !(v = f(v, ov))) - return; + break; - if (!lo[v]) { - cl.push({'class' : v}); - lo[v] = 1; - } - }); - } - break; + case 'multiple': + case 'compact': + case 'noshade': + case 'nowrap': + if (v === 65535) + return n; - // Import - case 3: - addClasses(r.styleSheet); - break; - } - }); - }; + return dv; - try { - each(t.doc.styleSheets, addClasses); - } catch (ex) { - // Ignore - } + case 'shape': + v = v.toLowerCase(); + break; - if (cl.length > 0) - t.classes = cl; + default: + // IE has odd anonymous function for event attributes + if (n.indexOf('on') === 0 && v) + v = tinymce._replace(/^function\s+\w+\(\)\s+\{\s+(.*)\s+\}$/, '$1', '' + v); + } + } - return cl; + return (v !== undef && v !== null && v !== '') ? '' + v : dv; }, - run : function(e, f, s) { - var t = this, o; + getPos : function(n, ro) { + var t = this, x = 0, y = 0, e, d = t.doc, r; - if (t.doc && typeof(e) === 'string') - e = t.get(e); + n = t.get(n); + ro = ro || d.body; - if (!e) - return false; + if (n) { + // Use getBoundingClientRect if it exists since it's faster than looping offset nodes + if (n.getBoundingClientRect) { + n = n.getBoundingClientRect(); + e = t.boxModel ? d.documentElement : d.body; - s = s || this; - if (!e.nodeType && (e.length || e.length === 0)) { - o = []; + // Add scroll offsets from documentElement or body since IE with the wrong box model will use d.body and so do WebKit + // Also remove the body/documentelement clientTop/clientLeft on IE 6, 7 since they offset the position + x = n.left + (d.documentElement.scrollLeft || d.body.scrollLeft) - e.clientTop; + y = n.top + (d.documentElement.scrollTop || d.body.scrollTop) - e.clientLeft; - each(e, function(e, i) { - if (e) { - if (typeof(e) == 'string') - e = t.doc.getElementById(e); + return {x : x, y : y}; + } - o.push(f.call(s, e, i)); - } - }); + r = n; + while (r && r != ro && r.nodeType) { + x += r.offsetLeft || 0; + y += r.offsetTop || 0; + r = r.offsetParent; + } - return o; + r = n.parentNode; + while (r && r != ro && r.nodeType) { + x -= r.scrollLeft || 0; + y -= r.scrollTop || 0; + r = r.parentNode; + } } - return f.call(s, e); + return {x : x, y : y}; }, - getAttribs : function(n) { - var o; + parseStyle : function(st) { + return this.styles.parse(st); + }, - n = this.get(n); + serializeStyle : function(o, name) { + return this.styles.serialize(o, name); + }, - if (!n) - return []; + addStyle: function(cssText) { + var doc = this.doc, head; - if (isIE) { - o = []; + // Create style element if needed + styleElm = doc.getElementById('mceDefaultStyles'); + if (!styleElm) { + styleElm = doc.createElement('style'), + styleElm.id = 'mceDefaultStyles'; + styleElm.type = 'text/css'; - // Object will throw exception in IE - if (n.nodeName == 'OBJECT') - return n.attributes; - - // IE doesn't keep the selected attribute if you clone option elements - if (n.nodeName === 'OPTION' && this.getAttrib(n, 'selected')) - o.push({specified : 1, nodeName : 'selected'}); - - // It's crazy that this is faster in IE but it's because it returns all attributes all the time - n.cloneNode(false).outerHTML.replace(/<\/?[\w:\-]+ ?|=[\"][^\"]+\"|=\'[^\']+\'|=[\w\-]+|>/gi, '').replace(/[\w:\-]+/gi, function(a) { - o.push({specified : 1, nodeName : a}); - }); - - return o; + head = doc.getElementsByTagName('head')[0]; + if (head.firstChild) { + head.insertBefore(styleElm, head.firstChild); + } else { + head.appendChild(styleElm); + } } - return n.attributes; + // Append style data to old or new style element + if (styleElm.styleSheet) { + styleElm.styleSheet.cssText += cssText; + } else { + styleElm.appendChild(doc.createTextNode(cssText)); + } }, - isEmpty : function(node, elements) { - var self = this, i, attributes, type, walker, name, parentNode; + loadCSS : function(u) { + var t = this, d = t.doc, head; - node = node.firstChild; - if (node) { - walker = new tinymce.dom.TreeWalker(node); - elements = elements || self.schema ? self.schema.getNonEmptyElements() : null; + if (!u) + u = ''; - do { - type = node.nodeType; + head = d.getElementsByTagName('head')[0]; - if (type === 1) { - // Ignore bogus elements - if (node.getAttribute('data-mce-bogus')) - continue; + each(u.split(','), function(u) { + var link; - // Keep empty elements like - name = node.nodeName.toLowerCase(); - if (elements && elements[name]) { - // Ignore single BR elements in blocks like


    - parentNode = node.parentNode; - if (name === 'br' && self.isBlock(parentNode) && parentNode.firstChild === node && parentNode.lastChild === node) { - continue; - } + if (t.files[u]) + return; - return false; - } + t.files[u] = true; + link = t.create('link', {rel : 'stylesheet', href : tinymce._addVer(u)}); - // Keep elements with data-bookmark attributes or name attribute like
    - attributes = self.getAttribs(node); - i = node.attributes.length; - while (i--) { - name = node.attributes[i].nodeName; - if (name === "name" || name === 'data-mce-bookmark') - return false; - } - } + // IE 8 has a bug where dynamically loading stylesheets would produce a 1 item remaining bug + // This fix seems to resolve that issue by realcing the document ones a stylesheet finishes loading + // It's ugly but it seems to work fine. + if (isIE && d.documentMode && d.recalc) { + link.onload = function() { + if (d.recalc) + d.recalc(); - // Keep non whitespace text nodes - if ((type === 3 && !whiteSpaceRegExp.test(node.nodeValue))) - return false; - } while (node = walker.next()); - } + link.onload = null; + }; + } - return true; + head.appendChild(link); + }); }, - destroy : function(s) { - var t = this; + addClass : function(e, c) { + return this.run(e, function(e) { + var o; + + if (!c) + return 0; - if (t.events) - t.events.destroy(); + if (this.hasClass(e, c)) + return e.className; - t.win = t.doc = t.root = t.events = null; + o = this.removeClass(e, c); - // Manual destroy then remove unload handler - if (!s) - tinymce.removeUnload(t.destroy); + return e.className = (o != '' ? (o + ' ') : '') + c; + }); }, - createRng : function() { - var d = this.doc; + removeClass : function(e, c) { + var t = this, re; - return d.createRange ? d.createRange() : new tinymce.dom.Range(this); - }, + return t.run(e, function(e) { + var v; - nodeIndex : function(node, normalized) { - var idx = 0, lastNodeType, lastNode, nodeType; + if (t.hasClass(e, c)) { + if (!re) + re = new RegExp("(^|\\s+)" + c + "(\\s+|$)", "g"); - if (node) { - for (lastNodeType = node.nodeType, node = node.previousSibling, lastNode = node; node; node = node.previousSibling) { - nodeType = node.nodeType; + v = e.className.replace(re, ' '); + v = tinymce.trim(v != ' ' ? v : ''); - // Normalize text nodes - if (normalized && nodeType == 3) { - if (nodeType == lastNodeType || !node.nodeValue.length) - continue; + e.className = v; + + // Empty class attr + if (!v) { + e.removeAttribute('class'); + e.removeAttribute('className'); } - idx++; - lastNodeType = nodeType; + + return v; } - } - return idx; + return e.className; + }); }, - split : function(pe, e, re) { - var t = this, r = t.createRng(), bef, aft, pa; + hasClass : function(n, c) { + n = this.get(n); - // W3C valid browsers tend to leave empty nodes to the left/right side of the contents, this makes sense - // but we don't want that in our code since it serves no purpose for the end user - // For example if this is chopped: - //

    text 1CHOPtext 2

    - // would produce: - //

    text 1

    CHOP

    text 2

    - // this function will then trim of empty edges and produce: - //

    text 1

    CHOP

    text 2

    - function trim(node) { - var i, children = node.childNodes, type = node.nodeType; + if (!n || !c) + return false; - if (type == 1 && node.getAttribute('data-mce-type') == 'bookmark') - return; + return (' ' + n.className + ' ').indexOf(' ' + c + ' ') !== -1; + }, - for (i = children.length - 1; i >= 0; i--) - trim(children[i]); + show : function(e) { + return this.setStyle(e, 'display', 'block'); + }, - if (type != 9) { - // Keep non whitespace text nodes - if (type == 3 && node.nodeValue.length > 0) { - // If parent element isn't a block or there isn't any useful contents for example "

    " - if (!t.isBlock(node.parentNode) || tinymce.trim(node.nodeValue).length > 0) - return; - } else if (type == 1) { - // If the only child is a bookmark then move it up - children = node.childNodes; - if (children.length == 1 && children[0] && children[0].nodeType == 1 && children[0].getAttribute('data-mce-type') == 'bookmark') - node.parentNode.insertBefore(children[0], node); + hide : function(e) { + return this.setStyle(e, 'display', 'none'); + }, - // Keep non empty elements or img, hr etc - if (children.length || /^(br|hr|input|img)$/i.test(node.nodeName)) - return; - } + isHidden : function(e) { + e = this.get(e); - t.remove(node); - } + return !e || e.style.display == 'none' || this.getStyle(e, 'display') == 'none'; + }, - return node; - }; + uniqueId : function(p) { + return (!p ? 'mce_' : p) + (this.counter++); + }, - if (pe && e) { - // Get before chunk - r.setStart(pe.parentNode, t.nodeIndex(pe)); - r.setEnd(e.parentNode, t.nodeIndex(e)); - bef = r.extractContents(); + setHTML : function(element, html) { + var self = this; - // Get after chunk - r = t.createRng(); - r.setStart(e.parentNode, t.nodeIndex(e) + 1); - r.setEnd(pe.parentNode, t.nodeIndex(pe) + 1); - aft = r.extractContents(); + return self.run(element, function(element) { + if (isIE) { + // Remove all child nodes, IE keeps empty text nodes in DOM + while (element.firstChild) + element.removeChild(element.firstChild); - // Insert before chunk - pa = pe.parentNode; - pa.insertBefore(trim(bef), pe); + try { + // IE will remove comments from the beginning + // unless you padd the contents with something + element.innerHTML = '
    ' + html; + element.removeChild(element.firstChild); + } catch (ex) { + // IE sometimes produces an unknown runtime error on innerHTML if it's an block element within a block element for example a div inside a p + // This seems to fix this problem - // Insert middle chunk - if (re) - pa.replaceChild(re, e); - else - pa.insertBefore(e, pe); + // Create new div with HTML contents and a BR infront to keep comments + var newElement = self.create('div'); + newElement.innerHTML = '
    ' + html; - // Insert after chunk - pa.insertBefore(trim(aft), pe); - t.remove(pe); + // Add all children from div to target + each (tinymce.grep(newElement.childNodes), function(node, i) { + // Skip br element + if (i && element.canHaveHTML) + element.appendChild(node); + }); + } + } else + element.innerHTML = html; - return re || e; - } + return html; + }); }, - bind : function(target, name, func, scope) { - var t = this; + getOuterHTML : function(elm) { + var doc, self = this; - if (!t.events) - t.events = new tinymce.dom.EventUtils(); + elm = self.get(elm); - return t.events.add(target, name, func, scope || this); - }, + if (!elm) + return null; - unbind : function(target, name, func) { - var t = this; + if (elm.nodeType === 1 && self.hasOuterHTML) + return elm.outerHTML; - if (!t.events) - t.events = new tinymce.dom.EventUtils(); + doc = (elm.ownerDocument || self.doc).createElement("body"); + doc.appendChild(elm.cloneNode(true)); - return t.events.remove(target, name, func); + return doc.innerHTML; }, + setOuterHTML : function(e, h, d) { + var t = this; - _findSib : function(node, selector, name) { - var t = this, f = selector; + function setHTML(e, h, d) { + var n, tp; - if (node) { - // If expression make a function of it using is - if (is(f, 'string')) { - f = function(node) { - return t.is(node, selector); - }; - } + tp = d.createElement("body"); + tp.innerHTML = h; - // Loop all siblings - for (node = node[name]; node; node = node[name]) { - if (f(node)) - return node; + n = tp.lastChild; + while (n) { + t.insertAfter(n.cloneNode(true), e); + n = n.previousSibling; } - } - return null; + t.remove(e); + }; + + return this.run(e, function(e) { + e = t.get(e); + + // Only set HTML on elements + if (e.nodeType == 1) { + d = d || e.ownerDocument || t.doc; + + if (isIE) { + try { + // Try outerHTML for IE it sometimes produces an unknown runtime error + if (isIE && e.nodeType == 1) + e.outerHTML = h; + else + setHTML(e, h, d); + } catch (ex) { + // Fix for unknown runtime error + setHTML(e, h, d); + } + } else + setHTML(e, h, d); + } + }); }, - _isRes : function(c) { - // Is live resizble element - return /^(top|left|bottom|right|width|height)/i.test(c) || /;\s*(top|left|bottom|right|width|height)/i.test(c); - } + decode : Entities.decode, - /* - walk : function(n, f, s) { - var d = this.doc, w; + encode : Entities.encodeAllRaw, - if (d.createTreeWalker) { - w = d.createTreeWalker(n, NodeFilter.SHOW_TEXT, null, false); + insertAfter : function(node, reference_node) { + reference_node = this.get(reference_node); - while ((n = w.nextNode()) != null) - f.call(s || this, n); - } else - tinymce.walk(n, f, 'childNodes', s); - } - */ + return this.run(node, function(node) { + var parent, nextSibling; - /* - toRGB : function(s) { - var c = /^\s*?#([0-9A-F]{2})([0-9A-F]{1,2})([0-9A-F]{2})?\s*?$/.exec(s); + parent = reference_node.parentNode; + nextSibling = reference_node.nextSibling; - if (c) { - // #FFF -> #FFFFFF - if (!is(c[3])) - c[3] = c[2] = c[1]; + if (nextSibling) + parent.insertBefore(node, nextSibling); + else + parent.appendChild(node); - return "rgb(" + parseInt(c[1], 16) + "," + parseInt(c[2], 16) + "," + parseInt(c[3], 16) + ")"; - } + return node; + }); + }, - return s; - } - */ - }); + replace : function(n, o, k) { + var t = this; - tinymce.DOM = new tinymce.dom.DOMUtils(document, {process_html : 0}); -})(tinymce); + if (is(o, 'array')) + n = n.cloneNode(true); -(function(ns) { - // Range constructor - function Range(dom) { - var t = this, - doc = dom.doc, - EXTRACT = 0, - CLONE = 1, - DELETE = 2, - TRUE = true, - FALSE = false, - START_OFFSET = 'startOffset', - START_CONTAINER = 'startContainer', - END_CONTAINER = 'endContainer', - END_OFFSET = 'endOffset', - extend = tinymce.extend, - nodeIndex = dom.nodeIndex; + return t.run(o, function(o) { + if (k) { + each(tinymce.grep(o.childNodes), function(c) { + n.appendChild(c); + }); + } - extend(t, { - // Inital states - startContainer : doc, - startOffset : 0, - endContainer : doc, - endOffset : 0, - collapsed : TRUE, - commonAncestorContainer : doc, + return o.parentNode.replaceChild(n, o); + }); + }, - // Range constants - START_TO_START : 0, - START_TO_END : 1, - END_TO_END : 2, - END_TO_START : 3, + rename : function(elm, name) { + var t = this, newElm; - // Public methods - setStart : setStart, - setEnd : setEnd, - setStartBefore : setStartBefore, - setStartAfter : setStartAfter, - setEndBefore : setEndBefore, - setEndAfter : setEndAfter, - collapse : collapse, - selectNode : selectNode, - selectNodeContents : selectNodeContents, - compareBoundaryPoints : compareBoundaryPoints, - deleteContents : deleteContents, - extractContents : extractContents, - cloneContents : cloneContents, - insertNode : insertNode, - surroundContents : surroundContents, - cloneRange : cloneRange - }); + if (elm.nodeName != name.toUpperCase()) { + // Rename block element + newElm = t.create(name); - function setStart(n, o) { - _setEndPoint(TRUE, n, o); - }; + // Copy attribs to new block + each(t.getAttribs(elm), function(attr_node) { + t.setAttrib(newElm, attr_node.nodeName, t.getAttrib(elm, attr_node.nodeName)); + }); - function setEnd(n, o) { - _setEndPoint(FALSE, n, o); - }; + // Replace block + t.replace(newElm, elm, 1); + } - function setStartBefore(n) { - setStart(n.parentNode, nodeIndex(n)); - }; + return newElm || elm; + }, - function setStartAfter(n) { - setStart(n.parentNode, nodeIndex(n) + 1); - }; + findCommonAncestor : function(a, b) { + var ps = a, pe; - function setEndBefore(n) { - setEnd(n.parentNode, nodeIndex(n)); - }; + while (ps) { + pe = b; - function setEndAfter(n) { - setEnd(n.parentNode, nodeIndex(n) + 1); - }; + while (pe && ps != pe) + pe = pe.parentNode; - function collapse(ts) { - if (ts) { - t[END_CONTAINER] = t[START_CONTAINER]; - t[END_OFFSET] = t[START_OFFSET]; - } else { - t[START_CONTAINER] = t[END_CONTAINER]; - t[START_OFFSET] = t[END_OFFSET]; + if (ps == pe) + break; + + ps = ps.parentNode; } - t.collapsed = TRUE; - }; + if (!ps && a.ownerDocument) + return a.ownerDocument.documentElement; - function selectNode(n) { - setStartBefore(n); - setEndAfter(n); - }; + return ps; + }, - function selectNodeContents(n) { - setStart(n, 0); - setEnd(n, n.nodeType === 1 ? n.childNodes.length : n.nodeValue.length); - }; + toHex : function(s) { + var c = /^\s*rgb\s*?\(\s*?([0-9]+)\s*?,\s*?([0-9]+)\s*?,\s*?([0-9]+)\s*?\)\s*$/i.exec(s); - function compareBoundaryPoints(h, r) { - var sc = t[START_CONTAINER], so = t[START_OFFSET], ec = t[END_CONTAINER], eo = t[END_OFFSET], - rsc = r.startContainer, rso = r.startOffset, rec = r.endContainer, reo = r.endOffset; + function hex(s) { + s = parseInt(s, 10).toString(16); - // Check START_TO_START - if (h === 0) - return _compareBoundaryPoints(sc, so, rsc, rso); - - // Check START_TO_END - if (h === 1) - return _compareBoundaryPoints(ec, eo, rsc, rso); - - // Check END_TO_END - if (h === 2) - return _compareBoundaryPoints(ec, eo, rec, reo); - - // Check END_TO_START - if (h === 3) - return _compareBoundaryPoints(sc, so, rec, reo); - }; + return s.length > 1 ? s : '0' + s; // 0 -> 00 + }; - function deleteContents() { - _traverse(DELETE); - }; + if (c) { + s = '#' + hex(c[1]) + hex(c[2]) + hex(c[3]); - function extractContents() { - return _traverse(EXTRACT); - }; + return s; + } - function cloneContents() { - return _traverse(CLONE); - }; + return s; + }, - function insertNode(n) { - var startContainer = this[START_CONTAINER], - startOffset = this[START_OFFSET], nn, o; + getClasses : function() { + var t = this, cl = [], i, lo = {}, f = t.settings.class_filter, ov; - // Node is TEXT_NODE or CDATA - if ((startContainer.nodeType === 3 || startContainer.nodeType === 4) && startContainer.nodeValue) { - if (!startOffset) { - // At the start of text - startContainer.parentNode.insertBefore(n, startContainer); - } else if (startOffset >= startContainer.nodeValue.length) { - // At the end of text - dom.insertAfter(n, startContainer); - } else { - // Middle, need to split - nn = startContainer.splitText(startOffset); - startContainer.parentNode.insertBefore(n, nn); - } - } else { - // Insert element node - if (startContainer.childNodes.length > 0) - o = startContainer.childNodes[startOffset]; - - if (o) - startContainer.insertBefore(n, o); - else - startContainer.appendChild(n); - } - }; + if (t.classes) + return t.classes; - function surroundContents(n) { - var f = t.extractContents(); + function addClasses(s) { + // IE style imports + each(s.imports, function(r) { + addClasses(r); + }); - t.insertNode(n); - n.appendChild(f); - t.selectNode(n); - }; + each(s.cssRules || s.rules, function(r) { + // Real type or fake it on IE + switch (r.type || 1) { + // Rule + case 1: + if (r.selectorText) { + each(r.selectorText.split(','), function(v) { + v = v.replace(/^\s*|\s*$|^\s\./g, ""); - function cloneRange() { - return extend(new Range(dom), { - startContainer : t[START_CONTAINER], - startOffset : t[START_OFFSET], - endContainer : t[END_CONTAINER], - endOffset : t[END_OFFSET], - collapsed : t.collapsed, - commonAncestorContainer : t.commonAncestorContainer - }); - }; + // Is internal or it doesn't contain a class + if (/\.mce/.test(v) || !/\.[\w\-]+$/.test(v)) + return; - // Private methods + // Remove everything but class name + ov = v; + v = tinymce._replace(/.*\.([a-z0-9_\-]+).*/i, '$1', v); - function _getSelectedNode(container, offset) { - var child; + // Filter classes + if (f && !(v = f(v, ov))) + return; - if (container.nodeType == 3 /* TEXT_NODE */) - return container; + if (!lo[v]) { + cl.push({'class' : v}); + lo[v] = 1; + } + }); + } + break; - if (offset < 0) - return container; + // Import + case 3: + addClasses(r.styleSheet); + break; + } + }); + }; - child = container.firstChild; - while (child && offset > 0) { - --offset; - child = child.nextSibling; + try { + each(t.doc.styleSheets, addClasses); + } catch (ex) { + // Ignore } - if (child) - return child; + if (cl.length > 0) + t.classes = cl; - return container; - }; + return cl; + }, - function _isCollapsed() { - return (t[START_CONTAINER] == t[END_CONTAINER] && t[START_OFFSET] == t[END_OFFSET]); - }; + run : function(e, f, s) { + var t = this, o; - function _compareBoundaryPoints(containerA, offsetA, containerB, offsetB) { - var c, offsetC, n, cmnRoot, childA, childB; - - // In the first case the boundary-points have the same container. A is before B - // if its offset is less than the offset of B, A is equal to B if its offset is - // equal to the offset of B, and A is after B if its offset is greater than the - // offset of B. - if (containerA == containerB) { - if (offsetA == offsetB) - return 0; // equal + if (t.doc && typeof(e) === 'string') + e = t.get(e); - if (offsetA < offsetB) - return -1; // before + if (!e) + return false; - return 1; // after - } + s = s || this; + if (!e.nodeType && (e.length || e.length === 0)) { + o = []; - // In the second case a child node C of the container of A is an ancestor - // container of B. In this case, A is before B if the offset of A is less than or - // equal to the index of the child node C and A is after B otherwise. - c = containerB; - while (c && c.parentNode != containerA) - c = c.parentNode; + each(e, function(e, i) { + if (e) { + if (typeof(e) == 'string') + e = t.doc.getElementById(e); - if (c) { - offsetC = 0; - n = containerA.firstChild; + o.push(f.call(s, e, i)); + } + }); - while (n != c && offsetC < offsetA) { - offsetC++; - n = n.nextSibling; - } + return o; + } - if (offsetA <= offsetC) - return -1; // before + return f.call(s, e); + }, - return 1; // after - } + getAttribs : function(n) { + var o; - // In the third case a child node C of the container of B is an ancestor container - // of A. In this case, A is before B if the index of the child node C is less than - // the offset of B and A is after B otherwise. - c = containerA; - while (c && c.parentNode != containerB) { - c = c.parentNode; - } + n = this.get(n); - if (c) { - offsetC = 0; - n = containerB.firstChild; + if (!n) + return []; - while (n != c && offsetC < offsetB) { - offsetC++; - n = n.nextSibling; - } + if (isIE) { + o = []; - if (offsetC < offsetB) - return -1; // before + // Object will throw exception in IE + if (n.nodeName == 'OBJECT') + return n.attributes; - return 1; // after - } + // IE doesn't keep the selected attribute if you clone option elements + if (n.nodeName === 'OPTION' && this.getAttrib(n, 'selected')) + o.push({specified : 1, nodeName : 'selected'}); - // In the fourth case, none of three other cases hold: the containers of A and B - // are siblings or descendants of sibling nodes. In this case, A is before B if - // the container of A is before the container of B in a pre-order traversal of the - // Ranges' context tree and A is after B otherwise. - cmnRoot = dom.findCommonAncestor(containerA, containerB); - childA = containerA; + // It's crazy that this is faster in IE but it's because it returns all attributes all the time + n.cloneNode(false).outerHTML.replace(/<\/?[\w:\-]+ ?|=[\"][^\"]+\"|=\'[^\']+\'|=[\w\-]+|>/gi, '').replace(/[\w:\-]+/gi, function(a) { + o.push({specified : 1, nodeName : a}); + }); - while (childA && childA.parentNode != cmnRoot) - childA = childA.parentNode; + return o; + } - if (!childA) - childA = cmnRoot; + return n.attributes; + }, - childB = containerB; - while (childB && childB.parentNode != cmnRoot) - childB = childB.parentNode; + isEmpty : function(node, elements) { + var self = this, i, attributes, type, walker, name, brCount = 0; - if (!childB) - childB = cmnRoot; + node = node.firstChild; + if (node) { + walker = new tinymce.dom.TreeWalker(node, node.parentNode); + elements = elements || self.schema ? self.schema.getNonEmptyElements() : null; - if (childA == childB) - return 0; // equal + do { + type = node.nodeType; - n = cmnRoot.firstChild; - while (n) { - if (n == childA) - return -1; // before + if (type === 1) { + // Ignore bogus elements + if (node.getAttribute('data-mce-bogus')) + continue; - if (n == childB) - return 1; // after + // Keep empty elements like + name = node.nodeName.toLowerCase(); + if (elements && elements[name]) { + // Ignore single BR elements in blocks like


    or


    + if (name === 'br') { + brCount++; + continue; + } - n = n.nextSibling; - } - }; + return false; + } - function _setEndPoint(st, n, o) { - var ec, sc; + // Keep elements with data-bookmark attributes or name attribute like + attributes = self.getAttribs(node); + i = node.attributes.length; + while (i--) { + name = node.attributes[i].nodeName; + if (name === "name" || name === 'data-mce-bookmark') + return false; + } + } - if (st) { - t[START_CONTAINER] = n; - t[START_OFFSET] = o; - } else { - t[END_CONTAINER] = n; - t[END_OFFSET] = o; + // Keep comment nodes + if (type == 8) + return false; + + // Keep non whitespace text nodes + if ((type === 3 && !whiteSpaceRegExp.test(node.nodeValue))) + return false; + } while (node = walker.next()); } - // If one boundary-point of a Range is set to have a root container - // other than the current one for the Range, the Range is collapsed to - // the new position. This enforces the restriction that both boundary- - // points of a Range must have the same root container. - ec = t[END_CONTAINER]; - while (ec.parentNode) - ec = ec.parentNode; + return brCount <= 1; + }, - sc = t[START_CONTAINER]; - while (sc.parentNode) - sc = sc.parentNode; - - if (sc == ec) { - // The start position of a Range is guaranteed to never be after the - // end position. To enforce this restriction, if the start is set to - // be at a position after the end, the Range is collapsed to that - // position. - if (_compareBoundaryPoints(t[START_CONTAINER], t[START_OFFSET], t[END_CONTAINER], t[END_OFFSET]) > 0) - t.collapse(st); - } else - t.collapse(st); - - t.collapsed = _isCollapsed(); - t.commonAncestorContainer = dom.findCommonAncestor(t[START_CONTAINER], t[END_CONTAINER]); - }; - - function _traverse(how) { - var c, endContainerDepth = 0, startContainerDepth = 0, p, depthDiff, startNode, endNode, sp, ep; + destroy : function(s) { + var t = this; - if (t[START_CONTAINER] == t[END_CONTAINER]) - return _traverseSameContainer(how); + t.win = t.doc = t.root = t.events = t.frag = null; - for (c = t[END_CONTAINER], p = c.parentNode; p; c = p, p = p.parentNode) { - if (p == t[START_CONTAINER]) - return _traverseCommonStartContainer(c, how); + // Manual destroy then remove unload handler + if (!s) + tinymce.removeUnload(t.destroy); + }, - ++endContainerDepth; - } + createRng : function() { + var d = this.doc; - for (c = t[START_CONTAINER], p = c.parentNode; p; c = p, p = p.parentNode) { - if (p == t[END_CONTAINER]) - return _traverseCommonEndContainer(c, how); + return d.createRange ? d.createRange() : new tinymce.dom.Range(this); + }, - ++startContainerDepth; - } + nodeIndex : function(node, normalized) { + var idx = 0, lastNodeType, lastNode, nodeType; - depthDiff = startContainerDepth - endContainerDepth; + if (node) { + for (lastNodeType = node.nodeType, node = node.previousSibling, lastNode = node; node; node = node.previousSibling) { + nodeType = node.nodeType; - startNode = t[START_CONTAINER]; - while (depthDiff > 0) { - startNode = startNode.parentNode; - depthDiff--; + // Normalize text nodes + if (normalized && nodeType == 3) { + if (nodeType == lastNodeType || !node.nodeValue.length) + continue; + } + idx++; + lastNodeType = nodeType; + } } - endNode = t[END_CONTAINER]; - while (depthDiff < 0) { - endNode = endNode.parentNode; - depthDiff++; - } + return idx; + }, - // ascend the ancestor hierarchy until we have a common parent. - for (sp = startNode.parentNode, ep = endNode.parentNode; sp != ep; sp = sp.parentNode, ep = ep.parentNode) { - startNode = sp; - endNode = ep; - } + split : function(pe, e, re) { + var t = this, r = t.createRng(), bef, aft, pa; - return _traverseCommonAncestors(startNode, endNode, how); - }; + // W3C valid browsers tend to leave empty nodes to the left/right side of the contents, this makes sense + // but we don't want that in our code since it serves no purpose for the end user + // For example if this is chopped: + //

    text 1CHOPtext 2

    + // would produce: + //

    text 1

    CHOP

    text 2

    + // this function will then trim of empty edges and produce: + //

    text 1

    CHOP

    text 2

    + function trim(node) { + var i, children = node.childNodes, type = node.nodeType; - function _traverseSameContainer(how) { - var frag, s, sub, n, cnt, sibling, xferNode; + function surroundedBySpans(node) { + var previousIsSpan = node.previousSibling && node.previousSibling.nodeName == 'SPAN'; + var nextIsSpan = node.nextSibling && node.nextSibling.nodeName == 'SPAN'; + return previousIsSpan && nextIsSpan; + } - if (how != DELETE) - frag = doc.createDocumentFragment(); + if (type == 1 && node.getAttribute('data-mce-type') == 'bookmark') + return; - // If selection is empty, just return the fragment - if (t[START_OFFSET] == t[END_OFFSET]) - return frag; + for (i = children.length - 1; i >= 0; i--) + trim(children[i]); - // Text node needs special case handling - if (t[START_CONTAINER].nodeType == 3 /* TEXT_NODE */) { - // get the substring - s = t[START_CONTAINER].nodeValue; - sub = s.substring(t[START_OFFSET], t[END_OFFSET]); + if (type != 9) { + // Keep non whitespace text nodes + if (type == 3 && node.nodeValue.length > 0) { + // If parent element isn't a block or there isn't any useful contents for example "

    " + // Also keep text nodes with only spaces if surrounded by spans. + // eg. "

    a b

    " should keep space between a and b + var trimmedLength = tinymce.trim(node.nodeValue).length; + if (!t.isBlock(node.parentNode) || trimmedLength > 0 || trimmedLength === 0 && surroundedBySpans(node)) + return; + } else if (type == 1) { + // If the only child is a bookmark then move it up + children = node.childNodes; + if (children.length == 1 && children[0] && children[0].nodeType == 1 && children[0].getAttribute('data-mce-type') == 'bookmark') + node.parentNode.insertBefore(children[0], node); - // set the original text node to its new value - if (how != CLONE) { - t[START_CONTAINER].deleteData(t[START_OFFSET], t[END_OFFSET] - t[START_OFFSET]); + // Keep non empty elements or img, hr etc + if (children.length || /^(br|hr|input|img)$/i.test(node.nodeName)) + return; + } - // Nothing is partially selected, so collapse to start point - t.collapse(TRUE); + t.remove(node); } - if (how == DELETE) - return; + return node; + }; - frag.appendChild(doc.createTextNode(sub)); - return frag; - } + if (pe && e) { + // Get before chunk + r.setStart(pe.parentNode, t.nodeIndex(pe)); + r.setEnd(e.parentNode, t.nodeIndex(e)); + bef = r.extractContents(); - // Copy nodes between the start/end offsets. - n = _getSelectedNode(t[START_CONTAINER], t[START_OFFSET]); - cnt = t[END_OFFSET] - t[START_OFFSET]; + // Get after chunk + r = t.createRng(); + r.setStart(e.parentNode, t.nodeIndex(e) + 1); + r.setEnd(pe.parentNode, t.nodeIndex(pe) + 1); + aft = r.extractContents(); - while (cnt > 0) { - sibling = n.nextSibling; - xferNode = _traverseFullySelected(n, how); + // Insert before chunk + pa = pe.parentNode; + pa.insertBefore(trim(bef), pe); - if (frag) - frag.appendChild( xferNode ); + // Insert middle chunk + if (re) + pa.replaceChild(re, e); + else + pa.insertBefore(e, pe); - --cnt; - n = sibling; + // Insert after chunk + pa.insertBefore(trim(aft), pe); + t.remove(pe); + + return re || e; } + }, - // Nothing is partially selected, so collapse to start point - if (how != CLONE) - t.collapse(TRUE); + bind : function(target, name, func, scope) { + return this.events.add(target, name, func, scope || this); + }, - return frag; - }; + unbind : function(target, name, func) { + return this.events.remove(target, name, func); + }, - function _traverseCommonStartContainer(endAncestor, how) { - var frag, n, endIdx, cnt, sibling, xferNode; + fire : function(target, name, evt) { + return this.events.fire(target, name, evt); + }, - if (how != DELETE) - frag = doc.createDocumentFragment(); + // Returns the content editable state of a node + getContentEditable: function(node) { + var contentEditable; - n = _traverseRightBoundary(endAncestor, how); + // Check type + if (node.nodeType != 1) { + return null; + } - if (frag) - frag.appendChild(n); + // Check for fake content editable + contentEditable = node.getAttribute("data-mce-contenteditable"); + if (contentEditable && contentEditable !== "inherit") { + return contentEditable; + } - endIdx = nodeIndex(endAncestor); - cnt = endIdx - t[START_OFFSET]; + // Check for real content editable + return node.contentEditable !== "inherit" ? node.contentEditable : null; + }, - if (cnt <= 0) { - // Collapse to just before the endAncestor, which - // is partially selected. - if (how != CLONE) { - t.setEndBefore(endAncestor); - t.collapse(FALSE); + + _findSib : function(node, selector, name) { + var t = this, f = selector; + + if (node) { + // If expression make a function of it using is + if (is(f, 'string')) { + f = function(node) { + return t.is(node, selector); + }; } - return frag; + // Loop all siblings + for (node = node[name]; node; node = node[name]) { + if (f(node)) + return node; + } } - n = endAncestor.previousSibling; - while (cnt > 0) { - sibling = n.previousSibling; - xferNode = _traverseFullySelected(n, how); - - if (frag) - frag.insertBefore(xferNode, frag.firstChild); + return null; + }, - --cnt; - n = sibling; - } + _isRes : function(c) { + // Is live resizble element + return /^(top|left|bottom|right|width|height)/i.test(c) || /;\s*(top|left|bottom|right|width|height)/i.test(c); + } - // Collapse to just before the endAncestor, which - // is partially selected. - if (how != CLONE) { - t.setEndBefore(endAncestor); - t.collapse(FALSE); - } + /* + walk : function(n, f, s) { + var d = this.doc, w; - return frag; - }; + if (d.createTreeWalker) { + w = d.createTreeWalker(n, NodeFilter.SHOW_TEXT, null, false); - function _traverseCommonEndContainer(startAncestor, how) { - var frag, startIdx, n, cnt, sibling, xferNode; + while ((n = w.nextNode()) != null) + f.call(s || this, n); + } else + tinymce.walk(n, f, 'childNodes', s); + } + */ - if (how != DELETE) - frag = doc.createDocumentFragment(); + /* + toRGB : function(s) { + var c = /^\s*?#([0-9A-F]{2})([0-9A-F]{1,2})([0-9A-F]{2})?\s*?$/.exec(s); - n = _traverseLeftBoundary(startAncestor, how); - if (frag) - frag.appendChild(n); + if (c) { + // #FFF -> #FFFFFF + if (!is(c[3])) + c[3] = c[2] = c[1]; - startIdx = nodeIndex(startAncestor); - ++startIdx; // Because we already traversed it + return "rgb(" + parseInt(c[1], 16) + "," + parseInt(c[2], 16) + "," + parseInt(c[3], 16) + ")"; + } - cnt = t[END_OFFSET] - startIdx; - n = startAncestor.nextSibling; - while (cnt > 0) { - sibling = n.nextSibling; - xferNode = _traverseFullySelected(n, how); - - if (frag) - frag.appendChild(xferNode); - - --cnt; - n = sibling; - } + return s; + } + */ + }); - if (how != CLONE) { - t.setStartAfter(startAncestor); - t.collapse(TRUE); - } + tinymce.DOM = new tinymce.dom.DOMUtils(document, {process_html : 0}); +})(tinymce); - return frag; - }; +(function(ns) { + // Range constructor + function Range(dom) { + var t = this, + doc = dom.doc, + EXTRACT = 0, + CLONE = 1, + DELETE = 2, + TRUE = true, + FALSE = false, + START_OFFSET = 'startOffset', + START_CONTAINER = 'startContainer', + END_CONTAINER = 'endContainer', + END_OFFSET = 'endOffset', + extend = tinymce.extend, + nodeIndex = dom.nodeIndex; - function _traverseCommonAncestors(startAncestor, endAncestor, how) { - var n, frag, commonParent, startOffset, endOffset, cnt, sibling, nextSibling; + extend(t, { + // Inital states + startContainer : doc, + startOffset : 0, + endContainer : doc, + endOffset : 0, + collapsed : TRUE, + commonAncestorContainer : doc, - if (how != DELETE) - frag = doc.createDocumentFragment(); + // Range constants + START_TO_START : 0, + START_TO_END : 1, + END_TO_END : 2, + END_TO_START : 3, - n = _traverseLeftBoundary(startAncestor, how); - if (frag) - frag.appendChild(n); + // Public methods + setStart : setStart, + setEnd : setEnd, + setStartBefore : setStartBefore, + setStartAfter : setStartAfter, + setEndBefore : setEndBefore, + setEndAfter : setEndAfter, + collapse : collapse, + selectNode : selectNode, + selectNodeContents : selectNodeContents, + compareBoundaryPoints : compareBoundaryPoints, + deleteContents : deleteContents, + extractContents : extractContents, + cloneContents : cloneContents, + insertNode : insertNode, + surroundContents : surroundContents, + cloneRange : cloneRange, + toStringIE : toStringIE + }); - commonParent = startAncestor.parentNode; - startOffset = nodeIndex(startAncestor); - endOffset = nodeIndex(endAncestor); - ++startOffset; + function createDocumentFragment() { + return doc.createDocumentFragment(); + }; - cnt = endOffset - startOffset; - sibling = startAncestor.nextSibling; + function setStart(n, o) { + _setEndPoint(TRUE, n, o); + }; - while (cnt > 0) { - nextSibling = sibling.nextSibling; - n = _traverseFullySelected(sibling, how); + function setEnd(n, o) { + _setEndPoint(FALSE, n, o); + }; - if (frag) - frag.appendChild(n); + function setStartBefore(n) { + setStart(n.parentNode, nodeIndex(n)); + }; - sibling = nextSibling; - --cnt; - } + function setStartAfter(n) { + setStart(n.parentNode, nodeIndex(n) + 1); + }; - n = _traverseRightBoundary(endAncestor, how); + function setEndBefore(n) { + setEnd(n.parentNode, nodeIndex(n)); + }; - if (frag) - frag.appendChild(n); + function setEndAfter(n) { + setEnd(n.parentNode, nodeIndex(n) + 1); + }; - if (how != CLONE) { - t.setStartAfter(startAncestor); - t.collapse(TRUE); + function collapse(ts) { + if (ts) { + t[END_CONTAINER] = t[START_CONTAINER]; + t[END_OFFSET] = t[START_OFFSET]; + } else { + t[START_CONTAINER] = t[END_CONTAINER]; + t[START_OFFSET] = t[END_OFFSET]; } - return frag; + t.collapsed = TRUE; }; - function _traverseRightBoundary(root, how) { - var next = _getSelectedNode(t[END_CONTAINER], t[END_OFFSET] - 1), parent, clonedParent, prevSibling, clonedChild, clonedGrandParent, isFullySelected = next != t[END_CONTAINER]; - - if (next == root) - return _traverseNode(next, isFullySelected, FALSE, how); + function selectNode(n) { + setStartBefore(n); + setEndAfter(n); + }; - parent = next.parentNode; - clonedParent = _traverseNode(parent, FALSE, FALSE, how); + function selectNodeContents(n) { + setStart(n, 0); + setEnd(n, n.nodeType === 1 ? n.childNodes.length : n.nodeValue.length); + }; - while (parent) { - while (next) { - prevSibling = next.previousSibling; - clonedChild = _traverseNode(next, isFullySelected, FALSE, how); + function compareBoundaryPoints(h, r) { + var sc = t[START_CONTAINER], so = t[START_OFFSET], ec = t[END_CONTAINER], eo = t[END_OFFSET], + rsc = r.startContainer, rso = r.startOffset, rec = r.endContainer, reo = r.endOffset; - if (how != DELETE) - clonedParent.insertBefore(clonedChild, clonedParent.firstChild); + // Check START_TO_START + if (h === 0) + return _compareBoundaryPoints(sc, so, rsc, rso); + + // Check START_TO_END + if (h === 1) + return _compareBoundaryPoints(ec, eo, rsc, rso); + + // Check END_TO_END + if (h === 2) + return _compareBoundaryPoints(ec, eo, rec, reo); + + // Check END_TO_START + if (h === 3) + return _compareBoundaryPoints(sc, so, rec, reo); + }; - isFullySelected = TRUE; - next = prevSibling; - } + function deleteContents() { + _traverse(DELETE); + }; - if (parent == root) - return clonedParent; + function extractContents() { + return _traverse(EXTRACT); + }; - next = parent.previousSibling; - parent = parent.parentNode; + function cloneContents() { + return _traverse(CLONE); + }; - clonedGrandParent = _traverseNode(parent, FALSE, FALSE, how); + function insertNode(n) { + var startContainer = this[START_CONTAINER], + startOffset = this[START_OFFSET], nn, o; - if (how != DELETE) - clonedGrandParent.appendChild(clonedParent); + // Node is TEXT_NODE or CDATA + if ((startContainer.nodeType === 3 || startContainer.nodeType === 4) && startContainer.nodeValue) { + if (!startOffset) { + // At the start of text + startContainer.parentNode.insertBefore(n, startContainer); + } else if (startOffset >= startContainer.nodeValue.length) { + // At the end of text + dom.insertAfter(n, startContainer); + } else { + // Middle, need to split + nn = startContainer.splitText(startOffset); + startContainer.parentNode.insertBefore(n, nn); + } + } else { + // Insert element node + if (startContainer.childNodes.length > 0) + o = startContainer.childNodes[startOffset]; - clonedParent = clonedGrandParent; + if (o) + startContainer.insertBefore(n, o); + else + startContainer.appendChild(n); } }; - function _traverseLeftBoundary(root, how) { - var next = _getSelectedNode(t[START_CONTAINER], t[START_OFFSET]), isFullySelected = next != t[START_CONTAINER], parent, clonedParent, nextSibling, clonedChild, clonedGrandParent; + function surroundContents(n) { + var f = t.extractContents(); - if (next == root) - return _traverseNode(next, isFullySelected, TRUE, how); + t.insertNode(n); + n.appendChild(f); + t.selectNode(n); + }; - parent = next.parentNode; - clonedParent = _traverseNode(parent, FALSE, TRUE, how); + function cloneRange() { + return extend(new Range(dom), { + startContainer : t[START_CONTAINER], + startOffset : t[START_OFFSET], + endContainer : t[END_CONTAINER], + endOffset : t[END_OFFSET], + collapsed : t.collapsed, + commonAncestorContainer : t.commonAncestorContainer + }); + }; - while (parent) { - while (next) { - nextSibling = next.nextSibling; - clonedChild = _traverseNode(next, isFullySelected, TRUE, how); + // Private methods - if (how != DELETE) - clonedParent.appendChild(clonedChild); + function _getSelectedNode(container, offset) { + var child; - isFullySelected = TRUE; - next = nextSibling; - } + if (container.nodeType == 3 /* TEXT_NODE */) + return container; - if (parent == root) - return clonedParent; + if (offset < 0) + return container; - next = parent.nextSibling; - parent = parent.parentNode; + child = container.firstChild; + while (child && offset > 0) { + --offset; + child = child.nextSibling; + } - clonedGrandParent = _traverseNode(parent, FALSE, TRUE, how); + if (child) + return child; - if (how != DELETE) - clonedGrandParent.appendChild(clonedParent); + return container; + }; - clonedParent = clonedGrandParent; - } + function _isCollapsed() { + return (t[START_CONTAINER] == t[END_CONTAINER] && t[START_OFFSET] == t[END_OFFSET]); }; - function _traverseNode(n, isFullySelected, isLeft, how) { - var txtValue, newNodeValue, oldNodeValue, offset, newNode; + function _compareBoundaryPoints(containerA, offsetA, containerB, offsetB) { + var c, offsetC, n, cmnRoot, childA, childB; + + // In the first case the boundary-points have the same container. A is before B + // if its offset is less than the offset of B, A is equal to B if its offset is + // equal to the offset of B, and A is after B if its offset is greater than the + // offset of B. + if (containerA == containerB) { + if (offsetA == offsetB) + return 0; // equal - if (isFullySelected) - return _traverseFullySelected(n, how); + if (offsetA < offsetB) + return -1; // before - if (n.nodeType == 3 /* TEXT_NODE */) { - txtValue = n.nodeValue; + return 1; // after + } - if (isLeft) { - offset = t[START_OFFSET]; - newNodeValue = txtValue.substring(offset); - oldNodeValue = txtValue.substring(0, offset); - } else { - offset = t[END_OFFSET]; - newNodeValue = txtValue.substring(0, offset); - oldNodeValue = txtValue.substring(offset); - } + // In the second case a child node C of the container of A is an ancestor + // container of B. In this case, A is before B if the offset of A is less than or + // equal to the index of the child node C and A is after B otherwise. + c = containerB; + while (c && c.parentNode != containerA) + c = c.parentNode; - if (how != CLONE) - n.nodeValue = oldNodeValue; + if (c) { + offsetC = 0; + n = containerA.firstChild; - if (how == DELETE) - return; + while (n != c && offsetC < offsetA) { + offsetC++; + n = n.nextSibling; + } - newNode = n.cloneNode(FALSE); - newNode.nodeValue = newNodeValue; + if (offsetA <= offsetC) + return -1; // before - return newNode; + return 1; // after } - if (how == DELETE) - return; + // In the third case a child node C of the container of B is an ancestor container + // of A. In this case, A is before B if the index of the child node C is less than + // the offset of B and A is after B otherwise. + c = containerA; + while (c && c.parentNode != containerB) { + c = c.parentNode; + } - return n.cloneNode(FALSE); - }; + if (c) { + offsetC = 0; + n = containerB.firstChild; - function _traverseFullySelected(n, how) { - if (how != DELETE) - return how == CLONE ? n.cloneNode(TRUE) : n; + while (n != c && offsetC < offsetB) { + offsetC++; + n = n.nextSibling; + } - n.parentNode.removeChild(n); - }; - }; + if (offsetC < offsetB) + return -1; // before - ns.Range = Range; -})(tinymce.dom); + return 1; // after + } -(function() { - function Selection(selection) { - var self = this, dom = selection.dom, TRUE = true, FALSE = false; + // In the fourth case, none of three other cases hold: the containers of A and B + // are siblings or descendants of sibling nodes. In this case, A is before B if + // the container of A is before the container of B in a pre-order traversal of the + // Ranges' context tree and A is after B otherwise. + cmnRoot = dom.findCommonAncestor(containerA, containerB); + childA = containerA; - function getPosition(rng, start) { - var checkRng, startIndex = 0, endIndex, inside, - children, child, offset, index, position = -1, parent; + while (childA && childA.parentNode != cmnRoot) + childA = childA.parentNode; - // Setup test range, collapse it and get the parent - checkRng = rng.duplicate(); - checkRng.collapse(start); - parent = checkRng.parentElement(); + if (!childA) + childA = cmnRoot; - // Check if the selection is within the right document - if (parent.ownerDocument !== selection.dom.doc) - return; + childB = containerB; + while (childB && childB.parentNode != cmnRoot) + childB = childB.parentNode; - // IE will report non editable elements as it's parent so look for an editable one - while (parent.contentEditable === "false") { - parent = parent.parentNode; - } + if (!childB) + childB = cmnRoot; - // If parent doesn't have any children then return that we are inside the element - if (!parent.hasChildNodes()) { - return {node : parent, inside : 1}; - } + if (childA == childB) + return 0; // equal - // Setup node list and endIndex - children = parent.children; - endIndex = children.length - 1; + n = cmnRoot.firstChild; + while (n) { + if (n == childA) + return -1; // before - // Perform a binary search for the position - while (startIndex <= endIndex) { - index = Math.floor((startIndex + endIndex) / 2); + if (n == childB) + return 1; // after - // Move selection to node and compare the ranges - child = children[index]; - checkRng.moveToElementText(child); - position = checkRng.compareEndPoints(start ? 'StartToStart' : 'EndToEnd', rng); + n = n.nextSibling; + } + }; - // Before/after or an exact match - if (position > 0) { - endIndex = index - 1; - } else if (position < 0) { - startIndex = index + 1; - } else { - return {node : child}; - } + function _setEndPoint(st, n, o) { + var ec, sc; + + if (st) { + t[START_CONTAINER] = n; + t[START_OFFSET] = o; + } else { + t[END_CONTAINER] = n; + t[END_OFFSET] = o; } - // Check if child position is before or we didn't find a position - if (position < 0) { - // No element child was found use the parent element and the offset inside that - if (!child) { - checkRng.moveToElementText(parent); - checkRng.collapse(true); - child = parent; - inside = true; - } else - checkRng.collapse(false); + // If one boundary-point of a Range is set to have a root container + // other than the current one for the Range, the Range is collapsed to + // the new position. This enforces the restriction that both boundary- + // points of a Range must have the same root container. + ec = t[END_CONTAINER]; + while (ec.parentNode) + ec = ec.parentNode; - checkRng.setEndPoint(start ? 'EndToStart' : 'EndToEnd', rng); + sc = t[START_CONTAINER]; + while (sc.parentNode) + sc = sc.parentNode; - // Fix for edge case:
    ..
    ab|c
    - if (checkRng.compareEndPoints(start ? 'StartToStart' : 'StartToEnd', rng) > 0) { - checkRng = rng.duplicate(); - checkRng.collapse(start); + if (sc == ec) { + // The start position of a Range is guaranteed to never be after the + // end position. To enforce this restriction, if the start is set to + // be at a position after the end, the Range is collapsed to that + // position. + if (_compareBoundaryPoints(t[START_CONTAINER], t[START_OFFSET], t[END_CONTAINER], t[END_OFFSET]) > 0) + t.collapse(st); + } else + t.collapse(st); - offset = -1; - while (parent == checkRng.parentElement()) { - if (checkRng.move('character', -1) == 0) - break; + t.collapsed = _isCollapsed(); + t.commonAncestorContainer = dom.findCommonAncestor(t[START_CONTAINER], t[END_CONTAINER]); + }; - offset++; - } - } + function _traverse(how) { + var c, endContainerDepth = 0, startContainerDepth = 0, p, depthDiff, startNode, endNode, sp, ep; - offset = offset || checkRng.text.replace('\r\n', ' ').length; - } else { - // Child position is after the selection endpoint - checkRng.collapse(true); - checkRng.setEndPoint(start ? 'StartToStart' : 'StartToEnd', rng); + if (t[START_CONTAINER] == t[END_CONTAINER]) + return _traverseSameContainer(how); + + for (c = t[END_CONTAINER], p = c.parentNode; p; c = p, p = p.parentNode) { + if (p == t[START_CONTAINER]) + return _traverseCommonStartContainer(c, how); - // Get the length of the text to find where the endpoint is relative to it's container - offset = checkRng.text.replace('\r\n', ' ').length; + ++endContainerDepth; } - return {node : child, position : position, offset : offset, inside : inside}; - }; + for (c = t[START_CONTAINER], p = c.parentNode; p; c = p, p = p.parentNode) { + if (p == t[END_CONTAINER]) + return _traverseCommonEndContainer(c, how); - // Returns a W3C DOM compatible range object by using the IE Range API - function getRange() { - var ieRange = selection.getRng(), domRange = dom.createRng(), element, collapsed, tmpRange, element2, bookmark, fail; + ++startContainerDepth; + } - // If selection is outside the current document just return an empty range - element = ieRange.item ? ieRange.item(0) : ieRange.parentElement(); - if (element.ownerDocument != dom.doc) - return domRange; + depthDiff = startContainerDepth - endContainerDepth; - collapsed = selection.isCollapsed(); + startNode = t[START_CONTAINER]; + while (depthDiff > 0) { + startNode = startNode.parentNode; + depthDiff--; + } - // Handle control selection - if (ieRange.item) { - domRange.setStart(element.parentNode, dom.nodeIndex(element)); - domRange.setEnd(domRange.startContainer, domRange.startOffset + 1); + endNode = t[END_CONTAINER]; + while (depthDiff < 0) { + endNode = endNode.parentNode; + depthDiff++; + } - return domRange; + // ascend the ancestor hierarchy until we have a common parent. + for (sp = startNode.parentNode, ep = endNode.parentNode; sp != ep; sp = sp.parentNode, ep = ep.parentNode) { + startNode = sp; + endNode = ep; } - function findEndPoint(start) { - var endPoint = getPosition(ieRange, start), container, offset, textNodeOffset = 0, sibling, undef, nodeValue; + return _traverseCommonAncestors(startNode, endNode, how); + }; - container = endPoint.node; - offset = endPoint.offset; + function _traverseSameContainer(how) { + var frag, s, sub, n, cnt, sibling, xferNode, start, len; - if (endPoint.inside && !container.hasChildNodes()) { - domRange[start ? 'setStart' : 'setEnd'](container, 0); - return; - } + if (how != DELETE) + frag = createDocumentFragment(); - if (offset === undef) { - domRange[start ? 'setStartBefore' : 'setEndAfter'](container); - return; - } + // If selection is empty, just return the fragment + if (t[START_OFFSET] == t[END_OFFSET]) + return frag; - if (endPoint.position < 0) { - sibling = endPoint.inside ? container.firstChild : container.nextSibling; + // Text node needs special case handling + if (t[START_CONTAINER].nodeType == 3 /* TEXT_NODE */) { + // get the substring + s = t[START_CONTAINER].nodeValue; + sub = s.substring(t[START_OFFSET], t[END_OFFSET]); - if (!sibling) { - domRange[start ? 'setStartAfter' : 'setEndAfter'](container); - return; + // set the original text node to its new value + if (how != CLONE) { + n = t[START_CONTAINER]; + start = t[START_OFFSET]; + len = t[END_OFFSET] - t[START_OFFSET]; + + if (start === 0 && len >= n.nodeValue.length - 1) { + n.parentNode.removeChild(n); + } else { + n.deleteData(start, len); } - if (!offset) { - if (sibling.nodeType == 3) - domRange[start ? 'setStart' : 'setEnd'](sibling, 0); - else - domRange[start ? 'setStartBefore' : 'setEndBefore'](sibling); + // Nothing is partially selected, so collapse to start point + t.collapse(TRUE); + } - return; - } + if (how == DELETE) + return; - // Find the text node and offset - while (sibling) { - nodeValue = sibling.nodeValue; - textNodeOffset += nodeValue.length; + if (sub.length > 0) { + frag.appendChild(doc.createTextNode(sub)); + } - // We are at or passed the position we where looking for - if (textNodeOffset >= offset) { - container = sibling; - textNodeOffset -= offset; - textNodeOffset = nodeValue.length - textNodeOffset; - break; - } + return frag; + } - sibling = sibling.nextSibling; - } - } else { - // Find the text node and offset - sibling = container.previousSibling; + // Copy nodes between the start/end offsets. + n = _getSelectedNode(t[START_CONTAINER], t[START_OFFSET]); + cnt = t[END_OFFSET] - t[START_OFFSET]; - if (!sibling) - return domRange[start ? 'setStartBefore' : 'setEndBefore'](container); + while (n && cnt > 0) { + sibling = n.nextSibling; + xferNode = _traverseFullySelected(n, how); - // If there isn't any text to loop then use the first position - if (!offset) { - if (container.nodeType == 3) - domRange[start ? 'setStart' : 'setEnd'](sibling, container.nodeValue.length); - else - domRange[start ? 'setStartAfter' : 'setEndAfter'](sibling); + if (frag) + frag.appendChild( xferNode ); - return; - } + --cnt; + n = sibling; + } - while (sibling) { - textNodeOffset += sibling.nodeValue.length; + // Nothing is partially selected, so collapse to start point + if (how != CLONE) + t.collapse(TRUE); - // We are at or passed the position we where looking for - if (textNodeOffset >= offset) { - container = sibling; - textNodeOffset -= offset; - break; - } + return frag; + }; - sibling = sibling.previousSibling; - } - } + function _traverseCommonStartContainer(endAncestor, how) { + var frag, n, endIdx, cnt, sibling, xferNode; - domRange[start ? 'setStart' : 'setEnd'](container, textNodeOffset); - }; + if (how != DELETE) + frag = createDocumentFragment(); - try { - // Find start point - findEndPoint(true); + n = _traverseRightBoundary(endAncestor, how); - // Find end point if needed - if (!collapsed) - findEndPoint(); - } catch (ex) { - // IE has a nasty bug where text nodes might throw "invalid argument" when you - // access the nodeValue or other properties of text nodes. This seems to happend when - // text nodes are split into two nodes by a delete/backspace call. So lets detect it and try to fix it. - if (ex.number == -2147024809) { - // Get the current selection - bookmark = self.getBookmark(2); + if (frag) + frag.appendChild(n); - // Get start element - tmpRange = ieRange.duplicate(); - tmpRange.collapse(true); - element = tmpRange.parentElement(); + endIdx = nodeIndex(endAncestor); + cnt = endIdx - t[START_OFFSET]; - // Get end element - if (!collapsed) { - tmpRange = ieRange.duplicate(); - tmpRange.collapse(false); - element2 = tmpRange.parentElement(); - element2.innerHTML = element2.innerHTML; - } + if (cnt <= 0) { + // Collapse to just before the endAncestor, which + // is partially selected. + if (how != CLONE) { + t.setEndBefore(endAncestor); + t.collapse(FALSE); + } - // Remove the broken elements - element.innerHTML = element.innerHTML; + return frag; + } - // Restore the selection - self.moveToBookmark(bookmark); + n = endAncestor.previousSibling; + while (cnt > 0) { + sibling = n.previousSibling; + xferNode = _traverseFullySelected(n, how); - // Since the range has moved we need to re-get it - ieRange = selection.getRng(); + if (frag) + frag.insertBefore(xferNode, frag.firstChild); - // Find start point - findEndPoint(true); + --cnt; + n = sibling; + } - // Find end point if needed - if (!collapsed) - findEndPoint(); - } else - throw ex; // Throw other errors + // Collapse to just before the endAncestor, which + // is partially selected. + if (how != CLONE) { + t.setEndBefore(endAncestor); + t.collapse(FALSE); } - return domRange; + return frag; }; - this.getBookmark = function(type) { - var rng = selection.getRng(), start, end, bookmark = {}; - - function getIndexes(node) { - var node, parent, root, children, i, indexes = []; - - parent = node.parentNode; - root = dom.getRoot().parentNode; - - while (parent != root && parent.nodeType !== 9) { - children = parent.children; + function _traverseCommonEndContainer(startAncestor, how) { + var frag, startIdx, n, cnt, sibling, xferNode; - i = children.length; - while (i--) { - if (node === children[i]) { - indexes.push(i); - break; - } - } + if (how != DELETE) + frag = createDocumentFragment(); - node = parent; - parent = parent.parentNode; - } + n = _traverseLeftBoundary(startAncestor, how); + if (frag) + frag.appendChild(n); - return indexes; - }; + startIdx = nodeIndex(startAncestor); + ++startIdx; // Because we already traversed it - function getBookmarkEndPoint(start) { - var position; + cnt = t[END_OFFSET] - startIdx; + n = startAncestor.nextSibling; + while (n && cnt > 0) { + sibling = n.nextSibling; + xferNode = _traverseFullySelected(n, how); - position = getPosition(rng, start); - if (position) { - return { - position : position.position, - offset : position.offset, - indexes : getIndexes(position.node), - inside : position.inside - }; - } - }; + if (frag) + frag.appendChild(xferNode); - // Non ubstructive bookmark - if (type === 2) { - // Handle text selection - if (!rng.item) { - bookmark.start = getBookmarkEndPoint(true); + --cnt; + n = sibling; + } - if (!selection.isCollapsed()) - bookmark.end = getBookmarkEndPoint(); - } else - bookmark.start = {ctrl : true, indexes : getIndexes(rng.item(0))}; + if (how != CLONE) { + t.setStartAfter(startAncestor); + t.collapse(TRUE); } - return bookmark; + return frag; }; - this.moveToBookmark = function(bookmark) { - var rng, body = dom.doc.body; - - function resolveIndexes(indexes) { - var node, i, idx, children; + function _traverseCommonAncestors(startAncestor, endAncestor, how) { + var n, frag, commonParent, startOffset, endOffset, cnt, sibling, nextSibling; - node = dom.getRoot(); - for (i = indexes.length - 1; i >= 0; i--) { - children = node.children; - idx = indexes[i]; + if (how != DELETE) + frag = createDocumentFragment(); - if (idx <= children.length - 1) { - node = children[idx]; - } - } + n = _traverseLeftBoundary(startAncestor, how); + if (frag) + frag.appendChild(n); - return node; - }; - - function setBookmarkEndPoint(start) { - var endPoint = bookmark[start ? 'start' : 'end'], moveLeft, moveRng, undef; + commonParent = startAncestor.parentNode; + startOffset = nodeIndex(startAncestor); + endOffset = nodeIndex(endAncestor); + ++startOffset; - if (endPoint) { - moveLeft = endPoint.position > 0; + cnt = endOffset - startOffset; + sibling = startAncestor.nextSibling; - moveRng = body.createTextRange(); - moveRng.moveToElementText(resolveIndexes(endPoint.indexes)); - - offset = endPoint.offset; - if (offset !== undef) { - moveRng.collapse(endPoint.inside || moveLeft); - moveRng.moveStart('character', moveLeft ? -offset : offset); - } else - moveRng.collapse(start); - - rng.setEndPoint(start ? 'StartToStart' : 'EndToStart', moveRng); + while (cnt > 0) { + nextSibling = sibling.nextSibling; + n = _traverseFullySelected(sibling, how); - if (start) - rng.collapse(true); - } - }; + if (frag) + frag.appendChild(n); - if (bookmark.start) { - if (bookmark.start.ctrl) { - rng = body.createControlRange(); - rng.addElement(resolveIndexes(bookmark.start.indexes)); - rng.select(); - } else { - rng = body.createTextRange(); - setBookmarkEndPoint(true); - setBookmarkEndPoint(); - rng.select(); - } + sibling = nextSibling; + --cnt; } - }; - this.addRange = function(rng) { - var ieRng, ctrlRng, startContainer, startOffset, endContainer, endOffset, doc = selection.dom.doc, body = doc.body; + n = _traverseRightBoundary(endAncestor, how); - function setEndPoint(start) { - var container, offset, marker, tmpRng, nodes; + if (frag) + frag.appendChild(n); - marker = dom.create('a'); - container = start ? startContainer : endContainer; - offset = start ? startOffset : endOffset; - tmpRng = ieRng.duplicate(); + if (how != CLONE) { + t.setStartAfter(startAncestor); + t.collapse(TRUE); + } - if (container == doc || container == doc.documentElement) { - container = body; - offset = 0; - } + return frag; + }; - 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 { - nodes = container.childNodes; + function _traverseRightBoundary(root, how) { + var next = _getSelectedNode(t[END_CONTAINER], t[END_OFFSET] - 1), parent, clonedParent, prevSibling, clonedChild, clonedGrandParent, isFullySelected = next != t[END_CONTAINER]; - if (nodes.length) { - if (offset >= nodes.length) { - dom.insertAfter(marker, nodes[nodes.length - 1]); - } else { - container.insertBefore(marker, nodes[offset]); - } + if (next == root) + return _traverseNode(next, isFullySelected, FALSE, how); - tmpRng.moveToElementText(marker); - } else { - // Empty node selection for example
    |
    - marker = doc.createTextNode('\uFEFF'); - container.appendChild(marker); - tmpRng.moveToElementText(marker.parentNode); - tmpRng.collapse(TRUE); - } + parent = next.parentNode; + clonedParent = _traverseNode(parent, FALSE, FALSE, how); - ieRng.setEndPoint(start ? 'StartToStart' : 'EndToEnd', tmpRng); - dom.remove(marker); - } - } + while (parent) { + while (next) { + prevSibling = next.previousSibling; + clonedChild = _traverseNode(next, isFullySelected, FALSE, how); - // Setup some shorter versions - startContainer = rng.startContainer; - startOffset = rng.startOffset; - endContainer = rng.endContainer; - endOffset = rng.endOffset; - ieRng = body.createTextRange(); + if (how != DELETE) + clonedParent.insertBefore(clonedChild, clonedParent.firstChild); - // 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(); - return; - } catch (ex) { - // Ignore - } + isFullySelected = TRUE; + next = prevSibling; } - } - // Set start/end point of selection - setEndPoint(true); - setEndPoint(); + if (parent == root) + return clonedParent; - // Select the new range and scroll it into view - ieRng.select(); - }; + next = parent.previousSibling; + parent = parent.parentNode; - // Expose range method - this.getRangeAt = getRange; - }; + clonedGrandParent = _traverseNode(parent, FALSE, FALSE, how); - // Expose the selection object - tinymce.dom.TridentSelection = Selection; -})(); + if (how != DELETE) + clonedGrandParent.appendChild(clonedParent); + clonedParent = clonedGrandParent; + } + }; -/* - * Sizzle CSS Selector Engine - v1.0 - * Copyright 2009, The Dojo Foundation - * Released under the MIT, BSD, and GPL Licenses. - * More information: http://sizzlejs.com/ - */ -(function(){ + function _traverseLeftBoundary(root, how) { + var next = _getSelectedNode(t[START_CONTAINER], t[START_OFFSET]), isFullySelected = next != t[START_CONTAINER], parent, clonedParent, nextSibling, clonedChild, clonedGrandParent; -var chunker = /((?:\((?:\([^()]+\)|[^()]+)+\)|\[(?:\[[^\[\]]*\]|['"][^'"]*['"]|[^\[\]'"]+)+\]|\\.|[^ >+~,(\[\\]+)+|[>+~])(\s*,\s*)?((?:.|\r|\n)*)/g, - done = 0, - toString = Object.prototype.toString, - hasDuplicate = false, - baseHasDuplicate = true; + if (next == root) + return _traverseNode(next, isFullySelected, TRUE, how); -// Here we check if the JavaScript engine is using some sort of -// optimization where it does not always call our comparision -// function. If that is the case, discard the hasDuplicate value. -// Thus far that includes Google Chrome. -[0, 0].sort(function(){ - baseHasDuplicate = false; - return 0; -}); + parent = next.parentNode; + clonedParent = _traverseNode(parent, FALSE, TRUE, how); -var Sizzle = function(selector, context, results, seed) { - results = results || []; - context = context || document; + while (parent) { + while (next) { + nextSibling = next.nextSibling; + clonedChild = _traverseNode(next, isFullySelected, TRUE, how); - var origContext = context; + if (how != DELETE) + clonedParent.appendChild(clonedChild); - if ( context.nodeType !== 1 && context.nodeType !== 9 ) { - return []; - } - - if ( !selector || typeof selector !== "string" ) { - return results; - } + isFullySelected = TRUE; + next = nextSibling; + } - var parts = [], m, set, checkSet, extra, prune = true, contextXML = Sizzle.isXML(context), - soFar = selector, ret, cur, pop, i; - - // Reset the position of the chunker regexp (start from head) - do { - chunker.exec(""); - m = chunker.exec(soFar); + if (parent == root) + return clonedParent; - if ( m ) { - soFar = m[3]; - - parts.push( m[1] ); - - if ( m[2] ) { - extra = m[3]; - break; - } - } - } while ( m ); + next = parent.nextSibling; + parent = parent.parentNode; - if ( parts.length > 1 && origPOS.exec( selector ) ) { - if ( parts.length === 2 && Expr.relative[ parts[0] ] ) { - set = posProcess( parts[0] + parts[1], context ); - } else { - set = Expr.relative[ parts[0] ] ? - [ context ] : - Sizzle( parts.shift(), context ); + clonedGrandParent = _traverseNode(parent, FALSE, TRUE, how); - while ( parts.length ) { - selector = parts.shift(); + if (how != DELETE) + clonedGrandParent.appendChild(clonedParent); - if ( Expr.relative[ selector ] ) { - selector += parts.shift(); - } - - set = posProcess( selector, set ); + clonedParent = clonedGrandParent; } - } - } else { - // Take a shortcut and set the context if the root selector is an ID - // (but not if it'll be faster if the inner selector is an ID) - if ( !seed && parts.length > 1 && context.nodeType === 9 && !contextXML && - Expr.match.ID.test(parts[0]) && !Expr.match.ID.test(parts[parts.length - 1]) ) { - ret = Sizzle.find( parts.shift(), context, contextXML ); - context = ret.expr ? Sizzle.filter( ret.expr, ret.set )[0] : ret.set[0]; - } + }; - if ( context ) { - ret = seed ? - { expr: parts.pop(), set: makeArray(seed) } : - Sizzle.find( parts.pop(), parts.length === 1 && (parts[0] === "~" || parts[0] === "+") && context.parentNode ? context.parentNode : context, contextXML ); - set = ret.expr ? Sizzle.filter( ret.expr, ret.set ) : ret.set; + function _traverseNode(n, isFullySelected, isLeft, how) { + var txtValue, newNodeValue, oldNodeValue, offset, newNode; - if ( parts.length > 0 ) { - checkSet = makeArray(set); - } else { - prune = false; - } + if (isFullySelected) + return _traverseFullySelected(n, how); - while ( parts.length ) { - cur = parts.pop(); - pop = cur; + if (n.nodeType == 3 /* TEXT_NODE */) { + txtValue = n.nodeValue; - if ( !Expr.relative[ cur ] ) { - cur = ""; + if (isLeft) { + offset = t[START_OFFSET]; + newNodeValue = txtValue.substring(offset); + oldNodeValue = txtValue.substring(0, offset); } else { - pop = parts.pop(); + offset = t[END_OFFSET]; + newNodeValue = txtValue.substring(0, offset); + oldNodeValue = txtValue.substring(offset); } - if ( pop == null ) { - pop = context; - } + if (how != CLONE) + n.nodeValue = oldNodeValue; - Expr.relative[ cur ]( checkSet, pop, contextXML ); - } - } else { - checkSet = parts = []; - } - } + if (how == DELETE) + return; - if ( !checkSet ) { - checkSet = set; - } + newNode = dom.clone(n, FALSE); + newNode.nodeValue = newNodeValue; - if ( !checkSet ) { - Sizzle.error( cur || selector ); - } - - if ( toString.call(checkSet) === "[object Array]" ) { - if ( !prune ) { - results.push.apply( results, checkSet ); - } else if ( context && context.nodeType === 1 ) { - for ( i = 0; checkSet[i] != null; i++ ) { - if ( checkSet[i] && (checkSet[i] === true || checkSet[i].nodeType === 1 && Sizzle.contains(context, checkSet[i])) ) { - results.push( set[i] ); - } - } - } else { - for ( i = 0; checkSet[i] != null; i++ ) { - if ( checkSet[i] && checkSet[i].nodeType === 1 ) { - results.push( set[i] ); - } + return newNode; } - } - } else { - makeArray( checkSet, results ); - } - if ( extra ) { - Sizzle( extra, origContext, results, seed ); - Sizzle.uniqueSort( results ); - } + if (how == DELETE) + return; - return results; -}; + return dom.clone(n, FALSE); + }; -Sizzle.uniqueSort = function(results){ - if ( sortOrder ) { - hasDuplicate = baseHasDuplicate; - results.sort(sortOrder); + function _traverseFullySelected(n, how) { + if (how != DELETE) + return how == CLONE ? dom.clone(n, TRUE) : n; - if ( hasDuplicate ) { - for ( var i = 1; i < results.length; i++ ) { - if ( results[i] === results[i-1] ) { - results.splice(i--, 1); - } - } - } - } + n.parentNode.removeChild(n); + }; - return results; -}; + function toStringIE() { + return dom.create('body', null, cloneContents()).outerText; + } + + return t; + }; -Sizzle.matches = function(expr, set){ - return Sizzle(expr, null, null, set); -}; + ns.Range = Range; -Sizzle.find = function(expr, context, isXML){ - var set; + // Older IE versions doesn't let you override toString by it's constructor so we have to stick it in the prototype + Range.prototype.toString = function() { + return this.toStringIE(); + }; +})(tinymce.dom); - if ( !expr ) { - return []; - } +(function() { + function Selection(selection) { + var self = this, dom = selection.dom, TRUE = true, FALSE = false; - for ( var i = 0, l = Expr.order.length; i < l; i++ ) { - var type = Expr.order[i], match; - - if ( (match = Expr.leftMatch[ type ].exec( expr )) ) { - var left = match[1]; - match.splice(1,1); + function getPosition(rng, start) { + var checkRng, startIndex = 0, endIndex, inside, + children, child, offset, index, position = -1, parent; - if ( left.substr( left.length - 1 ) !== "\\" ) { - match[1] = (match[1] || "").replace(/\\/g, ""); - set = Expr.find[ type ]( match, context, isXML ); - if ( set != null ) { - expr = expr.replace( Expr.match[ type ], "" ); - break; - } - } - } - } + // Setup test range, collapse it and get the parent + checkRng = rng.duplicate(); + checkRng.collapse(start); + parent = checkRng.parentElement(); - if ( !set ) { - set = context.getElementsByTagName("*"); - } + // Check if the selection is within the right document + if (parent.ownerDocument !== selection.dom.doc) + return; - return {set: set, expr: expr}; -}; + // IE will report non editable elements as it's parent so look for an editable one + while (parent.contentEditable === "false") { + parent = parent.parentNode; + } -Sizzle.filter = function(expr, set, inplace, not){ - var old = expr, result = [], curLoop = set, match, anyFound, - isXMLFilter = set && set[0] && Sizzle.isXML(set[0]); + // If parent doesn't have any children then return that we are inside the element + if (!parent.hasChildNodes()) { + return {node : parent, inside : 1}; + } - while ( expr && set.length ) { - for ( var type in Expr.filter ) { - if ( (match = Expr.leftMatch[ type ].exec( expr )) != null && match[2] ) { - var filter = Expr.filter[ type ], found, item, left = match[1]; - anyFound = false; + // Setup node list and endIndex + children = parent.children; + endIndex = children.length - 1; - match.splice(1,1); + // Perform a binary search for the position + while (startIndex <= endIndex) { + index = Math.floor((startIndex + endIndex) / 2); - if ( left.substr( left.length - 1 ) === "\\" ) { - continue; - } + // Move selection to node and compare the ranges + child = children[index]; + checkRng.moveToElementText(child); + position = checkRng.compareEndPoints(start ? 'StartToStart' : 'EndToEnd', rng); - if ( curLoop === result ) { - result = []; + // Before/after or an exact match + if (position > 0) { + endIndex = index - 1; + } else if (position < 0) { + startIndex = index + 1; + } else { + return {node : child}; } + } - if ( Expr.preFilter[ type ] ) { - match = Expr.preFilter[ type ]( match, curLoop, inplace, result, not, isXMLFilter ); + // Check if child position is before or we didn't find a position + if (position < 0) { + // No element child was found use the parent element and the offset inside that + if (!child) { + checkRng.moveToElementText(parent); + checkRng.collapse(true); + child = parent; + inside = true; + } else + checkRng.collapse(false); - if ( !match ) { - anyFound = found = true; - } else if ( match === true ) { - continue; + // Walk character by character in text node until we hit the selected range endpoint, hit the end of document or parent isn't the right one + // We need to walk char by char since rng.text or rng.htmlText will trim line endings + offset = 0; + while (checkRng.compareEndPoints(start ? 'StartToStart' : 'StartToEnd', rng) !== 0) { + if (checkRng.move('character', 1) === 0 || parent != checkRng.parentElement()) { + break; } - } - if ( match ) { - for ( var i = 0; (item = curLoop[i]) != null; i++ ) { - if ( item ) { - found = filter( item, match, i, curLoop ); - var pass = not ^ !!found; - - if ( inplace && found != null ) { - if ( pass ) { - anyFound = true; - } else { - curLoop[i] = false; - } - } else if ( pass ) { - result.push( item ); - anyFound = true; - } - } - } + offset++; } + } else { + // Child position is after the selection endpoint + checkRng.collapse(true); - if ( found !== undefined ) { - if ( !inplace ) { - curLoop = result; - } - - expr = expr.replace( Expr.match[ type ], "" ); - - if ( !anyFound ) { - return []; + // Walk character by character in text node until we hit the selected range endpoint, hit the end of document or parent isn't the right one + offset = 0; + while (checkRng.compareEndPoints(start ? 'StartToStart' : 'StartToEnd', rng) !== 0) { + if (checkRng.move('character', -1) === 0 || parent != checkRng.parentElement()) { + break; } - break; + offset++; } } - } - // Improper expression - if ( expr === old ) { - if ( anyFound == null ) { - Sizzle.error( expr ); - } else { - break; - } - } + return {node : child, position : position, offset : offset, inside : inside}; + }; - old = expr; - } + // Returns a W3C DOM compatible range object by using the IE Range API + function getRange() { + var ieRange = selection.getRng(), domRange = dom.createRng(), element, collapsed, tmpRange, element2, bookmark, fail; - return curLoop; -}; + // If selection is outside the current document just return an empty range + element = ieRange.item ? ieRange.item(0) : ieRange.parentElement(); + if (element.ownerDocument != dom.doc) + return domRange; -Sizzle.error = function( msg ) { - throw "Syntax error, unrecognized expression: " + msg; -}; + collapsed = selection.isCollapsed(); -var Expr = Sizzle.selectors = { - order: [ "ID", "NAME", "TAG" ], - match: { - ID: /#((?:[\w\u00c0-\uFFFF\-]|\\.)+)/, - CLASS: /\.((?:[\w\u00c0-\uFFFF\-]|\\.)+)/, - NAME: /\[name=['"]*((?:[\w\u00c0-\uFFFF\-]|\\.)+)['"]*\]/, - ATTR: /\[\s*((?:[\w\u00c0-\uFFFF\-]|\\.)+)\s*(?:(\S?=)\s*(['"]*)(.*?)\3|)\s*\]/, - TAG: /^((?:[\w\u00c0-\uFFFF\*\-]|\\.)+)/, - CHILD: /:(only|nth|last|first)-child(?:\((even|odd|[\dn+\-]*)\))?/, - POS: /:(nth|eq|gt|lt|first|last|even|odd)(?:\((\d*)\))?(?=[^\-]|$)/, - PSEUDO: /:((?:[\w\u00c0-\uFFFF\-]|\\.)+)(?:\((['"]?)((?:\([^\)]+\)|[^\(\)]*)+)\2\))?/ - }, - leftMatch: {}, - attrMap: { - "class": "className", - "for": "htmlFor" - }, - attrHandle: { - href: function(elem){ - return elem.getAttribute("href"); - } - }, - relative: { - "+": function(checkSet, part){ - var isPartStr = typeof part === "string", - isTag = isPartStr && !/\W/.test(part), - isPartStrNotTag = isPartStr && !isTag; + // Handle control selection + if (ieRange.item) { + domRange.setStart(element.parentNode, dom.nodeIndex(element)); + domRange.setEnd(domRange.startContainer, domRange.startOffset + 1); - if ( isTag ) { - part = part.toLowerCase(); + return domRange; } - for ( var i = 0, l = checkSet.length, elem; i < l; i++ ) { - if ( (elem = checkSet[i]) ) { - while ( (elem = elem.previousSibling) && elem.nodeType !== 1 ) {} + function findEndPoint(start) { + var endPoint = getPosition(ieRange, start), container, offset, textNodeOffset = 0, sibling, undef, nodeValue; - checkSet[i] = isPartStrNotTag || elem && elem.nodeName.toLowerCase() === part ? - elem || false : - elem === part; + container = endPoint.node; + offset = endPoint.offset; + + if (endPoint.inside && !container.hasChildNodes()) { + domRange[start ? 'setStart' : 'setEnd'](container, 0); + return; } - } - if ( isPartStrNotTag ) { - Sizzle.filter( part, checkSet, true ); - } - }, - ">": function(checkSet, part){ - var isPartStr = typeof part === "string", - elem, i = 0, l = checkSet.length; + if (offset === undef) { + domRange[start ? 'setStartBefore' : 'setEndAfter'](container); + return; + } - if ( isPartStr && !/\W/.test(part) ) { - part = part.toLowerCase(); + if (endPoint.position < 0) { + sibling = endPoint.inside ? container.firstChild : container.nextSibling; - for ( ; i < l; i++ ) { - elem = checkSet[i]; - if ( elem ) { - var parent = elem.parentNode; - checkSet[i] = parent.nodeName.toLowerCase() === part ? parent : false; + if (!sibling) { + domRange[start ? 'setStartAfter' : 'setEndAfter'](container); + return; } - } + + if (!offset) { + if (sibling.nodeType == 3) + domRange[start ? 'setStart' : 'setEnd'](sibling, 0); + else + domRange[start ? 'setStartBefore' : 'setEndBefore'](sibling); + + return; + } + + // Find the text node and offset + while (sibling) { + nodeValue = sibling.nodeValue; + textNodeOffset += nodeValue.length; + + // We are at or passed the position we where looking for + if (textNodeOffset >= offset) { + container = sibling; + textNodeOffset -= offset; + textNodeOffset = nodeValue.length - textNodeOffset; + break; + } + + sibling = sibling.nextSibling; + } + } else { + // Find the text node and offset + sibling = container.previousSibling; + + if (!sibling) + return domRange[start ? 'setStartBefore' : 'setEndBefore'](container); + + // If there isn't any text to loop then use the first position + if (!offset) { + if (container.nodeType == 3) + domRange[start ? 'setStart' : 'setEnd'](sibling, container.nodeValue.length); + else + domRange[start ? 'setStartAfter' : 'setEndAfter'](sibling); + + return; + } + + while (sibling) { + textNodeOffset += sibling.nodeValue.length; + + // We are at or passed the position we where looking for + if (textNodeOffset >= offset) { + container = sibling; + textNodeOffset -= offset; + break; + } + + sibling = sibling.previousSibling; + } + } + + domRange[start ? 'setStart' : 'setEnd'](container, textNodeOffset); + }; + + try { + // Find start point + findEndPoint(true); + + // Find end point if needed + if (!collapsed) + findEndPoint(); + } catch (ex) { + // IE has a nasty bug where text nodes might throw "invalid argument" when you + // access the nodeValue or other properties of text nodes. This seems to happend when + // text nodes are split into two nodes by a delete/backspace call. So lets detect it and try to fix it. + if (ex.number == -2147024809) { + // Get the current selection + bookmark = self.getBookmark(2); + + // Get start element + tmpRange = ieRange.duplicate(); + tmpRange.collapse(true); + element = tmpRange.parentElement(); + + // Get end element + if (!collapsed) { + tmpRange = ieRange.duplicate(); + tmpRange.collapse(false); + element2 = tmpRange.parentElement(); + element2.innerHTML = element2.innerHTML; + } + + // Remove the broken elements + element.innerHTML = element.innerHTML; + + // Restore the selection + self.moveToBookmark(bookmark); + + // Since the range has moved we need to re-get it + ieRange = selection.getRng(); + + // Find start point + findEndPoint(true); + + // Find end point if needed + if (!collapsed) + findEndPoint(); + } else + throw ex; // Throw other errors + } + + return domRange; + }; + + this.getBookmark = function(type) { + var rng = selection.getRng(), start, end, bookmark = {}; + + function getIndexes(node) { + var parent, root, children, i, indexes = []; + + parent = node.parentNode; + root = dom.getRoot().parentNode; + + while (parent != root && parent.nodeType !== 9) { + children = parent.children; + + i = children.length; + while (i--) { + if (node === children[i]) { + indexes.push(i); + break; + } + } + + node = parent; + parent = parent.parentNode; + } + + return indexes; + }; + + function getBookmarkEndPoint(start) { + var position; + + position = getPosition(rng, start); + if (position) { + return { + position : position.position, + offset : position.offset, + indexes : getIndexes(position.node), + inside : position.inside + }; + } + }; + + // Non ubstructive bookmark + if (type === 2) { + // Handle text selection + if (!rng.item) { + bookmark.start = getBookmarkEndPoint(true); + + if (!selection.isCollapsed()) + bookmark.end = getBookmarkEndPoint(); + } else + bookmark.start = {ctrl : true, indexes : getIndexes(rng.item(0))}; + } + + return bookmark; + }; + + this.moveToBookmark = function(bookmark) { + var rng, body = dom.doc.body; + + function resolveIndexes(indexes) { + var node, i, idx, children; + + node = dom.getRoot(); + for (i = indexes.length - 1; i >= 0; i--) { + children = node.children; + idx = indexes[i]; + + if (idx <= children.length - 1) { + node = children[idx]; + } + } + + return node; + }; + + function setBookmarkEndPoint(start) { + var endPoint = bookmark[start ? 'start' : 'end'], moveLeft, moveRng, undef; + + if (endPoint) { + moveLeft = endPoint.position > 0; + + moveRng = body.createTextRange(); + moveRng.moveToElementText(resolveIndexes(endPoint.indexes)); + + offset = endPoint.offset; + if (offset !== undef) { + moveRng.collapse(endPoint.inside || moveLeft); + moveRng.moveStart('character', moveLeft ? -offset : offset); + } else + moveRng.collapse(start); + + rng.setEndPoint(start ? 'StartToStart' : 'EndToStart', moveRng); + + if (start) + rng.collapse(true); + } + }; + + if (bookmark.start) { + if (bookmark.start.ctrl) { + rng = body.createControlRange(); + rng.addElement(resolveIndexes(bookmark.start.indexes)); + rng.select(); + } else { + rng = body.createTextRange(); + setBookmarkEndPoint(true); + setBookmarkEndPoint(); + rng.select(); + } + } + }; + + this.addRange = function(rng) { + var ieRng, ctrlRng, startContainer, startOffset, endContainer, endOffset, sibling, + doc = selection.dom.doc, body = doc.body, nativeRng, ctrlElm; + + function setEndPoint(start) { + var container, offset, marker, tmpRng, nodes; + + marker = dom.create('a'); + container = start ? startContainer : endContainer; + offset = start ? startOffset : endOffset; + tmpRng = ieRng.duplicate(); + + if (container == doc || container == doc.documentElement) { + container = body; + offset = 0; + } + + 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 { + nodes = container.childNodes; + + if (nodes.length) { + if (offset >= nodes.length) { + dom.insertAfter(marker, nodes[nodes.length - 1]); + } else { + container.insertBefore(marker, nodes[offset]); + } + + tmpRng.moveToElementText(marker); + } else if (container.canHaveHTML) { + // Empty node selection for example
    |
    + // Setting innerHTML with a span marker then remove that marker seems to keep empty block elements open + container.innerHTML = '\uFEFF'; + marker = container.firstChild; + tmpRng.moveToElementText(marker); + tmpRng.collapse(FALSE); // Collapse false works better than true for some odd reason + } + + ieRng.setEndPoint(start ? 'StartToStart' : 'EndToEnd', tmpRng); + dom.remove(marker); + } + } + + // Setup some shorter versions + startContainer = rng.startContainer; + startOffset = rng.startOffset; + endContainer = rng.endContainer; + endOffset = rng.endOffset; + ieRng = body.createTextRange(); + + // If single element selection then try making a control selection out of it + if (startContainer == endContainer && startContainer.nodeType == 1) { + // Trick to place the caret inside an empty block element like

    + if (startOffset == endOffset && !startContainer.hasChildNodes()) { + if (startContainer.canHaveHTML) { + // Check if previous sibling is an empty block if it is then we need to render it + // IE would otherwise move the caret into the sibling instead of the empty startContainer see: #5236 + // Example this:

    |

    would become this:

    |

    + sibling = startContainer.previousSibling; + if (sibling && !sibling.hasChildNodes() && dom.isBlock(sibling)) { + sibling.innerHTML = '\uFEFF'; + } else { + sibling = null; + } + + startContainer.innerHTML = '\uFEFF\uFEFF'; + ieRng.moveToElementText(startContainer.lastChild); + ieRng.select(); + dom.doc.selection.clear(); + startContainer.innerHTML = ''; + + if (sibling) { + sibling.innerHTML = ''; + } + return; + } else { + startOffset = dom.nodeIndex(startContainer); + startContainer = startContainer.parentNode; + } + } + + if (startOffset == endOffset - 1) { + try { + ctrlElm = startContainer.childNodes[startOffset]; + ctrlRng = body.createControlRange(); + ctrlRng.addElement(ctrlElm); + ctrlRng.select(); + + // Check if the range produced is on the correct element and is a control range + // On IE 8 it will select the parent contentEditable container if you select an inner element see: #5398 + nativeRng = selection.getRng(); + if (nativeRng.item && ctrlElm === nativeRng.item(0)) { + return; + } + } catch (ex) { + // Ignore + } + } + } + + // Set start/end point of selection + setEndPoint(true); + setEndPoint(); + + // Select the new range and scroll it into view + ieRng.select(); + }; + + // Expose range method + this.getRangeAt = getRange; + }; + + // Expose the selection object + tinymce.dom.TridentSelection = Selection; +})(); + + +/* + * Sizzle CSS Selector Engine + * Copyright, The Dojo Foundation + * Released under the MIT, BSD, and GPL Licenses. + * More information: http://sizzlejs.com/ + */ +(function(){ + +var chunker = /((?:\((?:\([^()]+\)|[^()]+)+\)|\[(?:\[[^\[\]]*\]|['"][^'"]*['"]|[^\[\]'"]+)+\]|\\.|[^ >+~,(\[\\]+)+|[>+~])(\s*,\s*)?((?:.|\r|\n)*)/g, + expando = "sizcache", + done = 0, + toString = Object.prototype.toString, + hasDuplicate = false, + baseHasDuplicate = true, + rBackslash = /\\/g, + rReturn = /\r\n/g, + rNonWord = /\W/; + +// Here we check if the JavaScript engine is using some sort of +// optimization where it does not always call our comparision +// function. If that is the case, discard the hasDuplicate value. +// Thus far that includes Google Chrome. +[0, 0].sort(function() { + baseHasDuplicate = false; + return 0; +}); + +var Sizzle = function( selector, context, results, seed ) { + results = results || []; + context = context || document; + + var origContext = context; + + if ( context.nodeType !== 1 && context.nodeType !== 9 ) { + return []; + } + + if ( !selector || typeof selector !== "string" ) { + return results; + } + + var m, set, checkSet, extra, ret, cur, pop, i, + prune = true, + contextXML = Sizzle.isXML( context ), + parts = [], + soFar = selector; + + // Reset the position of the chunker regexp (start from head) + do { + chunker.exec( "" ); + m = chunker.exec( soFar ); + + if ( m ) { + soFar = m[3]; + + parts.push( m[1] ); + + if ( m[2] ) { + extra = m[3]; + break; + } + } + } while ( m ); + + if ( parts.length > 1 && origPOS.exec( selector ) ) { + + if ( parts.length === 2 && Expr.relative[ parts[0] ] ) { + set = posProcess( parts[0] + parts[1], context, seed ); + + } else { + set = Expr.relative[ parts[0] ] ? + [ context ] : + Sizzle( parts.shift(), context ); + + while ( parts.length ) { + selector = parts.shift(); + + if ( Expr.relative[ selector ] ) { + selector += parts.shift(); + } + + set = posProcess( selector, set, seed ); + } + } + + } else { + // Take a shortcut and set the context if the root selector is an ID + // (but not if it'll be faster if the inner selector is an ID) + if ( !seed && parts.length > 1 && context.nodeType === 9 && !contextXML && + Expr.match.ID.test(parts[0]) && !Expr.match.ID.test(parts[parts.length - 1]) ) { + + ret = Sizzle.find( parts.shift(), context, contextXML ); + context = ret.expr ? + Sizzle.filter( ret.expr, ret.set )[0] : + ret.set[0]; + } + + if ( context ) { + ret = seed ? + { expr: parts.pop(), set: makeArray(seed) } : + Sizzle.find( parts.pop(), parts.length === 1 && (parts[0] === "~" || parts[0] === "+") && context.parentNode ? context.parentNode : context, contextXML ); + + set = ret.expr ? + Sizzle.filter( ret.expr, ret.set ) : + ret.set; + + if ( parts.length > 0 ) { + checkSet = makeArray( set ); + + } else { + prune = false; + } + + while ( parts.length ) { + cur = parts.pop(); + pop = cur; + + if ( !Expr.relative[ cur ] ) { + cur = ""; + } else { + pop = parts.pop(); + } + + if ( pop == null ) { + pop = context; + } + + Expr.relative[ cur ]( checkSet, pop, contextXML ); + } + + } else { + checkSet = parts = []; + } + } + + if ( !checkSet ) { + checkSet = set; + } + + if ( !checkSet ) { + Sizzle.error( cur || selector ); + } + + if ( toString.call(checkSet) === "[object Array]" ) { + if ( !prune ) { + results.push.apply( results, checkSet ); + + } else if ( context && context.nodeType === 1 ) { + for ( i = 0; checkSet[i] != null; i++ ) { + if ( checkSet[i] && (checkSet[i] === true || checkSet[i].nodeType === 1 && Sizzle.contains(context, checkSet[i])) ) { + results.push( set[i] ); + } + } + + } else { + for ( i = 0; checkSet[i] != null; i++ ) { + if ( checkSet[i] && checkSet[i].nodeType === 1 ) { + results.push( set[i] ); + } + } + } + + } else { + makeArray( checkSet, results ); + } + + if ( extra ) { + Sizzle( extra, origContext, results, seed ); + Sizzle.uniqueSort( results ); + } + + return results; +}; + +Sizzle.uniqueSort = function( results ) { + if ( sortOrder ) { + hasDuplicate = baseHasDuplicate; + results.sort( sortOrder ); + + if ( hasDuplicate ) { + for ( var i = 1; i < results.length; i++ ) { + if ( results[i] === results[ i - 1 ] ) { + results.splice( i--, 1 ); + } + } + } + } + + return results; +}; + +Sizzle.matches = function( expr, set ) { + return Sizzle( expr, null, null, set ); +}; + +Sizzle.matchesSelector = function( node, expr ) { + return Sizzle( expr, null, null, [node] ).length > 0; +}; + +Sizzle.find = function( expr, context, isXML ) { + var set, i, len, match, type, left; + + if ( !expr ) { + return []; + } + + for ( i = 0, len = Expr.order.length; i < len; i++ ) { + type = Expr.order[i]; + + if ( (match = Expr.leftMatch[ type ].exec( expr )) ) { + left = match[1]; + match.splice( 1, 1 ); + + if ( left.substr( left.length - 1 ) !== "\\" ) { + match[1] = (match[1] || "").replace( rBackslash, "" ); + set = Expr.find[ type ]( match, context, isXML ); + + if ( set != null ) { + expr = expr.replace( Expr.match[ type ], "" ); + break; + } + } + } + } + + if ( !set ) { + set = typeof context.getElementsByTagName !== "undefined" ? + context.getElementsByTagName( "*" ) : + []; + } + + return { set: set, expr: expr }; +}; + +Sizzle.filter = function( expr, set, inplace, not ) { + var match, anyFound, + type, found, item, filter, left, + i, pass, + old = expr, + result = [], + curLoop = set, + isXMLFilter = set && set[0] && Sizzle.isXML( set[0] ); + + while ( expr && set.length ) { + for ( type in Expr.filter ) { + if ( (match = Expr.leftMatch[ type ].exec( expr )) != null && match[2] ) { + filter = Expr.filter[ type ]; + left = match[1]; + + anyFound = false; + + match.splice(1,1); + + if ( left.substr( left.length - 1 ) === "\\" ) { + continue; + } + + if ( curLoop === result ) { + result = []; + } + + if ( Expr.preFilter[ type ] ) { + match = Expr.preFilter[ type ]( match, curLoop, inplace, result, not, isXMLFilter ); + + if ( !match ) { + anyFound = found = true; + + } else if ( match === true ) { + continue; + } + } + + if ( match ) { + for ( i = 0; (item = curLoop[i]) != null; i++ ) { + if ( item ) { + found = filter( item, match, i, curLoop ); + pass = not ^ found; + + if ( inplace && found != null ) { + if ( pass ) { + anyFound = true; + + } else { + curLoop[i] = false; + } + + } else if ( pass ) { + result.push( item ); + anyFound = true; + } + } + } + } + + if ( found !== undefined ) { + if ( !inplace ) { + curLoop = result; + } + + expr = expr.replace( Expr.match[ type ], "" ); + + if ( !anyFound ) { + return []; + } + + break; + } + } + } + + // Improper expression + if ( expr === old ) { + if ( anyFound == null ) { + Sizzle.error( expr ); + + } else { + break; + } + } + + old = expr; + } + + return curLoop; +}; + +Sizzle.error = function( msg ) { + throw new Error( "Syntax error, unrecognized expression: " + msg ); +}; + +var getText = Sizzle.getText = function( elem ) { + var i, node, + nodeType = elem.nodeType, + ret = ""; + + if ( nodeType ) { + if ( nodeType === 1 || nodeType === 9 || nodeType === 11 ) { + // Use textContent || innerText for elements + if ( typeof elem.textContent === 'string' ) { + return elem.textContent; + } else if ( typeof elem.innerText === 'string' ) { + // Replace IE's carriage returns + return elem.innerText.replace( rReturn, '' ); + } else { + // Traverse it's children + for ( elem = elem.firstChild; elem; elem = elem.nextSibling) { + ret += getText( elem ); + } + } + } else if ( nodeType === 3 || nodeType === 4 ) { + return elem.nodeValue; + } + } else { + + // If no nodeType, this is expected to be an array + for ( i = 0; (node = elem[i]); i++ ) { + // Do not traverse comment nodes + if ( node.nodeType !== 8 ) { + ret += getText( node ); + } + } + } + return ret; +}; + +var Expr = Sizzle.selectors = { + order: [ "ID", "NAME", "TAG" ], + + match: { + ID: /#((?:[\w\u00c0-\uFFFF\-]|\\.)+)/, + CLASS: /\.((?:[\w\u00c0-\uFFFF\-]|\\.)+)/, + NAME: /\[name=['"]*((?:[\w\u00c0-\uFFFF\-]|\\.)+)['"]*\]/, + ATTR: /\[\s*((?:[\w\u00c0-\uFFFF\-]|\\.)+)\s*(?:(\S?=)\s*(?:(['"])(.*?)\3|(#?(?:[\w\u00c0-\uFFFF\-]|\\.)*)|)|)\s*\]/, + TAG: /^((?:[\w\u00c0-\uFFFF\*\-]|\\.)+)/, + CHILD: /:(only|nth|last|first)-child(?:\(\s*(even|odd|(?:[+\-]?\d+|(?:[+\-]?\d*)?n\s*(?:[+\-]\s*\d+)?))\s*\))?/, + POS: /:(nth|eq|gt|lt|first|last|even|odd)(?:\((\d*)\))?(?=[^\-]|$)/, + PSEUDO: /:((?:[\w\u00c0-\uFFFF\-]|\\.)+)(?:\((['"]?)((?:\([^\)]+\)|[^\(\)]*)+)\2\))?/ + }, + + leftMatch: {}, + + attrMap: { + "class": "className", + "for": "htmlFor" + }, + + attrHandle: { + href: function( elem ) { + return elem.getAttribute( "href" ); + }, + type: function( elem ) { + return elem.getAttribute( "type" ); + } + }, + + relative: { + "+": function(checkSet, part){ + var isPartStr = typeof part === "string", + isTag = isPartStr && !rNonWord.test( part ), + isPartStrNotTag = isPartStr && !isTag; + + if ( isTag ) { + part = part.toLowerCase(); + } + + for ( var i = 0, l = checkSet.length, elem; i < l; i++ ) { + if ( (elem = checkSet[i]) ) { + while ( (elem = elem.previousSibling) && elem.nodeType !== 1 ) {} + + checkSet[i] = isPartStrNotTag || elem && elem.nodeName.toLowerCase() === part ? + elem || false : + elem === part; + } + } + + if ( isPartStrNotTag ) { + Sizzle.filter( part, checkSet, true ); + } + }, + + ">": function( checkSet, part ) { + var elem, + isPartStr = typeof part === "string", + i = 0, + l = checkSet.length; + + if ( isPartStr && !rNonWord.test( part ) ) { + part = part.toLowerCase(); + + for ( ; i < l; i++ ) { + elem = checkSet[i]; + + if ( elem ) { + var parent = elem.parentNode; + checkSet[i] = parent.nodeName.toLowerCase() === part ? parent : false; + } + } + } else { for ( ; i < l; i++ ) { elem = checkSet[i]; + if ( elem ) { checkSet[i] = isPartStr ? elem.parentNode : @@ -6299,39 +8197,50 @@ var Expr = Sizzle.selectors = { } } }, + "": function(checkSet, part, isXML){ - var doneName = done++, checkFn = dirCheck, nodeCheck; + var nodeCheck, + doneName = done++, + checkFn = dirCheck; - if ( typeof part === "string" && !/\W/.test(part) ) { + if ( typeof part === "string" && !rNonWord.test( part ) ) { part = part.toLowerCase(); nodeCheck = part; checkFn = dirNodeCheck; } - checkFn("parentNode", part, doneName, checkSet, nodeCheck, isXML); + checkFn( "parentNode", part, doneName, checkSet, nodeCheck, isXML ); }, - "~": function(checkSet, part, isXML){ - var doneName = done++, checkFn = dirCheck, nodeCheck; - if ( typeof part === "string" && !/\W/.test(part) ) { + "~": function( checkSet, part, isXML ) { + var nodeCheck, + doneName = done++, + checkFn = dirCheck; + + if ( typeof part === "string" && !rNonWord.test( part ) ) { part = part.toLowerCase(); nodeCheck = part; checkFn = dirNodeCheck; } - checkFn("previousSibling", part, doneName, checkSet, nodeCheck, isXML); + checkFn( "previousSibling", part, doneName, checkSet, nodeCheck, isXML ); } }, + find: { - ID: function(match, context, isXML){ + ID: function( match, context, isXML ) { if ( typeof context.getElementById !== "undefined" && !isXML ) { var m = context.getElementById(match[1]); - return m ? [m] : []; + // Check parentNode to catch when Blackberry 4.6 returns + // nodes that are no longer in the document #6963 + return m && m.parentNode ? [m] : []; } }, - NAME: function(match, context){ + + NAME: function( match, context ) { if ( typeof context.getElementsByName !== "undefined" ) { - var ret = [], results = context.getElementsByName(match[1]); + var ret = [], + results = context.getElementsByName( match[1] ); for ( var i = 0, l = results.length; i < l; i++ ) { if ( results[i].getAttribute("name") === match[1] ) { @@ -6342,13 +8251,16 @@ var Expr = Sizzle.selectors = { return ret.length === 0 ? null : ret; } }, - TAG: function(match, context){ - return context.getElementsByTagName(match[1]); + + TAG: function( match, context ) { + if ( typeof context.getElementsByTagName !== "undefined" ) { + return context.getElementsByTagName( match[1] ); + } } }, preFilter: { - CLASS: function(match, curLoop, inplace, result, not, isXML){ - match = " " + match[1].replace(/\\/g, "") + " "; + CLASS: function( match, curLoop, inplace, result, not, isXML ) { + match = " " + match[1].replace( rBackslash, "" ) + " "; if ( isXML ) { return match; @@ -6356,10 +8268,11 @@ var Expr = Sizzle.selectors = { for ( var i = 0, elem; (elem = curLoop[i]) != null; i++ ) { if ( elem ) { - if ( not ^ (elem.className && (" " + elem.className + " ").replace(/[\t\n]/g, " ").indexOf(match) >= 0) ) { + if ( not ^ (elem.className && (" " + elem.className + " ").replace(/[\t\n\r]/g, " ").indexOf(match) >= 0) ) { if ( !inplace ) { result.push( elem ); } + } else if ( inplace ) { curLoop[i] = false; } @@ -6368,16 +8281,25 @@ var Expr = Sizzle.selectors = { return false; }, - ID: function(match){ - return match[1].replace(/\\/g, ""); + + ID: function( match ) { + return match[1].replace( rBackslash, "" ); }, - TAG: function(match, curLoop){ - return match[1].toLowerCase(); + + TAG: function( match, curLoop ) { + return match[1].replace( rBackslash, "" ).toLowerCase(); }, - CHILD: function(match){ + + CHILD: function( match ) { if ( match[1] === "nth" ) { + if ( !match[2] ) { + Sizzle.error( match[0] ); + } + + match[2] = match[2].replace(/^\+|\s*/g, ''); + // parse equations like 'even', 'odd', '5', '2n', '3n+2', '4n-1', '-n+6' - var test = /(-?)(\d*)n((?:\+|-)?\d*)/.exec( + var test = /(-?)(\d*)(?:n([+\-]?\d*))?/.exec( match[2] === "even" && "2n" || match[2] === "odd" && "2n+1" || !/\D/.test( match[2] ) && "0n+" + match[2] || match[2]); @@ -6385,141 +8307,196 @@ var Expr = Sizzle.selectors = { match[2] = (test[1] + (test[2] || 1)) - 0; match[3] = test[3] - 0; } + else if ( match[2] ) { + Sizzle.error( match[0] ); + } // TODO: Move to normal caching system match[0] = done++; return match; }, - ATTR: function(match, curLoop, inplace, result, not, isXML){ - var name = match[1].replace(/\\/g, ""); - + + ATTR: function( match, curLoop, inplace, result, not, isXML ) { + var name = match[1] = match[1].replace( rBackslash, "" ); + if ( !isXML && Expr.attrMap[name] ) { match[1] = Expr.attrMap[name]; } + // Handle if an un-quoted value was used + match[4] = ( match[4] || match[5] || "" ).replace( rBackslash, "" ); + if ( match[2] === "~=" ) { match[4] = " " + match[4] + " "; } return match; }, - PSEUDO: function(match, curLoop, inplace, result, not){ + + PSEUDO: function( match, curLoop, inplace, result, not ) { if ( match[1] === "not" ) { // If we're dealing with a complex expression, or a simple one if ( ( chunker.exec(match[3]) || "" ).length > 1 || /^\w/.test(match[3]) ) { match[3] = Sizzle(match[3], null, null, curLoop); + } else { var ret = Sizzle.filter(match[3], curLoop, inplace, true ^ not); + if ( !inplace ) { result.push.apply( result, ret ); } + return false; } + } else if ( Expr.match.POS.test( match[0] ) || Expr.match.CHILD.test( match[0] ) ) { return true; } - + return match; }, - POS: function(match){ + + POS: function( match ) { match.unshift( true ); + return match; } }, + filters: { - enabled: function(elem){ + enabled: function( elem ) { return elem.disabled === false && elem.type !== "hidden"; }, - disabled: function(elem){ + + disabled: function( elem ) { return elem.disabled === true; }, - checked: function(elem){ + + checked: function( elem ) { return elem.checked === true; }, - selected: function(elem){ + + selected: function( elem ) { // Accessing this property makes selected-by-default // options in Safari work properly - elem.parentNode.selectedIndex; + if ( elem.parentNode ) { + elem.parentNode.selectedIndex; + } + return elem.selected === true; }, - parent: function(elem){ + + parent: function( elem ) { return !!elem.firstChild; }, - empty: function(elem){ + + empty: function( elem ) { return !elem.firstChild; }, - has: function(elem, i, match){ + + has: function( elem, i, match ) { return !!Sizzle( match[3], elem ).length; }, - header: function(elem){ + + header: function( elem ) { return (/h\d/i).test( elem.nodeName ); }, - text: function(elem){ - return "text" === elem.type; + + text: function( elem ) { + var attr = elem.getAttribute( "type" ), type = elem.type; + // IE6 and 7 will map elem.type to 'text' for new HTML5 types (search, etc) + // use getAttribute instead to test this case + return elem.nodeName.toLowerCase() === "input" && "text" === type && ( attr === type || attr === null ); + }, + + radio: function( elem ) { + return elem.nodeName.toLowerCase() === "input" && "radio" === elem.type; }, - radio: function(elem){ - return "radio" === elem.type; + + checkbox: function( elem ) { + return elem.nodeName.toLowerCase() === "input" && "checkbox" === elem.type; }, - checkbox: function(elem){ - return "checkbox" === elem.type; + + file: function( elem ) { + return elem.nodeName.toLowerCase() === "input" && "file" === elem.type; }, - file: function(elem){ - return "file" === elem.type; + + password: function( elem ) { + return elem.nodeName.toLowerCase() === "input" && "password" === elem.type; }, - password: function(elem){ - return "password" === elem.type; + + submit: function( elem ) { + var name = elem.nodeName.toLowerCase(); + return (name === "input" || name === "button") && "submit" === elem.type; }, - submit: function(elem){ - return "submit" === elem.type; + + image: function( elem ) { + return elem.nodeName.toLowerCase() === "input" && "image" === elem.type; }, - image: function(elem){ - return "image" === elem.type; + + reset: function( elem ) { + var name = elem.nodeName.toLowerCase(); + return (name === "input" || name === "button") && "reset" === elem.type; }, - reset: function(elem){ - return "reset" === elem.type; + + button: function( elem ) { + var name = elem.nodeName.toLowerCase(); + return name === "input" && "button" === elem.type || name === "button"; }, - button: function(elem){ - return "button" === elem.type || elem.nodeName.toLowerCase() === "button"; + + input: function( elem ) { + return (/input|select|textarea|button/i).test( elem.nodeName ); }, - input: function(elem){ - return (/input|select|textarea|button/i).test(elem.nodeName); + + focus: function( elem ) { + return elem === elem.ownerDocument.activeElement; } }, setFilters: { - first: function(elem, i){ + first: function( elem, i ) { return i === 0; }, - last: function(elem, i, match, array){ + + last: function( elem, i, match, array ) { return i === array.length - 1; }, - even: function(elem, i){ + + even: function( elem, i ) { return i % 2 === 0; }, - odd: function(elem, i){ + + odd: function( elem, i ) { return i % 2 === 1; }, - lt: function(elem, i, match){ + + lt: function( elem, i, match ) { return i < match[3] - 0; }, - gt: function(elem, i, match){ + + gt: function( elem, i, match ) { return i > match[3] - 0; }, - nth: function(elem, i, match){ + + nth: function( elem, i, match ) { return match[3] - 0 === i; }, - eq: function(elem, i, match){ + + eq: function( elem, i, match ) { return match[3] - 0 === i; } }, filter: { - PSEUDO: function(elem, match, i, array){ - var name = match[1], filter = Expr.filters[ name ]; + PSEUDO: function( elem, match, i, array ) { + var name = match[1], + filter = Expr.filters[ name ]; if ( filter ) { return filter( elem, i, match, array ); + } else if ( name === "contains" ) { - return (elem.textContent || elem.innerText || Sizzle.getText([ elem ]) || "").indexOf(match[3]) >= 0; + return (elem.textContent || elem.innerText || getText([ elem ]) || "").indexOf(match[3]) >= 0; + } else if ( name === "not" ) { var not = match[3]; @@ -6530,72 +8507,96 @@ var Expr = Sizzle.selectors = { } return true; + } else { - Sizzle.error( "Syntax error, unrecognized expression: " + name ); + Sizzle.error( name ); } }, - CHILD: function(elem, match){ - var type = match[1], node = elem; - switch (type) { - case 'only': - case 'first': - while ( (node = node.previousSibling) ) { - if ( node.nodeType === 1 ) { - return false; + + CHILD: function( elem, match ) { + var first, last, + doneName, parent, cache, + count, diff, + type = match[1], + node = elem; + + switch ( type ) { + case "only": + case "first": + while ( (node = node.previousSibling) ) { + if ( node.nodeType === 1 ) { + return false; } } - if ( type === "first" ) { - return true; + + if ( type === "first" ) { + return true; } + node = elem; - case 'last': - while ( (node = node.nextSibling) ) { - if ( node.nodeType === 1 ) { - return false; + + /* falls through */ + case "last": + while ( (node = node.nextSibling) ) { + if ( node.nodeType === 1 ) { + return false; } } + return true; - case 'nth': - var first = match[2], last = match[3]; + + case "nth": + first = match[2]; + last = match[3]; if ( first === 1 && last === 0 ) { return true; } - - var doneName = match[0], - parent = elem.parentNode; - - if ( parent && (parent.sizcache !== doneName || !elem.nodeIndex) ) { - var count = 0; + + doneName = match[0]; + parent = elem.parentNode; + + if ( parent && (parent[ expando ] !== doneName || !elem.nodeIndex) ) { + count = 0; + for ( node = parent.firstChild; node; node = node.nextSibling ) { if ( node.nodeType === 1 ) { node.nodeIndex = ++count; } - } - parent.sizcache = doneName; + } + + parent[ expando ] = doneName; } - - var diff = elem.nodeIndex - last; + + diff = elem.nodeIndex - last; + if ( first === 0 ) { return diff === 0; + } else { return ( diff % first === 0 && diff / first >= 0 ); } } }, - ID: function(elem, match){ + + ID: function( elem, match ) { return elem.nodeType === 1 && elem.getAttribute("id") === match; }, - TAG: function(elem, match){ - return (match === "*" && elem.nodeType === 1) || elem.nodeName.toLowerCase() === match; + + TAG: function( elem, match ) { + return (match === "*" && elem.nodeType === 1) || !!elem.nodeName && elem.nodeName.toLowerCase() === match; }, - CLASS: function(elem, match){ + + CLASS: function( elem, match ) { return (" " + (elem.className || elem.getAttribute("class")) + " ") .indexOf( match ) > -1; }, - ATTR: function(elem, match){ + + ATTR: function( elem, match ) { var name = match[1], - result = Expr.attrHandle[ name ] ? + result = Sizzle.attr ? + Sizzle.attr( elem, name ) : + Expr.attrHandle[ name ] ? Expr.attrHandle[ name ]( elem ) : elem[ name ] != null ? elem[ name ] : @@ -6606,6 +8607,8 @@ var Expr = Sizzle.selectors = { return result == null ? type === "!=" : + !type && Sizzle.attr ? + result != null : type === "=" ? value === check : type === "*=" ? @@ -6624,8 +8627,10 @@ var Expr = Sizzle.selectors = { value === check || value.substr(0, check.length + 1) === check + "-" : false; }, - POS: function(elem, match, i, array){ - var name = match[2], filter = Expr.setFilters[ name ]; + + POS: function( elem, match, i, array ) { + var name = match[2], + filter = Expr.setFilters[ name ]; if ( filter ) { return filter( elem, i, match, array ); @@ -6643,15 +8648,18 @@ for ( var type in Expr.match ) { Expr.match[ type ] = new RegExp( Expr.match[ type ].source + (/(?![^\[]*\])(?![^\(]*\))/.source) ); Expr.leftMatch[ type ] = new RegExp( /(^(?:.|\r|\n)*?)/.source + Expr.match[ type ].source.replace(/\\(\d+)/g, fescape) ); } +// Expose origPOS +// "global" as in regardless of relation to brackets/parens +Expr.match.globalPOS = origPOS; -var makeArray = function(array, results) { +var makeArray = function( array, results ) { array = Array.prototype.slice.call( array, 0 ); if ( results ) { results.push.apply( results, array ); return results; } - + return array; }; @@ -6663,17 +8671,20 @@ try { Array.prototype.slice.call( document.documentElement.childNodes, 0 )[0].nodeType; // Provide a fallback method if it does not work -} catch(e){ - makeArray = function(array, results) { - var ret = results || [], i = 0; +} catch( e ) { + makeArray = function( array, results ) { + var i = 0, + ret = results || []; if ( toString.call(array) === "[object Array]" ) { Array.prototype.push.apply( ret, array ); + } else { if ( typeof array.length === "number" ) { for ( var l = array.length; i < l; i++ ) { ret.push( array[i] ); } + } else { for ( ; array[i]; i++ ) { ret.push( array[i] ); @@ -6685,110 +8696,141 @@ try { }; } -var sortOrder; +var sortOrder, siblingCheck; if ( document.documentElement.compareDocumentPosition ) { sortOrder = function( a, b ) { + if ( a === b ) { + hasDuplicate = true; + return 0; + } + if ( !a.compareDocumentPosition || !b.compareDocumentPosition ) { - if ( a == b ) { - hasDuplicate = true; - } return a.compareDocumentPosition ? -1 : 1; } - var ret = a.compareDocumentPosition(b) & 4 ? -1 : a === b ? 0 : 1; - if ( ret === 0 ) { - hasDuplicate = true; - } - return ret; + return a.compareDocumentPosition(b) & 4 ? -1 : 1; }; -} else if ( "sourceIndex" in document.documentElement ) { + +} else { sortOrder = function( a, b ) { - if ( !a.sourceIndex || !b.sourceIndex ) { - if ( a == b ) { - hasDuplicate = true; - } - return a.sourceIndex ? -1 : 1; + // The nodes are identical, we can exit early + if ( a === b ) { + hasDuplicate = true; + return 0; + + // Fallback to using sourceIndex (in IE) if it's available on both nodes + } else if ( a.sourceIndex && b.sourceIndex ) { + return a.sourceIndex - b.sourceIndex; } - var ret = a.sourceIndex - b.sourceIndex; - if ( ret === 0 ) { - hasDuplicate = true; + var al, bl, + ap = [], + bp = [], + aup = a.parentNode, + bup = b.parentNode, + cur = aup; + + // If the nodes are siblings (or identical) we can do a quick check + if ( aup === bup ) { + return siblingCheck( a, b ); + + // If no parents were found then the nodes are disconnected + } else if ( !aup ) { + return -1; + + } else if ( !bup ) { + return 1; } - return ret; - }; -} else if ( document.createRange ) { - sortOrder = function( a, b ) { - if ( !a.ownerDocument || !b.ownerDocument ) { - if ( a == b ) { - hasDuplicate = true; - } - return a.ownerDocument ? -1 : 1; + + // Otherwise they're somewhere else in the tree so we need + // to build up a full list of the parentNodes for comparison + while ( cur ) { + ap.unshift( cur ); + cur = cur.parentNode; } - var aRange = a.ownerDocument.createRange(), bRange = b.ownerDocument.createRange(); - aRange.setStart(a, 0); - aRange.setEnd(a, 0); - bRange.setStart(b, 0); - bRange.setEnd(b, 0); - var ret = aRange.compareBoundaryPoints(Range.START_TO_END, bRange); - if ( ret === 0 ) { - hasDuplicate = true; + cur = bup; + + while ( cur ) { + bp.unshift( cur ); + cur = cur.parentNode; } - return ret; + + al = ap.length; + bl = bp.length; + + // Start walking down the tree looking for a discrepancy + for ( var i = 0; i < al && i < bl; i++ ) { + if ( ap[i] !== bp[i] ) { + return siblingCheck( ap[i], bp[i] ); + } + } + + // We ended someplace up the tree so do a sibling check + return i === al ? + siblingCheck( a, bp[i], -1 ) : + siblingCheck( ap[i], b, 1 ); }; -} -// Utility function for retreiving the text value of an array of DOM nodes -Sizzle.getText = function( elems ) { - var ret = "", elem; + siblingCheck = function( a, b, ret ) { + if ( a === b ) { + return ret; + } - for ( var i = 0; elems[i]; i++ ) { - elem = elems[i]; + var cur = a.nextSibling; - // Get the text from text nodes and CDATA nodes - if ( elem.nodeType === 3 || elem.nodeType === 4 ) { - ret += elem.nodeValue; + while ( cur ) { + if ( cur === b ) { + return -1; + } - // Traverse everything else, except comment nodes - } else if ( elem.nodeType !== 8 ) { - ret += Sizzle.getText( elem.childNodes ); + cur = cur.nextSibling; } - } - return ret; -}; + return 1; + }; +} // Check to see if the browser returns elements by name when // querying by getElementById (and provide a workaround) (function(){ // We're going to inject a fake input element with a specified name var form = document.createElement("div"), - id = "script" + (new Date()).getTime(); + id = "script" + (new Date()).getTime(), + root = document.documentElement; + form.innerHTML = ""; // Inject it into the root element, check its status, and remove it quickly - var root = document.documentElement; root.insertBefore( form, root.firstChild ); // The workaround has to do additional checks after a getElementById // Which slows things down for other browsers (hence the branching) if ( document.getElementById( id ) ) { - Expr.find.ID = function(match, context, isXML){ + Expr.find.ID = function( match, context, isXML ) { if ( typeof context.getElementById !== "undefined" && !isXML ) { var m = context.getElementById(match[1]); - return m ? m.id === match[1] || typeof m.getAttributeNode !== "undefined" && m.getAttributeNode("id").nodeValue === match[1] ? [m] : undefined : []; + + return m ? + m.id === match[1] || typeof m.getAttributeNode !== "undefined" && m.getAttributeNode("id").nodeValue === match[1] ? + [m] : + undefined : + []; } }; - Expr.filter.ID = function(elem, match){ + Expr.filter.ID = function( elem, match ) { var node = typeof elem.getAttributeNode !== "undefined" && elem.getAttributeNode("id"); + return elem.nodeType === 1 && node && node.nodeValue === match; }; } root.removeChild( form ); - root = form = null; // release memory in IE + + // release memory in IE + root = form = null; })(); (function(){ @@ -6801,8 +8843,8 @@ Sizzle.getText = function( elems ) { // Make sure no comments are found if ( div.getElementsByTagName("*").length > 0 ) { - Expr.find.TAG = function(match, context){ - var results = context.getElementsByTagName(match[1]); + Expr.find.TAG = function( match, context ) { + var results = context.getElementsByTagName( match[1] ); // Filter out possible comments if ( match[1] === "*" ) { @@ -6823,19 +8865,25 @@ Sizzle.getText = function( elems ) { // Check to see if an attribute returns normalized href attributes div.innerHTML = ""; + if ( div.firstChild && typeof div.firstChild.getAttribute !== "undefined" && div.firstChild.getAttribute("href") !== "#" ) { - Expr.attrHandle.href = function(elem){ - return elem.getAttribute("href", 2); + + Expr.attrHandle.href = function( elem ) { + return elem.getAttribute( "href", 2 ); }; } - div = null; // release memory in IE + // release memory in IE + div = null; })(); if ( document.querySelectorAll ) { (function(){ - var oldSizzle = Sizzle, div = document.createElement("div"); + var oldSizzle = Sizzle, + div = document.createElement("div"), + id = "__sizzle__"; + div.innerHTML = "

    "; // Safari can't handle uppercase or unicode characters when @@ -6843,18 +8891,89 @@ if ( document.querySelectorAll ) { if ( div.querySelectorAll && div.querySelectorAll(".TEST").length === 0 ) { return; } - - Sizzle = function(query, context, extra, seed){ + + Sizzle = function( query, context, extra, seed ) { context = context || document; // Only use querySelectorAll on non-XML documents // (ID selectors don't work in non-HTML documents) - if ( !seed && context.nodeType === 9 && !Sizzle.isXML(context) ) { - try { - return makeArray( context.querySelectorAll(query), extra ); - } catch(e){} + if ( !seed && !Sizzle.isXML(context) ) { + // See if we find a selector to speed up + var match = /^(\w+$)|^\.([\w\-]+$)|^#([\w\-]+$)/.exec( query ); + + if ( match && (context.nodeType === 1 || context.nodeType === 9) ) { + // Speed-up: Sizzle("TAG") + if ( match[1] ) { + return makeArray( context.getElementsByTagName( query ), extra ); + + // Speed-up: Sizzle(".CLASS") + } else if ( match[2] && Expr.find.CLASS && context.getElementsByClassName ) { + return makeArray( context.getElementsByClassName( match[2] ), extra ); + } + } + + if ( context.nodeType === 9 ) { + // Speed-up: Sizzle("body") + // The body element only exists once, optimize finding it + if ( query === "body" && context.body ) { + return makeArray( [ context.body ], extra ); + + // Speed-up: Sizzle("#ID") + } else if ( match && match[3] ) { + var elem = context.getElementById( match[3] ); + + // Check parentNode to catch when Blackberry 4.6 returns + // nodes that are no longer in the document #6963 + if ( elem && elem.parentNode ) { + // Handle the case where IE and Opera return items + // by name instead of ID + if ( elem.id === match[3] ) { + return makeArray( [ elem ], extra ); + } + + } else { + return makeArray( [], extra ); + } + } + + try { + return makeArray( context.querySelectorAll(query), extra ); + } catch(qsaError) {} + + // qSA works strangely on Element-rooted queries + // We can work around this by specifying an extra ID on the root + // and working up from there (Thanks to Andrew Dupont for the technique) + // IE 8 doesn't work on object elements + } else if ( context.nodeType === 1 && context.nodeName.toLowerCase() !== "object" ) { + var oldContext = context, + old = context.getAttribute( "id" ), + nid = old || id, + hasParent = context.parentNode, + relativeHierarchySelector = /^\s*[+~]/.test( query ); + + if ( !old ) { + context.setAttribute( "id", nid ); + } else { + nid = nid.replace( /'/g, "\\$&" ); + } + if ( relativeHierarchySelector && hasParent ) { + context = context.parentNode; + } + + try { + if ( !relativeHierarchySelector || hasParent ) { + return makeArray( context.querySelectorAll( "[id='" + nid + "'] " + query ), extra ); + } + + } catch(pseudoError) { + } finally { + if ( !old ) { + oldContext.removeAttribute( "id" ); + } + } + } } - + return oldSizzle(query, context, extra, seed); }; @@ -6862,10 +8981,55 @@ if ( document.querySelectorAll ) { Sizzle[ prop ] = oldSizzle[ prop ]; } - div = null; // release memory in IE + // release memory in IE + div = null; })(); } +(function(){ + var html = document.documentElement, + matches = html.matchesSelector || html.mozMatchesSelector || html.webkitMatchesSelector || html.msMatchesSelector; + + if ( matches ) { + // Check to see if it's possible to do matchesSelector + // on a disconnected node (IE 9 fails this) + var disconnectedMatch = !matches.call( document.createElement( "div" ), "div" ), + pseudoWorks = false; + + try { + // This should fail with an exception + // Gecko does not error, returns false instead + matches.call( document.documentElement, "[test!='']:sizzle" ); + + } catch( pseudoError ) { + pseudoWorks = true; + } + + Sizzle.matchesSelector = function( node, expr ) { + // Make sure that attribute selectors are quoted + expr = expr.replace(/\=\s*([^'"\]]*)\s*\]/g, "='$1']"); + + if ( !Sizzle.isXML( node ) ) { + try { + if ( pseudoWorks || !Expr.match.PSEUDO.test( expr ) && !/!=/.test( expr ) ) { + var ret = matches.call( node, expr ); + + // IE 9's matchesSelector returns false on disconnected nodes + if ( ret || !disconnectedMatch || + // As well, disconnected nodes are said to be in a document + // fragment in IE 9, so check for that + node.document && node.document.nodeType !== 11 ) { + return ret; + } + } + } catch(e) {} + } + + return Sizzle(expr, null, null, [node]).length > 0; + }; + } +})(); + (function(){ var div = document.createElement("div"); @@ -6882,410 +9046,145 @@ if ( document.querySelectorAll ) { if ( div.getElementsByClassName("e").length === 1 ) { return; - } - - Expr.order.splice(1, 0, "CLASS"); - Expr.find.CLASS = function(match, context, isXML) { - if ( typeof context.getElementsByClassName !== "undefined" && !isXML ) { - return context.getElementsByClassName(match[1]); - } - }; - - div = null; // release memory in IE -})(); - -function dirNodeCheck( dir, cur, doneName, checkSet, nodeCheck, isXML ) { - for ( var i = 0, l = checkSet.length; i < l; i++ ) { - var elem = checkSet[i]; - if ( elem ) { - elem = elem[dir]; - var match = false; - - while ( elem ) { - if ( elem.sizcache === doneName ) { - match = checkSet[elem.sizset]; - break; - } - - if ( elem.nodeType === 1 && !isXML ){ - elem.sizcache = doneName; - elem.sizset = i; - } - - if ( elem.nodeName.toLowerCase() === cur ) { - match = elem; - break; - } - - elem = elem[dir]; - } - - checkSet[i] = match; - } - } -} - -function dirCheck( dir, cur, doneName, checkSet, nodeCheck, isXML ) { - for ( var i = 0, l = checkSet.length; i < l; i++ ) { - var elem = checkSet[i]; - if ( elem ) { - elem = elem[dir]; - var match = false; - - while ( elem ) { - if ( elem.sizcache === doneName ) { - match = checkSet[elem.sizset]; - break; - } - - if ( elem.nodeType === 1 ) { - if ( !isXML ) { - elem.sizcache = doneName; - elem.sizset = i; - } - if ( typeof cur !== "string" ) { - if ( elem === cur ) { - match = true; - break; - } - - } else if ( Sizzle.filter( cur, [elem] ).length > 0 ) { - match = elem; - break; - } - } - - elem = elem[dir]; - } - - checkSet[i] = match; - } - } -} - -Sizzle.contains = document.compareDocumentPosition ? function(a, b){ - return !!(a.compareDocumentPosition(b) & 16); -} : function(a, b){ - return a !== b && (a.contains ? a.contains(b) : true); -}; - -Sizzle.isXML = function(elem){ - // documentElement is verified for cases where it doesn't yet exist - // (such as loading iframes in IE - #4833) - var documentElement = (elem ? elem.ownerDocument || elem : 0).documentElement; - return documentElement ? documentElement.nodeName !== "HTML" : false; -}; - -var posProcess = function(selector, context){ - var tmpSet = [], later = "", match, - root = context.nodeType ? [context] : context; - - // Position selectors must be done after the filter - // And so must :not(positional) so we move all PSEUDOs to the end - while ( (match = Expr.match.PSEUDO.exec( selector )) ) { - later += match[0]; - selector = selector.replace( Expr.match.PSEUDO, "" ); - } - - selector = Expr.relative[selector] ? selector + "*" : selector; - - for ( var i = 0, l = root.length; i < l; i++ ) { - Sizzle( selector, root[i], tmpSet ); - } - - return Sizzle.filter( later, tmpSet ); -}; - -// EXPOSE - -window.tinymce.dom.Sizzle = Sizzle; - -})(); - - -(function(tinymce) { - // Shorten names - var each = tinymce.each, DOM = tinymce.DOM, isIE = tinymce.isIE, isWebKit = tinymce.isWebKit, Event; - - tinymce.create('tinymce.dom.EventUtils', { - EventUtils : function() { - this.inits = []; - this.events = []; - }, - - add : function(o, n, f, s) { - var cb, t = this, el = t.events, r; - - if (n instanceof Array) { - r = []; - - each(n, function(n) { - r.push(t.add(o, n, f, s)); - }); - - return r; - } - - // Handle array - if (o && o.hasOwnProperty && o instanceof Array) { - r = []; - - each(o, function(o) { - o = DOM.get(o); - r.push(t.add(o, n, f, s)); - }); - - return r; - } - - o = DOM.get(o); - - if (!o) - return; - - // Setup event callback - cb = function(e) { - // Is all events disabled - if (t.disabled) - return; - - e = e || window.event; - - // Patch in target, preventDefault and stopPropagation in IE it's W3C valid - if (e && isIE) { - if (!e.target) - e.target = e.srcElement; - - // Patch in preventDefault, stopPropagation methods for W3C compatibility - tinymce.extend(e, t._stoppers); - } - - if (!s) - return f(e); - - return f.call(s, e); - }; - - if (n == 'unload') { - tinymce.unloads.unshift({func : cb}); - return cb; - } - - if (n == 'init') { - if (t.domLoaded) - cb(); - else - t.inits.push(cb); - - return cb; - } - - // Store away listener reference - el.push({ - obj : o, - name : n, - func : f, - cfunc : cb, - scope : s - }); - - t._add(o, n, cb); - - return f; - }, - - remove : function(o, n, f) { - var t = this, a = t.events, s = false, r; - - // Handle array - if (o && o.hasOwnProperty && o instanceof Array) { - r = []; - - each(o, function(o) { - o = DOM.get(o); - r.push(t.remove(o, n, f)); - }); - - return r; - } - - o = DOM.get(o); - - each(a, function(e, i) { - if (e.obj == o && e.name == n && (!f || (e.func == f || e.cfunc == f))) { - a.splice(i, 1); - t._remove(o, n, e.cfunc); - s = true; - return false; - } - }); + } - return s; - }, + Expr.order.splice(1, 0, "CLASS"); + Expr.find.CLASS = function( match, context, isXML ) { + if ( typeof context.getElementsByClassName !== "undefined" && !isXML ) { + return context.getElementsByClassName(match[1]); + } + }; - clear : function(o) { - var t = this, a = t.events, i, e; + // release memory in IE + div = null; +})(); - if (o) { - o = DOM.get(o); +function dirNodeCheck( dir, cur, doneName, checkSet, nodeCheck, isXML ) { + for ( var i = 0, l = checkSet.length; i < l; i++ ) { + var elem = checkSet[i]; - for (i = a.length - 1; i >= 0; i--) { - e = a[i]; + if ( elem ) { + var match = false; - if (e.obj === o) { - t._remove(e.obj, e.name, e.cfunc); - e.obj = e.cfunc = null; - a.splice(i, 1); - } - } - } - }, + elem = elem[dir]; - cancel : function(e) { - if (!e) - return false; + while ( elem ) { + if ( elem[ expando ] === doneName ) { + match = checkSet[elem.sizset]; + break; + } - this.stop(e); + if ( elem.nodeType === 1 && !isXML ){ + elem[ expando ] = doneName; + elem.sizset = i; + } - return this.prevent(e); - }, + if ( elem.nodeName.toLowerCase() === cur ) { + match = elem; + break; + } - stop : function(e) { - if (e.stopPropagation) - e.stopPropagation(); - else - e.cancelBubble = true; + elem = elem[dir]; + } - return false; - }, + checkSet[i] = match; + } + } +} - prevent : function(e) { - if (e.preventDefault) - e.preventDefault(); - else - e.returnValue = false; +function dirCheck( dir, cur, doneName, checkSet, nodeCheck, isXML ) { + for ( var i = 0, l = checkSet.length; i < l; i++ ) { + var elem = checkSet[i]; - return false; - }, + if ( elem ) { + var match = false; - destroy : function() { - var t = this; + elem = elem[dir]; - each(t.events, function(e, i) { - t._remove(e.obj, e.name, e.cfunc); - e.obj = e.cfunc = null; - }); + while ( elem ) { + if ( elem[ expando ] === doneName ) { + match = checkSet[elem.sizset]; + break; + } - t.events = []; - t = null; - }, + if ( elem.nodeType === 1 ) { + if ( !isXML ) { + elem[ expando ] = doneName; + elem.sizset = i; + } - _add : function(o, n, f) { - if (o.attachEvent) - o.attachEvent('on' + n, f); - else if (o.addEventListener) - o.addEventListener(n, f, false); - else - o['on' + n] = f; - }, + if ( typeof cur !== "string" ) { + if ( elem === cur ) { + match = true; + break; + } - _remove : function(o, n, f) { - if (o) { - try { - if (o.detachEvent) - o.detachEvent('on' + n, f); - else if (o.removeEventListener) - o.removeEventListener(n, f, false); - else - o['on' + n] = null; - } catch (ex) { - // Might fail with permission denined on IE so we just ignore that + } else if ( Sizzle.filter( cur, [elem] ).length > 0 ) { + match = elem; + break; + } } - } - }, - - _pageInit : function(win) { - var t = this; - // Keep it from running more than once - if (t.domLoaded) - return; + elem = elem[dir]; + } - t.domLoaded = true; + checkSet[i] = match; + } + } +} - each(t.inits, function(c) { - c(); - }); +if ( document.documentElement.contains ) { + Sizzle.contains = function( a, b ) { + return a !== b && (a.contains ? a.contains(b) : true); + }; - t.inits = []; - }, +} else if ( document.documentElement.compareDocumentPosition ) { + Sizzle.contains = function( a, b ) { + return !!(a.compareDocumentPosition(b) & 16); + }; - _wait : function(win) { - var t = this, doc = win.document; +} else { + Sizzle.contains = function() { + return false; + }; +} - // No need since the document is already loaded - if (win.tinyMCE_GZ && tinyMCE_GZ.loaded) { - t.domLoaded = 1; - return; - } +Sizzle.isXML = function( elem ) { + // documentElement is verified for cases where it doesn't yet exist + // (such as loading iframes in IE - #4833) + var documentElement = (elem ? elem.ownerDocument || elem : 0).documentElement; - // Use IE method - if (doc.attachEvent) { - doc.attachEvent("onreadystatechange", function() { - if (doc.readyState === "complete") { - doc.detachEvent("onreadystatechange", arguments.callee); - t._pageInit(win); - } - }); + return documentElement ? documentElement.nodeName !== "HTML" : false; +}; - if (doc.documentElement.doScroll && win == win.top) { - (function() { - if (t.domLoaded) - return; +var posProcess = function( selector, context, seed ) { + var match, + tmpSet = [], + later = "", + root = context.nodeType ? [context] : context; - try { - // If IE is used, use the trick by Diego Perini licensed under MIT by request to the author. - // http://javascript.nwbox.com/IEContentLoaded/ - doc.documentElement.doScroll("left"); - } catch (ex) { - setTimeout(arguments.callee, 0); - return; - } + // Position selectors must be done after the filter + // And so must :not(positional) so we move all PSEUDOs to the end + while ( (match = Expr.match.PSEUDO.exec( selector )) ) { + later += match[0]; + selector = selector.replace( Expr.match.PSEUDO, "" ); + } - t._pageInit(win); - })(); - } - } else if (doc.addEventListener) { - t._add(win, 'DOMContentLoaded', function() { - t._pageInit(win); - }); - } + selector = Expr.relative[selector] ? selector + "*" : selector; - t._add(win, 'load', function() { - t._pageInit(win); - }); - }, + for ( var i = 0, l = root.length; i < l; i++ ) { + Sizzle( selector, root[i], tmpSet, seed ); + } - _stoppers : { - preventDefault : function() { - this.returnValue = false; - }, + return Sizzle.filter( later, tmpSet ); +}; - stopPropagation : function() { - this.cancelBubble = true; - } - } - }); +// EXPOSE - Event = tinymce.dom.Event = new tinymce.dom.EventUtils(); +window.tinymce.dom.Sizzle = Sizzle; - // Dispatch DOM content loaded event for IE and Safari - Event._wait(window); +})(); - tinymce.addUnload(function() { - Event.destroy(); - }); -})(tinymce); (function(tinymce) { tinymce.dom.Element = function(id, settings) { @@ -7303,20 +9202,20 @@ window.tinymce.dom.Sizzle = Sizzle; ('getPos,getRect,getParent,add,setStyle,getStyle,setStyles,' + 'setAttrib,setAttribs,getAttrib,addClass,removeClass,' + 'hasClass,getOuterHTML,setOuterHTML,remove,show,hide,' + - 'isHidden,setHTML,get').split(/,/) - , function(k) { - t[k] = function() { - var a = [id], i; + 'isHidden,setHTML,get').split(/,/), function(k) { + t[k] = function() { + var a = [id], i; - for (i = 0; i < arguments.length; i++) - a.push(arguments[i]); + for (i = 0; i < arguments.length; i++) + a.push(arguments[i]); - a = dom[k].apply(dom, a); - t.update(k); + a = dom[k].apply(dom, a); + t.update(k); - return a; - }; - }); + return a; + }; + } + ); tinymce.extend(t, { on : function(n, f, s) { @@ -7402,15 +9301,16 @@ window.tinymce.dom.Sizzle = Sizzle; }; // Shorten names - var is = tinymce.is, isIE = tinymce.isIE, each = tinymce.each; + var is = tinymce.is, isIE = tinymce.isIE, each = tinymce.each, TreeWalker = tinymce.dom.TreeWalker; tinymce.create('tinymce.dom.Selection', { - Selection : function(dom, win, serializer) { + Selection : function(dom, win, serializer, editor) { var t = this; t.dom = dom; t.win = win; t.serializer = serializer; + t.editor = editor; // Add events each([ @@ -7509,7 +9409,7 @@ window.tinymce.dom.Sizzle = Sizzle; } else { rng.deleteContents(); - if (doc.body.childNodes.length == 0) { + if (doc.body.childNodes.length === 0) { doc.body.innerHTML = content; } else { // createContextualFragment doesn't exists in IE 9 DOMRanges @@ -7566,7 +9466,7 @@ window.tinymce.dom.Sizzle = Sizzle; }, getStart : function() { - var rng = this.getRng(), startElement, parentElement, checkRng, node; + var self = this, rng = self.getRng(), startElement, parentElement, checkRng, node; if (rng.duplicate || rng.item) { // Control selection, return first item @@ -7577,6 +9477,9 @@ window.tinymce.dom.Sizzle = Sizzle; checkRng = rng.duplicate(); checkRng.collapse(1); startElement = checkRng.parentElement(); + if (startElement.ownerDocument !== self.dom.doc) { + startElement = self.dom.getRoot(); + } // 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 @@ -7603,31 +9506,34 @@ window.tinymce.dom.Sizzle = Sizzle; }, getEnd : function() { - var t = this, r = t.getRng(), e, eo; + var self = this, rng = self.getRng(), endElement, endOffset; - if (r.duplicate || r.item) { - if (r.item) - return r.item(0); + if (rng.duplicate || rng.item) { + if (rng.item) + return rng.item(0); - r = r.duplicate(); - r.collapse(0); - e = r.parentElement(); + rng = rng.duplicate(); + rng.collapse(0); + endElement = rng.parentElement(); + if (endElement.ownerDocument !== self.dom.doc) { + endElement = self.dom.getRoot(); + } - if (e && e.nodeName == 'BODY') - return e.lastChild || e; + if (endElement && endElement.nodeName == 'BODY') + return endElement.lastChild || endElement; - return e; + return endElement; } else { - e = r.endContainer; - eo = r.endOffset; + endElement = rng.endContainer; + endOffset = rng.endOffset; - if (e.nodeType == 1 && e.hasChildNodes()) - e = e.childNodes[eo > 0 ? eo - 1 : eo]; + if (endElement.nodeType == 1 && endElement.hasChildNodes()) + endElement = endElement.childNodes[endOffset > 0 ? endOffset - 1 : endOffset]; - if (e && e.nodeType == 3) - return e.parentNode; + if (endElement && endElement.nodeType == 3) + return endElement.parentNode; - return e; + return endElement; } }, @@ -7645,46 +9551,69 @@ window.tinymce.dom.Sizzle = Sizzle; return index; }; - if (type == 2) { - function getLocation() { - var rng = t.getRng(true), root = dom.getRoot(), bookmark = {}; + function normalizeTableCellSelection(rng) { + function moveEndPoint(start) { + var container, offset, childNodes, prefix = start ? 'start' : 'end'; - function getPoint(rng, start) { - var container = rng[start ? 'startContainer' : 'endContainer'], - offset = rng[start ? 'startOffset' : 'endOffset'], point = [], node, childNodes, after = 0; + container = rng[prefix + 'Container']; + offset = rng[prefix + 'Offset']; - if (container.nodeType == 3) { - if (normalized) { - for (node = container.previousSibling; node && node.nodeType == 3; node = node.previousSibling) - offset += node.nodeValue.length; - } + if (container.nodeType == 1 && container.nodeName == "TR") { + childNodes = container.childNodes; + container = childNodes[Math.min(start ? offset : offset - 1, childNodes.length - 1)]; + if (container) { + offset = start ? 0 : container.childNodes.length; + rng['set' + (start ? 'Start' : 'End')](container, offset); + } + } + }; - point.push(offset); - } else { - childNodes = container.childNodes; + moveEndPoint(true); + moveEndPoint(); - if (offset >= childNodes.length && childNodes.length) { - after = 1; - offset = Math.max(0, childNodes.length - 1); - } + return rng; + }; + + function getLocation() { + var rng = t.getRng(true), root = dom.getRoot(), bookmark = {}; + + function getPoint(rng, start) { + var container = rng[start ? 'startContainer' : 'endContainer'], + offset = rng[start ? 'startOffset' : 'endOffset'], point = [], node, childNodes, after = 0; - point.push(t.dom.nodeIndex(childNodes[offset], normalized) + after); + if (container.nodeType == 3) { + if (normalized) { + for (node = container.previousSibling; node && node.nodeType == 3; node = node.previousSibling) + offset += node.nodeValue.length; } - for (; container && container != root; container = container.parentNode) - point.push(t.dom.nodeIndex(container, normalized)); + point.push(offset); + } else { + childNodes = container.childNodes; - return point; - }; + if (offset >= childNodes.length && childNodes.length) { + after = 1; + offset = Math.max(0, childNodes.length - 1); + } - bookmark.start = getPoint(rng, true); + point.push(t.dom.nodeIndex(childNodes[offset], normalized) + after); + } - if (!t.isCollapsed()) - bookmark.end = getPoint(rng); + for (; container && container != root; container = container.parentNode) + point.push(t.dom.nodeIndex(container, normalized)); - return bookmark; + return point; }; + bookmark.start = getPoint(rng, true); + + if (!t.isCollapsed()) + bookmark.end = getPoint(rng); + + return bookmark; + }; + + if (type == 2) { if (t.tridentSel) return t.tridentSel.getBookmark(type); @@ -7717,7 +9646,7 @@ window.tinymce.dom.Sizzle = Sizzle; // Detect the empty space after block elements in IE and move the end back one character

    ] becomes

    ]

    rng.moveToElementText(rng2.parentElement()); - if (rng.compareEndPoints('StartToEnd', rng2) == 0) + if (rng.compareEndPoints('StartToEnd', rng2) === 0) rng2.move('character', -1); rng2.pasteHTML('' + chr + ''); @@ -7740,7 +9669,7 @@ window.tinymce.dom.Sizzle = Sizzle; return {name : name, index : findIndex(name, element)}; // W3C method - rng2 = rng.cloneRange(); + rng2 = normalizeTableCellSelection(rng.cloneRange()); // Insert end marker if (!collapsed) { @@ -7748,6 +9677,7 @@ window.tinymce.dom.Sizzle = Sizzle; rng2.insertNode(dom.create('span', {'data-mce-type' : "bookmark", id : id + '_end', style : styles}, chr)); } + rng = normalizeTableCellSelection(rng); rng.collapse(true); rng.insertNode(dom.create('span', {'data-mce-type' : "bookmark", id : id + '_start', style : styles}, chr)); } @@ -7760,122 +9690,122 @@ window.tinymce.dom.Sizzle = Sizzle; moveToBookmark : function(bookmark) { var t = this, dom = t.dom, marker1, marker2, rng, root, startContainer, endContainer, startOffset, endOffset; - if (bookmark) { - if (bookmark.start) { - rng = dom.createRng(); - root = dom.getRoot(); + function setEndPoint(start) { + var point = bookmark[start ? 'start' : 'end'], i, node, offset, children; - function setEndPoint(start) { - var point = bookmark[start ? 'start' : 'end'], i, node, offset, children; + if (point) { + offset = point[0]; - if (point) { - offset = point[0]; + // Find container node + for (node = root, i = point.length - 1; i >= 1; i--) { + children = node.childNodes; - // Find container node - for (node = root, i = point.length - 1; i >= 1; i--) { - children = node.childNodes; + if (point[i] > children.length - 1) + return; - if (point[i] > children.length - 1) - return; + node = children[point[i]]; + } - node = children[point[i]]; - } + // Move text offset to best suitable location + if (node.nodeType === 3) + offset = Math.min(point[0], node.nodeValue.length); - // Move text offset to best suitable location - if (node.nodeType === 3) - offset = Math.min(point[0], node.nodeValue.length); + // Move element offset to best suitable location + if (node.nodeType === 1) + offset = Math.min(point[0], node.childNodes.length); - // Move element offset to best suitable location - if (node.nodeType === 1) - offset = Math.min(point[0], node.childNodes.length); + // Set offset within container node + if (start) + rng.setStart(node, offset); + else + rng.setEnd(node, offset); + } - // Set offset within container node - if (start) - rng.setStart(node, offset); - else - rng.setEnd(node, offset); - } + return true; + }; - return true; - }; + function restoreEndPoint(suffix) { + var marker = dom.get(bookmark.id + '_' + suffix), node, idx, next, prev, keep = bookmark.keep; - if (t.tridentSel) - return t.tridentSel.moveToBookmark(bookmark); + if (marker) { + node = marker.parentNode; - if (setEndPoint(true) && setEndPoint()) { - t.setRng(rng); + if (suffix == 'start') { + if (!keep) { + idx = dom.nodeIndex(marker); + } else { + node = marker.firstChild; + idx = 1; + } + + startContainer = endContainer = node; + startOffset = endOffset = idx; + } else { + if (!keep) { + idx = dom.nodeIndex(marker); + } else { + node = marker.firstChild; + idx = 1; + } + + endContainer = node; + endOffset = idx; } - } else if (bookmark.id) { - function restoreEndPoint(suffix) { - var marker = dom.get(bookmark.id + '_' + suffix), node, idx, next, prev, keep = bookmark.keep; - if (marker) { - node = marker.parentNode; + if (!keep) { + prev = marker.previousSibling; + next = marker.nextSibling; - if (suffix == 'start') { - if (!keep) { - idx = dom.nodeIndex(marker); - } else { - node = marker.firstChild; - idx = 1; - } + // Remove all marker text nodes + each(tinymce.grep(marker.childNodes), function(node) { + if (node.nodeType == 3) + node.nodeValue = node.nodeValue.replace(/\uFEFF/g, ''); + }); - startContainer = endContainer = node; + // Remove marker but keep children if for example contents where inserted into the marker + // Also remove duplicated instances of the marker for example by a split operation or by WebKit auto split on paste feature + while (marker = dom.get(bookmark.id + '_' + suffix)) + dom.remove(marker, 1); + + // If siblings are text nodes then merge them unless it's Opera since it some how removes the node + // and we are sniffing since adding a lot of detection code for a browser with 3% of the market isn't worth the effort. Sorry, Opera but it's just a fact + if (prev && next && prev.nodeType == next.nodeType && prev.nodeType == 3 && !tinymce.isOpera) { + idx = prev.nodeValue.length; + prev.appendData(next.nodeValue); + dom.remove(next); + + if (suffix == 'start') { + startContainer = endContainer = prev; startOffset = endOffset = idx; } else { - if (!keep) { - idx = dom.nodeIndex(marker); - } else { - node = marker.firstChild; - idx = 1; - } - - endContainer = node; + endContainer = prev; endOffset = idx; } + } + } + } + }; - if (!keep) { - prev = marker.previousSibling; - next = marker.nextSibling; - - // Remove all marker text nodes - each(tinymce.grep(marker.childNodes), function(node) { - if (node.nodeType == 3) - node.nodeValue = node.nodeValue.replace(/\uFEFF/g, ''); - }); + function addBogus(node) { + // Adds a bogus BR element for empty block elements + if (dom.isBlock(node) && !node.innerHTML && !isIE) + node.innerHTML = '
    '; - // Remove marker but keep children if for example contents where inserted into the marker - // Also remove duplicated instances of the marker for example by a split operation or by WebKit auto split on paste feature - while (marker = dom.get(bookmark.id + '_' + suffix)) - dom.remove(marker, 1); - - // If siblings are text nodes then merge them unless it's Opera since it some how removes the node - // and we are sniffing since adding a lot of detection code for a browser with 3% of the market isn't worth the effort. Sorry, Opera but it's just a fact - if (prev && next && prev.nodeType == next.nodeType && prev.nodeType == 3 && !tinymce.isOpera) { - idx = prev.nodeValue.length; - prev.appendData(next.nodeValue); - dom.remove(next); - - if (suffix == 'start') { - startContainer = endContainer = prev; - startOffset = endOffset = idx; - } else { - endContainer = prev; - endOffset = idx; - } - } - } - } - }; + return node; + }; - function addBogus(node) { - // Adds a bogus BR element for empty block elements or just a space on IE since it renders BR elements incorrectly - if (dom.isBlock(node) && !node.innerHTML) - node.innerHTML = !isIE ? '
    ' : ' '; + if (bookmark) { + if (bookmark.start) { + rng = dom.createRng(); + root = dom.getRoot(); - return node; - }; + if (t.tridentSel) + return t.tridentSel.moveToBookmark(bookmark); + if (setEndPoint(true) && setEndPoint()) { + t.setRng(rng); + } + } else if (bookmark.id) { // Restore start/end points restoreEndPoint('start'); restoreEndPoint('end'); @@ -7896,39 +9826,39 @@ window.tinymce.dom.Sizzle = Sizzle; select : function(node, content) { var t = this, dom = t.dom, rng = dom.createRng(), idx; - if (node) { - idx = dom.nodeIndex(node); - rng.setStart(node.parentNode, idx); - rng.setEnd(node.parentNode, idx + 1); + function setPoint(node, start) { + var walker = new TreeWalker(node, node); - // Find first/last text node or BR element - if (content) { - function setPoint(node, start) { - var walker = new tinymce.dom.TreeWalker(node, node); + do { + // Text node + if (node.nodeType == 3 && tinymce.trim(node.nodeValue).length !== 0) { + if (start) + rng.setStart(node, 0); + else + rng.setEnd(node, node.nodeValue.length); - do { - // Text node - if (node.nodeType == 3 && tinymce.trim(node.nodeValue).length != 0) { - if (start) - rng.setStart(node, 0); - else - rng.setEnd(node, node.nodeValue.length); + return; + } - return; - } + // BR element + if (node.nodeName == 'BR') { + if (start) + rng.setStartBefore(node); + else + rng.setEndBefore(node); - // BR element - if (node.nodeName == 'BR') { - if (start) - rng.setStartBefore(node); - else - rng.setEndBefore(node); + return; + } + } while (node = (start ? walker.next() : walker.prev())); + }; - return; - } - } while (node = (start ? walker.next() : walker.prev())); - }; + if (node) { + idx = dom.nodeIndex(node); + rng.setStart(node.parentNode, idx); + rng.setEnd(node.parentNode, idx + 1); + // Find first/last text node or BR element + if (content) { setPoint(node, 1); setPoint(node); } @@ -7972,50 +9902,60 @@ window.tinymce.dom.Sizzle = Sizzle; }, getRng : function(w3c) { - var t = this, s, r, elm, doc = t.win.document; + var self = this, selection, rng, elm, doc = self.win.document; // Found tridentSel object then we need to use that one - if (w3c && t.tridentSel) - return t.tridentSel.getRangeAt(0); + if (w3c && self.tridentSel) { + return self.tridentSel.getRangeAt(0); + } try { - if (s = t.getSel()) - r = s.rangeCount > 0 ? s.getRangeAt(0) : (s.createRange ? s.createRange() : doc.createRange()); + if (selection = self.getSel()) { + rng = selection.rangeCount > 0 ? selection.getRangeAt(0) : (selection.createRange ? selection.createRange() : doc.createRange()); + } } catch (ex) { // IE throws unspecified error here if TinyMCE is placed in a frame/iframe } // We have W3C ranges and it's IE then fake control selection since IE9 doesn't handle that correctly yet - if (tinymce.isIE && r && r.setStart && doc.selection.createRange().item) { + if (tinymce.isIE && rng && rng.setStart && doc.selection.createRange().item) { elm = doc.selection.createRange().item(0); - r = doc.createRange(); - r.setStartBefore(elm); - r.setEndAfter(elm); + rng = doc.createRange(); + rng.setStartBefore(elm); + rng.setEndAfter(elm); } // No range found then create an empty one // 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 = doc.createRange ? doc.createRange() : doc.body.createTextRange(); + if (!rng) { + rng = doc.createRange ? doc.createRange() : doc.body.createTextRange(); + } + + // If range is at start of document then move it to start of body + if (rng.setStart && rng.startContainer.nodeType === 9 && rng.collapsed) { + elm = self.dom.getRoot(); + rng.setStart(elm, 0); + rng.setEnd(elm, 0); + } - if (t.selectedRange && t.explicitRange) { - if (r.compareBoundaryPoints(r.START_TO_START, t.selectedRange) === 0 && r.compareBoundaryPoints(r.END_TO_END, t.selectedRange) === 0) { + if (self.selectedRange && self.explicitRange) { + if (rng.compareBoundaryPoints(rng.START_TO_START, self.selectedRange) === 0 && rng.compareBoundaryPoints(rng.END_TO_END, self.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; + rng = self.explicitRange; } else { - t.selectedRange = null; - t.explicitRange = null; + self.selectedRange = null; + self.explicitRange = null; } } - return r; + return rng; }, - setRng : function(r) { + setRng : function(r, forward) { var s, t = this; - + if (!t.tridentSel) { s = t.getSel(); @@ -8029,13 +9969,25 @@ window.tinymce.dom.Sizzle = Sizzle; } s.addRange(r); - t.selectedRange = s.getRangeAt(0); + + // Forward is set to false and we have an extend function + if (forward === false && s.extend) { + s.collapse(r.endContainer, r.endOffset); + s.extend(r.startContainer, r.startOffset); + } + + // adding range isn't always successful so we need to check range count otherwise an exception can occur + t.selectedRange = s.rangeCount > 0 ? s.getRangeAt(0) : null; } } else { // Is W3C Range if (r.cloneRange) { - t.tridentSel.addRange(r); - return; + try { + t.tridentSel.addRange(r); + return; + } catch (ex) { + //IE9 throws an error here if called before selection is placed in the editor + } } // Is IE specific range @@ -8058,6 +10010,14 @@ window.tinymce.dom.Sizzle = Sizzle; getNode : function() { var t = this, rng = t.getRng(), sel = t.getSel(), elm, start = rng.startContainer, end = rng.endContainer; + function skipEmptyTextNodes(n, forwards) { + var orig = n; + while (n && n.nodeType === 3 && n.length === 0) { + n = forwards ? n.nextSibling : n.previousSibling; + } + return n || orig; + }; + // Range maybe lost after the editor is made visible again if (!rng) return t.dom.getRoot(); @@ -8075,19 +10035,12 @@ window.tinymce.dom.Sizzle = Sizzle; } // If the anchor node is a element instead of a text node then return this element - //if (tinymce.isWebKit && sel.anchorNode && sel.anchorNode.nodeType == 1) + //if (tinymce.isWebKit && sel.anchorNode && sel.anchorNode.nodeType == 1) // return sel.anchorNode.childNodes[sel.anchorOffset]; // Handle cases where the selection is immediately wrapped around a node and return that node instead of it's parent. // This happens when you double click an underlined word in FireFox. if (start.nodeType === 3 && end.nodeType === 3) { - function skipEmptyTextNodes(n, forwards) { - var orig = n; - while (n && n.nodeType === 3 && n.length === 0) { - n = forwards ? n.nextSibling : n.previousSibling; - } - return n || orig; - } if (start.length === rng.startOffset) { start = skipEmptyTextNodes(start.nextSibling, true); } else { @@ -8125,7 +10078,7 @@ window.tinymce.dom.Sizzle = Sizzle; if (sb && eb && sb != eb) { n = sb; - var walker = new tinymce.dom.TreeWalker(sb, dom.getRoot()); + var walker = new TreeWalker(sb, dom.getRoot()); while ((n = walker.next()) && n != eb) { if (dom.isBlock(n)) bl.push(n); @@ -8138,96 +10091,270 @@ window.tinymce.dom.Sizzle = Sizzle; return bl; }, - normalize : function() { - var self = this, rng, normalized; + isForward: function(){ + var dom = this.dom, sel = this.getSel(), anchorRange, focusRange; - // Normalize only on non IE browsers for now - if (tinymce.isIE) - return; + // No support for selection direction then always return true + if (!sel || sel.anchorNode == null || sel.focusNode == null) { + return true; + } + + anchorRange = dom.createRng(); + anchorRange.setStart(sel.anchorNode, sel.anchorOffset); + anchorRange.collapse(true); + + focusRange = dom.createRng(); + focusRange.setStart(sel.focusNode, sel.focusOffset); + focusRange.collapse(true); + + return anchorRange.compareBoundaryPoints(anchorRange.START_TO_START, focusRange) <= 0; + }, + + normalize : function() { + var self = this, rng, normalized, collapsed, node, sibling; function normalizeEndPoint(start) { - var container, offset, walker, dom = self.dom, body = dom.getRoot(), node; + var container, offset, walker, dom = self.dom, body = dom.getRoot(), node, nonEmptyElementsMap, nodeName; + + function hasBrBeforeAfter(node, left) { + var walker = new TreeWalker(node, dom.getParent(node.parentNode, dom.isBlock) || body); + + while (node = walker[left ? 'prev' : 'next']()) { + if (node.nodeName === "BR") { + return true; + } + } + }; + + // Walks the dom left/right to find a suitable text node to move the endpoint into + // It will only walk within the current parent block or body and will stop if it hits a block or a BR/IMG + function findTextNodeRelative(left, startNode) { + var walker, lastInlineElement; + + startNode = startNode || container; + walker = new TreeWalker(startNode, dom.getParent(startNode.parentNode, dom.isBlock) || body); + + // Walk left until we hit a text node we can move to or a block/br/img + while (node = walker[left ? 'prev' : 'next']()) { + // Found text node that has a length + if (node.nodeType === 3 && node.nodeValue.length > 0) { + container = node; + offset = left ? node.nodeValue.length : 0; + normalized = true; + return; + } + + // Break if we find a block or a BR/IMG/INPUT etc + if (dom.isBlock(node) || nonEmptyElementsMap[node.nodeName.toLowerCase()]) { + return; + } + + lastInlineElement = node; + } + + // Only fetch the last inline element when in caret mode for now + if (collapsed && lastInlineElement) { + container = lastInlineElement; + normalized = true; + offset = 0; + } + }; container = rng[(start ? 'start' : 'end') + 'Container']; offset = rng[(start ? 'start' : 'end') + 'Offset']; + nonEmptyElementsMap = dom.schema.getNonEmptyElements(); // If the container is a document move it to the body element if (container.nodeType === 9) { - container = container.body; + container = dom.getRoot(); offset = 0; } // If the container is body try move it into the closest text node or position - // TODO: Add more logic here to handle element selection cases if (container === body) { + // If start is before/after a image, table etc + if (start) { + node = container.childNodes[offset > 0 ? offset - 1 : 0]; + if (node) { + nodeName = node.nodeName.toLowerCase(); + if (nonEmptyElementsMap[node.nodeName] || node.nodeName == "TABLE") { + return; + } + } + } + // Resolve the index if (container.hasChildNodes()) { container = container.childNodes[Math.min(!start && offset > 0 ? offset - 1 : offset, container.childNodes.length - 1)]; offset = 0; // Don't walk into elements that doesn't have any child nodes like a IMG - if (container.hasChildNodes()) { + if (container.hasChildNodes() && !/TABLE/.test(container.nodeName)) { // Walk the DOM to find a text node to place the caret at or a BR node = container; - walker = new tinymce.dom.TreeWalker(container, body); + walker = new TreeWalker(container, body); + do { // Found a text node use that position - if (node.nodeType === 3) { - offset = start ? 0 : node.nodeValue.length - 1; + if (node.nodeType === 3 && node.nodeValue.length > 0) { + offset = start ? 0 : node.nodeValue.length; container = node; + normalized = true; break; } - // Found a BR element that we can place the caret before - if (node.nodeName === 'BR') { + // Found a BR/IMG element that we can place the caret before + if (nonEmptyElementsMap[node.nodeName.toLowerCase()]) { offset = dom.nodeIndex(node); container = node.parentNode; + + // Put caret after image when moving the end point + if (node.nodeName == "IMG" && !start) { + offset++; + } + + normalized = true; break; } } while (node = (start ? walker.next() : walker.prev())); + } + } + } - normalized = true; + // Lean the caret to the left if possible + if (collapsed) { + // So this: x|x + // Becomes: x|x + // Seems that only gecko has issues with this + if (container.nodeType === 3 && offset === 0) { + findTextNodeRelative(true); + } + + // Lean left into empty inline elements when the caret is before a BR + // So this: |
    + // Becomes: |
    + // Seems that only gecko has issues with this + if (container.nodeType === 1) { + node = container.childNodes[offset]; + if(node && node.nodeName === 'BR' && !hasBrBeforeAfter(node) && !hasBrBeforeAfter(node, true)) { + findTextNodeRelative(true, container.childNodes[offset]); } } } + // Lean the start of the selection right if possible + // So this: x[x] + // Becomes: x[x] + if (start && !collapsed && container.nodeType === 3 && offset === container.nodeValue.length) { + findTextNodeRelative(false); + } + // Set endpoint if it was normalized if (normalized) rng['set' + (start ? 'Start' : 'End')](container, offset); }; + // Normalize only on non IE browsers for now + if (tinymce.isIE) + return; + rng = self.getRng(); + collapsed = rng.collapsed; // Normalize the end points normalizeEndPoint(true); - - if (rng.collapsed) + + if (!collapsed) normalizeEndPoint(); // Set the selection if it was normalized if (normalized) { + // If it was collapsed then make sure it still is + if (collapsed) { + rng.collapse(true); + } + //console.log(self.dom.dumpRng(rng)); - self.setRng(rng); + self.setRng(rng, self.isForward()); } }, - destroy : function(s) { - var t = this; + selectorChanged: function(selector, callback) { + var self = this, currentSelectors; - t.win = null; + if (!self.selectorChangedData) { + self.selectorChangedData = {}; + currentSelectors = {}; + + self.editor.onNodeChange.addToTop(function(ed, cm, node) { + var dom = self.dom, parents = dom.getParents(node, null, dom.getRoot()), matchedSelectors = {}; + + // Check for new matching selectors + each(self.selectorChangedData, function(callbacks, selector) { + each(parents, function(node) { + if (dom.is(node, selector)) { + if (!currentSelectors[selector]) { + // Execute callbacks + each(callbacks, function(callback) { + callback(true, {node: node, selector: selector, parents: parents}); + }); + + currentSelectors[selector] = callbacks; + } + + matchedSelectors[selector] = callbacks; + return false; + } + }); + }); + + // Check if current selectors still match + each(currentSelectors, function(callbacks, selector) { + if (!matchedSelectors[selector]) { + delete currentSelectors[selector]; + + each(callbacks, function(callback) { + callback(false, {node: node, selector: selector, parents: parents}); + }); + } + }); + }); + } + + // Add selector listeners + if (!self.selectorChangedData[selector]) { + self.selectorChangedData[selector] = []; + } + + self.selectorChangedData[selector].push(callback); + + return self; + }, + + scrollIntoView: function(elm) { + var y, viewPort, self = this, dom = self.dom; + + viewPort = dom.getViewPort(self.editor.getWin()); + y = dom.getPos(elm).y; + if (y < viewPort.y || y + 25 > viewPort.y + viewPort.h) { + self.editor.getWin().scrollTo(0, y < viewPort.y ? y : y - viewPort.h + 25); + } + }, + + destroy : function(manual) { + var self = this; + + self.win = null; // Manual destroy then remove unload handler - if (!s) - tinymce.removeUnload(t.destroy); + if (!manual) + tinymce.removeUnload(self.destroy); }, // IE has an issue where you can't select/move the caret by clicking outside the body if the document is in standards mode _fixIESelection : function() { var dom = this.dom, doc = dom.doc, body = doc.body, started, startRng, htmlElm; - // Make HTML element unselectable since we are going to handle selection by hand - doc.documentElement.unselectable = true; - // Return range from point or null if it failed function rngFromPoint(x, y) { var rng = body.createTextRange(); @@ -8277,6 +10404,9 @@ window.tinymce.dom.Sizzle = Sizzle; startRng = started = 0; }; + // Make HTML element unselectable since we are going to handle selection by hand + doc.documentElement.unselectable = true; + // Detect when user selects outside BODY dom.bind(doc, ['mousedown', 'contextmenu'], function(e) { if (e.target.nodeName === 'HTML') { @@ -8313,12 +10443,11 @@ window.tinymce.dom.Sizzle = Sizzle; if (!settings.apply_source_formatting) settings.indent = false; - settings.remove_trailing_brs = true; - // Default DOM and Schema if they are undefined dom = dom || tinymce.DOM; schema = schema || new tinymce.html.Schema(settings); settings.entity_encoding = settings.entity_encoding || 'named'; + settings.remove_trailing_brs = "remove_trailing_brs" in settings ? settings.remove_trailing_brs : true; onPreProcess = new tinymce.util.Dispatcher(self); @@ -8352,13 +10481,13 @@ window.tinymce.dom.Sizzle = Sizzle; } }); - // Remove internal classes mceItem<..> + // Remove internal classes mceItem<..> or mceSelected htmlParser.addAttributeFilter('class', function(nodes, name) { var i = nodes.length, node, value; while (i--) { node = nodes[i]; - value = node.attr('class').replace(/\s*mce(Item\w+|Selected)\s*/g, ''); + value = node.attr('class').replace(/(?:^|\s)mce(Item\w+|Selected)(?!\S)/g, ''); node.attr('class', value.length > 0 ? value : null); } }); @@ -8375,6 +10504,27 @@ window.tinymce.dom.Sizzle = Sizzle; } }); + // Remove expando attributes + htmlParser.addAttributeFilter('data-mce-expando', function(nodes, name, args) { + var i = nodes.length; + + while (i--) { + nodes[i].attr(name, null); + } + }); + + htmlParser.addNodeFilter('noscript', function(nodes) { + var i = nodes.length, node; + + while (i--) { + node = nodes[i].firstChild; + + if (node) { + node.value = tinymce.html.Entities.decode(node.value); + } + } + }); + // Force script into CDATA sections and remove the mce- prefix also add comments around styles htmlParser.addNodeFilter('script,style', function(nodes, name) { var i = nodes.length, node, value; @@ -8382,8 +10532,8 @@ window.tinymce.dom.Sizzle = Sizzle; function trim(value) { return value.replace(/()/g, '\n') .replace(/^[\r\n]*|[\r\n]*$/g, '') - .replace(/^\s*(\/\/\s*|\]\]>|-->|\]\]-->)\s*$/g, ''); + .replace(/^\s*(()?|\s*\/\/\s*\]\]>(-->)?|\/\/\s*(-->)?|\]\]>|\/\*\s*-->\s*\*\/|\s*-->\s*)\s*$/g, ''); }; while (i--) { @@ -8527,12 +10677,12 @@ window.tinymce.dom.Sizzle = Sizzle; // Parse and serialize HTML args.content = htmlSerializer.serialize( - htmlParser.parse(args.getInner ? node.innerHTML : tinymce.trim(dom.getOuterHTML(node), args), args) + htmlParser.parse(tinymce.trim(args.getInner ? node.innerHTML : dom.getOuterHTML(node)), args) ); // Replace all BOM characters for now until we can find a better solution if (!args.cleanup) - args.content = args.content.replace(/\uFEFF|\u200B/g, ''); + args.content = args.content.replace(/\uFEFF/g, ''); // Post process if (!args.no_events) @@ -8567,7 +10717,7 @@ window.tinymce.dom.Sizzle = Sizzle; scriptLoadedCallbacks = {}, queueLoadedCallbacks = [], loading = 0, - undefined; + undef; function loadScript(url, callback) { var t = this, dom = tinymce.DOM, elm, uri, loc, id; @@ -8626,11 +10776,10 @@ window.tinymce.dom.Sizzle = Sizzle; } // Create new script element - elm = dom.create('script', { - id : id, - type : 'text/javascript', - src : tinymce._addVer(url) - }); + elm = document.createElement('script'); + elm.id = id; + elm.type = 'text/javascript'; + elm.src = tinymce._addVer(url); // Add onload listener for non IE browsers since IE9 // fires onload event before the script is parsed and executed @@ -8676,7 +10825,7 @@ window.tinymce.dom.Sizzle = Sizzle; var item, state = states[url]; // Add url to load queue - if (state == undefined) { + if (state == undef) { queue.push(url); states[url] = QUEUED; } @@ -8706,7 +10855,7 @@ window.tinymce.dom.Sizzle = Sizzle; callback.func.call(callback.scope); }); - scriptLoadedCallbacks[url] = undefined; + scriptLoadedCallbacks[url] = undef; }; queueLoadedCallbacks.push({ @@ -8763,46 +10912,6 @@ window.tinymce.dom.Sizzle = Sizzle; tinymce.ScriptLoader = new tinymce.dom.ScriptLoader(); })(tinymce); -tinymce.dom.TreeWalker = function(start_node, root_node) { - var node = start_node; - - function findSibling(node, start_name, sibling_name, shallow) { - var sibling, parent; - - if (node) { - // Walk into nodes if it has a start - if (!shallow && node[start_name]) - return node[start_name]; - - // Return the sibling if it has one - if (node != root_node) { - sibling = node[sibling_name]; - if (sibling) - return sibling; - - // Walk up the parents to look for siblings - for (parent = node.parentNode; parent && parent != root_node; parent = parent.parentNode) { - sibling = parent[sibling_name]; - if (sibling) - return sibling; - } - } - } - }; - - this.current = function() { - return node; - }; - - this.next = function(shallow) { - return (node = findSibling(node, 'firstChild', 'nextSibling', shallow)); - }; - - this.prev = function(shallow) { - return (node = findSibling(node, 'lastChild', 'previousSibling', shallow)); - }; -}; - (function(tinymce) { tinymce.dom.RangeUtils = function(dom) { var INVISIBLE_CHAR = '\uFEFF'; @@ -9034,12 +11143,15 @@ tinymce.dom.TreeWalker = function(start_node, root_node) { t.destroy = function() { each(items, function(item) { - dom.unbind(dom.get(item.id), 'focus', itemFocussed); - dom.unbind(dom.get(item.id), 'blur', itemBlurred); + var elm = dom.get(item.id); + + dom.unbind(elm, 'focus', itemFocussed); + dom.unbind(elm, 'blur', itemBlurred); }); - dom.unbind(dom.get(root), 'focus', rootFocussed); - dom.unbind(dom.get(root), 'keydown', rootKeydown); + var rootElm = dom.get(root); + dom.unbind(rootElm, 'focus', rootFocussed); + dom.unbind(rootElm, 'keydown', rootKeydown); items = dom = root = t.focus = itemFocussed = itemBlurred = rootKeydown = rootFocussed = null; t.destroy = function() {}; @@ -9118,21 +11230,23 @@ tinymce.dom.TreeWalker = function(start_node, root_node) { // Set up state and listeners for each item. each(items, function(item, idx) { - var tabindex; + var tabindex, elm; if (!item.id) { item.id = dom.uniqueId('_mce_item_'); } + elm = dom.get(item.id); + if (excludeFromTabOrder) { - dom.bind(item.id, 'blur', itemBlurred); + dom.bind(elm, 'blur', itemBlurred); tabindex = '-1'; } else { tabindex = (idx === 0 ? '0' : '-1'); } - dom.setAttrib(item.id, 'tabindex', tabindex); - dom.bind(dom.get(item.id), 'focus', itemFocussed); + elm.setAttribute('tabindex', tabindex); + dom.bind(elm, 'focus', itemFocussed); }); // Setup initial state for root element. @@ -9141,10 +11255,11 @@ tinymce.dom.TreeWalker = function(start_node, root_node) { } dom.setAttrib(root, 'tabindex', '-1'); - + // Setup listeners for root element. - dom.bind(dom.get(root), 'focus', rootFocussed); - dom.bind(dom.get(root), 'keydown', rootKeydown); + var rootElm = dom.get(root); + dom.bind(rootElm, 'focus', rootFocussed); + dom.bind(rootElm, 'keydown', rootKeydown); } }); })(tinymce); @@ -9463,8 +11578,8 @@ tinymce.create('tinymce.ui.Separator:tinymce.ui.Control', { update : function() { var t = this, s = t.settings, tb = DOM.get('menu_' + t.id + '_tbl'), co = DOM.get('menu_' + t.id + '_co'), tw, th; - tw = s.max_width ? Math.min(tb.clientWidth, s.max_width) : tb.clientWidth; - th = s.max_height ? Math.min(tb.clientHeight, s.max_height) : tb.clientHeight; + tw = s.max_width ? Math.min(tb.offsetWidth, s.max_width) : tb.offsetWidth; + th = s.max_height ? Math.min(tb.offsetHeight, s.max_height) : tb.offsetHeight; if (!DOM.boxModel) t.element.setStyles({width : tw + 2, height : th + 2}); @@ -9554,7 +11669,7 @@ tinymce.create('tinymce.ui.Separator:tinymce.ui.Control', { if (m.settings.onclick) m.settings.onclick(e); - return Event.cancel(e); // Cancel to fix onbeforeunload problem + return false; // Cancel to fix onbeforeunload problem } }); @@ -9684,7 +11799,7 @@ tinymce.create('tinymce.ui.Separator:tinymce.ui.Control', { // Internal functions _setupKeyboardNav : function(){ var contextMenu, menuItems, t=this; - contextMenu = DOM.select('#menu_' + t.id)[0]; + contextMenu = DOM.get('menu_' + t.id); menuItems = DOM.select('a[role=option]', 'menu_' + t.id); menuItems.splice(0,0,contextMenu); t.keyboardNav = new tinymce.ui.KeyboardNavigation({ @@ -9747,8 +11862,12 @@ tinymce.create('tinymce.ui.Separator:tinymce.ui.Control', { n = DOM.add(n, s.element || 'span', {'class' : 'mceText', title : o.settings.title}, o.settings.title); - if (o.settings.style) + if (o.settings.style) { + if (typeof o.settings.style == "function") + o.settings.style = o.settings.style(); + DOM.setAttrib(n, 'style', o.settings.style); + } if (tb.childNodes.length == 1) DOM.addClass(ro, 'mceFirst'); @@ -9781,7 +11900,7 @@ tinymce.create('tinymce.ui.Separator:tinymce.ui.Control', { l = DOM.encode(s.label || ''); h = ''; if (s.image && !(this.editor &&this.editor.forcedHighContrastMode) ) - h += '' + DOM.encode(s.title) + '' + l; + h += '' + DOM.encode(s.title) + '' + (l ? '' + l + '' : ''); else h += '' + (l ? '' + l + '' : ''); @@ -9791,10 +11910,27 @@ tinymce.create('tinymce.ui.Separator:tinymce.ui.Control', { }, postRender : function() { - var t = this, s = t.settings; - + var t = this, s = t.settings, imgBookmark; + + // In IE a large image that occupies the entire editor area will be deselected when a button is clicked, so + // need to keep the selection in case the selection is lost + if (tinymce.isIE && t.editor) { + tinymce.dom.Event.add(t.id, 'mousedown', function(e) { + var nodeName = t.editor.selection.getNode().nodeName; + imgBookmark = nodeName === 'IMG' ? t.editor.selection.getBookmark() : null; + }); + } tinymce.dom.Event.add(t.id, 'click', function(e) { - if (!t.isDisabled()) + if (!t.isDisabled()) { + // restore the selection in case the selection is lost in IE + if (tinymce.isIE && t.editor && imgBookmark !== null) { + t.editor.selection.moveToBookmark(imgBookmark); + } + return s.onclick.call(s.scope, e); + } + }); + tinymce.dom.Event.add(t.id, 'keyup', function(e) { + if (!t.isDisabled() && e.keyCode==tinymce.VK.SPACEBAR) return s.onclick.call(s.scope, e); }); } @@ -9802,7 +11938,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; + var DOM = tinymce.DOM, Event = tinymce.dom.Event, each = tinymce.each, Dispatcher = tinymce.util.Dispatcher, undef; tinymce.create('tinymce.ui.ListBox:tinymce.ui.Control', { ListBox : function(id, s, ed) { @@ -9821,16 +11957,19 @@ tinymce.create('tinymce.ui.Separator:tinymce.ui.Control', { t.onRenderMenu = new tinymce.util.Dispatcher(this); t.classPrefix = 'mceListBox'; + t.marked = {}; }, select : function(va) { var t = this, fv, f; - if (va == undefined) + t.marked = {}; + + if (va == undef) return t.selectByIndex(-1); // Is string or number make function selector - if (va && va.call) + if (va && typeof(va)=="function") f = va; else { f = function(v) { @@ -9857,6 +11996,8 @@ tinymce.create('tinymce.ui.Separator:tinymce.ui.Control', { selectByIndex : function(idx) { var t = this, e, o, label; + t.marked = {}; + if (idx != t.selectedIndex) { e = DOM.get(t.id + '_text'); label = DOM.get(t.id + '_voiceDesc'); @@ -9880,6 +12021,10 @@ tinymce.create('tinymce.ui.Separator:tinymce.ui.Control', { } }, + mark : function(value) { + this.marked[value] = true; + }, + add : function(n, v, o) { var t = this; @@ -9912,7 +12057,7 @@ tinymce.create('tinymce.ui.Separator:tinymce.ui.Control', { showMenu : function() { var t = this, p2, e = DOM.get(this.id), m; - if (t.isDisabled() || t.items.length == 0) + if (t.isDisabled() || t.items.length === 0) return; if (t.menu && t.menu.isMenuVisible) @@ -9931,13 +12076,19 @@ tinymce.create('tinymce.ui.Separator:tinymce.ui.Control', { m.settings.keyboard_focus = !tinymce.isOpera; // Opera is buggy when it comes to auto focus // Select in menu - if (t.oldID) - m.items[t.oldID].setSelected(0); + each(t.items, function(o) { + if (m.items[o.id]) { + m.items[o.id].setSelected(0); + } + }); each(t.items, function(o) { + if (m.items[o.id] && t.marked[o.value]) { + m.items[o.id].setSelected(1); + } + if (o.value === t.selectedValue) { m.items[o.id].setSelected(1); - t.oldID = o.id; } }); @@ -9973,7 +12124,7 @@ tinymce.create('tinymce.ui.Separator:tinymce.ui.Control', { m = t.settings.control_manager.createDropMenu(t.id + '_menu', { menu_line : 1, 'class' : t.classPrefix + 'Menu mceNoIcons', - max_width : 150, + max_width : 250, max_height : 150 }); @@ -9993,7 +12144,7 @@ tinymce.create('tinymce.ui.Separator:tinymce.ui.Control', { each(t.items, function(o) { // No value then treat it as a title - if (o.value === undefined) { + if (o.value === undef) { m.add({ title : o.title, role : "option", @@ -10083,7 +12234,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; + var DOM = tinymce.DOM, Event = tinymce.dom.Event, each = tinymce.each, Dispatcher = tinymce.util.Dispatcher, undef; tinymce.create('tinymce.ui.NativeListBox:tinymce.ui.ListBox', { NativeListBox : function(id, s) { @@ -10103,11 +12254,11 @@ tinymce.create('tinymce.ui.Separator:tinymce.ui.Control', { select : function(va) { var t = this, fv, f; - if (va == undefined) + if (va == undef) return t.selectByIndex(-1); // Is string or number make function selector - if (va && va.call) + if (va && typeof(va)=="function") f = va; else { f = function(v) { @@ -10256,7 +12407,7 @@ tinymce.create('tinymce.ui.Separator:tinymce.ui.Control', { m.settings.vp_offset_x = p2.x; m.settings.vp_offset_y = p2.y; m.settings.keyboard_focus = t._focused; - m.showMenu(0, e.clientHeight); + m.showMenu(0, e.firstChild.clientHeight); Event.add(DOM.doc, 'mousedown', t.hideMenu, t); t.setState('Selected', 1); @@ -10440,7 +12591,7 @@ tinymce.create('tinymce.ui.Separator:tinymce.ui.Control', { p2 = DOM.getPos(e); DOM.setStyles(t.id + '_menu', { left : p2.x, - top : p2.y + e.clientHeight, + top : p2.y + e.firstChild.clientHeight, zIndex : 200000 }); e = 0; @@ -10457,6 +12608,16 @@ tinymce.create('tinymce.ui.Separator:tinymce.ui.Control', { DOM.select('a', t.id + '_menu')[0].focus(); // Select first link } + t.keyboardNav = new tinymce.ui.KeyboardNavigation({ + root: t.id + '_menu', + items: DOM.select('a', t.id + '_menu'), + onCancel: function() { + t.hideMenu(); + t.focus(); + } + }); + + t.keyboardNav.focus(); t.isMenuVisible = 1; }, @@ -10477,13 +12638,14 @@ tinymce.create('tinymce.ui.Separator:tinymce.ui.Control', { t.isMenuVisible = 0; t.onHideMenu.dispatch(); + t.keyboardNav.destroy(); } }, renderMenu : function() { var t = this, m, i = 0, s = t.settings, n, tb, tr, w, context; - w = DOM.add(s.menu_container, 'div', {role: 'listbox', id : t.id + '_menu', 'class' : s['menu_class'] + ' ' + s['class'], style : 'position:absolute;left:0;top:-1000px;'}); + w = DOM.add(s.menu_container, 'div', {role: 'listbox', id : t.id + '_menu', 'class' : s.menu_class + ' ' + s['class'], style : 'position:absolute;left:0;top:-1000px;'}); m = DOM.add(w, 'div', {'class' : s['class'] + ' mceSplitButtonMenu'}); DOM.add(m, 'span', {'class' : 'mceMenuLine'}); @@ -10501,15 +12663,21 @@ tinymce.create('tinymce.ui.Separator:tinymce.ui.Control', { } n = DOM.add(tr, 'td'); - n = DOM.add(n, 'a', { - role : 'option', + var settings = { href : 'javascript:;', style : { backgroundColor : '#' + c }, 'title': t.editor.getLang('colors.' + c, c), 'data-mce-color' : '#' + c - }); + }; + + // adding a proper ARIA role = button causes JAWS to read things incorrectly on IE. + if (!tinymce.isIE ) { + settings.role = 'option'; + } + + n = DOM.add(n, 'a', settings); if (t.editor.forcedHighContrastMode) { n = DOM.add(n, 'canvas', { width: 16, height: 16, 'aria-hidden': 'true' }); @@ -10535,15 +12703,6 @@ tinymce.create('tinymce.ui.Separator:tinymce.ui.Control', { } DOM.addClass(m, 'mceColorSplitMenu'); - - new tinymce.ui.KeyboardNavigation({ - root: t.id + '_menu', - items: DOM.select('a', t.id + '_menu'), - onCancel: function() { - t.hideMenu(); - t.focus(); - } - }); // Prevent IE from scrolling and hindering click to occur #4019 Event.add(t.id + '_menu', 'mousedown', function(e) {return Event.cancel(e);}); @@ -10556,7 +12715,7 @@ tinymce.create('tinymce.ui.Separator:tinymce.ui.Control', { if (e && e.nodeName.toLowerCase() == 'a' && (c = e.getAttribute('data-mce-color'))) t.setColor(c); - return Event.cancel(e); // Prevent IE auto save warning + return false; // Prevent IE auto save warning }); return w; @@ -10585,11 +12744,17 @@ tinymce.create('tinymce.ui.Separator:tinymce.ui.Control', { }, destroy : function() { - this.parent(); + var self = this; - Event.clear(this.id + '_menu'); - Event.clear(this.id + '_more'); - DOM.remove(this.id + '_menu'); + self.parent(); + + Event.clear(self.id + '_menu'); + Event.clear(self.id + '_more'); + DOM.remove(self.id + '_menu'); + + if (self.keyboardNav) { + self.keyboardNav.destroy(); + } } }); })(tinymce); @@ -10802,7 +12967,7 @@ tinymce.create('tinymce.ui.Toolbar:tinymce.ui.Container', { if (typeof u === "object") url = u.prefix + u.resource + u.suffix; - if (url.indexOf('/') != 0 && url.indexOf('://') == -1) + if (url.indexOf('/') !== 0 && url.indexOf('://') == -1) url = tinymce.baseURL + '/' + url; t.urls[n] = url.substring(0, url.lastIndexOf('/')); @@ -10826,7 +12991,7 @@ tinymce.create('tinymce.ui.Toolbar:tinymce.ui.Container', { DOM = tinymce.DOM, Event = tinymce.dom.Event, ThemeManager = tinymce.ThemeManager, PluginManager = tinymce.PluginManager, explode = tinymce.explode, - Dispatcher = tinymce.util.Dispatcher, undefined, instanceCounter = 0; + Dispatcher = tinymce.util.Dispatcher, undef, instanceCounter = 0; // Setup some URLs where the editor API is located and where the document is tinymce.documentBaseURL = window.location.href.replace(/[\?#].*$/, '').replace(/[\/\\][^\/]+$/, ''); @@ -10861,6 +13026,26 @@ tinymce.create('tinymce.ui.Toolbar:tinymce.ui.Container', { init : function(s) { var t = this, pl, sl = tinymce.ScriptLoader, e, el = [], ed; + function createId(elm) { + var id = elm.id; + + // Use element id, or unique name or generate a unique id + if (!id) { + id = elm.name; + + if (id && !DOM.get(id)) { + id = elm.name; + } else { + // Generate unique name + id = DOM.uniqueId(); + } + + elm.setAttribute('id', id); + } + + return id; + }; + function execCallback(se, n, s) { var f = se[n]; @@ -10876,15 +13061,14 @@ tinymce.create('tinymce.ui.Toolbar:tinymce.ui.Container', { return f.apply(s || this, Array.prototype.slice.call(arguments, 2)); }; - s = extend({ - theme : "simple", - language : "en" - }, s); + function hasClass(n, c) { + return c.constructor === RegExp ? c.test(n.className) : DOM.hasClass(n, c); + }; t.settings = s; // Legacy call - Event.add(document, 'init', function() { + Event.bind(window, 'ready', function() { var l, co; execCallback(s, 'onpageload'); @@ -10919,30 +13103,36 @@ tinymce.create('tinymce.ui.Toolbar:tinymce.ui.Container', { case "textareas": case "specific_textareas": - function hasClass(n, c) { - return c.constructor === RegExp ? c.test(n.className) : DOM.hasClass(n, c); - }; - - each(DOM.select('textarea'), function(v) { - if (s.editor_deselector && hasClass(v, s.editor_deselector)) + each(DOM.select('textarea'), function(elm) { + if (s.editor_deselector && hasClass(elm, s.editor_deselector)) return; - if (!s.editor_selector || hasClass(v, s.editor_selector)) { - // Can we use the name - e = DOM.get(v.name); - if (!v.id && !e) - v.id = v.name; - - // Generate unique name if missing or already exists - if (!v.id || t.get(v.id)) - v.id = DOM.uniqueId(); - - ed = new tinymce.Editor(v.id, s); + if (!s.editor_selector || hasClass(elm, s.editor_selector)) { + ed = new tinymce.Editor(createId(elm), s); el.push(ed); ed.render(1); } }); break; + + default: + if (s.types) { + // Process type specific selector + each(s.types, function(type) { + each(DOM.select(type.selector), function(elm) { + var editor = new tinymce.Editor(createId(elm), tinymce.extend({}, s, type)); + el.push(editor); + editor.render(1); + }); + }); + } else if (s.selector) { + // Process global selector + each(DOM.select(s.selector), function(elm) { + var editor = new tinymce.Editor(createId(elm), s); + el.push(editor); + editor.render(1); + }); + } } // Call onInit when all editors are initialized @@ -10973,9 +13163,12 @@ tinymce.create('tinymce.ui.Toolbar:tinymce.ui.Container', { }, get : function(id) { - if (id === undefined) + if (id === undef) return this.editors; + if (!this.editors.hasOwnProperty(id)) + return undef; + return this.editors[id]; }, @@ -11026,6 +13219,12 @@ tinymce.create('tinymce.ui.Toolbar:tinymce.ui.Container', { execCommand : function(c, u, v) { var t = this, ed = t.get(v), w; + function clr() { + ed.destroy(); + w.detachEvent('onunload', clr); + w = w.tinyMCE = w.tinymce = null; // IE leak + }; + // Manager commands switch (c) { case "mceFocus": @@ -11054,12 +13253,6 @@ tinymce.create('tinymce.ui.Toolbar:tinymce.ui.Container', { // Fix IE memory leaks if (tinymce.isIE) { - function clr() { - ed.destroy(); - w.detachEvent('onunload', clr); - w = w.tinyMCE = w.tinymce = null; // IE leak - }; - w.attachEvent('onunload', clr); } @@ -11142,163 +13335,84 @@ tinymce.create('tinymce.ui.Toolbar:tinymce.ui.Container', { (function(tinymce) { // Shorten these names var DOM = tinymce.DOM, Event = tinymce.dom.Event, extend = tinymce.extend, - Dispatcher = tinymce.util.Dispatcher, each = tinymce.each, isGecko = tinymce.isGecko, + each = tinymce.each, isGecko = tinymce.isGecko, isIE = tinymce.isIE, isWebKit = tinymce.isWebKit, is = tinymce.is, ThemeManager = tinymce.ThemeManager, PluginManager = tinymce.PluginManager, - inArray = tinymce.inArray, grep = tinymce.grep, explode = tinymce.explode; + explode = tinymce.explode; tinymce.create('tinymce.Editor', { - Editor : function(id, s) { - var t = this; - - t.id = t.editorId = id; - - t.execCommands = {}; - t.queryStateCommands = {}; - t.queryValueCommands = {}; - - t.isNotDirty = false; - - t.plugins = {}; - - // Add events to the editor - each([ - 'onPreInit', - - 'onBeforeRenderUI', - - 'onPostRender', - - 'onInit', - - 'onRemove', - - 'onActivate', - - 'onDeactivate', - - 'onClick', - - 'onEvent', - - 'onMouseUp', - - 'onMouseDown', - - 'onDblClick', - - 'onKeyDown', - - 'onKeyUp', - - 'onKeyPress', - - 'onContextMenu', - - 'onSubmit', - - 'onReset', - - 'onPaste', - - 'onPreProcess', - - 'onPostProcess', - - 'onBeforeSetContent', - - 'onBeforeGetContent', - - 'onSetContent', - - 'onGetContent', - - 'onLoadContent', - - 'onSaveContent', - - 'onNodeChange', - - 'onChange', - - 'onBeforeExecCommand', - - 'onExecCommand', - - 'onUndo', - - 'onRedo', - - 'onVisualAid', - - 'onSetProgressState' - ], function(e) { - t[e] = new Dispatcher(t); - }); + Editor : function(id, settings) { + var self = this, TRUE = true; - t.settings = s = extend({ + self.settings = settings = extend({ id : id, language : 'en', - docs_language : 'en', - theme : 'simple', + theme : 'advanced', skin : 'default', delta_width : 0, delta_height : 0, popup_css : '', plugins : '', document_base_url : tinymce.documentBaseURL, - add_form_submit_trigger : 1, - submit_patch : 1, - add_unload_trigger : 1, - convert_urls : 1, - relative_urls : 1, - remove_script_host : 1, - table_inline_editing : 0, - object_resizing : 1, - cleanup : 1, - accessibility_focus : 1, - custom_shortcuts : 1, - custom_undo_redo_keyboard_shortcuts : 1, - custom_undo_redo_restore_selection : 1, - custom_undo_redo : 1, + add_form_submit_trigger : TRUE, + submit_patch : TRUE, + add_unload_trigger : TRUE, + convert_urls : TRUE, + relative_urls : TRUE, + remove_script_host : TRUE, + table_inline_editing : false, + object_resizing : TRUE, + accessibility_focus : TRUE, doctype : tinymce.isIE6 ? '' : '', // Use old doctype on IE 6 to avoid horizontal scroll - visual_table_class : 'mceItemTable', - visual : 1, + visual : TRUE, font_size_style_values : 'xx-small,x-small,small,medium,large,x-large,xx-large', font_size_legacy_values : 'xx-small,small,medium,large,x-large,xx-large,300%', // See: http://www.w3.org/TR/CSS2/fonts.html#propdef-font-size - apply_source_formatting : 1, + apply_source_formatting : TRUE, directionality : 'ltr', forced_root_block : 'p', - hidden_input : 1, - padd_empty_editor : 1, - render_ui : 1, - init_theme : 1, - force_p_newlines : 1, + hidden_input : TRUE, + padd_empty_editor : TRUE, + render_ui : TRUE, indentation : '30px', - keep_styles : 1, - fix_table_elements : 1, - inline_styles : 1, - convert_fonts_to_spans : true, + fix_table_elements : TRUE, + inline_styles : TRUE, + convert_fonts_to_spans : TRUE, indent : 'simple', - indent_before : 'p,h1,h2,h3,h4,h5,h6,blockquote,div,title,style,pre,script,td,ul,li,area,table,thead,tfoot,tbody,tr', - indent_after : 'p,h1,h2,h3,h4,h5,h6,blockquote,div,title,style,pre,script,td,ul,li,area,table,thead,tfoot,tbody,tr', - validate : true, + indent_before : 'p,h1,h2,h3,h4,h5,h6,blockquote,div,title,style,pre,script,td,ul,li,area,table,thead,tfoot,tbody,tr,section,article,hgroup,aside,figure,option,optgroup,datalist', + indent_after : 'p,h1,h2,h3,h4,h5,h6,blockquote,div,title,style,pre,script,td,ul,li,area,table,thead,tfoot,tbody,tr,section,article,hgroup,aside,figure,option,optgroup,datalist', + validate : TRUE, entity_encoding : 'named', - url_converter : t.convertURL, - url_converter_scope : t, - ie7_compat : true - }, s); + url_converter : self.convertURL, + url_converter_scope : self, + ie7_compat : TRUE + }, settings); + + self.id = self.editorId = id; + + self.isNotDirty = false; + + self.plugins = {}; - t.documentBaseURI = new tinymce.util.URI(s.document_base_url || tinymce.documentBaseURL, { + self.documentBaseURI = new tinymce.util.URI(settings.document_base_url || tinymce.documentBaseURL, { base_uri : tinyMCE.baseURI }); - t.baseURI = tinymce.baseURI; + self.baseURI = tinymce.baseURI; - t.contentCSS = []; + self.contentCSS = []; + + self.contentStyles = []; + + // Creates all events like onClick, onSetContent etc see Editor.Events.js for the actual logic + self.setupEvents(); + + // Internal command handler objects + self.execCommands = {}; + self.queryStateCommands = {}; + self.queryValueCommands = {}; // Call setup - t.execCallback('setup', t); + self.execCallback('setup', self); }, render : function(nst) { @@ -11306,7 +13420,7 @@ tinymce.create('tinymce.ui.Toolbar:tinymce.ui.Container', { // Page is not loaded yet, wait for it if (!Event.domLoaded) { - Event.add(document, 'init', function() { + Event.add(window, 'ready', function() { t.render(); }); return; @@ -11319,8 +13433,7 @@ tinymce.create('tinymce.ui.Toolbar:tinymce.ui.Container', { return; // Is a iPad/iPhone and not on iOS5, 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 + // here since the browser says it has contentEditable support but there is no visible caret. if (tinymce.isIDevice && !tinymce.isIOS5) return; @@ -11328,6 +13441,12 @@ tinymce.create('tinymce.ui.Toolbar:tinymce.ui.Container', { 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); + // Hide target element early to prevent content flashing + if (!s.content_editable) { + t.orgVisibility = t.getElement().style.visibility; + t.getElement().style.visibility = 'hidden'; + } + if (tinymce.WindowManager) t.windowManager = new tinymce.WindowManager(t); @@ -11389,7 +13508,7 @@ tinymce.create('tinymce.ui.Toolbar:tinymce.ui.Container', { if (s.language && s.language_load !== false) sl.add(tinymce.baseURL + '/langs/' + s.language + '.js'); - if (s.theme && s.theme.charAt(0) != '-' && !ThemeManager.urls[s.theme]) + if (s.theme && typeof s.theme != "function" && s.theme.charAt(0) != '-' && !ThemeManager.urls[s.theme]) ThemeManager.load(s.theme, 'themes/' + s.theme + '/editor_template' + tinymce.suffix + '.js'); each(explode(s.plugins), function(p) { @@ -11399,9 +13518,8 @@ tinymce.create('tinymce.ui.Toolbar:tinymce.ui.Container', { var dependencies = PluginManager.dependencies(p); each(dependencies, function(dep) { var defaultSettings = {prefix:'plugins/', resource: dep, suffix:'/editor_plugin' + tinymce.suffix + '.js'}; - var dep = PluginManager.createUrl(defaultSettings, dep); + dep = PluginManager.createUrl(defaultSettings, dep); PluginManager.load(dep.resource, dep); - }); } else { // Skip safari plugin, since it is removed as of 3.3b1 @@ -11424,20 +13542,25 @@ tinymce.create('tinymce.ui.Toolbar:tinymce.ui.Container', { }, init : function() { - var n, t = this, s = t.settings, w, h, e = t.getElement(), o, ti, u, bi, bc, re, i, initializedPlugins = []; + var n, t = this, s = t.settings, w, h, mh, e = t.getElement(), o, ti, u, bi, bc, re, i, initializedPlugins = []; tinymce.add(t); s.aria_label = s.aria_label || DOM.getAttrib(e, 'aria-label', t.getLang('aria.rich_text_area')); if (s.theme) { - s.theme = s.theme.replace(/-/, ''); - o = ThemeManager.get(s.theme); - t.theme = new o(); + if (typeof s.theme != "function") { + s.theme = s.theme.replace(/-/, ''); + o = ThemeManager.get(s.theme); + t.theme = new o(); - if (t.theme.init && s.init_theme) - t.theme.init(t, ThemeManager.urls[s.theme] || tinymce.documentBaseURL.replace(/\/$/, '')); + if (t.theme.init) + t.theme.init(t, ThemeManager.urls[s.theme] || tinymce.documentBaseURL.replace(/\/$/, '')); + } else { + t.theme = s.theme; + } } + function initPlugin(p) { var c = PluginManager.get(p), u = PluginManager.urls[p] || tinymce.documentBaseURL.replace(/\/$/, ''), po; if (c && tinymce.inArray(initializedPlugins,p) === -1) { @@ -11471,85 +13594,93 @@ tinymce.create('tinymce.ui.Toolbar:tinymce.ui.Container', { t.controlManager = new tinymce.ControlManager(t); - if (s.custom_undo_redo) { - t.onBeforeExecCommand.add(function(ed, cmd, ui, val, a) { - if (cmd != 'Undo' && cmd != 'Redo' && cmd != 'mceRepaint' && (!a || !a.skip_undo)) - t.undoManager.beforeChange(); - }); - - t.onExecCommand.add(function(ed, cmd, ui, val, a) { - if (cmd != 'Undo' && cmd != 'Redo' && cmd != 'mceRepaint' && (!a || !a.skip_undo)) - t.undoManager.add(); - }); - } - - t.onExecCommand.add(function(ed, c) { - // Don't refresh the select lists until caret move - if (!/^(FontName|FontSize)$/.test(c)) - t.nodeChanged(); - }); - - // Remove ghost selections on images and tables in Gecko - if (isGecko) { - function repaint(a, o) { - if (!o || !o.initial) - t.execCommand('mceRepaint'); - }; - - t.onUndo.add(repaint); - t.onRedo.add(repaint); - t.onSetContent.add(repaint); - } - // Enables users to override the control factory t.onBeforeRenderUI.dispatch(t, t.controlManager); // Measure box - if (s.render_ui) { - w = s.width || e.style.width || e.offsetWidth; - h = s.height || e.style.height || e.offsetHeight; + if (s.render_ui && t.theme) { t.orgDisplay = e.style.display; - re = /^[0-9\.]+(|px)$/i; - if (re.test('' + w)) - w = Math.max(parseInt(w) + (o.deltaWidth || 0), 100); + if (typeof s.theme != "function") { + w = s.width || e.style.width || e.offsetWidth; + h = s.height || e.style.height || e.offsetHeight; + mh = s.min_height || 100; + re = /^[0-9\.]+(|px)$/i; + + if (re.test('' + w)) + w = Math.max(parseInt(w, 10) + (o.deltaWidth || 0), 100); + + if (re.test('' + h)) + h = Math.max(parseInt(h, 10) + (o.deltaHeight || 0), mh); + + // Render UI + o = t.theme.renderUI({ + targetNode : e, + width : w, + height : h, + deltaWidth : s.delta_width, + deltaHeight : s.delta_height + }); - if (re.test('' + h)) - h = Math.max(parseInt(h) + (o.deltaHeight || 0), 100); + // Resize editor + DOM.setStyles(o.sizeContainer || o.editorContainer, { + width : w, + height : h + }); - // Render UI - o = t.theme.renderUI({ - targetNode : e, - width : w, - height : h, - deltaWidth : s.delta_width, - deltaHeight : s.delta_height - }); + h = (o.iframeHeight || h) + (typeof(h) == 'number' ? (o.deltaHeight || 0) : ''); + if (h < mh) + h = mh; + } else { + o = s.theme(t, e); - t.editorContainer = o.editorContainer; - } + // Convert element type to id:s + if (o.editorContainer.nodeType) { + o.editorContainer = o.editorContainer.id = o.editorContainer.id || t.id + "_parent"; + } + // Convert element type to id:s + if (o.iframeContainer.nodeType) { + o.iframeContainer = o.iframeContainer.id = o.iframeContainer.id || t.id + "_iframecontainer"; + } - // User specified a document.domain value - if (document.domain && location.hostname != document.domain) - tinymce.relaxedDomain = document.domain; + // Use specified iframe height or the targets offsetHeight + h = o.iframeHeight || e.offsetHeight; - // Resize editor - DOM.setStyles(o.sizeContainer || o.editorContainer, { - width : w, - height : h - }); + // Store away the selection when it's changed to it can be restored later with a editor.focus() call + if (isIE) { + t.onInit.add(function(ed) { + ed.dom.bind(ed.getBody(), 'beforedeactivate keydown', function() { + ed.lastIERng = ed.selection.getRng(); + }); + }); + } + } + + t.editorContainer = o.editorContainer; + } // Load specified content CSS last if (s.content_css) { - tinymce.each(explode(s.content_css), function(u) { + each(explode(s.content_css), function(u) { t.contentCSS.push(t.documentBaseURI.toAbsolute(u)); }); } - h = (o.iframeHeight || h) + (typeof(h) == 'number' ? (o.deltaHeight || 0) : ''); - if (h < 100) - h = 100; + // Load specified content CSS last + if (s.content_style) { + t.contentStyles.push(s.content_style); + } + + // Content editable mode ends here + if (s.content_editable) { + e = n = o = null; // Fix IE leak + return t.initContentBody(); + } + + // User specified a document.domain value + if (document.domain && location.hostname != document.domain) + tinymce.relaxedDomain = document.domain; t.iframeHTML = s.doctype + ''; @@ -11559,10 +13690,12 @@ tinymce.create('tinymce.ui.Toolbar:tinymce.ui.Container', { t.iframeHTML += ''; // IE8 doesn't support carets behind images setting ie7_compat would force IE8+ to run in IE7 compat mode. - if (s.ie7_compat) - t.iframeHTML += ''; - else - t.iframeHTML += ''; + if (tinymce.isIE8) { + if (s.ie7_compat) + t.iframeHTML += ''; + else + t.iframeHTML += ''; + } t.iframeHTML += ''; @@ -11571,6 +13704,8 @@ tinymce.create('tinymce.ui.Toolbar:tinymce.ui.Container', { t.iframeHTML += ''; } + t.contentCSS = []; + bi = s.body_id || 'tinymce'; if (bi.indexOf('=') != -1) { bi = t.getParam('body_id', '', 'hash'); @@ -11583,12 +13718,12 @@ tinymce.create('tinymce.ui.Toolbar:tinymce.ui.Container', { bc = bc[t.id] || ''; } - t.iframeHTML += '
    '; + t.iframeHTML += '
    '; // Domain relaxing enabled, then set document domain if (tinymce.relaxedDomain && (isIE || (tinymce.isOpera && parseFloat(opera.version()) < 11))) { // We need to write the contents here in IE since multiple writes messes up refresh button and back button - u = 'javascript:(function(){document.open();document.domain="' + document.domain + '";var ed = window.parent.tinyMCE.get("' + t.id + '");document.write(ed.iframeHTML);document.close();ed.setupIframe();})()'; + u = 'javascript:(function(){document.open();document.domain="' + document.domain + '";var ed = window.parent.tinyMCE.get("' + t.id + '");document.write(ed.iframeHTML);document.close();ed.initContentBody();})()'; } // Create iframe @@ -11607,78 +13742,73 @@ tinymce.create('tinymce.ui.Toolbar:tinymce.ui.Container', { }); t.contentAreaContainer = o.iframeContainer; - DOM.get(o.editorContainer).style.display = t.orgDisplay; + + if (o.editorContainer) { + DOM.get(o.editorContainer).style.display = t.orgDisplay; + } + + // Restore visibility on target element + e.style.visibility = t.orgVisibility; + DOM.get(t.id).style.display = 'none'; DOM.setAttrib(t.id, 'aria-hidden', true); if (!tinymce.relaxedDomain || !u) - t.setupIframe(); + t.initContentBody(); e = n = o = null; // Cleanup }, - setupIframe : function() { - var t = this, s = t.settings, e = DOM.get(t.id), d = t.getDoc(), h, b; + initContentBody : function() { + var self = this, settings = self.settings, targetElm = DOM.get(self.id), doc = self.getDoc(), html, body, contentCssText; // Setup iframe body - if (!isIE || !tinymce.relaxedDomain) { - d.open(); - d.write(t.iframeHTML); - d.close(); + if ((!isIE || !tinymce.relaxedDomain) && !settings.content_editable) { + doc.open(); + doc.write(self.iframeHTML); + doc.close(); if (tinymce.relaxedDomain) - d.domain = tinymce.relaxedDomain; + doc.domain = tinymce.relaxedDomain; + } + + if (settings.content_editable) { + DOM.addClass(targetElm, 'mceContentBody'); + self.contentDocument = doc = settings.content_document || document; + self.contentWindow = settings.content_window || window; + self.bodyElement = targetElm; + + // Prevent leak in IE + settings.content_document = settings.content_window = null; } // It will not steal focus while setting contentEditable - b = t.getBody(); - b.disabled = true; + body = self.getBody(); + body.disabled = true; - if (!s.readonly) - b.contentEditable = true; + if (!settings.readonly) + body.contentEditable = self.getParam('content_editable_state', true); - b.disabled = false; + body.disabled = false; - t.schema = new tinymce.html.Schema(s); + self.schema = new tinymce.html.Schema(settings); - t.dom = new tinymce.dom.DOMUtils(t.getDoc(), { + self.dom = new tinymce.dom.DOMUtils(doc, { keep_values : true, - url_converter : t.convertURL, - url_converter_scope : t, - hex_colors : s.force_hex_style_colors, - class_filter : s.class_filter, - update_styles : 1, - fix_ie_paragraphs : 1, - schema : t.schema + url_converter : self.convertURL, + url_converter_scope : self, + hex_colors : settings.force_hex_style_colors, + class_filter : settings.class_filter, + update_styles : true, + root_element : settings.content_editable ? self.id : null, + schema : self.schema }); - t.parser = new tinymce.html.DomParser(s, t.schema); - - // Force anchor names closed, unless the setting "allow_html_in_named_anchor" is explicitly included. - if (!t.settings.allow_html_in_named_anchor) { - t.parser.addAttributeFilter('name', function(nodes, name) { - var i = nodes.length, sibling, prevSibling, parent, node; - - while (i--) { - node = nodes[i]; - if (node.name === 'a' && node.firstChild) { - parent = node.parent; - - // Move children after current node - sibling = node.lastChild; - do { - prevSibling = sibling.prev; - parent.insert(sibling, node); - sibling = prevSibling; - } while (sibling); - } - } - }); - } + self.parser = new tinymce.html.DomParser(settings, self.schema); // Convert src and href into data-mce-src, data-mce-href and data-mce-style - t.parser.addAttributeFilter('src,href,style', function(nodes, name) { - var i = nodes.length, node, dom = t.dom, value, internalName; + self.parser.addAttributeFilter('src,href,style', function(nodes, name) { + var i = nodes.length, node, dom = self.dom, value, internalName; while (i--) { node = nodes[i]; @@ -11690,13 +13820,13 @@ tinymce.create('tinymce.ui.Toolbar:tinymce.ui.Container', { if (name === "style") node.attr(internalName, dom.serializeStyle(dom.parseStyle(value), node.name)); else - node.attr(internalName, t.convertURL(value, name, node.name)); + node.attr(internalName, self.convertURL(value, name, node.name)); } } }); // Keep scripts from executing - t.parser.addNodeFilter('script', function(nodes, name) { + self.parser.addNodeFilter('script', function(nodes, name) { var i = nodes.length, node; while (i--) { @@ -11705,7 +13835,7 @@ tinymce.create('tinymce.ui.Toolbar:tinymce.ui.Container', { } }); - t.parser.addNodeFilter('#cdata', function(nodes, name) { + self.parser.addNodeFilter('#cdata', function(nodes, name) { var i = nodes.length, node; while (i--) { @@ -11716,318 +13846,117 @@ tinymce.create('tinymce.ui.Toolbar:tinymce.ui.Container', { } }); - t.parser.addNodeFilter('p,h1,h2,h3,h4,h5,h6,div', function(nodes, name) { - var i = nodes.length, node, nonEmptyElements = t.schema.getNonEmptyElements(); - - while (i--) { - node = nodes[i]; - - if (node.isEmpty(nonEmptyElements)) - node.empty().append(new tinymce.html.Node('br', 1)).shortEnded = true; - } - }); - - t.serializer = new tinymce.dom.Serializer(s, t.dom, t.schema); - - t.selection = new tinymce.dom.Selection(t.dom, t.getWin(), t.serializer); - - t.formatter = new tinymce.Formatter(this); - - // Register default formats - t.formatter.register({ - alignleft : [ - {selector : 'p,h1,h2,h3,h4,h5,h6,td,th,div,ul,ol,li', styles : {textAlign : 'left'}}, - {selector : 'img,table', collapsed : false, styles : {'float' : 'left'}} - ], - - aligncenter : [ - {selector : 'p,h1,h2,h3,h4,h5,h6,td,th,div,ul,ol,li', styles : {textAlign : 'center'}}, - {selector : 'img', collapsed : false, styles : {display : 'block', marginLeft : 'auto', marginRight : 'auto'}}, - {selector : 'table', collapsed : false, styles : {marginLeft : 'auto', marginRight : 'auto'}} - ], - - alignright : [ - {selector : 'p,h1,h2,h3,h4,h5,h6,td,th,div,ul,ol,li', styles : {textAlign : 'right'}}, - {selector : 'img,table', collapsed : false, styles : {'float' : 'right'}} - ], - - alignfull : [ - {selector : 'p,h1,h2,h3,h4,h5,h6,td,th,div,ul,ol,li', styles : {textAlign : 'justify'}} - ], - - bold : [ - {inline : 'strong', remove : 'all'}, - {inline : 'span', styles : {fontWeight : 'bold'}}, - {inline : 'b', remove : 'all'} - ], - - italic : [ - {inline : 'em', remove : 'all'}, - {inline : 'span', styles : {fontStyle : 'italic'}}, - {inline : 'i', remove : 'all'} - ], - - underline : [ - {inline : 'span', styles : {textDecoration : 'underline'}, exact : true}, - {inline : 'u', remove : 'all'} - ], - - strikethrough : [ - {inline : 'span', styles : {textDecoration : 'line-through'}, exact : true}, - {inline : 'strike', remove : 'all'} - ], - - forecolor : {inline : 'span', styles : {color : '%value'}, wrap_links : false}, - hilitecolor : {inline : 'span', styles : {backgroundColor : '%value'}, wrap_links : false}, - fontname : {inline : 'span', styles : {fontFamily : '%value'}}, - fontsize : {inline : 'span', styles : {fontSize : '%value'}}, - fontsize_class : {inline : 'span', attributes : {'class' : '%value'}}, - blockquote : {block : 'blockquote', wrapper : 1, remove : 'all'}, - subscript : {inline : 'sub'}, - superscript : {inline : 'sup'}, - - link : {inline : 'a', selector : 'a', remove : 'all', split : true, deep : true, - onmatch : function(node) { - return true; - }, - - onformat : function(elm, fmt, vars) { - each(vars, function(value, key) { - t.dom.setAttrib(elm, key, value); - }); - } - }, - - 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'], 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, remove : 'all'}); - }); - - // Register user defined formats - t.formatter.register(t.settings.formats); - - t.undoManager = new tinymce.UndoManager(t); - - // Pass through - t.undoManager.onAdd.add(function(um, l) { - if (um.hasUndo()) - return t.onChange.dispatch(t, l, um); - }); - - t.undoManager.onUndo.add(function(um, l) { - return t.onUndo.dispatch(t, l, um); - }); - - t.undoManager.onRedo.add(function(um, l) { - return t.onRedo.dispatch(t, l, um); - }); - - t.forceBlocks = new tinymce.ForceBlocks(t, { - forced_root_block : s.forced_root_block - }); - - t.editorCommands = new tinymce.EditorCommands(t); - - // Pass through - t.serializer.onPreProcess.add(function(se, o) { - return t.onPreProcess.dispatch(t, o, se); - }); - - t.serializer.onPostProcess.add(function(se, o) { - return t.onPostProcess.dispatch(t, o, se); - }); - - t.onPreInit.dispatch(t); - - if (!s.gecko_spellcheck) - t.getBody().spellcheck = 0; - - if (!s.readonly) - t._addEvents(); - - t.controlManager.onPostRender.dispatch(t, t.controlManager); - t.onPostRender.dispatch(t); - - t.quirks = new tinymce.util.Quirks(this); - - if (s.directionality) - t.getBody().dir = s.directionality; - - if (s.nowrap) - t.getBody().style.whiteSpace = "nowrap"; - - if (s.handle_node_change_callback) { - t.onNodeChange.add(function(ed, cm, n) { - t.execCallback('handle_node_change_callback', t.id, n, -1, -1, true, t.selection.isCollapsed()); - }); - } - - if (s.save_callback) { - t.onSaveContent.add(function(ed, o) { - var h = t.execCallback('save_callback', t.id, o.content, t.getBody()); - - if (h) - o.content = h; - }); - } - - if (s.onchange_callback) { - t.onChange.add(function(ed, l) { - t.execCallback('onchange_callback', t, l); - }); - } - - if (s.protect) { - t.onBeforeSetContent.add(function(ed, o) { - if (s.protect) { - each(s.protect, function(pattern) { - o.content = o.content.replace(pattern, function(str) { - return ''; - }); - }); - } - }); - } + self.parser.addNodeFilter('p,h1,h2,h3,h4,h5,h6,div', function(nodes, name) { + var i = nodes.length, node, nonEmptyElements = self.schema.getNonEmptyElements(); - if (s.convert_newlines_to_brs) { - t.onBeforeSetContent.add(function(ed, o) { - if (o.initial) - o.content = o.content.replace(/\r?\n/g, '
    '); - }); - } + while (i--) { + node = nodes[i]; - if (s.preformatted) { - t.onPostProcess.add(function(ed, o) { - o.content = o.content.replace(/^\s*/, ''); - o.content = o.content.replace(/<\/pre>\s*$/, ''); + if (node.isEmpty(nonEmptyElements)) + node.empty().append(new tinymce.html.Node('br', 1)).shortEnded = true; + } + }); - if (o.set) - o.content = '
    ' + o.content + '
    '; - }); - } + self.serializer = new tinymce.dom.Serializer(settings, self.dom, self.schema); - if (s.verify_css_classes) { - t.serializer.attribValueFilter = function(n, v) { - var s, cl; + self.selection = new tinymce.dom.Selection(self.dom, self.getWin(), self.serializer, self); - if (n == 'class') { - // Build regexp for classes - if (!t.classesRE) { - cl = t.dom.getClasses(); + self.formatter = new tinymce.Formatter(self); - if (cl.length > 0) { - s = ''; + self.undoManager = new tinymce.UndoManager(self); - each (cl, function(o) { - s += (s ? '|' : '') + o['class']; - }); + self.forceBlocks = new tinymce.ForceBlocks(self); + self.enterKey = new tinymce.EnterKey(self); + self.editorCommands = new tinymce.EditorCommands(self); - t.classesRE = new RegExp('(' + s + ')', 'gi'); - } - } + self.onExecCommand.add(function(editor, command) { + // Don't refresh the select lists until caret move + if (!/^(FontName|FontSize)$/.test(command)) + self.nodeChanged(); + }); - return !t.classesRE || /(\bmceItem\w+\b|\bmceTemp\w+\b)/g.test(v) || t.classesRE.test(v) ? v : ''; - } + // Pass through + self.serializer.onPreProcess.add(function(se, o) { + return self.onPreProcess.dispatch(self, o, se); + }); - return v; - }; - } + self.serializer.onPostProcess.add(function(se, o) { + return self.onPostProcess.dispatch(self, o, se); + }); - if (s.cleanup_callback) { - t.onBeforeSetContent.add(function(ed, o) { - o.content = t.execCallback('cleanup_callback', 'insert_to_editor', o.content, o); - }); + self.onPreInit.dispatch(self); - t.onPreProcess.add(function(ed, o) { - if (o.set) - t.execCallback('cleanup_callback', 'insert_to_editor_dom', o.node, o); + if (!settings.browser_spellcheck && !settings.gecko_spellcheck) + doc.body.spellcheck = false; - if (o.get) - t.execCallback('cleanup_callback', 'get_from_editor_dom', o.node, o); - }); + if (!settings.readonly) { + self.bindNativeEvents(); + } - t.onPostProcess.add(function(ed, o) { - if (o.set) - o.content = t.execCallback('cleanup_callback', 'insert_to_editor', o.content, o); + self.controlManager.onPostRender.dispatch(self, self.controlManager); + self.onPostRender.dispatch(self); - if (o.get) - o.content = t.execCallback('cleanup_callback', 'get_from_editor', o.content, o); - }); - } + self.quirks = tinymce.util.Quirks(self); - if (s.save_callback) { - t.onGetContent.add(function(ed, o) { - if (o.save) - o.content = t.execCallback('save_callback', t.id, o.content, t.getBody()); - }); - } + if (settings.directionality) + body.dir = settings.directionality; - if (s.handle_event_callback) { - t.onEvent.add(function(ed, e, o) { - if (t.execCallback('handle_event_callback', e, ed, o) === false) - Event.cancel(e); + if (settings.nowrap) + body.style.whiteSpace = "nowrap"; + + if (settings.protect) { + self.onBeforeSetContent.add(function(ed, o) { + each(settings.protect, function(pattern) { + o.content = o.content.replace(pattern, function(str) { + return ''; + }); + }); }); } // Add visual aids when new contents is added - t.onSetContent.add(function() { - t.addVisual(t.getBody()); + self.onSetContent.add(function() { + self.addVisual(self.getBody()); }); // Remove empty contents - if (s.padd_empty_editor) { - t.onPostProcess.add(function(ed, o) { + if (settings.padd_empty_editor) { + self.onPostProcess.add(function(ed, o) { o.content = o.content.replace(/^(]*>( | |\s|\u00a0|)<\/p>[\r\n]*|
    [\r\n]*)$/, ''); }); } - if (isGecko) { - // Fix gecko link bug, when a link is placed at the end of block elements there is - // no way to move the caret behind the link. This fix adds a bogus br element after the link - function fixLinks(ed, o) { - each(ed.dom.select('a'), function(n) { - var pn = n.parentNode; - - if (ed.dom.isBlock(pn) && pn.lastChild === n) - ed.dom.add(pn, 'br', {'data-mce-bogus' : 1}); - }); - }; + self.load({initial : true, format : 'html'}); + self.startContent = self.getContent({format : 'raw'}); - t.onExecCommand.add(function(ed, cmd) { - if (cmd === 'CreateLink') - fixLinks(ed); - }); + self.initialized = true; - t.onSetContent.add(t.selection.onSetContent.add(fixLinks)); - } + self.onInit.dispatch(self); + self.execCallback('setupcontent_callback', self.id, body, doc); + self.execCallback('init_instance_callback', self); + self.focus(true); + self.nodeChanged({initial : true}); - t.load({initial : true, format : 'html'}); - t.startContent = t.getContent({format : 'raw'}); - t.undoManager.add(); - t.initialized = true; + // Add editor specific CSS styles + if (self.contentStyles.length > 0) { + contentCssText = ''; - t.onInit.dispatch(t); - t.execCallback('setupcontent_callback', t.id, t.getBody(), t.getDoc()); - t.execCallback('init_instance_callback', t); - t.focus(true); - t.nodeChanged({initial : 1}); + each(self.contentStyles, function(style) { + contentCssText += style + "\r\n"; + }); + + self.dom.addStyle(contentCssText); + } // Load specified content CSS last - each(t.contentCSS, function(u) { - t.dom.loadCSS(u); + each(self.contentCSS, function(url) { + self.dom.loadCSS(url); }); // Handle auto focus - if (s.auto_focus) { + if (settings.auto_focus) { setTimeout(function () { - var ed = tinymce.get(s.auto_focus); + var ed = tinymce.get(settings.auto_focus); ed.selection.select(ed.getBody(), 1); ed.selection.collapse(1); @@ -12036,30 +13965,45 @@ tinymce.create('tinymce.ui.Toolbar:tinymce.ui.Container', { }, 100); } - e = null; + // Clean up references for IE + targetElm = doc = body = null; }, + focus : function(skip_focus) { + var oed, self = this, selection = self.selection, contentEditable = self.settings.content_editable, ieRng, controlElm, doc = self.getDoc(), body; - focus : function(sf) { - var oed, t = this, selection = t.selection, ce = t.settings.content_editable, ieRng, controlElm, doc = t.getDoc(); + if (!skip_focus) { + if (self.lastIERng) { + selection.setRng(self.lastIERng); + } - if (!sf) { // Get selected control element ieRng = selection.getRng(); if (ieRng.item) { controlElm = ieRng.item(0); } - t._refreshContentEditable(); - selection.normalize(); + self._refreshContentEditable(); - // Is not content editable - if (!ce) - t.getWin().focus(); + // Focus the window iframe + if (!contentEditable) { + self.getWin().focus(); + } // Focus the body as well since it's contentEditable - if (tinymce.isGecko) { - t.getBody().focus(); + if (tinymce.isGecko || contentEditable) { + body = self.getBody(); + + // Check for setActive since it doesn't scroll to the element + if (body.setActive) { + body.setActive(); + } else { + body.focus(); + } + + if (contentEditable) { + selection.normalize(); + } } // Restore selected control element @@ -12070,17 +14014,16 @@ tinymce.create('tinymce.ui.Toolbar:tinymce.ui.Container', { ieRng.addElement(controlElm); ieRng.select(); } - } - if (tinymce.activeEditor != t) { + if (tinymce.activeEditor != self) { if ((oed = tinymce.activeEditor) != null) - oed.onDeactivate.dispatch(oed, t); + oed.onDeactivate.dispatch(oed, self); - t.onActivate.dispatch(t, oed); + self.onActivate.dispatch(self, oed); } - tinymce._setActive(t); + tinymce._setActive(self); }, execCallback : function(n) { @@ -12112,7 +14055,7 @@ tinymce.create('tinymce.ui.Toolbar:tinymce.ui.Container', { if (!s) return ''; - return i18n[c + '.' + s] || s.replace(/{\#([^}]+)\}/g, function(a, b) { + return i18n[c + '.' + s] || s.replace(/\{\#([^\}]+)\}/g, function(a, b) { return i18n[c + '.' + b] || '{#' + b + '}'; }); }, @@ -12146,37 +14089,40 @@ tinymce.create('tinymce.ui.Toolbar:tinymce.ui.Container', { }, nodeChanged : function(o) { - var t = this, s = t.selection, n = s.getStart() || t.getBody(); + var self = this, selection = self.selection, node; // Fix for bug #1896577 it seems that this can not be fired while the editor is loading - if (t.initialized) { + if (self.initialized) { o = o || {}; - n = isIE && n.ownerDocument != t.getDoc() ? t.getBody() : n; // Fix for IE initial state + + // Get start node + node = selection.getStart() || self.getBody(); + node = isIE && node.ownerDocument != self.getDoc() ? self.getBody() : node; // Fix for IE initial state // Get parents and add them to object o.parents = []; - t.dom.getParent(n, function(node) { + self.dom.getParent(node, function(node) { if (node.nodeName == 'BODY') return true; o.parents.push(node); }); - t.onNodeChange.dispatch( - t, - o ? o.controlManager || t.controlManager : t.controlManager, - n, - s.isCollapsed(), + self.onNodeChange.dispatch( + self, + o ? o.controlManager || self.controlManager : self.controlManager, + node, + selection.isCollapsed(), o ); } }, - addButton : function(n, s) { - var t = this; + addButton : function(name, settings) { + var self = this; - t.buttons = t.buttons || {}; - t.buttons[n] = s; + self.buttons = self.buttons || {}; + self.buttons[name] = settings; }, addCommand : function(name, callback, scope) { @@ -12194,7 +14140,7 @@ tinymce.create('tinymce.ui.Toolbar:tinymce.ui.Container', { addShortcut : function(pa, desc, cmd_func, sc) { var t = this, c; - if (!t.settings.custom_shortcuts) + if (t.settings.custom_shortcuts === false) return false; t.shortcuts = t.shortcuts || {}; @@ -12219,7 +14165,7 @@ tinymce.create('tinymce.ui.Toolbar:tinymce.ui.Container', { var o = { func : cmd_func, scope : sc || this, - desc : desc, + desc : t.translate(desc), alt : false, ctrl : false, shift : false @@ -12251,9 +14197,9 @@ tinymce.create('tinymce.ui.Toolbar:tinymce.ui.Container', { if (!/^(mceAddUndoLevel|mceEndUndoLevel|mceBeginUndoLevel|mceRepaint|SelectAll)$/.test(cmd) && (!a || !a.skip_focus)) t.focus(); - o = {}; - t.onBeforeExecCommand.dispatch(t, cmd, ui, val, o); - if (o.terminate) + a = extend({}, a); + t.onBeforeExecCommand.dispatch(t, cmd, ui, val, a); + if (a.terminate) return false; // Command callback @@ -12361,24 +14307,26 @@ tinymce.create('tinymce.ui.Toolbar:tinymce.ui.Container', { }, show : function() { - var t = this; + var self = this; - DOM.show(t.getContainer()); - DOM.hide(t.id); - t.load(); + DOM.show(self.getContainer()); + DOM.hide(self.id); + self.load(); }, hide : function() { - var t = this, d = t.getDoc(); + var self = this, doc = self.getDoc(); // Fixed bug where IE has a blinking cursor left from the editor - if (isIE && d) - d.execCommand('SelectAll'); + if (isIE && doc) + doc.execCommand('SelectAll'); // We must save before we hide so Safari doesn't crash - t.save(); - DOM.hide(t.getContainer()); - DOM.setStyle(t.id, 'display', t.orgDisplay); + self.save(); + + // defer the call to hide to prevent an IE9 crash #4921 + DOM.hide(self.getContainer()); + DOM.setStyle(self.id, 'display', self.orgDisplay); }, isHidden : function() { @@ -12420,12 +14368,6 @@ tinymce.create('tinymce.ui.Toolbar:tinymce.ui.Container', { o = o || {}; o.save = true; - // Add undo level will trigger onchange event - if (!o.no_events) { - t.undoManager.typing = false; - t.undoManager.add(); - } - o.element = e; h = o.content = t.getContent(o); @@ -12499,18 +14441,22 @@ tinymce.create('tinymce.ui.Toolbar:tinymce.ui.Container', { if (!args.no_events) self.onSetContent.dispatch(self, args); - self.selection.normalize(); + // Don't normalize selection if the focused element isn't the body in content editable mode since it will steal focus otherwise + if (!self.settings.content_editable || document.activeElement === self.getBody()) { + self.selection.normalize(); + } return args.content; }, getContent : function(args) { - var self = this, content; + var self = this, content, body = self.getBody(); // Setup args object args = args || {}; args.format = args.format || 'html'; args.get = true; + args.getInner = true; // Do preprocessing if (!args.no_events) @@ -12518,11 +14464,18 @@ tinymce.create('tinymce.ui.Toolbar:tinymce.ui.Container', { // Get raw contents or by default the cleaned contents if (args.format == 'raw') - content = self.getBody().innerHTML; + content = body.innerHTML; + else if (args.format == 'text') + content = body.innerText || body.textContent; else - content = self.serializer.serialize(self.getBody(), args); + content = self.serializer.serialize(body, args); - args.content = tinymce.trim(content); + // Trim whitespace in beginning/end of HTML + if (args.format != 'text') { + args.content = tinymce.trim(content); + } else { + args.content = content; + } // Do post processing if (!args.no_events) @@ -12538,12 +14491,12 @@ tinymce.create('tinymce.ui.Toolbar:tinymce.ui.Container', { }, getContainer : function() { - var t = this; + var self = this; - if (!t.container) - t.container = DOM.get(t.editorContainer || t.id + '_parent'); + if (!self.container) + self.container = DOM.get(self.editorContainer || self.id + '_parent'); - return t.container; + return self.container; }, getContentAreaContainer : function() { @@ -12555,111 +14508,135 @@ tinymce.create('tinymce.ui.Toolbar:tinymce.ui.Container', { }, getWin : function() { - var t = this, e; + var self = this, elm; - if (!t.contentWindow) { - e = DOM.get(t.id + "_ifr"); + if (!self.contentWindow) { + elm = DOM.get(self.id + "_ifr"); - if (e) - t.contentWindow = e.contentWindow; + if (elm) + self.contentWindow = elm.contentWindow; } - return t.contentWindow; + return self.contentWindow; }, getDoc : function() { - var t = this, w; + var self = this, win; - if (!t.contentDocument) { - w = t.getWin(); + if (!self.contentDocument) { + win = self.getWin(); - if (w) - t.contentDocument = w.document; + if (win) + self.contentDocument = win.document; } - return t.contentDocument; + return self.contentDocument; }, getBody : function() { return this.bodyElement || this.getDoc().body; }, - convertURL : function(u, n, e) { - var t = this, s = t.settings; + convertURL : function(url, name, elm) { + var self = this, settings = self.settings; // Use callback instead - if (s.urlconverter_callback) - return t.execCallback('urlconverter_callback', u, e, true, n); + if (settings.urlconverter_callback) + return self.execCallback('urlconverter_callback', url, elm, true, name); // Don't convert link href since thats the CSS files that gets loaded into the editor also skip local file URLs - if (!s.convert_urls || (e && e.nodeName == 'LINK') || u.indexOf('file:') === 0) - return u; + if (!settings.convert_urls || (elm && elm.nodeName == 'LINK') || url.indexOf('file:') === 0) + return url; // Convert to relative - if (s.relative_urls) - return t.documentBaseURI.toRelative(u); + if (settings.relative_urls) + return self.documentBaseURI.toRelative(url); // Convert to absolute - u = t.documentBaseURI.toAbsolute(u, s.remove_script_host); + url = self.documentBaseURI.toAbsolute(url, settings.remove_script_host); - return u; + return url; }, - addVisual : function(e) { - var t = this, s = t.settings; + addVisual : function(elm) { + var self = this, settings = self.settings, dom = self.dom, cls; - e = e || t.getBody(); + elm = elm || self.getBody(); - if (!is(t.hasVisual)) - t.hasVisual = s.visual; + if (!is(self.hasVisual)) + self.hasVisual = settings.visual; - each(t.dom.select('table,a', e), function(e) { - var v; + each(dom.select('table,a', elm), function(elm) { + var value; - switch (e.nodeName) { + switch (elm.nodeName) { case 'TABLE': - v = t.dom.getAttrib(e, 'border'); + cls = settings.visual_table_class || 'mceItemTable'; + value = dom.getAttrib(elm, 'border'); - if (!v || v == '0') { - if (t.hasVisual) - t.dom.addClass(e, s.visual_table_class); + if (!value || value == '0') { + if (self.hasVisual) + dom.addClass(elm, cls); else - t.dom.removeClass(e, s.visual_table_class); + dom.removeClass(elm, cls); } return; case 'A': - v = t.dom.getAttrib(e, 'name'); + if (!dom.getAttrib(elm, 'href', false)) { + value = dom.getAttrib(elm, 'name') || elm.id; + cls = 'mceItemAnchor'; - if (v) { - if (t.hasVisual) - t.dom.addClass(e, 'mceItemAnchor'); - else - t.dom.removeClass(e, 'mceItemAnchor'); + if (value) { + if (self.hasVisual) + dom.addClass(elm, cls); + else + dom.removeClass(elm, cls); + } } return; } }); - t.onVisualAid.dispatch(t, e, t.hasVisual); + self.onVisualAid.dispatch(self, elm, self.hasVisual); }, remove : function() { - var t = this, e = t.getContainer(); + var self = this, elm = self.getContainer(), doc = self.getDoc(); + + if (!self.removed) { + self.removed = 1; // Cancels post remove event execution + + // Fixed bug where IE has a blinking cursor left from the editor + if (isIE && doc) + doc.execCommand('SelectAll'); + + // We must save before we hide so Safari doesn't crash + self.save(); + + DOM.setStyle(self.id, 'display', self.orgDisplay); + + // Don't clear the window or document if content editable + // is enabled since other instances might still be present + if (!self.settings.content_editable) { + Event.unbind(self.getWin()); + Event.unbind(self.getDoc()); + } - t.removed = 1; // Cancels post remove event execution - t.hide(); + Event.unbind(self.getBody()); + Event.clear(elm); - t.execCallback('remove_instance_callback', t); - t.onRemove.dispatch(t); + self.execCallback('remove_instance_callback', self); + self.onRemove.dispatch(self); - // Clear all execCommand listeners this is required to avoid errors if the editor was removed inside another command - t.onExecCommand.listeners = []; + // Clear all execCommand listeners this is required to avoid errors if the editor was removed inside another command + self.onExecCommand.listeners = []; - tinymce.remove(t); - DOM.remove(e); + tinymce.remove(self); + DOM.remove(elm); + } }, destroy : function(s) { @@ -12669,6 +14646,13 @@ tinymce.create('tinymce.ui.Toolbar:tinymce.ui.Container', { if (t.destroyed) return; + // We must unbind on Gecko since it would otherwise produce the pesky "attempt to run compile-and-go script on a cleared scope" message + if (isGecko) { + Event.unbind(t.getDoc()); + Event.unbind(t.getWin()); + Event.unbind(t.getBody()); + } + if (!s) { tinymce.removeUnload(t.destroy); tinyMCE.onBeforeUnload.remove(t._beforeUnload); @@ -12681,18 +14665,6 @@ tinymce.create('tinymce.ui.Toolbar:tinymce.ui.Container', { t.controlManager.destroy(); t.selection.destroy(); t.dom.destroy(); - - // Remove all events - - // Don't clear the window or document if content editable - // is enabled since other instances might still be present - if (!t.settings.content_editable) { - Event.clear(t.getWin()); - Event.clear(t.getDoc()); - } - - Event.clear(t.getBody()); - Event.clear(t.formElement); } if (t.formElement) { @@ -12710,422 +14682,322 @@ tinymce.create('tinymce.ui.Toolbar:tinymce.ui.Container', { // Internal functions - _addEvents : function() { - // 'focus', 'blur', 'dblclick', 'beforedeactivate', submit, reset - var t = this, i, s = t.settings, dom = t.dom, lo = { - mouseup : 'onMouseUp', - mousedown : 'onMouseDown', - click : 'onClick', - keyup : 'onKeyUp', - keydown : 'onKeyDown', - keypress : 'onKeyPress', - submit : 'onSubmit', - reset : 'onReset', - contextmenu : 'onContextMenu', - dblclick : 'onDblClick', - paste : 'onPaste' // Doesn't work in all browsers yet - }; + _refreshContentEditable : function() { + var self = this, body, parent; - function eventHandler(e, o) { - var ty = e.type; + // Check if the editor was hidden and the re-initalize contentEditable mode by removing and adding the body again + if (self._isHidden()) { + body = self.getBody(); + parent = body.parentNode; - // Don't fire events when it's removed - if (t.removed) - return; + parent.removeChild(body); + parent.appendChild(body); - // Generic event handler - if (t.onEvent.dispatch(t, e, o) !== false) { - // Specific event handler - t[lo[e.fakeType || e.type]].dispatch(t, e, o); - } - }; + body.focus(); + } + }, - // Add DOM events - each(lo, function(v, k) { - switch (k) { - case 'contextmenu': - dom.bind(t.getDoc(), k, eventHandler); - break; + _isHidden : function() { + var s; - case 'paste': - dom.bind(t.getBody(), k, function(e) { - eventHandler(e); - }); - break; + if (!isGecko) + return 0; - case 'submit': - case 'reset': - dom.bind(t.getElement().form || DOM.getParent(t.id, 'form'), k, eventHandler); - break; + // Weird, wheres that cursor selection? + s = this.selection.getSel(); + return (!s || !s.rangeCount || s.rangeCount === 0); + } + }); +})(tinymce); +(function(tinymce) { + var each = tinymce.each; - default: - dom.bind(s.content_editable ? t.getBody() : t.getDoc(), k, eventHandler); - } - }); + tinymce.Editor.prototype.setupEvents = function() { + var self = this, settings = self.settings; - dom.bind(s.content_editable ? t.getBody() : (isGecko ? t.getDoc() : t.getWin()), 'focus', function(e) { - t.focus(true); - }); + // Add events to the editor + each([ + 'onPreInit', + 'onBeforeRenderUI', - // Fixes bug where a specified document_base_uri could result in broken images - // This will also fix drag drop of images in Gecko - if (tinymce.isGecko) { - dom.bind(t.getDoc(), 'DOMNodeInserted', function(e) { - var v; + 'onPostRender', - e = e.target; + 'onLoad', - if (e.nodeType === 1 && e.nodeName === 'IMG' && (v = e.getAttribute('data-mce-src'))) - e.src = t.documentBaseURI.toAbsolute(v); - }); - } + 'onInit', - // Set various midas options in Gecko - if (isGecko) { - function setOpts() { - var t = this, d = t.getDoc(), s = t.settings; + 'onRemove', - if (isGecko && !s.readonly) { - t._refreshContentEditable(); + 'onActivate', - try { - // Try new Gecko method - d.execCommand("styleWithCSS", 0, false); - } catch (ex) { - // Use old method - if (!t._isHidden()) - try {d.execCommand("useCSS", 0, true);} catch (ex) {} - } + 'onDeactivate', - if (!s.table_inline_editing) - try {d.execCommand('enableInlineTableEditing', false, false);} catch (ex) {} + 'onClick', - if (!s.object_resizing) - try {d.execCommand('enableObjectResizing', false, false);} catch (ex) {} - } - }; + 'onEvent', - t.onBeforeExecCommand.add(setOpts); - t.onMouseDown.add(setOpts); - } + 'onMouseUp', - // Add node change handlers - t.onMouseUp.add(t.nodeChanged); - //t.onClick.add(t.nodeChanged); - t.onKeyUp.add(function(ed, e) { - var c = e.keyCode; + 'onMouseDown', - if ((c >= 33 && c <= 36) || (c >= 37 && c <= 40) || c == 13 || c == 45 || c == 46 || c == 8 || (tinymce.isMac && (c == 91 || c == 93)) || e.ctrlKey) - t.nodeChanged(); - }); + 'onDblClick', + 'onKeyDown', - // Add block quote deletion handler - t.onKeyDown.add(function(ed, e) { - // Was the BACKSPACE key pressed? - if (e.keyCode != 8) - return; + 'onKeyUp', - var n = ed.selection.getRng().startContainer; - var offset = ed.selection.getRng().startOffset; + 'onKeyPress', - while (n && n.nodeType && n.nodeType != 1 && n.parentNode) - n = n.parentNode; - - // Is the cursor at the beginning of a blockquote? - if (n && n.parentNode && n.parentNode.tagName === 'BLOCKQUOTE' && n.parentNode.firstChild == n && offset == 0) { - // Remove the blockquote - ed.formatter.toggle('blockquote', null, n.parentNode); - - // Move the caret to the beginning of n - var rng = ed.selection.getRng(); - rng.setStart(n, 0); - rng.setEnd(n, 0); - ed.selection.setRng(rng); - ed.selection.collapse(false); - } - }); - + 'onContextMenu', + 'onSubmit', - // Add reset handler - t.onReset.add(function() { - t.setContent(t.startContent, {format : 'raw'}); - }); + 'onReset', - // Add shortcuts - if (s.custom_shortcuts) { - if (s.custom_undo_redo_keyboard_shortcuts) { - t.addShortcut('ctrl+z', t.getLang('undo_desc'), 'Undo'); - t.addShortcut('ctrl+y', t.getLang('redo_desc'), 'Redo'); - } + 'onPaste', - // Add default shortcuts for gecko - 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'); + 'onPreProcess', - // BlockFormat shortcuts keys - for (i=1; i<=6; i++) - t.addShortcut('ctrl+' + i, '', ['FormatBlock', false, 'h' + i]); + 'onPostProcess', - t.addShortcut('ctrl+7', '', ['FormatBlock', false, 'p']); - t.addShortcut('ctrl+8', '', ['FormatBlock', false, 'div']); - t.addShortcut('ctrl+9', '', ['FormatBlock', false, 'address']); + 'onBeforeSetContent', - function find(e) { - var v = null; + 'onBeforeGetContent', - if (!e.altKey && !e.ctrlKey && !e.metaKey) - return v; + 'onSetContent', - each(t.shortcuts, function(o) { - if (tinymce.isMac && o.ctrl != e.metaKey) - return; - else if (!tinymce.isMac && o.ctrl != e.ctrlKey) - return; + 'onGetContent', - if (o.alt != e.altKey) - return; + 'onLoadContent', - if (o.shift != e.shiftKey) - return; + 'onSaveContent', - if (e.keyCode == o.keyCode || (e.charCode && e.charCode == o.charCode)) { - v = o; - return false; - } - }); + 'onNodeChange', - return v; - }; + 'onChange', - t.onKeyUp.add(function(ed, e) { - var o = find(e); + 'onBeforeExecCommand', - if (o) - return Event.cancel(e); - }); + 'onExecCommand', - t.onKeyPress.add(function(ed, e) { - var o = find(e); + 'onUndo', - if (o) - return Event.cancel(e); - }); + 'onRedo', - t.onKeyDown.add(function(ed, e) { - var o = find(e); + 'onVisualAid', - if (o) { - o.func.call(o.scope); - return Event.cancel(e); - } - }); - } + 'onSetProgressState', - if (tinymce.isIE) { - // Fix so resize will only update the width and height attributes not the styles of an image - // It will also block mceItemNoResize items - dom.bind(t.getDoc(), 'controlselect', function(e) { - var re = t.resizeInfo, cb; + 'onSetAttrib' + ], function(name) { + self[name] = new tinymce.util.Dispatcher(self); + }); - e = e.target; + // Handle legacy cleanup_callback option + if (settings.cleanup_callback) { + self.onBeforeSetContent.add(function(ed, o) { + o.content = ed.execCallback('cleanup_callback', 'insert_to_editor', o.content, o); + }); - // Don't do this action for non image elements - if (e.nodeName !== 'IMG') - return; + self.onPreProcess.add(function(ed, o) { + if (o.set) + ed.execCallback('cleanup_callback', 'insert_to_editor_dom', o.node, o); - if (re) - dom.unbind(re.node, re.ev, re.cb); + if (o.get) + ed.execCallback('cleanup_callback', 'get_from_editor_dom', o.node, o); + }); - if (!dom.hasClass(e, 'mceItemNoResize')) { - ev = 'resizeend'; - cb = dom.bind(e, ev, function(e) { - var v; + self.onPostProcess.add(function(ed, o) { + if (o.set) + o.content = ed.execCallback('cleanup_callback', 'insert_to_editor', o.content, o); - e = e.target; + if (o.get) + o.content = ed.execCallback('cleanup_callback', 'get_from_editor', o.content, o); + }); + } - if (v = dom.getStyle(e, 'width')) { - dom.setAttrib(e, 'width', v.replace(/[^0-9%]+/g, '')); - dom.setStyle(e, 'width', ''); - } + // Handle legacy save_callback option + if (settings.save_callback) { + self.onGetContent.add(function(ed, o) { + if (o.save) + o.content = ed.execCallback('save_callback', ed.id, o.content, ed.getBody()); + }); + } - if (v = dom.getStyle(e, 'height')) { - dom.setAttrib(e, 'height', v.replace(/[^0-9%]+/g, '')); - dom.setStyle(e, 'height', ''); - } - }); - } else { - ev = 'resizestart'; - cb = dom.bind(e, 'resizestart', Event.cancel, Event); - } + // Handle legacy handle_event_callback option + if (settings.handle_event_callback) { + self.onEvent.add(function(ed, e, o) { + if (self.execCallback('handle_event_callback', e, ed, o) === false) { + e.preventDefault(); + e.stopPropagation(); + } + }); + } - re = t.resizeInfo = { - node : e, - ev : ev, - cb : cb - }; - }); - } + // Handle legacy handle_node_change_callback option + if (settings.handle_node_change_callback) { + self.onNodeChange.add(function(ed, cm, n) { + ed.execCallback('handle_node_change_callback', ed.id, n, -1, -1, true, ed.selection.isCollapsed()); + }); + } - if (tinymce.isOpera) { - t.onClick.add(function(ed, e) { - Event.prevent(e); - }); - } + // Handle legacy save_callback option + if (settings.save_callback) { + self.onSaveContent.add(function(ed, o) { + var h = ed.execCallback('save_callback', ed.id, o.content, ed.getBody()); - // Add custom undo/redo handlers - if (s.custom_undo_redo) { - function addUndo() { - t.undoManager.typing = false; - t.undoManager.add(); - }; + if (h) + o.content = h; + }); + } - dom.bind(t.getDoc(), 'focusout', function(e) { - if (!t.removed && t.undoManager.typing) - addUndo(); - }); + // Handle legacy onchange_callback option + if (settings.onchange_callback) { + self.onChange.add(function(ed, l) { + ed.execCallback('onchange_callback', ed, l); + }); + } + }; - // Add undo level when contents is drag/dropped within the editor - t.dom.bind(t.dom.getRoot(), 'dragend', function(e) { - addUndo(); - }); + tinymce.Editor.prototype.bindNativeEvents = function() { + // 'focus', 'blur', 'dblclick', 'beforedeactivate', submit, reset + var self = this, i, settings = self.settings, dom = self.dom, nativeToDispatcherMap; + + nativeToDispatcherMap = { + mouseup : 'onMouseUp', + mousedown : 'onMouseDown', + click : 'onClick', + keyup : 'onKeyUp', + keydown : 'onKeyDown', + keypress : 'onKeyPress', + submit : 'onSubmit', + reset : 'onReset', + contextmenu : 'onContextMenu', + dblclick : 'onDblClick', + paste : 'onPaste' // Doesn't work in all browsers yet + }; - t.onKeyUp.add(function(ed, e) { - var keyCode = e.keyCode; + // Handler that takes a native event and sends it out to a dispatcher like onKeyDown + function eventHandler(evt, args) { + var type = evt.type; - if ((keyCode >= 33 && keyCode <= 36) || (keyCode >= 37 && keyCode <= 40) || keyCode == 13 || keyCode == 45 || e.ctrlKey) - addUndo(); - }); + // Don't fire events when it's removed + if (self.removed) + return; - t.onKeyDown.add(function(ed, e) { - var keyCode = e.keyCode, sel; + // Sends the native event out to a global dispatcher then to the specific event dispatcher + if (self.onEvent.dispatch(self, evt, args) !== false) { + self[nativeToDispatcherMap[evt.fakeType || evt.type]].dispatch(self, evt, args); + } + }; - if (keyCode == 8) { - sel = t.getDoc().selection; + // Opera doesn't support focus event for contentEditable elements so we need to fake it + function doOperaFocus(e) { + self.focus(true); + }; - // Fix IE control + backspace browser bug - if (sel && sel.createRange && sel.createRange().item) { - t.undoManager.beforeChange(); - ed.dom.remove(sel.createRange().item(0)); - addUndo(); + function nodeChanged(ed, e) { + // Normalize selection for example a|a becomes a|a except for Ctrl+A since it selects everything + if (e.keyCode != 65 || !tinymce.VK.metaKeyPressed(e)) { + self.selection.normalize(); + } - return Event.cancel(e); - } - } + self.nodeChanged(); + } - // Is caracter positon keys left,right,up,down,home,end,pgdown,pgup,enter - if ((keyCode >= 33 && keyCode <= 36) || (keyCode >= 37 && keyCode <= 40) || keyCode == 13 || keyCode == 45) { - // Add position before enter key is pressed, used by IE since it still uses the default browser behavior - // Todo: Remove this once we normalize enter behavior on IE - if (tinymce.isIE && keyCode == 13) - t.undoManager.beforeChange(); + // Add DOM events + each(nativeToDispatcherMap, function(dispatcherName, nativeName) { + var root = settings.content_editable ? self.getBody() : self.getDoc(); - if (t.undoManager.typing) - addUndo(); + switch (nativeName) { + case 'contextmenu': + dom.bind(root, nativeName, eventHandler); + break; - return; - } + case 'paste': + dom.bind(self.getBody(), nativeName, eventHandler); + break; - // If key isn't shift,ctrl,alt,capslock,metakey - if ((keyCode < 16 || keyCode > 20) && keyCode != 224 && keyCode != 91 && !t.undoManager.typing) { - t.undoManager.beforeChange(); - t.undoManager.typing = true; - t.undoManager.add(); - } - }); + case 'submit': + case 'reset': + dom.bind(self.getElement().form || tinymce.DOM.getParent(self.id, 'form'), nativeName, eventHandler); + break; - t.onMouseDown.add(function() { - if (t.undoManager.typing) - addUndo(); - }); + default: + dom.bind(root, nativeName, eventHandler); } + }); - // Bug fix for FireFox keeping styles from end of selection instead of start. - if (tinymce.isGecko) { - function getAttributeApplyFunction() { - var template = t.dom.getAttribs(t.selection.getStart().cloneNode(false)); - - return function() { - var target = t.selection.getStart(); + // Set the editor as active when focused + dom.bind(settings.content_editable ? self.getBody() : (tinymce.isGecko ? self.getDoc() : self.getWin()), 'focus', function(e) { + self.focus(true); + }); - if (target !== t.getBody()) { - t.dom.setAttrib(target, "style", null); + if (settings.content_editable && tinymce.isOpera) { + dom.bind(self.getBody(), 'click', doOperaFocus); + dom.bind(self.getBody(), 'keydown', doOperaFocus); + } - each(template, function(attr) { - target.setAttributeNode(attr.cloneNode(true)); - }); - } - }; - } + // Add node change handler + self.onMouseUp.add(nodeChanged); - function isSelectionAcrossElements() { - var s = t.selection; + self.onKeyUp.add(function(ed, e) { + var keyCode = e.keyCode; - return !s.isCollapsed() && s.getStart() != s.getEnd(); - } + if ((keyCode >= 33 && keyCode <= 36) || (keyCode >= 37 && keyCode <= 40) || keyCode == 13 || keyCode == 45 || keyCode == 46 || keyCode == 8 || (tinymce.isMac && (keyCode == 91 || keyCode == 93)) || e.ctrlKey) + nodeChanged(ed, e); + }); - t.onKeyPress.add(function(ed, e) { - var applyAttributes; + // Add reset handler + self.onReset.add(function() { + self.setContent(self.startContent, {format : 'raw'}); + }); - if ((e.keyCode == 8 || e.keyCode == 46) && isSelectionAcrossElements()) { - applyAttributes = getAttributeApplyFunction(); - t.getDoc().execCommand('delete', false, null); - applyAttributes(); + // Add shortcuts + function handleShortcut(e, execute) { + if (e.altKey || e.ctrlKey || e.metaKey) { + each(self.shortcuts, function(shortcut) { + var ctrlState = tinymce.isMac ? e.metaKey : e.ctrlKey; - return Event.cancel(e); - } - }); + if (shortcut.ctrl != ctrlState || shortcut.alt != e.altKey || shortcut.shift != e.shiftKey) + return; - t.dom.bind(t.getDoc(), 'cut', function(e) { - var applyAttributes; + if (e.keyCode == shortcut.keyCode || (e.charCode && e.charCode == shortcut.charCode)) { + e.preventDefault(); - if (isSelectionAcrossElements()) { - applyAttributes = getAttributeApplyFunction(); - t.onKeyUp.addToTop(Event.cancel, Event); + if (execute) { + shortcut.func.call(shortcut.scope); + } - setTimeout(function() { - applyAttributes(); - t.onKeyUp.remove(Event.cancel, Event); - }, 0); + return true; } }); } - }, - - _refreshContentEditable : function() { - var self = this, body, parent; - - // Check if the editor was hidden and the re-initalize contentEditable mode by removing and adding the body again - if (self._isHidden()) { - body = self.getBody(); - parent = body.parentNode; - - parent.removeChild(body); - parent.appendChild(body); - - body.focus(); - } - }, + }; - _isHidden : function() { - var s; + self.onKeyUp.add(function(ed, e) { + handleShortcut(e); + }); - if (!isGecko) - return 0; + self.onKeyPress.add(function(ed, e) { + handleShortcut(e); + }); - // Weird, wheres that cursor selection? - s = this.selection.getSel(); - return (!s || !s.rangeCount || s.rangeCount == 0); + self.onKeyDown.add(function(ed, e) { + handleShortcut(e, true); + }); + + if (tinymce.isOpera) { + self.onClick.add(function(ed, e) { + e.preventDefault(); + }); } - }); + }; })(tinymce); - (function(tinymce) { // Added for compression purposes - var each = tinymce.each, undefined, TRUE = true, FALSE = false; + var each = tinymce.each, undef, TRUE = true, FALSE = false; tinymce.EditorCommands = function(editor) { var dom = editor.dom, @@ -13188,10 +15060,10 @@ tinymce.create('tinymce.ui.Toolbar:tinymce.ui.Container', { // Private methods function execNativeCommand(command, ui, value) { - if (ui === undefined) + if (ui === undef) ui = FALSE; - if (value === undefined) + if (value === undef) value = null; return editor.getDoc().execCommand(command, ui, value); @@ -13202,7 +15074,7 @@ tinymce.create('tinymce.ui.Toolbar:tinymce.ui.Container', { }; function toggleFormat(name, value) { - formatter.toggle(name, value ? {value : value} : undefined); + formatter.toggle(name, value ? {value : value} : undef); }; function storeSelection(type) { @@ -13368,6 +15240,8 @@ tinymce.create('tinymce.ui.Toolbar:tinymce.ui.Container', { var parser, serializer, parentNode, rootNode, fragment, args, marker, nodeRect, viewPortRect, rng, node, node2, bookmarkHtml, viewportBodyElement; + //selection.normalize(); + // Setup parser and serializer parser = editor.parser; serializer = new tinymce.html.Serializer({}, editor.schema); @@ -13425,7 +15299,7 @@ tinymce.create('tinymce.ui.Toolbar:tinymce.ui.Container', { // Insert bookmark node and get the parent selection.setContent(bookmarkHtml); - parentNode = editor.selection.getNode(); + parentNode = selection.getNode(); rootNode = editor.getBody(); // Opera will return the document node when selection is in root @@ -13499,6 +15373,10 @@ tinymce.create('tinymce.ui.Toolbar:tinymce.ui.Container', { editor.setContent(editor.getContent().replace(/tiny_mce_marker/g, function() { return value })); }, + mceToggleFormat : function(command, ui, value) { + toggleFormat(value); + }, + mceSetContent : function(command, ui, value) { editor.setContent(value); }, @@ -13512,6 +15390,11 @@ tinymce.create('tinymce.ui.Toolbar:tinymce.ui.Container', { intentValue = parseInt(intentValue); if (!queryCommandState('InsertUnorderedList') && !queryCommandState('InsertOrderedList')) { + // If forced_root_blocks is set to false we don't have a block to indent so lets create a div + if (!settings.forced_root_block && !dom.getParent(selection.getNode(), dom.isBlock)) { + formatter.apply('div'); + } + each(selection.getSelectedBlocks(), function(element) { if (command == 'outdent') { value = Math.max(0, parseInt(element.style.paddingLeft || 0) - intentValue); @@ -13583,10 +15466,15 @@ tinymce.create('tinymce.ui.Toolbar:tinymce.ui.Container', { selectAll : function() { var root = dom.getRoot(), rng = dom.createRng(); - rng.setStart(root, 0); - rng.setEnd(root, root.childNodes.length); + // Old IE does a better job with selectall than new versions + if (selection.getRng().setStart) { + rng.setStart(root, 0); + rng.setEnd(root, root.childNodes.length); - editor.selection.setRng(rng); + selection.setRng(rng); + } else { + execNativeCommand('SelectAll'); + } } }); @@ -13594,7 +15482,12 @@ tinymce.create('tinymce.ui.Toolbar:tinymce.ui.Container', { addCommands({ // Override justify commands 'JustifyLeft,JustifyCenter,JustifyRight,JustifyFull' : function(command) { - return isFormatMatch('align' + command.substring(7)); + var name = 'align' + command.substring(7); + var nodes = selection.isCollapsed() ? [dom.getParent(selection.getNode(), dom.isBlock)] : selection.getSelectedBlocks(); + var matches = tinymce.map(nodes, function(node) { + return !!formatter.matchNode(node, name); + }); + return tinymce.inArray(matches, TRUE) !== -1; }, 'Bold,Italic,Underline,Strikethrough,Superscript,Subscript' : function(command) { @@ -13620,7 +15513,10 @@ tinymce.create('tinymce.ui.Toolbar:tinymce.ui.Container', { }, 'InsertUnorderedList,InsertOrderedList' : function(command) { - return dom.getParent(selection.getNode(), command == 'insertunorderedlist' ? 'UL' : 'OL'); + var list = dom.getParent(selection.getNode(), 'ul,ol'); + return list && + (command === 'insertunorderedlist' && list.tagName === 'UL' + || command === 'insertorderedlist' && list.tagName === 'OL'); } }, 'state'); @@ -13641,3088 +15537,3494 @@ tinymce.create('tinymce.ui.Toolbar:tinymce.ui.Container', { }, 'value'); // Add undo manager logic - if (settings.custom_undo_redo) { - addCommands({ - Undo : function() { - editor.undoManager.undo(); - }, + addCommands({ + Undo : function() { + editor.undoManager.undo(); + }, - Redo : function() { - editor.undoManager.redo(); - } - }); - } + Redo : function() { + editor.undoManager.redo(); + } + }); }; })(tinymce); (function(tinymce) { var Dispatcher = tinymce.util.Dispatcher; - tinymce.UndoManager = function(editor) { - var self, index = 0, data = [], beforeBookmark; + tinymce.UndoManager = function(editor) { + var self, index = 0, data = [], beforeBookmark, onAdd, onUndo, onRedo; + + function getContent() { + // Remove whitespace before/after and remove pure bogus nodes + return tinymce.trim(editor.getContent({format : 'raw', no_events : 1}).replace(/]+data-mce-bogus[^>]+>[\u200B\uFEFF]+<\/span>/g, '')); + }; + + function addNonTypingUndoLevel() { + self.typing = false; + self.add(); + }; + + // Create event instances + onBeforeAdd = new Dispatcher(self); + onAdd = new Dispatcher(self); + onUndo = new Dispatcher(self); + onRedo = new Dispatcher(self); + + // Pass though onAdd event from UndoManager to Editor as onChange + onAdd.add(function(undoman, level) { + if (undoman.hasUndo()) + return editor.onChange.dispatch(editor, level, undoman); + }); + + // Pass though onUndo event from UndoManager to Editor + onUndo.add(function(undoman, level) { + return editor.onUndo.dispatch(editor, level, undoman); + }); + + // Pass though onRedo event from UndoManager to Editor + onRedo.add(function(undoman, level) { + return editor.onRedo.dispatch(editor, level, undoman); + }); + + // Add initial undo level when the editor is initialized + editor.onInit.add(function() { + self.add(); + }); + + // Get position before an execCommand is processed + editor.onBeforeExecCommand.add(function(ed, cmd, ui, val, args) { + if (cmd != 'Undo' && cmd != 'Redo' && cmd != 'mceRepaint' && (!args || !args.skip_undo)) { + self.beforeChange(); + } + }); + + // Add undo level after an execCommand call was made + editor.onExecCommand.add(function(ed, cmd, ui, val, args) { + if (cmd != 'Undo' && cmd != 'Redo' && cmd != 'mceRepaint' && (!args || !args.skip_undo)) { + self.add(); + } + }); + + // Add undo level on save contents, drag end and blur/focusout + editor.onSaveContent.add(addNonTypingUndoLevel); + editor.dom.bind(editor.dom.getRoot(), 'dragend', addNonTypingUndoLevel); + editor.dom.bind(editor.getBody(), 'focusout', function(e) { + if (!editor.removed && self.typing) { + addNonTypingUndoLevel(); + } + }); + + editor.onKeyUp.add(function(editor, e) { + var keyCode = e.keyCode; + + if ((keyCode >= 33 && keyCode <= 36) || (keyCode >= 37 && keyCode <= 40) || keyCode == 45 || keyCode == 13 || e.ctrlKey) { + addNonTypingUndoLevel(); + } + }); + + editor.onKeyDown.add(function(editor, e) { + var keyCode = e.keyCode; + + // Is caracter positon keys left,right,up,down,home,end,pgdown,pgup,enter + if ((keyCode >= 33 && keyCode <= 36) || (keyCode >= 37 && keyCode <= 40) || keyCode == 45) { + if (self.typing) { + addNonTypingUndoLevel(); + } + + return; + } + + // If key isn't shift,ctrl,alt,capslock,metakey + if ((keyCode < 16 || keyCode > 20) && keyCode != 224 && keyCode != 91 && !self.typing) { + self.beforeChange(); + self.typing = true; + self.add(); + } + }); + + editor.onMouseDown.add(function(editor, e) { + if (self.typing) { + addNonTypingUndoLevel(); + } + }); + + // Add keyboard shortcuts for undo/redo keys + editor.addShortcut('ctrl+z', 'undo_desc', 'Undo'); + editor.addShortcut('ctrl+y', 'redo_desc', 'Redo'); + + self = { + // Explose for debugging reasons + data : data, + + typing : false, + + onBeforeAdd: onBeforeAdd, + + onAdd : onAdd, + + onUndo : onUndo, + + onRedo : onRedo, + + beforeChange : function() { + beforeBookmark = editor.selection.getBookmark(2, true); + }, + + add : function(level) { + var i, settings = editor.settings, lastLevel; + + level = level || {}; + level.content = getContent(); + + self.onBeforeAdd.dispatch(self, level); + + // Add undo level if needed + lastLevel = data[index]; + if (lastLevel && lastLevel.content == level.content) + return null; + + // Set before bookmark on previous level + if (data[index]) + data[index].beforeBookmark = beforeBookmark; + + // 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]; + + data.length--; + index = data.length; + } + } + + // Get a non intrusive normalized bookmark + level.bookmark = editor.selection.getBookmark(2, true); + + // Crop array if needed + if (index < data.length - 1) + data.length = index + 1; + + data.push(level); + index = data.length - 1; + + self.onAdd.dispatch(self, level); + editor.isNotDirty = 0; + + return level; + }, + + undo : function() { + var level, i; + + if (self.typing) { + self.add(); + self.typing = false; + } + + if (index > 0) { + level = data[--index]; + + editor.setContent(level.content, {format : 'raw'}); + editor.selection.moveToBookmark(level.beforeBookmark); + + self.onUndo.dispatch(self, level); + } + + return level; + }, + + redo : function() { + var level; + + if (index < data.length - 1) { + level = data[++index]; + + editor.setContent(level.content, {format : 'raw'}); + editor.selection.moveToBookmark(level.bookmark); + + self.onRedo.dispatch(self, level); + } + + return level; + }, + + clear : function() { + data = []; + index = 0; + self.typing = false; + }, + + hasUndo : function() { + return index > 0 || this.typing; + }, + + hasRedo : function() { + return index < data.length - 1 && !this.typing; + } + }; + + return self; + }; +})(tinymce); + +tinymce.ForceBlocks = function(editor) { + var settings = editor.settings, dom = editor.dom, selection = editor.selection, blockElements = editor.schema.getBlockElements(); + + function addRootBlocks() { + var node = selection.getStart(), rootNode = editor.getBody(), rng, startContainer, startOffset, endContainer, endOffset, rootBlockNode, tempNode, offset = -0xFFFFFF, wrapped, isInEditorDocument; + + if (!node || node.nodeType !== 1 || !settings.forced_root_block) + return; + + // Check if node is wrapped in block + while (node && node != rootNode) { + if (blockElements[node.nodeName]) + return; + + node = node.parentNode; + } + + // Get current selection + rng = selection.getRng(); + if (rng.setStart) { + startContainer = rng.startContainer; + startOffset = rng.startOffset; + endContainer = rng.endContainer; + endOffset = rng.endOffset; + } else { + // Force control range into text range + if (rng.item) { + node = rng.item(0); + rng = editor.getDoc().body.createTextRange(); + rng.moveToElementText(node); + } + + isInEditorDocument = rng.parentElement().ownerDocument === editor.getDoc(); + tmpRng = rng.duplicate(); + tmpRng.collapse(true); + startOffset = tmpRng.move('character', offset) * -1; + + if (!tmpRng.collapsed) { + tmpRng = rng.duplicate(); + tmpRng.collapse(false); + endOffset = (tmpRng.move('character', offset) * -1) - startOffset; + } + } + + // Wrap non block elements and text nodes + node = rootNode.firstChild; + while (node) { + if (node.nodeType === 3 || (node.nodeType == 1 && !blockElements[node.nodeName])) { + // Remove empty text nodes + if (node.nodeType === 3 && node.nodeValue.length == 0) { + tempNode = node; + node = node.nextSibling; + dom.remove(tempNode); + continue; + } + + if (!rootBlockNode) { + rootBlockNode = dom.create(settings.forced_root_block); + node.parentNode.insertBefore(rootBlockNode, node); + wrapped = true; + } + + tempNode = node; + node = node.nextSibling; + rootBlockNode.appendChild(tempNode); + } else { + rootBlockNode = null; + node = node.nextSibling; + } + } + + if (wrapped) { + if (rng.setStart) { + rng.setStart(startContainer, startOffset); + rng.setEnd(endContainer, endOffset); + selection.setRng(rng); + } else { + // Only select if the previous selection was inside the document to prevent auto focus in quirks mode + if (isInEditorDocument) { + try { + rng = editor.getDoc().body.createTextRange(); + rng.moveToElementText(rootNode); + rng.collapse(true); + rng.moveStart('character', startOffset); + + if (endOffset > 0) + rng.moveEnd('character', endOffset); + + rng.select(); + } catch (ex) { + // Ignore + } + } + } + + editor.nodeChanged(); + } + }; + + // Force root blocks + if (settings.forced_root_block) { + editor.onKeyUp.add(addRootBlocks); + editor.onNodeChange.add(addRootBlocks); + } +}; + +(function(tinymce) { + // Shorten names + var DOM = tinymce.DOM, Event = tinymce.dom.Event, each = tinymce.each, extend = tinymce.extend; + + tinymce.create('tinymce.ControlManager', { + ControlManager : function(ed, s) { + var t = this, i; + + s = s || {}; + t.editor = ed; + t.controls = {}; + t.onAdd = new tinymce.util.Dispatcher(t); + t.onPostRender = new tinymce.util.Dispatcher(t); + t.prefix = s.prefix || ed.id + '_'; + t._cls = {}; + + t.onPostRender.add(function() { + each(t.controls, function(c) { + c.postRender(); + }); + }); + }, + + get : function(id) { + return this.controls[this.prefix + id] || this.controls[id]; + }, + + setActive : function(id, s) { + var c = null; - function getContent() { - return tinymce.trim(editor.getContent({format : 'raw', no_events : 1})); - }; + if (c = this.get(id)) + c.setActive(s); - return self = { - typing : false, + return c; + }, - onAdd : new Dispatcher(self), + setDisabled : function(id, s) { + var c = null; - onUndo : new Dispatcher(self), + if (c = this.get(id)) + c.setDisabled(s); - onRedo : new Dispatcher(self), + return c; + }, - beforeChange : function() { - beforeBookmark = editor.selection.getBookmark(2, true); - }, + add : function(c) { + var t = this; - add : function(level) { - var i, settings = editor.settings, lastLevel; + if (c) { + t.controls[c.id] = c; + t.onAdd.dispatch(c, t); + } - level = level || {}; - level.content = getContent(); + return c; + }, - // Add undo level if needed - lastLevel = data[index]; - if (lastLevel && lastLevel.content == level.content) - return null; + createControl : function(name) { + var ctrl, i, l, self = this, editor = self.editor, factories, ctrlName; - // Set before bookmark on previous level - if (data[index]) - data[index].beforeBookmark = beforeBookmark; + // Build control factory cache + if (!self.controlFactories) { + self.controlFactories = []; + each(editor.plugins, function(plugin) { + if (plugin.createControl) { + self.controlFactories.push(plugin); + } + }); + } - // 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]; + // Create controls by asking cached factories + factories = self.controlFactories; + for (i = 0, l = factories.length; i < l; i++) { + ctrl = factories[i].createControl(name, self); - data.length--; - index = data.length; - } + if (ctrl) { + return self.add(ctrl); } + } - // Get a non intrusive normalized bookmark - level.bookmark = editor.selection.getBookmark(2, true); + // Create sepearator + if (name === "|" || name === "separator") { + return self.createSeparator(); + } - // Crop array if needed - if (index < data.length - 1) - data.length = index + 1; + // Create control from button collection + if (editor.buttons && (ctrl = editor.buttons[name])) { + return self.createButton(name, ctrl); + } - data.push(level); - index = data.length - 1; + return self.add(ctrl); + }, - self.onAdd.dispatch(self, level); - editor.isNotDirty = 0; + createDropMenu : function(id, s, cc) { + var t = this, ed = t.editor, c, bm, v, cls; - return level; - }, + s = extend({ + 'class' : 'mceDropDown', + constrain : ed.settings.constrain_menus + }, s); - undo : function() { - var level, i; + s['class'] = s['class'] + ' ' + ed.getParam('skin') + 'Skin'; + if (v = ed.getParam('skin_variant')) + s['class'] += ' ' + ed.getParam('skin') + 'Skin' + v.substring(0, 1).toUpperCase() + v.substring(1); - if (self.typing) { - self.add(); - self.typing = false; - } + s['class'] += ed.settings.directionality == "rtl" ? ' mceRtl' : ''; - if (index > 0) { - level = data[--index]; + id = t.prefix + id; + cls = cc || t._cls.dropmenu || tinymce.ui.DropMenu; + c = t.controls[id] = new cls(id, s); + c.onAddItem.add(function(c, o) { + var s = o.settings; - editor.setContent(level.content, {format : 'raw'}); - editor.selection.moveToBookmark(level.beforeBookmark); + s.title = ed.getLang(s.title, s.title); - self.onUndo.dispatch(self, level); + if (!s.onclick) { + s.onclick = function(v) { + if (s.cmd) + ed.execCommand(s.cmd, s.ui || false, s.value); + }; } + }); - return level; - }, - - redo : function() { - var level; - - if (index < data.length - 1) { - level = data[++index]; + ed.onRemove.add(function() { + c.destroy(); + }); - editor.setContent(level.content, {format : 'raw'}); - editor.selection.moveToBookmark(level.bookmark); + // Fix for bug #1897785, #1898007 + if (tinymce.isIE) { + c.onShowMenu.add(function() { + // IE 8 needs focus in order to store away a range with the current collapsed caret location + ed.focus(); - self.onRedo.dispatch(self, level); - } + bm = ed.selection.getBookmark(1); + }); - return level; - }, + c.onHideMenu.add(function() { + if (bm) { + ed.selection.moveToBookmark(bm); + bm = 0; + } + }); + } - clear : function() { - data = []; - index = 0; - self.typing = false; - }, + return t.add(c); + }, - hasUndo : function() { - return index > 0 || this.typing; - }, + createListBox : function(id, s, cc) { + var t = this, ed = t.editor, cmd, c, cls; - hasRedo : function() { - return index < data.length - 1 && !this.typing; - } - }; - }; -})(tinymce); + if (t.get(id)) + return null; -(function(tinymce) { - // Shorten names - var Event = tinymce.dom.Event, - isIE = tinymce.isIE, - isGecko = tinymce.isGecko, - isOpera = tinymce.isOpera, - each = tinymce.each, - extend = tinymce.extend, - 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); - } + s.title = ed.translate(s.title); + s.scope = s.scope || ed; - clone.removeAttribute('id'); + if (!s.onselect) { + s.onselect = function(v) { + ed.execCommand(s.cmd, s.ui || false, v || s.value); + }; } - } 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(); - - rng2.setStart(rng.endContainer, rng.endOffset); - rng2.setEndAfter(par); + s = extend({ + title : s.title, + 'class' : 'mce_' + id, + scope : s.scope, + control_manager : t + }, s); - // Get number of characters to the right of the cursor if it's zero then we are at the end and need to merge the next block element - return rng2.cloneContents().textContent.length == 0; - }; + id = t.prefix + id; - function splitList(selection, dom, li) { - var listBlock, block; - if (dom.isEmpty(li)) { - listBlock = dom.getParent(li, 'ul,ol'); + function useNativeListForAccessibility(ed) { + return ed.settings.use_accessible_selects && !tinymce.isGecko + } - if (!dom.getParent(listBlock.parentNode, 'ul,ol')) { - dom.split(listBlock, li); - block = dom.create('p', 0, '
    '); - dom.replace(block, li); - selection.select(block, 1); + if (ed.settings.use_native_selects || useNativeListForAccessibility(ed)) + c = new tinymce.ui.NativeListBox(id, s); + else { + cls = cc || t._cls.listbox || tinymce.ui.ListBox; + c = new cls(id, s, ed); } - return FALSE; - } + t.controls[id] = c; - return TRUE; - }; + // Fix focus problem in Safari + if (tinymce.isWebKit) { + c.onPostRender.add(function(c, n) { + // Store bookmark on mousedown + Event.add(n, 'mousedown', function() { + ed.bookmark = ed.selection.getBookmark(1); + }); - tinymce.create('tinymce.ForceBlocks', { - ForceBlocks : function(ed) { - var t = this, s = ed.settings, elm; + // Restore on focus, since it might be lost + Event.add(n, 'focus', function() { + ed.selection.moveToBookmark(ed.bookmark); + ed.bookmark = null; + }); + }); + } - t.editor = ed; - t.dom = ed.dom; - elm = (s.forced_root_block || 'p').toLowerCase(); - s.element = elm.toUpperCase(); + if (c.hideMenu) + ed.onMouseDown.add(c.hideMenu, c); - ed.onPreInit.add(t.setup, t); + return t.add(c); }, - setup : function() { - var t = this, ed = t.editor, s = ed.settings, dom = ed.dom, selection = ed.selection, blockElements = ed.schema.getBlockElements(); - - // Force root blocks - if (s.forced_root_block) { - function addRootBlocks() { - var node = selection.getStart(), rootNode = ed.getBody(), rng, startContainer, startOffset, endContainer, endOffset, rootBlockNode, tempNode, offset = -0xFFFFFF; + createButton : function(id, s, cc) { + var t = this, ed = t.editor, o, c, cls; - if (!node || node.nodeType !== 1) - return; + if (t.get(id)) + return null; - // Check if node is wrapped in block - while (node != rootNode) { - if (blockElements[node.nodeName]) - return; + s.title = ed.translate(s.title); + s.label = ed.translate(s.label); + s.scope = s.scope || ed; - node = node.parentNode; - } + if (!s.onclick && !s.menu_button) { + s.onclick = function() { + ed.execCommand(s.cmd, s.ui || false, s.value); + }; + } - // Get current selection - rng = selection.getRng(); - if (rng.setStart) { - startContainer = rng.startContainer; - startOffset = rng.startOffset; - endContainer = rng.endContainer; - endOffset = rng.endOffset; - } else { - // Force control range into text range - if (rng.item) { - rng = ed.getDoc().body.createTextRange(); - rng.moveToElementText(rng.item(0)); - } + s = extend({ + title : s.title, + 'class' : 'mce_' + id, + unavailable_prefix : ed.getLang('unavailable', ''), + scope : s.scope, + control_manager : t + }, s); - tmpRng = rng.duplicate(); - tmpRng.collapse(true); - startOffset = tmpRng.move('character', offset) * -1; + id = t.prefix + id; - if (!tmpRng.collapsed) { - tmpRng = rng.duplicate(); - tmpRng.collapse(false); - endOffset = (tmpRng.move('character', offset) * -1) - startOffset; - } - } + if (s.menu_button) { + cls = cc || t._cls.menubutton || tinymce.ui.MenuButton; + c = new cls(id, s, ed); + ed.onMouseDown.add(c.hideMenu, c); + } else { + cls = t._cls.button || tinymce.ui.Button; + c = new cls(id, s, ed); + } - // Wrap non block elements and text nodes - for (node = rootNode.firstChild; node; node) { - if (node.nodeType === 3 || (node.nodeType == 1 && !blockElements[node.nodeName])) { - if (!rootBlockNode) { - rootBlockNode = dom.create(s.forced_root_block); - node.parentNode.insertBefore(rootBlockNode, node); - } + return t.add(c); + }, - tempNode = node; - node = node.nextSibling; - rootBlockNode.appendChild(tempNode); - } else { - rootBlockNode = null; - node = node.nextSibling; - } - } + createMenuButton : function(id, s, cc) { + s = s || {}; + s.menu_button = 1; - if (rng.setStart) { - rng.setStart(startContainer, startOffset); - rng.setEnd(endContainer, endOffset); - selection.setRng(rng); - } else { - try { - rng = ed.getDoc().body.createTextRange(); - rng.moveToElementText(rootNode); - rng.collapse(true); - rng.moveStart('character', startOffset); + return this.createButton(id, s, cc); + }, - if (endOffset > 0) - rng.moveEnd('character', endOffset); + createSplitButton : function(id, s, cc) { + var t = this, ed = t.editor, cmd, c, cls; - rng.select(); - } catch (ex) { - // Ignore - } - } + if (t.get(id)) + return null; - ed.nodeChanged(); - }; + s.title = ed.translate(s.title); + s.scope = s.scope || ed; - ed.onKeyUp.add(addRootBlocks); - ed.onClick.add(addRootBlocks); + if (!s.onclick) { + s.onclick = function(v) { + ed.execCommand(s.cmd, s.ui || false, v || s.value); + }; } - if (s.force_br_newlines) { - // Force IE to produce BRs on enter - if (isIE) { - ed.onKeyPress.add(function(ed, e) { - var n; - - if (e.keyCode == 13 && selection.getNode().nodeName != 'LI') { - selection.setContent('
    ', {format : 'raw'}); - n = dom.get('__'); - n.removeAttribute('id'); - selection.select(n); - selection.collapse(); - return Event.cancel(e); - } - }); - } + if (!s.onselect) { + s.onselect = function(v) { + ed.execCommand(s.cmd, s.ui || false, v || s.value); + }; } - 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; + s = extend({ + title : s.title, + 'class' : 'mce_' + id, + scope : s.scope, + control_manager : t + }, s); - // 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()); - }); + id = t.prefix + id; + cls = cc || t._cls.splitbutton || tinymce.ui.SplitButton; + c = t.add(new cls(id, s, ed)); + ed.onMouseDown.add(c.hideMenu, c); - 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; + return c; + }, - // Parent is an empty block - if (!parent.hasChildNodes() && fmt) { - parent = dom.getParent(parent, dom.isBlock); + createColorSplitButton : function(id, s, cc) { + var t = this, ed = t.editor, cmd, c, cls, bm; - if (parent && parent.nodeName != 'LI') { - parent.innerHTML = ''; + if (t.get(id)) + return null; - if (t._previousFormats) { - parent.appendChild(fmt.wrapper); - fmt.inner.innerHTML = '\uFEFF'; - } else - parent.innerHTML = '\uFEFF'; + s.title = ed.translate(s.title); + s.scope = s.scope || ed; - selection.select(parent, 1); - selection.collapse(true); - ed.getDoc().execCommand('Delete', false, null); - t._previousFormats = 0; - } - } - } - }); - } + if (!s.onclick) { + s.onclick = function(v) { + if (tinymce.isIE) + bm = ed.selection.getBookmark(1); - if (isGecko) { - ed.onKeyDown.add(function(ed, e) { - if ((e.keyCode == 8 || e.keyCode == 46) && !e.shiftKey) - t.backspaceDelete(e, e.keyCode == 8); - }); - } + ed.execCommand(s.cmd, s.ui || false, v || s.value); + }; } - // 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, div = dom.create('div', null, ' '), divYPos, vpHeight = dom.getViewPort(ed.getWin()).h; - - // Insert BR element - rng.insertNode(br = dom.create('br')); + if (!s.onselect) { + s.onselect = function(v) { + ed.execCommand(s.cmd, s.ui || false, v || s.value); + }; + } - // Place caret after BR - rng.setStartAfter(br); - rng.setEndAfter(br); - selection.setRng(rng); + s = extend({ + title : s.title, + 'class' : 'mce_' + id, + 'menu_class' : ed.getParam('skin') + 'Skin', + scope : s.scope, + more_colors_title : ed.getLang('more_colors') + }, s); - // Could not place caret after BR then insert an nbsp entity and move the caret - if (selection.getSel().focusNode == br.previousSibling) { - selection.select(dom.insertAfter(dom.doc.createTextNode('\u00a0'), br)); - selection.collapse(TRUE); - } + id = t.prefix + id; + cls = cc || t._cls.colorsplitbutton || tinymce.ui.ColorSplitButton; + c = new cls(id, s, ed); + ed.onMouseDown.add(c.hideMenu, c); - // 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); + // Remove the menu element when the editor is removed + ed.onRemove.add(function() { + c.destroy(); + }); - // Scroll to new position, scrollIntoView can't be used due to bug: http://bugs.webkit.org/show_bug.cgi?id=16117 - if (divYPos > vpHeight) // It is not necessary to scroll if the DIV is inside the view port. - ed.getWin().scrollTo(0, divYPos); - }; + // Fix for bug #1897785, #1898007 + if (tinymce.isIE) { + c.onShowMenu.add(function() { + // IE 8 needs focus in order to store away a range with the current collapsed caret location + ed.focus(); + bm = ed.selection.getBookmark(1); + }); - ed.onKeyPress.add(function(ed, e) { - 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); + c.onHideMenu.add(function() { + if (bm) { + ed.selection.moveToBookmark(bm); + bm = 0; } }); } - // IE specific fixes - if (isIE) { - // Replaces IE:s auto generated paragraphs with the specified element name - if (s.element != 'P') { - ed.onKeyPress.add(function(ed, e) { - t.lastElm = selection.getNode().nodeName; - }); - - ed.onKeyUp.add(function(ed, e) { - var bl, n = selection.getNode(), b = ed.getBody(); + return t.add(c); + }, - if (b.childNodes.length === 1 && n.nodeName == 'P') { - n = dom.rename(n, s.element); - selection.select(n); - selection.collapse(); - ed.nodeChanged(); - } else if (e.keyCode == 13 && !e.shiftKey && t.lastElm != 'P') { - bl = dom.getParent(n, 'p'); + createToolbar : function(id, s, cc) { + var c, t = this, cls; - if (bl) { - dom.rename(bl, s.element); - ed.nodeChanged(); - } - } - }); - } - } - }, + id = t.prefix + id; + cls = cc || t._cls.toolbar || tinymce.ui.Toolbar; + c = new cls(id, s, t.editor); - getParentBlock : function(n) { - var d = this.dom; + if (t.get(id)) + return null; - return d.getParent(n, d.isBlock); + return t.add(c); + }, + + createToolbarGroup : function(id, s, cc) { + var c, t = this, cls; + id = t.prefix + id; + cls = cc || this._cls.toolbarGroup || tinymce.ui.ToolbarGroup; + c = new cls(id, s, t.editor); + + if (t.get(id)) + return null; + + return t.add(c); }, - insertPara : function(e) { - var t = this, ed = t.editor, dom = ed.dom, d = ed.getDoc(), se = ed.settings, s = ed.selection.getSel(), r = s.getRangeAt(0), b = d.body; - var rb, ra, dir, sn, so, en, eo, sb, eb, bn, bef, aft, sc, ec, n, vp = dom.getViewPort(ed.getWin()), y, ch, car; + createSeparator : function(cc) { + var cls = cc || this._cls.separator || tinymce.ui.Separator; - ed.undoManager.beforeChange(); + return new cls(); + }, - // If root blocks are forced then use Operas default behavior since it's really good -// Removed due to bug: #1853816 -// if (se.forced_root_block && isOpera) -// return TRUE; + setControlType : function(n, c) { + return this._cls[n.toLowerCase()] = c; + }, + + destroy : function() { + each(this.controls, function(c) { + c.destroy(); + }); - // Setup before range - rb = d.createRange(); + this.controls = null; + } + }); +})(tinymce); - // If is before the first block element and in body, then move it into first block element - rb.setStart(s.anchorNode, s.anchorOffset); - rb.collapse(TRUE); +(function(tinymce) { + var Dispatcher = tinymce.util.Dispatcher, each = tinymce.each, isIE = tinymce.isIE, isOpera = tinymce.isOpera; - // Setup after range - ra = d.createRange(); + tinymce.create('tinymce.WindowManager', { + WindowManager : function(ed) { + var t = this; - // If is before the first block element and in body, then move it into first block element - ra.setStart(s.focusNode, s.focusOffset); - ra.collapse(TRUE); + t.editor = ed; + t.onOpen = new Dispatcher(t); + t.onClose = new Dispatcher(t); + t.params = {}; + t.features = {}; + }, - // Setup start/end points - dir = rb.compareBoundaryPoints(rb.START_TO_END, ra) < 0; - sn = dir ? s.anchorNode : s.focusNode; - so = dir ? s.anchorOffset : s.focusOffset; - en = dir ? s.focusNode : s.anchorNode; - eo = dir ? s.focusOffset : s.anchorOffset; + open : function(s, p) { + var t = this, f = '', x, y, mo = t.editor.settings.dialog_type == 'modal', w, sw, sh, vp = tinymce.DOM.getViewPort(), u; - // If selection is in empty table cell - if (sn === en && /^(TD|TH)$/.test(sn.nodeName)) { - if (sn.firstChild.nodeName == 'BR') - dom.remove(sn.firstChild); // Remove BR + // Default some options + s = s || {}; + p = p || {}; + sw = isOpera ? vp.w : screen.width; // Opera uses windows inside the Opera window + sh = isOpera ? vp.h : screen.height; + s.name = s.name || 'mc_' + new Date().getTime(); + s.width = parseInt(s.width || 320); + s.height = parseInt(s.height || 240); + s.resizable = true; + s.left = s.left || parseInt(sw / 2.0) - (s.width / 2.0); + s.top = s.top || parseInt(sh / 2.0) - (s.height / 2.0); + p.inline = false; + p.mce_width = s.width; + p.mce_height = s.height; + p.mce_auto_focus = s.auto_focus; - // Create two new block elements - if (sn.childNodes.length == 0) { - ed.dom.add(sn, se.element, null, '
    '); - aft = ed.dom.add(sn, se.element, null, '
    '); - } else { - n = sn.innerHTML; - sn.innerHTML = ''; - ed.dom.add(sn, se.element, null, n); - aft = ed.dom.add(sn, se.element, null, '
    '); + if (mo) { + if (isIE) { + s.center = true; + s.help = false; + s.dialogWidth = s.width + 'px'; + s.dialogHeight = s.height + 'px'; + s.scroll = s.scrollbars || false; } - - // Move caret into the last one - r = d.createRange(); - r.selectNodeContents(aft); - r.collapse(1); - ed.selection.setRng(r); - - return FALSE; } - // If the caret is in an invalid location in FF we need to move it into the first block - if (sn == b && en == b && b.firstChild && ed.dom.isBlock(b.firstChild)) { - sn = en = sn.firstChild; - so = eo = 0; - rb = d.createRange(); - rb.setStart(sn, 0); - ra = d.createRange(); - ra.setStart(en, 0); - } - - // If the body is totally empty add a BR element this might happen on webkit - if (!d.body.hasChildNodes()) { - d.body.appendChild(dom.create('br')); - } + // Build features string + each(s, function(v, k) { + if (tinymce.is(v, 'boolean')) + v = v ? 'yes' : 'no'; - // Never use body as start or end node - sn = sn.nodeName == "HTML" ? d.body : sn; // Fix for Opera bug: https://bugs.opera.com/show_bug.cgi?id=273224&comments=yes - sn = sn.nodeName == "BODY" ? sn.firstChild : sn; - en = en.nodeName == "HTML" ? d.body : en; // Fix for Opera bug: https://bugs.opera.com/show_bug.cgi?id=273224&comments=yes - en = en.nodeName == "BODY" ? en.firstChild : en; + if (!/^(name|url)$/.test(k)) { + if (isIE && mo) + f += (f ? ';' : '') + k + ':' + v; + else + f += (f ? ',' : '') + k + '=' + v; + } + }); - // Get start and end blocks - sb = t.getParentBlock(sn); - eb = t.getParentBlock(en); - bn = sb ? sb.nodeName : se.element; // Get block name to create + t.features = s; + t.params = p; + t.onOpen.dispatch(t, s, p); - // Return inside list use default browser behavior - if (n = t.dom.getParent(sb, 'li,pre')) { - if (n.nodeName == 'LI') - return splitList(ed.selection, t.dom, n); + u = s.url || s.file; + u = tinymce._addVer(u); - return TRUE; + try { + if (isIE && mo) { + w = 1; + window.showModalDialog(u, window, f); + } else + w = window.open(u, s.name, f); + } catch (ex) { + // Ignore } - // If caption or absolute layers then always generate new blocks within - if (sb && (sb.nodeName == 'CAPTION' || /absolute|relative|fixed/gi.test(dom.getStyle(sb, 'position', 1)))) { - bn = se.element; - sb = null; - } + if (!w) + alert(t.editor.getLang('popup_blocked')); + }, - // If caption or absolute layers then always generate new blocks within - if (eb && (eb.nodeName == 'CAPTION' || /absolute|relative|fixed/gi.test(dom.getStyle(sb, 'position', 1)))) { - bn = se.element; - eb = null; - } + close : function(w) { + w.close(); + this.onClose.dispatch(this); + }, - // Use P instead - if (/(TD|TABLE|TH|CAPTION)/.test(bn) || (sb && bn == "DIV" && /left|right/gi.test(dom.getStyle(sb, 'float', 1)))) { - bn = se.element; - sb = eb = null; - } + createInstance : function(cl, a, b, c, d, e) { + var f = tinymce.resolve(cl); - // Setup new before and after blocks - bef = (sb && sb.nodeName == bn) ? sb.cloneNode(0) : ed.dom.create(bn); - aft = (eb && eb.nodeName == bn) ? eb.cloneNode(0) : ed.dom.create(bn); + return new f(a, b, c, d, e); + }, - // Remove id from after clone - aft.removeAttribute('id'); + confirm : function(t, cb, s, w) { + w = w || window; - // Is header and cursor is at the end, then force paragraph under - if (/^(H[1-6])$/.test(bn) && isAtEnd(r, sb)) - aft = ed.dom.create(se.element); + cb.call(s || this, w.confirm(this._decode(this.editor.getLang(t, t)))); + }, - // Find start chop node - n = sc = sn; - do { - if (n == b || n.nodeType == 9 || t.dom.isBlock(n) || /(TD|TABLE|TH|CAPTION)/.test(n.nodeName)) - break; + alert : function(tx, cb, s, w) { + var t = this; - sc = n; - } while ((n = n.previousSibling ? n.previousSibling : n.parentNode)); + w = w || window; + w.alert(t._decode(t.editor.getLang(tx, tx))); - // Find end chop node - n = ec = en; - do { - if (n == b || n.nodeType == 9 || t.dom.isBlock(n) || /(TD|TABLE|TH|CAPTION)/.test(n.nodeName)) - break; + if (cb) + cb.call(s || t); + }, + + resizeBy : function(dw, dh, win) { + win.resizeBy(dw, dh); + }, - ec = n; - } while ((n = n.nextSibling ? n.nextSibling : n.parentNode)); + // Internal functions - // Place first chop part into before block element - if (sc.nodeName == bn) - rb.setStart(sc, 0); - else - rb.setStartBefore(sc); + _decode : function(s) { + return tinymce.DOM.decode(s).replace(/\\n/g, '\n'); + } + }); +}(tinymce)); +(function(tinymce) { + tinymce.Formatter = function(ed) { + var formats = {}, + each = tinymce.each, + dom = ed.dom, + selection = ed.selection, + TreeWalker = tinymce.dom.TreeWalker, + rangeUtils = new tinymce.dom.RangeUtils(dom), + isValid = ed.schema.isValidChild, + isArray = tinymce.isArray, + isBlock = dom.isBlock, + forcedRootBlock = ed.settings.forced_root_block, + nodeIndex = dom.nodeIndex, + INVISIBLE_CHAR = '\uFEFF', + MCE_ATTR_RE = /^(src|href|style)$/, + FALSE = false, + TRUE = true, + formatChangeData, + undef, + getContentEditable = dom.getContentEditable; - rb.setEnd(sn, so); - bef.appendChild(rb.cloneContents() || d.createTextNode('')); // Empty text node needed for Safari + function isTextBlock(name) { + return !!ed.schema.getTextBlocks()[name.toLowerCase()]; + } - // Place secnd chop part within new block element - try { - ra.setEndAfter(ec); - } catch(ex) { - //console.debug(s.focusNode, s.focusOffset); - } + function getParents(node, selector) { + return dom.getParents(node, selector, dom.getRoot()); + }; - ra.setStart(en, eo); - aft.appendChild(ra.cloneContents() || d.createTextNode('')); // Empty text node needed for Safari + function isCaretNode(node) { + return node.nodeType === 1 && node.id === '_mce_caret'; + }; - // Create range around everything - r = d.createRange(); - if (!sc.previousSibling && sc.parentNode.nodeName == bn) { - r.setStartBefore(sc.parentNode); - } else { - if (rb.startContainer.nodeName == bn && rb.startOffset == 0) - r.setStartBefore(rb.startContainer); - else - r.setStart(rb.startContainer, rb.startOffset); - } + function defaultFormats() { + register({ + alignleft : [ + {selector : 'figure,p,h1,h2,h3,h4,h5,h6,td,th,div,ul,ol,li', styles : {textAlign : 'left'}, defaultBlock: 'div'}, + {selector : 'img,table', collapsed : false, styles : {'float' : 'left'}} + ], - if (!ec.nextSibling && ec.parentNode.nodeName == bn) - r.setEndAfter(ec.parentNode); - else - r.setEnd(ra.endContainer, ra.endOffset); + aligncenter : [ + {selector : 'figure,p,h1,h2,h3,h4,h5,h6,td,th,div,ul,ol,li', styles : {textAlign : 'center'}, defaultBlock: 'div'}, + {selector : 'img', collapsed : false, styles : {display : 'block', marginLeft : 'auto', marginRight : 'auto'}}, + {selector : 'table', collapsed : false, styles : {marginLeft : 'auto', marginRight : 'auto'}} + ], - // Delete and replace it with new block elements - r.deleteContents(); + alignright : [ + {selector : 'figure,p,h1,h2,h3,h4,h5,h6,td,th,div,ul,ol,li', styles : {textAlign : 'right'}, defaultBlock: 'div'}, + {selector : 'img,table', collapsed : false, styles : {'float' : 'right'}} + ], - if (isOpera) - ed.getWin().scrollTo(0, vp.y); + alignfull : [ + {selector : 'figure,p,h1,h2,h3,h4,h5,h6,td,th,div,ul,ol,li', styles : {textAlign : 'justify'}, defaultBlock: 'div'} + ], - // Never wrap blocks in blocks - if (bef.firstChild && bef.firstChild.nodeName == bn) - bef.innerHTML = bef.firstChild.innerHTML; + bold : [ + {inline : 'strong', remove : 'all'}, + {inline : 'span', styles : {fontWeight : 'bold'}}, + {inline : 'b', remove : 'all'} + ], - if (aft.firstChild && aft.firstChild.nodeName == bn) - aft.innerHTML = aft.firstChild.innerHTML; + italic : [ + {inline : 'em', remove : 'all'}, + {inline : 'span', styles : {fontStyle : 'italic'}}, + {inline : 'i', remove : 'all'} + ], - function appendStyles(e, en) { - var nl = [], nn, n, i; + underline : [ + {inline : 'span', styles : {textDecoration : 'underline'}, exact : true}, + {inline : 'u', remove : 'all'} + ], - e.innerHTML = ''; + strikethrough : [ + {inline : 'span', styles : {textDecoration : 'line-through'}, exact : true}, + {inline : 'strike', remove : 'all'} + ], - // Make clones of style elements - if (se.keep_styles) { - n = en; - do { - // We only want style specific elements - if (/^(SPAN|STRONG|B|EM|I|FONT|STRIKE|U)$/.test(n.nodeName)) { - nn = n.cloneNode(FALSE); - dom.setAttrib(nn, 'id', ''); // Remove ID since it needs to be unique - nl.push(nn); - } - } while (n = n.parentNode); - } + forecolor : {inline : 'span', styles : {color : '%value'}, wrap_links : false}, + hilitecolor : {inline : 'span', styles : {backgroundColor : '%value'}, wrap_links : false}, + fontname : {inline : 'span', styles : {fontFamily : '%value'}}, + fontsize : {inline : 'span', styles : {fontSize : '%value'}}, + fontsize_class : {inline : 'span', attributes : {'class' : '%value'}}, + blockquote : {block : 'blockquote', wrapper : 1, remove : 'all'}, + subscript : {inline : 'sub'}, + superscript : {inline : 'sup'}, - // Append style elements to aft - if (nl.length > 0) { - for (i = nl.length - 1, nn = e; i >= 0; i--) - nn = nn.appendChild(nl[i]); + link : {inline : 'a', selector : 'a', remove : 'all', split : true, deep : true, + onmatch : function(node) { + return true; + }, - // Padd most inner style element - nl[0].innerHTML = isOpera ? '\u00a0' : '
    '; // Extra space for Opera so that the caret can move there - return nl[0]; // Move caret to most inner element - } else - e.innerHTML = isOpera ? '\u00a0' : '
    '; // Extra space for Opera so that the caret can move there - }; - - // Padd empty blocks - if (dom.isEmpty(bef)) - appendStyles(bef, sn); - - // Fill empty afterblook with current style - if (dom.isEmpty(aft)) - car = appendStyles(aft, en); - - // Opera needs this one backwards for older versions - if (isOpera && parseFloat(opera.version()) < 9.5) { - r.insertNode(bef); - r.insertNode(aft); - } else { - r.insertNode(aft); - r.insertNode(bef); - } + onformat : function(elm, fmt, vars) { + each(vars, function(value, key) { + dom.setAttrib(elm, key, value); + }); + } + }, - // Normalize - aft.normalize(); - bef.normalize(); + 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'], split : false, expand : false, deep : true} + ] + }); - // Move cursor and scroll into view - ed.selection.select(aft, true); - ed.selection.collapse(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) { + register(name, {block : name, remove : 'all'}); + }); - // scrollIntoView seems to scroll the parent window in most browsers now including FF 3.0b4 so it's time to stop using it and do it our selfs - y = ed.dom.getPos(aft).y; - //ch = aft.clientHeight; + // Register user defined formats + register(ed.settings.formats); + }; - // Is element within viewport - if (y < vp.y || y + 25 > vp.y + vp.h) { - ed.getWin().scrollTo(0, y < vp.y ? y : y - vp.h + 25); // Needs to be hardcoded to roughly one line of text if a huge text block is broken into two blocks + function addKeyboardShortcuts() { + // Add some inline shortcuts + ed.addShortcut('ctrl+b', 'bold_desc', 'Bold'); + ed.addShortcut('ctrl+i', 'italic_desc', 'Italic'); + ed.addShortcut('ctrl+u', 'underline_desc', 'Underline'); - /*console.debug( - 'Element: y=' + y + ', h=' + ch + ', ' + - 'Viewport: y=' + vp.y + ", h=" + vp.h + ', bottom=' + (vp.y + vp.h) - );*/ + // BlockFormat shortcuts keys + for (var i = 1; i <= 6; i++) { + ed.addShortcut('ctrl+' + i, '', ['FormatBlock', false, 'h' + i]); } - ed.undoManager.add(); - - return FALSE; - }, + ed.addShortcut('ctrl+7', '', ['FormatBlock', false, 'p']); + ed.addShortcut('ctrl+8', '', ['FormatBlock', false, 'div']); + ed.addShortcut('ctrl+9', '', ['FormatBlock', false, 'address']); + }; - 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, walker; + // Public functions - // 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); + function get(name) { + return name ? formats[name] : formats; + }; - // 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; - } - } - } + function register(name, format) { + if (name) { + if (typeof(name) !== 'string') { + each(name, function(format, name) { + register(name, format); + }); + } else { + // Force format into array and add it to internal collection + format = format.length ? format : [format]; - // 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 - if (sc && ed.dom.isBlock(sc) && !/^(TD|TH)$/.test(sc.nodeName) && bs) { - if (sc.childNodes.length == 0 || (sc.childNodes.length == 1 && sc.firstChild.nodeName == 'BR')) { - // Find previous block element - n = sc; - while ((n = n.previousSibling) && !ed.dom.isBlock(n)) ; + each(format, function(format) { + // Set deep to false by default on selector formats this to avoid removing + // alignment on images inside paragraphs when alignment is changed on paragraphs + if (format.deep === undef) + format.deep = !format.selector; - if (n) { - if (sc != b.firstChild) { - // Find last text node - w = ed.dom.doc.createTreeWalker(n, NodeFilter.SHOW_TEXT, null, FALSE); - while (tn = w.nextNode()) - n = tn; + // Default to true + if (format.split === undef) + format.split = !format.selector || format.inline; - // Place caret at the end of last text node - r = ed.getDoc().createRange(); - r.setStart(n, n.nodeValue ? n.nodeValue.length : 0); - r.setEnd(n, n.nodeValue ? n.nodeValue.length : 0); - se.setRng(r); + // Default to true + if (format.remove === undef && format.selector && !format.inline) + format.remove = 'none'; - // Remove the target container - ed.dom.remove(sc); + // Mark format as a mixed format inline + block level + if (format.selector && format.inline) { + format.mixed = true; + format.block_expand = true; } - return Event.cancel(e); - } + // Split classes if needed + if (typeof(format.classes) === 'string') + format.classes = format.classes.split(/\s+/); + }); + + formats[name] = format; } } - } - }); -})(tinymce); - -(function(tinymce) { - // Shorten names - var DOM = tinymce.DOM, Event = tinymce.dom.Event, each = tinymce.each, extend = tinymce.extend; - - tinymce.create('tinymce.ControlManager', { - ControlManager : function(ed, s) { - var t = this, i; + }; - s = s || {}; - t.editor = ed; - t.controls = {}; - t.onAdd = new tinymce.util.Dispatcher(t); - t.onPostRender = new tinymce.util.Dispatcher(t); - t.prefix = s.prefix || ed.id + '_'; - t._cls = {}; + var getTextDecoration = function(node) { + var decoration; - t.onPostRender.add(function() { - each(t.controls, function(c) { - c.postRender(); - }); + ed.dom.getParent(node, function(n) { + decoration = ed.dom.getStyle(n, 'text-decoration'); + return decoration && decoration !== 'none'; }); - }, - - get : function(id) { - return this.controls[this.prefix + id] || this.controls[id]; - }, - - setActive : function(id, s) { - var c = null; - - if (c = this.get(id)) - c.setActive(s); - - return c; - }, - - setDisabled : function(id, s) { - var c = null; - - if (c = this.get(id)) - c.setDisabled(s); - - return c; - }, - - add : function(c) { - var t = this; - - if (c) { - t.controls[c.id] = c; - t.onAdd.dispatch(c, t); - } - - return c; - }, - - createControl : function(n) { - var c, t = this, ed = t.editor; - each(ed.plugins, function(p) { - if (p.createControl) { - c = p.createControl(n, t); + return decoration; + }; - if (c) - return false; + var processUnderlineAndColor = function(node) { + var textDecoration; + if (node.nodeType === 1 && node.parentNode && node.parentNode.nodeType === 1) { + textDecoration = getTextDecoration(node.parentNode); + if (ed.dom.getStyle(node, 'color') && textDecoration) { + ed.dom.setStyle(node, 'text-decoration', textDecoration); + } else if (ed.dom.getStyle(node, 'textdecoration') === textDecoration) { + ed.dom.setStyle(node, 'text-decoration', null); } - }); - - switch (n) { - case "|": - case "separator": - return t.createSeparator(); } + }; - if (!c && ed.buttons && (c = ed.buttons[n])) - return t.createButton(n, c); - - return t.add(c); - }, + function apply(name, vars, node) { + var formatList = get(name), format = formatList[0], bookmark, rng, i, isCollapsed = selection.isCollapsed(); - createDropMenu : function(id, s, cc) { - var t = this, ed = t.editor, c, bm, v, cls; + function setElementFormat(elm, fmt) { + fmt = fmt || format; - s = extend({ - 'class' : 'mceDropDown', - constrain : ed.settings.constrain_menus - }, s); + if (elm) { + if (fmt.onformat) { + fmt.onformat(elm, fmt, vars, node); + } - s['class'] = s['class'] + ' ' + ed.getParam('skin') + 'Skin'; - if (v = ed.getParam('skin_variant')) - s['class'] += ' ' + ed.getParam('skin') + 'Skin' + v.substring(0, 1).toUpperCase() + v.substring(1); + each(fmt.styles, function(value, name) { + dom.setStyle(elm, name, replaceVars(value, vars)); + }); - id = t.prefix + id; - cls = cc || t._cls.dropmenu || tinymce.ui.DropMenu; - c = t.controls[id] = new cls(id, s); - c.onAddItem.add(function(c, o) { - var s = o.settings; + each(fmt.attributes, function(value, name) { + dom.setAttrib(elm, name, replaceVars(value, vars)); + }); - s.title = ed.getLang(s.title, s.title); + each(fmt.classes, function(value) { + value = replaceVars(value, vars); - if (!s.onclick) { - s.onclick = function(v) { - if (s.cmd) - ed.execCommand(s.cmd, s.ui || false, s.value); - }; + if (!dom.hasClass(elm, value)) + dom.addClass(elm, value); + }); } - }); + }; + function adjustSelectionToVisibleSelection() { + function findSelectionEnd(start, end) { + var walker = new TreeWalker(end); + for (node = walker.current(); node; node = walker.prev()) { + if (node.childNodes.length > 1 || node == start || node.tagName == 'BR') { + return node; + } + } + }; + + // Adjust selection so that a end container with a end offset of zero is not included in the selection + // as this isn't visible to the user. + var rng = ed.selection.getRng(); + var start = rng.startContainer; + var end = rng.endContainer; - ed.onRemove.add(function() { - c.destroy(); - }); + if (start != end && rng.endOffset === 0) { + var newEnd = findSelectionEnd(start, end); + var endOffset = newEnd.nodeType == 3 ? newEnd.length : newEnd.childNodes.length; - // Fix for bug #1897785, #1898007 - if (tinymce.isIE) { - c.onShowMenu.add(function() { - // IE 8 needs focus in order to store away a range with the current collapsed caret location - ed.focus(); + rng.setEnd(newEnd, endOffset); + } - bm = ed.selection.getBookmark(1); + return rng; + } + + function applyStyleToList(node, bookmark, wrapElm, newWrappers, process){ + var nodes = [], listIndex = -1, list, startIndex = -1, endIndex = -1, currentWrapElm; + + // find the index of the first child list. + each(node.childNodes, function(n, index) { + if (n.nodeName === "UL" || n.nodeName === "OL") { + listIndex = index; + list = n; + return false; + } }); - - c.onHideMenu.add(function() { - if (bm) { - ed.selection.moveToBookmark(bm); - bm = 0; + + // get the index of the bookmarks + each(node.childNodes, function(n, index) { + if (n.nodeName === "SPAN" && dom.getAttrib(n, "data-mce-type") == "bookmark") { + if (n.id == bookmark.id + "_start") { + startIndex = index; + } else if (n.id == bookmark.id + "_end") { + endIndex = index; + } } }); - } - - return t.add(c); - }, - - createListBox : function(id, s, cc) { - var t = this, ed = t.editor, cmd, c, cls; - - if (t.get(id)) - return null; - - s.title = ed.translate(s.title); - s.scope = s.scope || ed; + + // if the selection spans across an embedded list, or there isn't an embedded list - handle processing normally + if (listIndex <= 0 || (startIndex < listIndex && endIndex > listIndex)) { + each(tinymce.grep(node.childNodes), process); + return 0; + } else { + currentWrapElm = dom.clone(wrapElm, FALSE); - if (!s.onselect) { - s.onselect = function(v) { - ed.execCommand(s.cmd, s.ui || false, v || s.value); - }; - } + // create a list of the nodes on the same side of the list as the selection + each(tinymce.grep(node.childNodes), function(n, index) { + if ((startIndex < listIndex && index < listIndex) || (startIndex > listIndex && index > listIndex)) { + nodes.push(n); + n.parentNode.removeChild(n); + } + }); - s = extend({ - title : s.title, - 'class' : 'mce_' + id, - scope : s.scope, - control_manager : t - }, s); + // insert the wrapping element either before or after the list. + if (startIndex < listIndex) { + node.insertBefore(currentWrapElm, list); + } else if (startIndex > listIndex) { + node.insertBefore(currentWrapElm, list.nextSibling); + } + + // add the new nodes to the list. + newWrappers.push(currentWrapElm); - id = t.prefix + id; + each(nodes, function(node) { + currentWrapElm.appendChild(node); + }); + return currentWrapElm; + } + }; - function useNativeListForAccessibility(ed) { - return ed.settings.use_accessible_selects && !tinymce.isGecko - } + function applyRngStyle(rng, bookmark, node_specific) { + var newWrappers = [], wrapName, wrapElm, contentEditable = true; - if (ed.settings.use_native_selects || useNativeListForAccessibility(ed)) - c = new tinymce.ui.NativeListBox(id, s); - else { - cls = cc || t._cls.listbox || tinymce.ui.ListBox; - c = new cls(id, s, ed); - } + // Setup wrapper element + wrapName = format.inline || format.block; + wrapElm = dom.create(wrapName); + setElementFormat(wrapElm); - t.controls[id] = c; + rangeUtils.walk(rng, function(nodes) { + var currentWrapElm; - // Fix focus problem in Safari - if (tinymce.isWebKit) { - c.onPostRender.add(function(c, n) { - // Store bookmark on mousedown - Event.add(n, 'mousedown', function() { - ed.bookmark = ed.selection.getBookmark(1); - }); + function process(node) { + var nodeName, parentName, found, hasContentEditableState, lastContentEditable; - // Restore on focus, since it might be lost - Event.add(n, 'focus', function() { - ed.selection.moveToBookmark(ed.bookmark); - ed.bookmark = null; - }); - }); - } + lastContentEditable = contentEditable; + nodeName = node.nodeName.toLowerCase(); + parentName = node.parentNode.nodeName.toLowerCase(); - if (c.hideMenu) - ed.onMouseDown.add(c.hideMenu, c); + // Node has a contentEditable value + if (node.nodeType === 1 && getContentEditable(node)) { + lastContentEditable = contentEditable; + contentEditable = getContentEditable(node) === "true"; + hasContentEditableState = true; // We don't want to wrap the container only it's children + } - return t.add(c); - }, + // Stop wrapping on br elements + if (isEq(nodeName, 'br')) { + currentWrapElm = 0; - createButton : function(id, s, cc) { - var t = this, ed = t.editor, o, c, cls; + // Remove any br elements when we wrap things + if (format.block) + dom.remove(node); - if (t.get(id)) - return null; + return; + } - s.title = ed.translate(s.title); - s.label = ed.translate(s.label); - s.scope = s.scope || ed; + // If node is wrapper type + if (format.wrapper && matchNode(node, name, vars)) { + currentWrapElm = 0; + return; + } - if (!s.onclick && !s.menu_button) { - s.onclick = function() { - ed.execCommand(s.cmd, s.ui || false, s.value); - }; - } + // Can we rename the block + if (contentEditable && !hasContentEditableState && format.block && !format.wrapper && isTextBlock(nodeName)) { + node = dom.rename(node, wrapName); + setElementFormat(node); + newWrappers.push(node); + currentWrapElm = 0; + return; + } - s = extend({ - title : s.title, - 'class' : 'mce_' + id, - unavailable_prefix : ed.getLang('unavailable', ''), - scope : s.scope, - control_manager : t - }, s); + // Handle selector patterns + if (format.selector) { + // Look for matching formats + each(formatList, function(format) { + // Check collapsed state if it exists + if ('collapsed' in format && format.collapsed !== isCollapsed) { + return; + } - id = t.prefix + id; + if (dom.is(node, format.selector) && !isCaretNode(node)) { + setElementFormat(node, format); + found = true; + } + }); - if (s.menu_button) { - cls = cc || t._cls.menubutton || tinymce.ui.MenuButton; - c = new cls(id, s, ed); - ed.onMouseDown.add(c.hideMenu, c); - } else { - cls = t._cls.button || tinymce.ui.Button; - c = new cls(id, s, ed); - } + // Continue processing if a selector match wasn't found and a inline element is defined + if (!format.inline || found) { + currentWrapElm = 0; + return; + } + } - return t.add(c); - }, + // Is it valid to wrap this item + if (contentEditable && !hasContentEditableState && isValid(wrapName, nodeName) && isValid(parentName, wrapName) && + !(!node_specific && node.nodeType === 3 && node.nodeValue.length === 1 && node.nodeValue.charCodeAt(0) === 65279) && !isCaretNode(node)) { + // Start wrapping + if (!currentWrapElm) { + // Wrap the node + currentWrapElm = dom.clone(wrapElm, FALSE); + node.parentNode.insertBefore(currentWrapElm, node); + newWrappers.push(currentWrapElm); + } - createMenuButton : function(id, s, cc) { - s = s || {}; - s.menu_button = 1; + currentWrapElm.appendChild(node); + } else if (nodeName == 'li' && bookmark) { + // Start wrapping - if we are in a list node and have a bookmark, then we will always begin by wrapping in a new element. + currentWrapElm = applyStyleToList(node, bookmark, wrapElm, newWrappers, process); + } else { + // Start a new wrapper for possible children + currentWrapElm = 0; + + each(tinymce.grep(node.childNodes), process); - return this.createButton(id, s, cc); - }, + if (hasContentEditableState) { + contentEditable = lastContentEditable; // Restore last contentEditable state from stack + } - createSplitButton : function(id, s, cc) { - var t = this, ed = t.editor, cmd, c, cls; + // End the last wrapper + currentWrapElm = 0; + } + }; - if (t.get(id)) - return null; + // Process siblings from range + each(nodes, process); + }); - s.title = ed.translate(s.title); - s.scope = s.scope || ed; + // Wrap links inside as well, for example color inside a link when the wrapper is around the link + if (format.wrap_links === false) { + each(newWrappers, function(node) { + function process(node) { + var i, currentWrapElm, children; - if (!s.onclick) { - s.onclick = function(v) { - ed.execCommand(s.cmd, s.ui || false, v || s.value); - }; - } + if (node.nodeName === 'A') { + currentWrapElm = dom.clone(wrapElm, FALSE); + newWrappers.push(currentWrapElm); - if (!s.onselect) { - s.onselect = function(v) { - ed.execCommand(s.cmd, s.ui || false, v || s.value); - }; - } + children = tinymce.grep(node.childNodes); + for (i = 0; i < children.length; i++) + currentWrapElm.appendChild(children[i]); - s = extend({ - title : s.title, - 'class' : 'mce_' + id, - scope : s.scope, - control_manager : t - }, s); + node.appendChild(currentWrapElm); + } - id = t.prefix + id; - cls = cc || t._cls.splitbutton || tinymce.ui.SplitButton; - c = t.add(new cls(id, s, ed)); - ed.onMouseDown.add(c.hideMenu, c); + each(tinymce.grep(node.childNodes), process); + }; - return c; - }, + process(node); + }); + } - createColorSplitButton : function(id, s, cc) { - var t = this, ed = t.editor, cmd, c, cls, bm; + // Cleanup + + each(newWrappers, function(node) { + var childCount; - if (t.get(id)) - return null; + function getChildCount(node) { + var count = 0; - s.title = ed.translate(s.title); - s.scope = s.scope || ed; + each(node.childNodes, function(node) { + if (!isWhiteSpaceNode(node) && !isBookmarkNode(node)) + count++; + }); - if (!s.onclick) { - s.onclick = function(v) { - if (tinymce.isIE) - bm = ed.selection.getBookmark(1); + return count; + }; - ed.execCommand(s.cmd, s.ui || false, v || s.value); - }; - } + function mergeStyles(node) { + var child, clone; - if (!s.onselect) { - s.onselect = function(v) { - ed.execCommand(s.cmd, s.ui || false, v || s.value); - }; - } + each(node.childNodes, function(node) { + if (node.nodeType == 1 && !isBookmarkNode(node) && !isCaretNode(node)) { + child = node; + return FALSE; // break loop + } + }); - s = extend({ - title : s.title, - 'class' : 'mce_' + id, - 'menu_class' : ed.getParam('skin') + 'Skin', - scope : s.scope, - more_colors_title : ed.getLang('more_colors') - }, s); + // If child was found and of the same type as the current node + if (child && matchName(child, format)) { + clone = dom.clone(child, FALSE); + setElementFormat(clone); - id = t.prefix + id; - cls = cc || t._cls.colorsplitbutton || tinymce.ui.ColorSplitButton; - c = new cls(id, s, ed); - ed.onMouseDown.add(c.hideMenu, c); + dom.replace(clone, node, TRUE); + dom.remove(child, 1); + } - // Remove the menu element when the editor is removed - ed.onRemove.add(function() { - c.destroy(); - }); + return clone || node; + }; - // Fix for bug #1897785, #1898007 - if (tinymce.isIE) { - c.onShowMenu.add(function() { - // IE 8 needs focus in order to store away a range with the current collapsed caret location - ed.focus(); - bm = ed.selection.getBookmark(1); - }); + childCount = getChildCount(node); - c.onHideMenu.add(function() { - if (bm) { - ed.selection.moveToBookmark(bm); - bm = 0; + // Remove empty nodes but only if there is multiple wrappers and they are not block + // elements so never remove single

    since that would remove the currrent empty block element where the caret is at + if ((newWrappers.length > 1 || !isBlock(node)) && childCount === 0) { + dom.remove(node, 1); + return; } - }); - } - return t.add(c); - }, + 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) + node = mergeStyles(node); - createToolbar : function(id, s, cc) { - var c, t = this, cls; + // Remove/merge children + each(formatList, function(format) { + // Merge all children of similar type will move styles from child to parent + // this: text + // will become: text + each(dom.select(format.inline, node), function(child) { + var parent; - id = t.prefix + id; - cls = cc || t._cls.toolbar || tinymce.ui.Toolbar; - c = new cls(id, s, t.editor); + // When wrap_links is set to false we don't want + // to remove the format on children within links + if (format.wrap_links === false) { + parent = child.parentNode; - if (t.get(id)) - return null; + do { + if (parent.nodeName === 'A') + return; + } while (parent = parent.parentNode); + } - return t.add(c); - }, - - createToolbarGroup : function(id, s, cc) { - var c, t = this, cls; - id = t.prefix + id; - cls = cc || this._cls.toolbarGroup || tinymce.ui.ToolbarGroup; - c = new cls(id, s, t.editor); - - if (t.get(id)) - return null; - - return t.add(c); - }, + removeFormat(format, vars, child, format.exact ? child : null); + }); + }); - createSeparator : function(cc) { - var cls = cc || this._cls.separator || tinymce.ui.Separator; + // Remove child if direct parent is of same type + if (matchNode(node.parentNode, name, vars)) { + dom.remove(node, 1); + node = 0; + return TRUE; + } - return new cls(); - }, + // Look for parent with similar style format + if (format.merge_with_parents) { + dom.getParent(node.parentNode, function(parent) { + if (matchNode(parent, name, vars)) { + dom.remove(node, 1); + node = 0; + return TRUE; + } + }); + } - setControlType : function(n, c) { - return this._cls[n.toLowerCase()] = c; - }, - - destroy : function() { - each(this.controls, function(c) { - c.destroy(); - }); + // Merge next and previous siblings if they are similar texttext becomes texttext + if (node && format.merge_siblings !== false) { + node = mergeSiblings(getNonWhiteSpaceSibling(node), node); + node = mergeSiblings(node, getNonWhiteSpaceSibling(node, TRUE)); + } + } + }); + }; - this.controls = null; - } - }); -})(tinymce); + if (format) { + if (node) { + if (node.nodeType) { + rng = dom.createRng(); + rng.setStartBefore(node); + rng.setEndAfter(node); + applyRngStyle(expandRng(rng, formatList), null, true); + } else { + applyRngStyle(node, null, true); + } + } else { + if (!isCollapsed || !format.inline || dom.select('td.mceSelected,th.mceSelected').length) { + // Obtain selection node before selection is unselected by applyRngStyle() + var curSelNode = ed.selection.getNode(); -(function(tinymce) { - var Dispatcher = tinymce.util.Dispatcher, each = tinymce.each, isIE = tinymce.isIE, isOpera = tinymce.isOpera; + // If the formats have a default block and we can't find a parent block then start wrapping it with a DIV this is for forced_root_blocks: false + // It's kind of a hack but people should be using the default block type P since all desktop editors work that way + if (!forcedRootBlock && formatList[0].defaultBlock && !dom.getParent(curSelNode, dom.isBlock)) { + apply(formatList[0].defaultBlock); + } - tinymce.create('tinymce.WindowManager', { - WindowManager : function(ed) { - var t = this; + // Apply formatting to selection + ed.selection.setRng(adjustSelectionToVisibleSelection()); + bookmark = selection.getBookmark(); + applyRngStyle(expandRng(selection.getRng(TRUE), formatList), bookmark); - t.editor = ed; - t.onOpen = new Dispatcher(t); - t.onClose = new Dispatcher(t); - t.params = {}; - t.features = {}; - }, + // Colored nodes should be underlined so that the color of the underline matches the text color. + if (format.styles && (format.styles.color || format.styles.textDecoration)) { + tinymce.walk(curSelNode, processUnderlineAndColor, 'childNodes'); + processUnderlineAndColor(curSelNode); + } - open : function(s, p) { - var t = this, f = '', x, y, mo = t.editor.settings.dialog_type == 'modal', w, sw, sh, vp = tinymce.DOM.getViewPort(), u; + selection.moveToBookmark(bookmark); + moveStart(selection.getRng(TRUE)); + ed.nodeChanged(); + } else + performCaretAction('apply', name, vars); + } + } + }; - // Default some options - s = s || {}; - p = p || {}; - sw = isOpera ? vp.w : screen.width; // Opera uses windows inside the Opera window - sh = isOpera ? vp.h : screen.height; - s.name = s.name || 'mc_' + new Date().getTime(); - s.width = parseInt(s.width || 320); - s.height = parseInt(s.height || 240); - s.resizable = true; - s.left = s.left || parseInt(sw / 2.0) - (s.width / 2.0); - s.top = s.top || parseInt(sh / 2.0) - (s.height / 2.0); - p.inline = false; - p.mce_width = s.width; - p.mce_height = s.height; - p.mce_auto_focus = s.auto_focus; + function remove(name, vars, node) { + var formatList = get(name), format = formatList[0], bookmark, i, rng, contentEditable = true; - if (mo) { - if (isIE) { - s.center = true; - s.help = false; - s.dialogWidth = s.width + 'px'; - s.dialogHeight = s.height + 'px'; - s.scroll = s.scrollbars || false; + // Merges the styles for each node + function process(node) { + var children, i, l, localContentEditable, lastContentEditable, hasContentEditableState; + + // Skip on text nodes as they have neither format to remove nor children + if (node.nodeType === 3) { + return; } - } - // Build features string - each(s, function(v, k) { - if (tinymce.is(v, 'boolean')) - v = v ? 'yes' : 'no'; + // Node has a contentEditable value + if (node.nodeType === 1 && getContentEditable(node)) { + lastContentEditable = contentEditable; + contentEditable = getContentEditable(node) === "true"; + hasContentEditableState = true; // We don't want to wrap the container only it's children + } - if (!/^(name|url)$/.test(k)) { - if (isIE && mo) - f += (f ? ';' : '') + k + ':' + v; - else - f += (f ? ',' : '') + k + '=' + v; + // Grab the children first since the nodelist might be changed + children = tinymce.grep(node.childNodes); + + // Process current node + if (contentEditable && !hasContentEditableState) { + for (i = 0, l = formatList.length; i < l; i++) { + if (removeFormat(formatList[i], vars, node, node)) + break; + } } - }); - t.features = s; - t.params = p; - t.onOpen.dispatch(t, s, p); + // Process the children + if (format.deep) { + if (children.length) { + for (i = 0, l = children.length; i < l; i++) + process(children[i]); - u = s.url || s.file; - u = tinymce._addVer(u); + if (hasContentEditableState) { + contentEditable = lastContentEditable; // Restore last contentEditable state from stack + } + } + } + }; - try { - if (isIE && mo) { - w = 1; - window.showModalDialog(u, window, f); - } else - w = window.open(u, s.name, f); - } catch (ex) { - // Ignore - } + function findFormatRoot(container) { + var formatRoot; - if (!w) - alert(t.editor.getLang('popup_blocked')); - }, + // Find format root + each(getParents(container.parentNode).reverse(), function(parent) { + var format; - close : function(w) { - w.close(); - this.onClose.dispatch(this); - }, + // Find format root element + if (!formatRoot && parent.id != '_start' && parent.id != '_end') { + // Is the node matching the format we are looking for + format = matchNode(parent, name, vars); + if (format && format.split !== false) + formatRoot = parent; + } + }); - createInstance : function(cl, a, b, c, d, e) { - var f = tinymce.resolve(cl); + return formatRoot; + }; - return new f(a, b, c, d, e); - }, + function wrapAndSplit(format_root, container, target, split) { + var parent, clone, lastClone, firstClone, i, formatRootParent; - confirm : function(t, cb, s, w) { - w = w || window; + // Format root found then clone formats and split it + if (format_root) { + formatRootParent = format_root.parentNode; - cb.call(s || this, w.confirm(this._decode(this.editor.getLang(t, t)))); - }, + for (parent = container.parentNode; parent && parent != formatRootParent; parent = parent.parentNode) { + clone = dom.clone(parent, FALSE); - alert : function(tx, cb, s, w) { - var t = this; + for (i = 0; i < formatList.length; i++) { + if (removeFormat(formatList[i], vars, clone, clone)) { + clone = 0; + break; + } + } - w = w || window; - w.alert(t._decode(t.editor.getLang(tx, tx))); + // Build wrapper node + if (clone) { + if (lastClone) + clone.appendChild(lastClone); - if (cb) - cb.call(s || t); - }, + if (!firstClone) + firstClone = clone; - resizeBy : function(dw, dh, win) { - win.resizeBy(dw, dh); - }, + lastClone = clone; + } + } - // Internal functions + // Never split block elements if the format is mixed + if (split && (!format.mixed || !isBlock(format_root))) + container = dom.split(format_root, container); - _decode : function(s) { - return tinymce.DOM.decode(s).replace(/\\n/g, '\n'); - } - }); -}(tinymce)); -(function(tinymce) { - tinymce.Formatter = function(ed) { - var formats = {}, - each = tinymce.each, - dom = ed.dom, - selection = ed.selection, - TreeWalker = tinymce.dom.TreeWalker, - rangeUtils = new tinymce.dom.RangeUtils(dom), - isValid = ed.schema.isValidChild, - isBlock = dom.isBlock, - forcedRootBlock = ed.settings.forced_root_block, - nodeIndex = dom.nodeIndex, - INVISIBLE_CHAR = '\uFEFF', - MCE_ATTR_RE = /^(src|href|style)$/, - FALSE = false, - TRUE = true, - undefined; + // Wrap container in cloned formats + if (lastClone) { + target.parentNode.insertBefore(lastClone, target); + firstClone.appendChild(target); + } + } - function isArray(obj) { - return obj instanceof Array; - }; + return container; + }; - function getParents(node, selector) { - return dom.getParents(node, selector, dom.getRoot()); - }; + function splitToFormatRoot(container) { + return wrapAndSplit(findFormatRoot(container), container, container, true); + }; - function isCaretNode(node) { - return node.nodeType === 1 && (node.face === 'mceinline' || node.style.fontFamily === 'mceinline'); - }; + function unwrap(start) { + var node = dom.get(start ? '_start' : '_end'), + out = node[start ? 'firstChild' : 'lastChild']; - // Public functions + // 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']; - function get(name) { - return name ? formats[name] : formats; - }; + dom.remove(node, true); - function register(name, format) { - if (name) { - if (typeof(name) !== 'string') { - each(name, function(format, name) { - register(name, format); - }); - } else { - // Force format into array and add it to internal collection - format = format.length ? format : [format]; + return out; + }; - each(format, function(format) { - // Set deep to false by default on selector formats this to avoid removing - // alignment on images inside paragraphs when alignment is changed on paragraphs - if (format.deep === undefined) - format.deep = !format.selector; + function removeRngStyle(rng) { + var startContainer, endContainer, node; - // Default to true - if (format.split === undefined) - format.split = !format.selector || format.inline; + rng = expandRng(rng, formatList, TRUE); - // Default to true - if (format.remove === undefined && format.selector && !format.inline) - format.remove = 'none'; + if (format.split) { + startContainer = getContainer(rng, TRUE); + endContainer = getContainer(rng); - // Mark format as a mixed format inline + block level - if (format.selector && format.inline) { - format.mixed = true; - format.block_expand = true; + if (startContainer != endContainer) { + // WebKit will render the table incorrectly if we wrap a TD in a SPAN so lets see if the can use the first child instead + // This will happen if you tripple click a table cell and use remove formatting + if (/^(TR|TD)$/.test(startContainer.nodeName) && startContainer.firstChild) { + startContainer = (startContainer.nodeName == "TD" ? startContainer.firstChild : startContainer.firstChild.firstChild) || startContainer; } - // Split classes if needed - if (typeof(format.classes) === 'string') - format.classes = format.classes.split(/\s+/); + // Wrap start/end nodes in span element since these might be cloned/moved + startContainer = wrap(startContainer, 'span', {id : '_start', 'data-mce-type' : 'bookmark'}); + endContainer = wrap(endContainer, 'span', {id : '_end', 'data-mce-type' : 'bookmark'}); + + // Split start/end + splitToFormatRoot(startContainer); + splitToFormatRoot(endContainer); + + // Unwrap start/end to get real elements again + startContainer = unwrap(TRUE); + endContainer = unwrap(); + } else + startContainer = endContainer = splitToFormatRoot(startContainer); + + // Update range positions since they might have changed after the split operations + rng.startContainer = startContainer.parentNode; + rng.startOffset = nodeIndex(startContainer); + rng.endContainer = endContainer.parentNode; + rng.endOffset = nodeIndex(endContainer) + 1; + } + + // Remove items between start/end + rangeUtils.walk(rng, function(nodes) { + each(nodes, function(node) { + process(node); + + // Remove parent span if it only contains text-decoration: underline, yet a parent node is also underlined. + if (node.nodeType === 1 && ed.dom.getStyle(node, 'text-decoration') === 'underline' && node.parentNode && getTextDecoration(node.parentNode) === 'underline') { + removeFormat({'deep': false, 'exact': true, 'inline': 'span', 'styles': {'textDecoration' : 'underline'}}, null, node); + } }); + }); + }; - formats[name] = format; + // Handle node + if (node) { + if (node.nodeType) { + rng = dom.createRng(); + rng.setStartBefore(node); + rng.setEndAfter(node); + removeRngStyle(rng); + } else { + removeRngStyle(node); } + + return; } - }; - var getTextDecoration = function(node) { - var decoration; + if (!selection.isCollapsed() || !format.inline || dom.select('td.mceSelected,th.mceSelected').length) { + bookmark = selection.getBookmark(); + removeRngStyle(selection.getRng(TRUE)); + selection.moveToBookmark(bookmark); - ed.dom.getParent(node, function(n) { - decoration = ed.dom.getStyle(n, 'text-decoration'); - return decoration && decoration !== 'none'; - }); + // 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 (format.inline && match(name, vars, selection.getStart())) { + moveStart(selection.getRng(true)); + } - return decoration; + ed.nodeChanged(); + } else + performCaretAction('remove', name, vars); + }; + + function toggle(name, vars, node) { + var fmt = get(name); + + if (match(name, vars, node) && (!('toggle' in fmt[0]) || fmt[0].toggle)) + remove(name, vars, node); + else + apply(name, vars, node); }; - var processUnderlineAndColor = function(node) { - var textDecoration; - if (node.nodeType === 1 && node.parentNode && node.parentNode.nodeType === 1) { - textDecoration = getTextDecoration(node.parentNode); - if (ed.dom.getStyle(node, 'color') && textDecoration) { - ed.dom.setStyle(node, 'text-decoration', textDecoration); - } else if (ed.dom.getStyle(node, 'textdecoration') === textDecoration) { - ed.dom.setStyle(node, 'text-decoration', null); + function matchNode(node, name, vars, similar) { + var formatList = get(name), format, i, classes; + + function matchItems(node, format, item_name) { + var key, value, items = format[item_name], i; + + // Custom match + if (format.onmatch) { + return format.onmatch(node, format, item_name); } - } - }; - function apply(name, vars, node) { - var formatList = get(name), format = formatList[0], bookmark, rng, i, isCollapsed = selection.isCollapsed(); + // Check all items + if (items) { + // Non indexed object + if (items.length === undef) { + for (key in items) { + if (items.hasOwnProperty(key)) { + if (item_name === 'attributes') + value = dom.getAttrib(node, key); + else + value = getStyle(node, key); - function moveStart(rng) { - var container = rng.startContainer, - offset = rng.startOffset, - walker, node; - - // Move startContainer/startOffset in to a suitable node - if (container.nodeType == 1 || container.nodeValue === "") { - 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; + if (similar && !value && !format.exact) + return; + + if ((!similar || format.exact) && !isEq(value, replaceVars(items[key], vars))) + return; } } + } else { + // 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 format; + } } } - return rng; + return format; }; - function setElementFormat(elm, fmt) { - fmt = fmt || format; + if (formatList && node) { + // Check each format in list + for (i = 0; i < formatList.length; i++) { + format = formatList[i]; - if (elm) { - if (fmt.onformat) { - fmt.onformat(elm, fmt, vars, node); - } + // Name name, attributes, styles and classes + if (matchName(node, format) && matchItems(node, format, 'attributes') && matchItems(node, format, 'styles')) { + // Match classes + if (classes = format.classes) { + for (i = 0; i < classes.length; i++) { + if (!dom.hasClass(node, classes[i])) + return; + } + } - each(fmt.styles, function(value, name) { - dom.setStyle(elm, name, replaceVars(value, vars)); - }); + return format; + } + } + } + }; - each(fmt.attributes, function(value, name) { - dom.setAttrib(elm, name, replaceVars(value, vars)); - }); + function match(name, vars, node) { + var startNode; - each(fmt.classes, function(value) { - value = replaceVars(value, vars); + function matchParents(node) { + // Find first node with similar format settings + node = dom.getParent(node, function(node) { + return !!matchNode(node, name, vars, true); + }); - if (!dom.hasClass(elm, value)) - dom.addClass(elm, value); - }); - } + // Do an exact check on the similar format element + return matchNode(node, name, vars); }; - function adjustSelectionToVisibleSelection() { - function findSelectionEnd(start, end) { - var walker = new TreeWalker(end); - for (node = walker.current(); node; node = walker.prev()) { - if (node.childNodes.length > 1 || node == start) { - return node; - } - } - }; - - // Adjust selection so that a end container with a end offset of zero is not included in the selection - // as this isn't visible to the user. - var rng = ed.selection.getRng(); - var start = rng.startContainer; - var end = rng.endContainer; - if (start != end && rng.endOffset == 0) { - var newEnd = findSelectionEnd(start, end); - var endOffset = newEnd.nodeType == 3 ? newEnd.length : newEnd.childNodes.length; + // Check specified node + if (node) + return matchParents(node); - rng.setEnd(newEnd, endOffset); - } + // Check selected node + node = selection.getNode(); + if (matchParents(node)) + return TRUE; - return rng; + // Check start node if it's different + startNode = selection.getStart(); + if (startNode != node) { + if (matchParents(startNode)) + return TRUE; } - - function applyStyleToList(node, bookmark, wrapElm, newWrappers, process){ - var nodes = [], listIndex = -1, list, startIndex = -1, endIndex = -1, currentWrapElm; - - // find the index of the first child list. - each(node.childNodes, function(n, index) { - if (n.nodeName === "UL" || n.nodeName === "OL") { - listIndex = index; - list = n; - return false; - } - }); - - // get the index of the bookmarks - each(node.childNodes, function(n, index) { - if (n.nodeName === "SPAN" && dom.getAttrib(n, "data-mce-type") == "bookmark") { - if (n.id == bookmark.id + "_start") { - startIndex = index; - } else if (n.id == bookmark.id + "_end") { - endIndex = index; - } - } - }); - - // if the selection spans across an embedded list, or there isn't an embedded list - handle processing normally - if (listIndex <= 0 || (startIndex < listIndex && endIndex > listIndex)) { - each(tinymce.grep(node.childNodes), process); - return 0; - } else { - currentWrapElm = wrapElm.cloneNode(FALSE); - - // create a list of the nodes on the same side of the list as the selection - each(tinymce.grep(node.childNodes), function(n, index) { - if ((startIndex < listIndex && index < listIndex) || (startIndex > listIndex && index > listIndex)) { - nodes.push(n); - n.parentNode.removeChild(n); - } - }); - - // insert the wrapping element either before or after the list. - if (startIndex < listIndex) { - node.insertBefore(currentWrapElm, list); - } else if (startIndex > listIndex) { - node.insertBefore(currentWrapElm, list.nextSibling); - } - - // add the new nodes to the list. - newWrappers.push(currentWrapElm); - each(nodes, function(node) { - currentWrapElm.appendChild(node); - }); + return FALSE; + }; - return currentWrapElm; - } - }; - - function applyRngStyle(rng, bookmark, node_specific) { - var newWrappers = [], wrapName, wrapElm; + function matchAll(names, vars) { + var startElement, matchedFormatNames = [], checkedMap = {}, i, ni, name; - // Setup wrapper element - wrapName = format.inline || format.block; - wrapElm = dom.create(wrapName); - setElementFormat(wrapElm); + // Check start of selection for formats + startElement = selection.getStart(); + dom.getParent(startElement, function(node) { + var i, name; - rangeUtils.walk(rng, function(nodes) { - var currentWrapElm; + for (i = 0; i < names.length; i++) { + name = names[i]; - function process(node) { - var nodeName = node.nodeName.toLowerCase(), parentName = node.parentNode.nodeName.toLowerCase(), found; + if (!checkedMap[name] && matchNode(node, name, vars)) { + checkedMap[name] = true; + matchedFormatNames.push(name); + } + } + }, dom.getRoot()); - // Stop wrapping on br elements - if (isEq(nodeName, 'br')) { - currentWrapElm = 0; + return matchedFormatNames; + }; - // Remove any br elements when we wrap things - if (format.block) - dom.remove(node); + function canApply(name) { + var formatList = get(name), startNode, parents, i, x, selector; - return; - } + if (formatList) { + startNode = selection.getStart(); + parents = getParents(startNode); - // If node is wrapper type - if (format.wrapper && matchNode(node, name, vars)) { - currentWrapElm = 0; - return; - } + for (x = formatList.length - 1; x >= 0; x--) { + selector = formatList[x].selector; - // Can we rename the block - if (format.block && !format.wrapper && isTextBlock(nodeName)) { - node = dom.rename(node, wrapName); - setElementFormat(node); - newWrappers.push(node); - currentWrapElm = 0; - return; - } + // Format is not selector based, then always return TRUE + if (!selector) + return TRUE; - // Handle selector patterns - if (format.selector) { - // Look for matching formats - each(formatList, function(format) { - // Check collapsed state if it exists - if ('collapsed' in format && format.collapsed !== isCollapsed) { - return; - } + for (i = parents.length - 1; i >= 0; i--) { + if (dom.is(parents[i], selector)) + return TRUE; + } + } + } - if (dom.is(node, format.selector) && !isCaretNode(node)) { - setElementFormat(node, format); - found = true; - } - }); + return FALSE; + }; - // Continue processing if a selector match wasn't found and a inline element is defined - if (!format.inline || found) { - currentWrapElm = 0; - return; + function formatChanged(formats, callback, similar) { + var currentFormats; + + // Setup format node change logic + if (!formatChangeData) { + formatChangeData = {}; + currentFormats = {}; + + ed.onNodeChange.addToTop(function(ed, cm, node) { + var parents = getParents(node), matchedFormats = {}; + + // Check for new formats + each(formatChangeData, function(callbacks, format) { + each(parents, function(node) { + if (matchNode(node, format, {}, callbacks.similar)) { + if (!currentFormats[format]) { + // Execute callbacks + each(callbacks, function(callback) { + callback(true, {node: node, format: format, parents: parents}); + }); + + currentFormats[format] = callbacks; + } + + matchedFormats[format] = callbacks; + return false; } + }); + }); + + // Check if current formats still match + each(currentFormats, function(callbacks, format) { + if (!matchedFormats[format]) { + delete currentFormats[format]; + + each(callbacks, function(callback) { + callback(false, {node: node, format: format, parents: parents}); + }); } + }); + }); + } - // Is it valid to wrap this item - if (isValid(wrapName, nodeName) && isValid(parentName, wrapName) && - !(!node_specific && node.nodeType === 3 && node.nodeValue.length === 1 && node.nodeValue.charCodeAt(0) === 65279) && node.id !== '_mce_caret') { - // Start wrapping - if (!currentWrapElm) { - // Wrap the node - currentWrapElm = wrapElm.cloneNode(FALSE); - node.parentNode.insertBefore(currentWrapElm, node); - newWrappers.push(currentWrapElm); - } + // Add format listeners + each(formats.split(','), function(format) { + if (!formatChangeData[format]) { + formatChangeData[format] = []; + formatChangeData[format].similar = similar; + } - currentWrapElm.appendChild(node); - } else if (nodeName == 'li' && bookmark) { - // Start wrapping - if we are in a list node and have a bookmark, then we will always begin by wrapping in a new element. - currentWrapElm = applyStyleToList(node, bookmark, wrapElm, newWrappers, process); - } else { - // Start a new wrapper for possible children - currentWrapElm = 0; + formatChangeData[format].push(callback); + }); - each(tinymce.grep(node.childNodes), process); + return this; + }; - // End the last wrapper - currentWrapElm = 0; - } - }; + // Expose to public + tinymce.extend(this, { + get : get, + register : register, + apply : apply, + remove : remove, + toggle : toggle, + match : match, + matchAll : matchAll, + matchNode : matchNode, + canApply : canApply, + formatChanged: formatChanged + }); - // Process siblings from range - each(nodes, process); - }); + // Initialize + defaultFormats(); + addKeyboardShortcuts(); - // Wrap links inside as well, for example color inside a link when the wrapper is around the link - if (format.wrap_links === false) { - each(newWrappers, function(node) { - function process(node) { - var i, currentWrapElm, children; + // Private functions - if (node.nodeName === 'A') { - currentWrapElm = wrapElm.cloneNode(FALSE); - newWrappers.push(currentWrapElm); + function matchName(node, format) { + // Check for inline match + if (isEq(node, format.inline)) + return TRUE; - children = tinymce.grep(node.childNodes); - for (i = 0; i < children.length; i++) - currentWrapElm.appendChild(children[i]); + // Check for block match + if (isEq(node, format.block)) + return TRUE; - node.appendChild(currentWrapElm); - } + // Check for selector match + if (format.selector) + return dom.is(node, format.selector); + }; - each(tinymce.grep(node.childNodes), process); - }; + function isEq(str1, str2) { + str1 = str1 || ''; + str2 = str2 || ''; - process(node); - }); - } + str1 = '' + (str1.nodeName || str1); + str2 = '' + (str2.nodeName || str2); - // Cleanup - each(newWrappers, function(node) { - var childCount; + return str1.toLowerCase() == str2.toLowerCase(); + }; - function getChildCount(node) { - var count = 0; + function getStyle(node, name) { + var styleVal = dom.getStyle(node, name); - each(node.childNodes, function(node) { - if (!isWhiteSpaceNode(node) && !isBookmarkNode(node)) - count++; - }); + // Force the format to hex + if (name == 'color' || name == 'backgroundColor') + styleVal = dom.toHex(styleVal); - return count; - }; + // Opera will return bold as 700 + if (name == 'fontWeight' && styleVal == 700) + styleVal = 'bold'; - function mergeStyles(node) { - var child, clone; + return '' + styleVal; + }; - each(node.childNodes, function(node) { - if (node.nodeType == 1 && !isBookmarkNode(node) && !isCaretNode(node)) { - child = node; - return FALSE; // break loop - } - }); + function replaceVars(value, vars) { + if (typeof(value) != "string") + value = value(vars); + else if (vars) { + value = value.replace(/%(\w+)/g, function(str, name) { + return vars[name] || str; + }); + } - // If child was found and of the same type as the current node - if (child && matchName(child, format)) { - clone = child.cloneNode(FALSE); - setElementFormat(clone); + return value; + }; - dom.replace(clone, node, TRUE); - dom.remove(child, 1); - } + function isWhiteSpaceNode(node) { + return node && node.nodeType === 3 && /^([\t \r\n]+|)$/.test(node.nodeValue); + }; - return clone || node; - }; + function wrap(node, name, attrs) { + var wrapper = dom.create(name, attrs); - childCount = getChildCount(node); + node.parentNode.insertBefore(wrapper, node); + wrapper.appendChild(node); - // Remove empty nodes but only if there is multiple wrappers and they are not block - // elements so never remove single

    since that would remove the currrent empty block element where the caret is at - if ((newWrappers.length > 1 || !isBlock(node)) && childCount === 0) { - dom.remove(node, 1); - return; - } + return wrapper; + }; - 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) - node = mergeStyles(node); + function expandRng(rng, format, remove) { + var sibling, lastIdx, leaf, endPoint, + startContainer = rng.startContainer, + startOffset = rng.startOffset, + endContainer = rng.endContainer, + endOffset = rng.endOffset; - // Remove/merge children - each(formatList, function(format) { - // Merge all children of similar type will move styles from child to parent - // this: text - // will become: text - each(dom.select(format.inline, node), function(child) { - var parent; + // This function walks up the tree if there is no siblings before/after the node + function findParentContainer(start) { + var container, parent, child, sibling, siblingName, root; - // When wrap_links is set to false we don't want - // to remove the format on children within links - if (format.wrap_links === false) { - parent = child.parentNode; + container = parent = start ? startContainer : endContainer; + siblingName = start ? 'previousSibling' : 'nextSibling'; + root = dom.getRoot(); - do { - if (parent.nodeName === 'A') - return; - } while (parent = parent.parentNode); - } + function isBogusBr(node) { + return node.nodeName == "BR" && node.getAttribute('data-mce-bogus') && !node.nextSibling; + }; - removeFormat(format, vars, child, format.exact ? child : null); - }); - }); + // If it's a text node and the offset is inside the text + if (container.nodeType == 3 && !isWhiteSpaceNode(container)) { + if (start ? startOffset > 0 : endOffset < container.nodeValue.length) { + return container; + } + } - // Remove child if direct parent is of same type - if (matchNode(node.parentNode, name, vars)) { - dom.remove(node, 1); - node = 0; - return TRUE; - } + for (;;) { + // Stop expanding on block elements + if (!format[0].block_expand && isBlock(parent)) + return parent; - // Look for parent with similar style format - if (format.merge_with_parents) { - dom.getParent(node.parentNode, function(parent) { - if (matchNode(parent, name, vars)) { - dom.remove(node, 1); - node = 0; - return TRUE; - } - }); + // Walk left/right + for (sibling = parent[siblingName]; sibling; sibling = sibling[siblingName]) { + if (!isBookmarkNode(sibling) && !isWhiteSpaceNode(sibling) && !isBogusBr(sibling)) { + return parent; } + } - // Merge next and previous siblings if they are similar texttext becomes texttext - if (node && format.merge_siblings !== false) { - node = mergeSiblings(getNonWhiteSpaceSibling(node), node); - node = mergeSiblings(node, getNonWhiteSpaceSibling(node, TRUE)); - } + // Check if we can move up are we at root level or body level + if (parent.parentNode == root) { + container = parent; + break; } - }); + + parent = parent.parentNode; + } + + return container; }; - if (format) { - if (node) { - if (node.nodeType) { - rng = dom.createRng(); - rng.setStartBefore(node); - rng.setEndAfter(node); - applyRngStyle(expandRng(rng, formatList), null, true); - } else { - applyRngStyle(node, null, true); - } - } else { - if (!isCollapsed || !format.inline || dom.select('td.mceSelected,th.mceSelected').length) { - // Obtain selection node before selection is unselected by applyRngStyle() - var curSelNode = ed.selection.getNode(); + // This function walks down the tree to find the leaf at the selection. + // The offset is also returned as if node initially a leaf, the offset may be in the middle of the text node. + function findLeaf(node, offset) { + if (offset === undef) + offset = node.nodeType === 3 ? node.length : node.childNodes.length; + while (node && node.hasChildNodes()) { + node = node.childNodes[offset]; + if (node) + offset = node.nodeType === 3 ? node.length : node.childNodes.length; + } + return { node: node, offset: offset }; + } - // Apply formatting to selection - ed.selection.setRng(adjustSelectionToVisibleSelection()); - bookmark = selection.getBookmark(); - applyRngStyle(expandRng(selection.getRng(TRUE), formatList), bookmark); + // If index based start position then resolve it + if (startContainer.nodeType == 1 && startContainer.hasChildNodes()) { + lastIdx = startContainer.childNodes.length - 1; + startContainer = startContainer.childNodes[startOffset > lastIdx ? lastIdx : startOffset]; + + if (startContainer.nodeType == 3) + startOffset = 0; + } - // Colored nodes should be underlined so that the color of the underline matches the text color. - if (format.styles && (format.styles.color || format.styles.textDecoration)) { - tinymce.walk(curSelNode, processUnderlineAndColor, 'childNodes'); - processUnderlineAndColor(curSelNode); - } + // If index based end position then resolve it + if (endContainer.nodeType == 1 && endContainer.hasChildNodes()) { + lastIdx = endContainer.childNodes.length - 1; + endContainer = endContainer.childNodes[endOffset > lastIdx ? lastIdx : endOffset - 1]; - selection.moveToBookmark(bookmark); - selection.setRng(moveStart(selection.getRng(TRUE))); - ed.nodeChanged(); - } else - performCaretAction('apply', name, vars); - } + if (endContainer.nodeType == 3) + endOffset = endContainer.nodeValue.length; } - }; - 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; + // Expands the node to the closes contentEditable false element if it exists + function findParentContentEditable(node) { + var parent = node; - // Convert text node into index if possible - if (container.nodeType == 3 && offset >= container.nodeValue.length - 1) { - container = container.parentNode; - offset = nodeIndex(container) + 1; + while (parent) { + if (parent.nodeType === 1 && getContentEditable(parent)) { + return getContentEditable(parent) === "false" ? parent : node; + } + + parent = parent.parentNode; } - // 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); + return node; + }; - // If offset is at end of the parent node walk to the next one - if (offset > nodes.length - 1) - walker.next(); + function findWordEndPoint(container, offset, start) { + var walker, node, pos, lastTextNode; - 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); + function findSpace(node, offset) { + var pos, pos2, str = node.nodeValue; - // Set selection and remove tmpNode - rng.setStart(node, 0); - selection.setRng(rng); - dom.remove(tmpNode); + if (typeof(offset) == "undefined") { + offset = start ? str.length : 0; + } - return; + if (start) { + pos = str.lastIndexOf(' ', offset); + pos2 = str.lastIndexOf('\u00a0', offset); + pos = pos > pos2 ? pos : pos2; + + // Include the space on remove to avoid tag soup + if (pos !== -1 && !remove) { + pos++; } + } else { + pos = str.indexOf(' ', offset); + pos2 = str.indexOf('\u00a0', offset); + pos = pos !== -1 && (pos2 === -1 || pos < pos2) ? pos : pos2; } - } - }; - // Merges the styles for each node - function process(node) { - var children, i, l; + return pos; + }; - // Grab the children first since the nodelist might be changed - children = tinymce.grep(node.childNodes); + if (container.nodeType === 3) { + pos = findSpace(container, offset); - // Process current node - for (i = 0, l = formatList.length; i < l; i++) { - if (removeFormat(formatList[i], vars, node, node)) + if (pos !== -1) { + return {container : container, offset : pos}; + } + + lastTextNode = container; + } + + // Walk the nodes inside the block + walker = new TreeWalker(container, dom.getParent(container, isBlock) || ed.getBody()); + while (node = walker[start ? 'prev' : 'next']()) { + if (node.nodeType === 3) { + lastTextNode = node; + pos = findSpace(node); + + if (pos !== -1) { + return {container : node, offset : pos}; + } + } else if (isBlock(node)) { break; + } } - // Process the children - if (format.deep) { - for (i = 0, l = children.length; i < l; i++) - process(children[i]); + if (lastTextNode) { + if (start) { + offset = 0; + } else { + offset = lastTextNode.length; + } + + return {container: lastTextNode, offset: offset}; } }; - function findFormatRoot(container) { - var formatRoot; + function findSelectorEndPoint(container, sibling_name) { + var parents, i, y, curFormat; - // Find format root - each(getParents(container.parentNode).reverse(), function(parent) { - var format; + if (container.nodeType == 3 && container.nodeValue.length === 0 && container[sibling_name]) + container = container[sibling_name]; - // Find format root element - if (!formatRoot && parent.id != '_start' && parent.id != '_end') { - // Is the node matching the format we are looking for - format = matchNode(parent, name, vars); - if (format && format.split !== false) - formatRoot = parent; - } - }); + parents = getParents(container); + for (i = 0; i < parents.length; i++) { + for (y = 0; y < format.length; y++) { + curFormat = format[y]; - return formatRoot; - }; + // If collapsed state is set then skip formats that doesn't match that + if ("collapsed" in curFormat && curFormat.collapsed !== rng.collapsed) + continue; - function wrapAndSplit(format_root, container, target, split) { - var parent, clone, lastClone, firstClone, i, formatRootParent; + if (dom.is(parents[i], curFormat.selector)) + return parents[i]; + } + } - // Format root found then clone formats and split it - if (format_root) { - formatRootParent = format_root.parentNode; + return container; + }; - for (parent = container.parentNode; parent && parent != formatRootParent; parent = parent.parentNode) { - clone = parent.cloneNode(FALSE); + function findBlockEndPoint(container, sibling_name, sibling_name2) { + var node; - for (i = 0; i < formatList.length; i++) { - if (removeFormat(formatList[i], vars, clone, clone)) { - clone = 0; - break; - } - } + // Expand to block of similar type + if (!format[0].wrapper) + node = dom.getParent(container, format[0].block); - // Build wrapper node - if (clone) { - if (lastClone) - clone.appendChild(lastClone); + // Expand to first wrappable block element or any block element + if (!node) + node = dom.getParent(container.nodeType == 3 ? container.parentNode : container, isTextBlock); - if (!firstClone) - firstClone = clone; + // Exclude inner lists from wrapping + if (node && format[0].wrapper) + node = getParents(node, 'ul,ol').reverse()[0] || node; - lastClone = clone; - } - } + // Didn't find a block element look for first/last wrappable element + if (!node) { + node = container; - // Never split block elements if the format is mixed - if (split && (!format.mixed || !isBlock(format_root))) - container = dom.split(format_root, container); + while (node[sibling_name] && !isBlock(node[sibling_name])) { + node = node[sibling_name]; - // Wrap container in cloned formats - if (lastClone) { - target.parentNode.insertBefore(lastClone, target); - firstClone.appendChild(target); + // Break on BR but include it will be removed later on + // we can't remove it now since we need to check if it can be wrapped + if (isEq(node, 'br')) + break; } } - return container; + return node || container; }; - function splitToFormatRoot(container) { - return wrapAndSplit(findFormatRoot(container), container, container, true); - }; + // Expand to closest contentEditable element + startContainer = findParentContentEditable(startContainer); + endContainer = findParentContentEditable(endContainer); - function unwrap(start) { - var node = dom.get(start ? '_start' : '_end'), - out = node[start ? 'firstChild' : 'lastChild']; + // Exclude bookmark nodes if possible + if (isBookmarkNode(startContainer.parentNode) || isBookmarkNode(startContainer)) { + startContainer = isBookmarkNode(startContainer) ? startContainer : startContainer.parentNode; + startContainer = startContainer.nextSibling || startContainer; - // 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']; + if (startContainer.nodeType == 3) + startOffset = 0; + } - dom.remove(node, true); + if (isBookmarkNode(endContainer.parentNode) || isBookmarkNode(endContainer)) { + endContainer = isBookmarkNode(endContainer) ? endContainer : endContainer.parentNode; + endContainer = endContainer.previousSibling || endContainer; - return out; - }; + if (endContainer.nodeType == 3) + endOffset = endContainer.length; + } - function removeRngStyle(rng) { - var startContainer, endContainer; + if (format[0].inline) { + if (rng.collapsed) { + // Expand left to closest word boundery + endPoint = findWordEndPoint(startContainer, startOffset, true); + if (endPoint) { + startContainer = endPoint.container; + startOffset = endPoint.offset; + } - rng = expandRng(rng, formatList, TRUE); + // Expand right to closest word boundery + endPoint = findWordEndPoint(endContainer, endOffset); + if (endPoint) { + endContainer = endPoint.container; + endOffset = endPoint.offset; + } + } - if (format.split) { - startContainer = getContainer(rng, TRUE); - endContainer = getContainer(rng); + // Avoid applying formatting to a trailing space. + leaf = findLeaf(endContainer, endOffset); + if (leaf.node) { + while (leaf.node && leaf.offset === 0 && leaf.node.previousSibling) + leaf = findLeaf(leaf.node.previousSibling); - if (startContainer != endContainer) { - // Wrap start/end nodes in span element since these might be cloned/moved - startContainer = wrap(startContainer, 'span', {id : '_start', 'data-mce-type' : 'bookmark'}); - endContainer = wrap(endContainer, 'span', {id : '_end', 'data-mce-type' : 'bookmark'}); + if (leaf.node && leaf.offset > 0 && leaf.node.nodeType === 3 && + leaf.node.nodeValue.charAt(leaf.offset - 1) === ' ') { - // Split start/end - splitToFormatRoot(startContainer); - splitToFormatRoot(endContainer); + if (leaf.offset > 1) { + endContainer = leaf.node; + endContainer.splitText(leaf.offset - 1); + } + } + } + } - // Unwrap start/end to get real elements again - startContainer = unwrap(TRUE); - endContainer = unwrap(); - } else - startContainer = endContainer = splitToFormatRoot(startContainer); + // Move start/end point up the tree if the leaves are sharp and if we are in different containers + // Example * becomes !: !

    *texttext*

    ! + // This will reduce the number of wrapper elements that needs to be created + // Move start point up the tree + if (format[0].inline || format[0].block_expand) { + if (!format[0].inline || (startContainer.nodeType != 3 || startOffset === 0)) { + startContainer = findParentContainer(true); + } - // Update range positions since they might have changed after the split operations - rng.startContainer = startContainer.parentNode; - rng.startOffset = nodeIndex(startContainer); - rng.endContainer = endContainer.parentNode; - rng.endOffset = nodeIndex(endContainer) + 1; + if (!format[0].inline || (endContainer.nodeType != 3 || endOffset === endContainer.nodeValue.length)) { + endContainer = findParentContainer(); } + } - // Remove items between start/end - rangeUtils.walk(rng, function(nodes) { - each(nodes, function(node) { - process(node); + // Expand start/end container to matching selector + if (format[0].selector && format[0].expand !== FALSE && !format[0].inline) { + // Find new startContainer/endContainer if there is better one + startContainer = findSelectorEndPoint(startContainer, 'previousSibling'); + endContainer = findSelectorEndPoint(endContainer, 'nextSibling'); + } - // Remove parent span if it only contains text-decoration: underline, yet a parent node is also underlined. - if (node.nodeType === 1 && ed.dom.getStyle(node, 'text-decoration') === 'underline' && node.parentNode && getTextDecoration(node.parentNode) === 'underline') { - removeFormat({'deep': false, 'exact': true, 'inline': 'span', 'styles': {'textDecoration' : 'underline'}}, null, node); - } - }); - }); - }; + // Expand start/end container to matching block element or text node + if (format[0].block || format[0].selector) { + // Find new startContainer/endContainer if there is better one + startContainer = findBlockEndPoint(startContainer, 'previousSibling'); + endContainer = findBlockEndPoint(endContainer, 'nextSibling'); - // Handle node - if (node) { - if (node.nodeType) { - rng = dom.createRng(); - rng.setStartBefore(node); - rng.setEndAfter(node); - removeRngStyle(rng); - } else { - removeRngStyle(node); + // Non block element then try to expand up the leaf + if (format[0].block) { + if (!isBlock(startContainer)) + startContainer = findParentContainer(true); + + if (!isBlock(endContainer)) + endContainer = findParentContainer(); } + } - return; + // Setup index for startContainer + if (startContainer.nodeType == 1) { + startOffset = nodeIndex(startContainer); + startContainer = startContainer.parentNode; } - if (!selection.isCollapsed() || !format.inline || dom.select('td.mceSelected,th.mceSelected').length) { - bookmark = selection.getBookmark(); - removeRngStyle(selection.getRng(TRUE)); - selection.moveToBookmark(bookmark); + // Setup index for endContainer + if (endContainer.nodeType == 1) { + endOffset = nodeIndex(endContainer) + 1; + endContainer = endContainer.parentNode; + } - // 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 (format.inline && match(name, vars, selection.getStart())) { - moveStart(selection.getRng(true)); - } + // Return new range like object + return { + startContainer : startContainer, + startOffset : startOffset, + endContainer : endContainer, + endOffset : endOffset + }; + } - ed.nodeChanged(); - } else - performCaretAction('remove', name, vars); + function removeFormat(format, vars, node, compare_node) { + var i, attrs, stylesModified; - // When you remove formatting from a table cell in WebKit (cell, not the contents of a cell) there is a rendering issue with column width - if (tinymce.isWebKit) { - ed.execCommand('mceCleanup'); - } - }; + // Check if node matches format + if (!matchName(node, format)) + return FALSE; - function toggle(name, vars, node) { - var fmt = get(name); + // Should we compare with format attribs and styles + if (format.remove != 'all') { + // Remove styles + each(format.styles, function(value, name) { + value = replaceVars(value, vars); - if (match(name, vars, node) && (!('toggle' in fmt[0]) || fmt[0]['toggle'])) - remove(name, vars, node); - else - apply(name, vars, node); - }; + // Indexed array + if (typeof(name) === 'number') { + name = value; + compare_node = 0; + } - function matchNode(node, name, vars, similar) { - var formatList = get(name), format, i, classes; + if (!compare_node || isEq(getStyle(compare_node, name), value)) + dom.setStyle(node, name, ''); - function matchItems(node, format, item_name) { - var key, value, items = format[item_name], i; + stylesModified = 1; + }); - // Custom match - if (format.onmatch) { - return format.onmatch(node, format, item_name); + // Remove style attribute if it's empty + if (stylesModified && dom.getAttrib(node, 'style') == '') { + node.removeAttribute('style'); + node.removeAttribute('data-mce-style'); } - // Check all items - if (items) { - // Non indexed object - if (items.length === undefined) { - for (key in items) { - if (items.hasOwnProperty(key)) { - if (item_name === 'attributes') - value = dom.getAttrib(node, key); - else - value = getStyle(node, key); + // Remove attributes + each(format.attributes, function(value, name) { + var valueOut; - if (similar && !value && !format.exact) - return; + value = replaceVars(value, vars); - if ((!similar || format.exact) && !isEq(value, replaceVars(items[key], vars))) - return; - } - } - } else { - // 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 format; - } + // Indexed array + if (typeof(name) === 'number') { + name = value; + compare_node = 0; } - } - return format; - }; - - if (formatList && node) { - // Check each format in list - for (i = 0; i < formatList.length; i++) { - format = formatList[i]; + if (!compare_node || isEq(dom.getAttrib(compare_node, name), value)) { + // Keep internal classes + if (name == 'class') { + value = dom.getAttrib(node, name); + if (value) { + // Build new class value where everything is removed except the internal prefixed classes + valueOut = ''; + each(value.split(/\s+/), function(cls) { + if (/mce\w+/.test(cls)) + valueOut += (valueOut ? ' ' : '') + cls; + }); - // Name name, attributes, styles and classes - if (matchName(node, format) && matchItems(node, format, 'attributes') && matchItems(node, format, 'styles')) { - // Match classes - if (classes = format.classes) { - for (i = 0; i < classes.length; i++) { - if (!dom.hasClass(node, classes[i])) + // We got some internal classes left + if (valueOut) { + dom.setAttrib(node, name, valueOut); return; + } } } - return format; - } - } - } - }; + // IE6 has a bug where the attribute doesn't get removed correctly + if (name == "class") + node.removeAttribute('className'); - function match(name, vars, node) { - var startNode; + // Remove mce prefixed attributes + if (MCE_ATTR_RE.test(name)) + node.removeAttribute('data-mce-' + name); - function matchParents(node) { - // Find first node with similar format settings - node = dom.getParent(node, function(node) { - return !!matchNode(node, name, vars, true); + node.removeAttribute(name); + } }); - // Do an exact check on the similar format element - return matchNode(node, name, vars); - }; - - // Check specified node - if (node) - return matchParents(node); + // Remove classes + each(format.classes, function(value) { + value = replaceVars(value, vars); - // Check selected node - node = selection.getNode(); - if (matchParents(node)) - return TRUE; + if (!compare_node || dom.hasClass(compare_node, value)) + dom.removeClass(node, value); + }); - // Check start node if it's different - startNode = selection.getStart(); - if (startNode != node) { - if (matchParents(startNode)) - return TRUE; + // Check for non internal attributes + attrs = dom.getAttribs(node); + for (i = 0; i < attrs.length; i++) { + if (attrs[i].nodeName.indexOf('_') !== 0) + return FALSE; + } } - return FALSE; + // Remove the inline child if it's empty for example or + if (format.remove != 'none') { + removeNode(node, format); + return TRUE; + } }; - function matchAll(names, vars) { - var startElement, matchedFormatNames = [], checkedMap = {}, i, ni, name; + function removeNode(node, format) { + var parentNode = node.parentNode, rootBlockElm; - // Check start of selection for formats - startElement = selection.getStart(); - dom.getParent(startElement, function(node) { - var i, name; + function find(node, next, inc) { + node = getNonWhiteSpaceSibling(node, next, inc); - for (i = 0; i < names.length; i++) { - name = names[i]; + return !node || (node.nodeName == 'BR' || isBlock(node)); + }; - if (!checkedMap[name] && matchNode(node, name, vars)) { - checkedMap[name] = true; - matchedFormatNames.push(name); + if (format.block) { + if (!forcedRootBlock) { + // Append BR elements if needed before we remove the block + if (isBlock(node) && !isBlock(parentNode)) { + if (!find(node, FALSE) && !find(node.firstChild, TRUE, 1)) + node.insertBefore(dom.create('br'), node.firstChild); + + if (!find(node, TRUE) && !find(node.lastChild, FALSE, 1)) + node.appendChild(dom.create('br')); + } + } else { + // Wrap the block in a forcedRootBlock if we are at the root of document + if (parentNode == dom.getRoot()) { + if (!format.list_block || !isEq(node, format.list_block)) { + each(tinymce.grep(node.childNodes), function(node) { + if (isValid(forcedRootBlock, node.nodeName.toLowerCase())) { + if (!rootBlockElm) + rootBlockElm = wrap(node, forcedRootBlock); + else + rootBlockElm.appendChild(node); + } else + rootBlockElm = 0; + }); + } } } - }); - - return matchedFormatNames; - }; - - function canApply(name) { - var formatList = get(name), startNode, parents, i, x, selector; + } - if (formatList) { - startNode = selection.getStart(); - parents = getParents(startNode); + // 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; - for (x = formatList.length - 1; x >= 0; x--) { - selector = formatList[x].selector; + dom.remove(node, 1); + }; - // Format is not selector based, then always return TRUE - if (!selector) - return TRUE; + function getNonWhiteSpaceSibling(node, next, inc) { + if (node) { + next = next ? 'nextSibling' : 'previousSibling'; - for (i = parents.length - 1; i >= 0; i--) { - if (dom.is(parents[i], selector)) - return TRUE; - } + for (node = inc ? node : node[next]; node; node = node[next]) { + if (node.nodeType == 1 || !isWhiteSpaceNode(node)) + return node; } } - - return FALSE; }; - // Expose to public - tinymce.extend(this, { - get : get, - register : register, - apply : apply, - remove : remove, - toggle : toggle, - match : match, - matchAll : matchAll, - matchNode : matchNode, - canApply : canApply - }); - - // Private functions - - function matchName(node, format) { - // Check for inline match - if (isEq(node, format.inline)) - return TRUE; - - // Check for block match - if (isEq(node, format.block)) - return TRUE; - - // Check for selector match - if (format.selector) - return dom.is(node, format.selector); + function isBookmarkNode(node) { + return node && node.nodeType == 1 && node.getAttribute('data-mce-type') == 'bookmark'; }; - function isEq(str1, str2) { - str1 = str1 || ''; - str2 = str2 || ''; + function mergeSiblings(prev, next) { + var marker, sibling, tmpSibling; - str1 = '' + (str1.nodeName || str1); - str2 = '' + (str2.nodeName || str2); + function compareElements(node1, node2) { + // Not the same name + if (node1.nodeName != node2.nodeName) + return FALSE; - return str1.toLowerCase() == str2.toLowerCase(); - }; + function getAttribs(node) { + var attribs = {}; - function getStyle(node, name) { - var styleVal = dom.getStyle(node, name); + each(dom.getAttribs(node), function(attr) { + var name = attr.nodeName.toLowerCase(); - // Force the format to hex - if (name == 'color' || name == 'backgroundColor') - styleVal = dom.toHex(styleVal); + // Don't compare internal attributes or style + if (name.indexOf('_') !== 0 && name !== 'style') + attribs[name] = dom.getAttrib(node, name); + }); - // Opera will return bold as 700 - if (name == 'fontWeight' && styleVal == 700) - styleVal = 'bold'; + return attribs; + }; - return '' + styleVal; - }; + function compareObjects(obj1, obj2) { + var value, name; - function replaceVars(value, vars) { - if (typeof(value) != "string") - value = value(vars); - else if (vars) { - value = value.replace(/%(\w+)/g, function(str, name) { - return vars[name] || str; - }); - } + for (name in obj1) { + // Obj1 has item obj2 doesn't have + if (obj1.hasOwnProperty(name)) { + value = obj2[name]; - return value; - }; + // Obj2 doesn't have obj1 item + if (value === undef) + return FALSE; - function isWhiteSpaceNode(node) { - return node && node.nodeType === 3 && /^([\t \r\n]+|)$/.test(node.nodeValue); - }; + // Obj2 item has a different value + if (obj1[name] != value) + return FALSE; - function wrap(node, name, attrs) { - var wrapper = dom.create(name, attrs); + // Delete similar value + delete obj2[name]; + } + } - node.parentNode.insertBefore(wrapper, node); - wrapper.appendChild(node); + // Check if obj 2 has something obj 1 doesn't have + for (name in obj2) { + // Obj2 has item obj1 doesn't have + if (obj2.hasOwnProperty(name)) + return FALSE; + } - return wrapper; - }; + return TRUE; + }; - function expandRng(rng, format, remove) { - var startContainer = rng.startContainer, - startOffset = rng.startOffset, - endContainer = rng.endContainer, - endOffset = rng.endOffset, sibling, lastIdx, leaf, endPoint; + // Attribs are not the same + if (!compareObjects(getAttribs(node1), getAttribs(node2))) + return FALSE; - // This function walks up the tree if there is no siblings before/after the node - function findParentContainer(start) { - var container, parent, child, sibling, siblingName; + // Styles are not the same + if (!compareObjects(dom.parseStyle(dom.getAttrib(node1, 'style')), dom.parseStyle(dom.getAttrib(node2, 'style')))) + return FALSE; - container = parent = start ? startContainer : endContainer; - siblingName = start ? 'previousSibling' : 'nextSibling'; - root = dom.getRoot(); + return TRUE; + }; - // If it's a text node and the offset is inside the text - if (container.nodeType == 3 && !isWhiteSpaceNode(container)) { - if (start ? startOffset > 0 : endOffset < container.nodeValue.length) { - return container; - } + function findElementSibling(node, sibling_name) { + for (sibling = node; sibling; sibling = sibling[sibling_name]) { + if (sibling.nodeType == 3 && sibling.nodeValue.length !== 0) + return node; + + if (sibling.nodeType == 1 && !isBookmarkNode(sibling)) + return sibling; } - for (;;) { - // Stop expanding on block elements or root depending on format - if (parent == root || (!format[0].block_expand && isBlock(parent))) - return parent; + return node; + }; - // Walk left/right - for (sibling = parent[siblingName]; sibling; sibling = sibling[siblingName]) { - if (!isBookmarkNode(sibling) && !isWhiteSpaceNode(sibling)) { - return parent; - } + // Check if next/prev exists and that they are elements + if (prev && next) { + // If previous sibling is empty then jump over it + prev = findElementSibling(prev, 'previousSibling'); + next = findElementSibling(next, 'nextSibling'); + + // Compare next and previous nodes + if (compareElements(prev, next)) { + // Append nodes between + for (sibling = prev.nextSibling; sibling && sibling != next;) { + tmpSibling = sibling; + sibling = sibling.nextSibling; + prev.appendChild(tmpSibling); } - // Check if we can move up are we at root level or body level - parent = parent.parentNode; - } + // Remove next node + dom.remove(next); - return container; - }; + // Move children into prev node + each(tinymce.grep(next.childNodes), function(node) { + prev.appendChild(node); + }); - // This function walks down the tree to find the leaf at the selection. - // The offset is also returned as if node initially a leaf, the offset may be in the middle of the text node. - function findLeaf(node, offset) { - if (offset === undefined) - offset = node.nodeType === 3 ? node.length : node.childNodes.length; - while (node && node.hasChildNodes()) { - node = node.childNodes[offset]; - if (node) - offset = node.nodeType === 3 ? node.length : node.childNodes.length; + return prev; } - return { node: node, offset: offset }; } - // If index based start position then resolve it - if (startContainer.nodeType == 1 && startContainer.hasChildNodes()) { - lastIdx = startContainer.childNodes.length - 1; - startContainer = startContainer.childNodes[startOffset > lastIdx ? lastIdx : startOffset]; + return next; + }; - if (startContainer.nodeType == 3) - startOffset = 0; - } + function isTextBlock(name) { + return /^(h[1-6]|p|div|pre|address|dl|dt|dd)$/.test(name); + }; - // If index based end position then resolve it - if (endContainer.nodeType == 1 && endContainer.hasChildNodes()) { - lastIdx = endContainer.childNodes.length - 1; - endContainer = endContainer.childNodes[endOffset > lastIdx ? lastIdx : endOffset - 1]; + function getContainer(rng, start) { + var container, offset, lastIdx, walker; - if (endContainer.nodeType == 3) - endOffset = endContainer.nodeValue.length; - } + container = rng[start ? 'startContainer' : 'endContainer']; + offset = rng[start ? 'startOffset' : 'endOffset']; - // Exclude bookmark nodes if possible - if (isBookmarkNode(startContainer.parentNode) || isBookmarkNode(startContainer)) { - startContainer = isBookmarkNode(startContainer) ? startContainer : startContainer.parentNode; - startContainer = startContainer.nextSibling || startContainer; + if (container.nodeType == 1) { + lastIdx = container.childNodes.length - 1; - if (startContainer.nodeType == 3) - startOffset = 0; + if (!start && offset) + offset--; + + container = container.childNodes[offset > lastIdx ? lastIdx : offset]; } - if (isBookmarkNode(endContainer.parentNode) || isBookmarkNode(endContainer)) { - endContainer = isBookmarkNode(endContainer) ? endContainer : endContainer.parentNode; - endContainer = endContainer.previousSibling || endContainer; + // If start text node is excluded then walk to the next node + if (container.nodeType === 3 && start && offset >= container.nodeValue.length) { + container = new TreeWalker(container, ed.getBody()).next() || container; + } - if (endContainer.nodeType == 3) - endOffset = endContainer.length; + // If end text node is excluded then walk to the previous node + if (container.nodeType === 3 && !start && offset === 0) { + container = new TreeWalker(container, ed.getBody()).prev() || container; } - if (format[0].inline) { - if (rng.collapsed) { - function findWordEndPoint(container, offset, start) { - var walker, node, pos, lastTextNode; + return container; + }; - function findSpace(node, offset) { - var pos, pos2, str = node.nodeValue; + function performCaretAction(type, name, vars) { + var caretContainerId = '_mce_caret', debug = ed.settings.caret_debug; - if (typeof(offset) == "undefined") { - offset = start ? str.length : 0; - } + // Creates a caret container bogus element + function createCaretContainer(fill) { + var caretContainer = dom.create('span', {id: caretContainerId, 'data-mce-bogus': true, style: debug ? 'color:red' : ''}); - if (start) { - pos = str.lastIndexOf(' ', offset); - pos2 = str.lastIndexOf('\u00a0', offset); - pos = pos > pos2 ? pos : pos2; + if (fill) { + caretContainer.appendChild(ed.getDoc().createTextNode(INVISIBLE_CHAR)); + } - // Include the space on remove to avoid tag soup - if (pos !== -1 && !remove) { - pos++; - } - } else { - pos = str.indexOf(' ', offset); - pos2 = str.indexOf('\u00a0', offset); - pos = pos !== -1 && (pos2 === -1 || pos < pos2) ? pos : pos2; - } + return caretContainer; + }; - return pos; - }; + function isCaretContainerEmpty(node, nodes) { + while (node) { + if ((node.nodeType === 3 && node.nodeValue !== INVISIBLE_CHAR) || node.childNodes.length > 1) { + return false; + } - if (container.nodeType === 3) { - pos = findSpace(container, offset); + // Collect nodes + if (nodes && node.nodeType === 1) { + nodes.push(node); + } - if (pos !== -1) { - return {container : container, offset : pos}; - } + node = node.firstChild; + } + + return true; + }; + + // Returns any parent caret container element + function getParentCaretContainer(node) { + while (node) { + if (node.id === caretContainerId) { + return node; + } + + node = node.parentNode; + } + }; + + // Finds the first text node in the specified node + function findFirstTextNode(node) { + var walker; + + if (node) { + walker = new TreeWalker(node, node); - lastTextNode = container; + for (node = walker.current(); node; node = walker.next()) { + if (node.nodeType === 3) { + return node; } + } + } + }; + + // Removes the caret container for the specified node or all on the current document + function removeCaretContainer(node, move_caret) { + var child, rng; + + if (!node) { + node = getParentCaretContainer(selection.getStart()); - // Walk the nodes inside the block - walker = new TreeWalker(container, dom.getParent(container, isBlock) || ed.getBody()); - while (node = walker[start ? 'prev' : 'next']()) { - if (node.nodeType === 3) { - lastTextNode = node; - pos = findSpace(node); + if (!node) { + while (node = dom.get(caretContainerId)) { + removeCaretContainer(node, false); + } + } + } else { + rng = selection.getRng(true); - if (pos !== -1) { - return {container : node, offset : pos}; - } - } else if (isBlock(node)) { - break; - } + if (isCaretContainerEmpty(node)) { + if (move_caret !== false) { + rng.setStartBefore(node); + rng.setEndBefore(node); } - if (lastTextNode) { - if (start) { - offset = 0; - } else { - offset = lastTextNode.length; - } + dom.remove(node); + } else { + child = findFirstTextNode(node); - return {container: lastTextNode, offset: offset}; + if (child.nodeValue.charAt(0) === INVISIBLE_CHAR) { + child = child.deleteData(0, 1); } - } - // Expand left to closest word boundery - endPoint = findWordEndPoint(startContainer, startOffset, true); - if (endPoint) { - startContainer = endPoint.container; - startOffset = endPoint.offset; + dom.remove(node, 1); } - // Expand right to closest word boundery - endPoint = findWordEndPoint(endContainer, endOffset); - if (endPoint) { - endContainer = endPoint.container; - endOffset = endPoint.offset; - } + selection.setRng(rng); } + }; + + // Applies formatting to the caret postion + function applyCaretFormat() { + var rng, caretContainer, textNode, offset, bookmark, container, text; - // Avoid applying formatting to a trailing space. - leaf = findLeaf(endContainer, endOffset); - if (leaf.node) { - while (leaf.node && leaf.offset === 0 && leaf.node.previousSibling) - leaf = findLeaf(leaf.node.previousSibling); - - if (leaf.node && leaf.offset > 0 && leaf.node.nodeType === 3 && - leaf.node.nodeValue.charAt(leaf.offset - 1) === ' ') { + rng = selection.getRng(true); + offset = rng.startOffset; + container = rng.startContainer; + text = container.nodeValue; - if (leaf.offset > 1) { - endContainer = leaf.node; - endContainer.splitText(leaf.offset - 1); - } else if (leaf.node.previousSibling) { - // TODO: Figure out why this is in here - //endContainer = leaf.node.previousSibling; - } - } + caretContainer = getParentCaretContainer(selection.getStart()); + if (caretContainer) { + textNode = findFirstTextNode(caretContainer); } - } - // Move start/end point up the tree if the leaves are sharp and if we are in different containers - // Example * becomes !: !

    *texttext*

    ! - // This will reduce the number of wrapper elements that needs to be created - // Move start point up the tree - if (format[0].inline || format[0].block_expand) { - if (!format[0].inline || (startContainer.nodeType != 3 || startOffset === 0)) { - startContainer = findParentContainer(true); - } + // Expand to word is caret is in the middle of a text node and the char before/after is a alpha numeric character + if (text && offset > 0 && offset < text.length && /\w/.test(text.charAt(offset)) && /\w/.test(text.charAt(offset - 1))) { + // Get bookmark of caret position + bookmark = selection.getBookmark(); - if (!format[0].inline || (endContainer.nodeType != 3 || endOffset === endContainer.nodeValue.length)) { - endContainer = findParentContainer(); - } - } + // Collapse bookmark range (WebKit) + rng.collapse(true); - // Expand start/end container to matching selector - if (format[0].selector && format[0].expand !== FALSE && !format[0].inline) { - function findSelectorEndPoint(container, sibling_name) { - var parents, i, y, curFormat; + // Expand the range to the closest word and split it at those points + rng = expandRng(rng, get(name)); + rng = rangeUtils.split(rng); - if (container.nodeType == 3 && container.nodeValue.length == 0 && container[sibling_name]) - container = container[sibling_name]; + // Apply the format to the range + apply(name, vars, rng); - parents = getParents(container); - for (i = 0; i < parents.length; i++) { - for (y = 0; y < format.length; y++) { - curFormat = format[y]; + // Move selection back to caret position + selection.moveToBookmark(bookmark); + } else { + if (!caretContainer || textNode.nodeValue !== INVISIBLE_CHAR) { + caretContainer = createCaretContainer(true); + textNode = caretContainer.firstChild; - // If collapsed state is set then skip formats that doesn't match that - if ("collapsed" in curFormat && curFormat.collapsed !== rng.collapsed) - continue; + rng.insertNode(caretContainer); + offset = 1; - if (dom.is(parents[i], curFormat.selector)) - return parents[i]; - } + apply(name, vars, caretContainer); + } else { + apply(name, vars, caretContainer); } - return container; - }; - - // Find new startContainer/endContainer if there is better one - startContainer = findSelectorEndPoint(startContainer, 'previousSibling'); - endContainer = findSelectorEndPoint(endContainer, 'nextSibling'); - } - - // Expand start/end container to matching block element or text node - if (format[0].block || format[0].selector) { - function findBlockEndPoint(container, sibling_name, sibling_name2) { - var node; - - // Expand to block of similar type - if (!format[0].wrapper) - node = dom.getParent(container, format[0].block); - - // Expand to first wrappable block element or any block element - if (!node) - node = dom.getParent(container.nodeType == 3 ? container.parentNode : container, isBlock); - - // Exclude inner lists from wrapping - if (node && format[0].wrapper) - node = getParents(node, 'ul,ol').reverse()[0] || node; + // Move selection to text node + selection.setCursorLocation(textNode, offset); + } + }; - // Didn't find a block element look for first/last wrappable element - if (!node) { - node = container; + function removeCaretFormat() { + var rng = selection.getRng(true), container, offset, bookmark, + hasContentAfter, node, formatNode, parents = [], i, caretContainer; - while (node[sibling_name] && !isBlock(node[sibling_name])) { - node = node[sibling_name]; + container = rng.startContainer; + offset = rng.startOffset; + node = container; - // Break on BR but include it will be removed later on - // we can't remove it now since we need to check if it can be wrapped - if (isEq(node, 'br')) - break; - } + if (container.nodeType == 3) { + if (offset != container.nodeValue.length || container.nodeValue === INVISIBLE_CHAR) { + hasContentAfter = true; } - return node || container; - }; + node = node.parentNode; + } - // Find new startContainer/endContainer if there is better one - startContainer = findBlockEndPoint(startContainer, 'previousSibling'); - endContainer = findBlockEndPoint(endContainer, 'nextSibling'); + while (node) { + if (matchNode(node, name, vars)) { + formatNode = node; + break; + } - // Non block element then try to expand up the leaf - if (format[0].block) { - if (!isBlock(startContainer)) - startContainer = findParentContainer(true); + if (node.nextSibling) { + hasContentAfter = true; + } - if (!isBlock(endContainer)) - endContainer = findParentContainer(); + parents.push(node); + node = node.parentNode; } - } - // Setup index for startContainer - if (startContainer.nodeType == 1) { - startOffset = nodeIndex(startContainer); - startContainer = startContainer.parentNode; - } + // Node doesn't have the specified format + if (!formatNode) { + return; + } - // Setup index for endContainer - if (endContainer.nodeType == 1) { - endOffset = nodeIndex(endContainer) + 1; - endContainer = endContainer.parentNode; - } + // Is there contents after the caret then remove the format on the element + if (hasContentAfter) { + // Get bookmark of caret position + bookmark = selection.getBookmark(); - // Return new range like object - return { - startContainer : startContainer, - startOffset : startOffset, - endContainer : endContainer, - endOffset : endOffset - }; - } + // Collapse bookmark range (WebKit) + rng.collapse(true); - function removeFormat(format, vars, node, compare_node) { - var i, attrs, stylesModified; + // Expand the range to the closest word and split it at those points + rng = expandRng(rng, get(name), true); + rng = rangeUtils.split(rng); - // Check if node matches format - if (!matchName(node, format)) - return FALSE; + // Remove the format from the range + remove(name, vars, rng); - // Should we compare with format attribs and styles - if (format.remove != 'all') { - // Remove styles - each(format.styles, function(value, name) { - value = replaceVars(value, vars); + // Move selection back to caret position + selection.moveToBookmark(bookmark); + } else { + caretContainer = createCaretContainer(); - // Indexed array - if (typeof(name) === 'number') { - name = value; - compare_node = 0; + node = caretContainer; + for (i = parents.length - 1; i >= 0; i--) { + node.appendChild(dom.clone(parents[i], false)); + node = node.firstChild; } - if (!compare_node || isEq(getStyle(compare_node, name), value)) - dom.setStyle(node, name, ''); + // Insert invisible character into inner most format element + node.appendChild(dom.doc.createTextNode(INVISIBLE_CHAR)); + node = node.firstChild; - stylesModified = 1; - }); + // Insert caret container after the formated node + dom.insertAfter(caretContainer, formatNode); - // Remove style attribute if it's empty - if (stylesModified && dom.getAttrib(node, 'style') == '') { - node.removeAttribute('style'); - node.removeAttribute('data-mce-style'); + // Move selection to text node + selection.setCursorLocation(node, 1); } + }; - // Remove attributes - each(format.attributes, function(value, name) { - var valueOut; - - value = replaceVars(value, vars); - - // Indexed array - if (typeof(name) === 'number') { - name = value; - compare_node = 0; - } - - if (!compare_node || isEq(dom.getAttrib(compare_node, name), value)) { - // Keep internal classes - if (name == 'class') { - value = dom.getAttrib(node, name); - if (value) { - // Build new class value where everything is removed except the internal prefixed classes - valueOut = ''; - each(value.split(/\s+/), function(cls) { - if (/mce\w+/.test(cls)) - valueOut += (valueOut ? ' ' : '') + cls; - }); + // Checks if the parent caret container node isn't empty if that is the case it + // will remove the bogus state on all children that isn't empty + function unmarkBogusCaretParents() { + var i, caretContainer, node; - // We got some internal classes left - if (valueOut) { - dom.setAttrib(node, name, valueOut); - return; - } - } + caretContainer = getParentCaretContainer(selection.getStart()); + if (caretContainer && !dom.isEmpty(caretContainer)) { + tinymce.walk(caretContainer, function(node) { + if (node.nodeType == 1 && node.id !== caretContainerId && !dom.isEmpty(node)) { + dom.setAttrib(node, 'data-mce-bogus', null); } + }, 'childNodes'); + } + }; - // IE6 has a bug where the attribute doesn't get removed correctly - if (name == "class") - node.removeAttribute('className'); - - // Remove mce prefixed attributes - if (MCE_ATTR_RE.test(name)) - node.removeAttribute('data-mce-' + name); + // Only bind the caret events once + if (!self._hasCaretEvents) { + // Mark current caret container elements as bogus when getting the contents so we don't end up with empty elements + ed.onBeforeGetContent.addToTop(function() { + var nodes = [], i; - node.removeAttribute(name); + if (isCaretContainerEmpty(getParentCaretContainer(selection.getStart()), nodes)) { + // Mark children + i = nodes.length; + while (i--) { + dom.setAttrib(nodes[i], 'data-mce-bogus', '1'); + } } }); - // Remove classes - each(format.classes, function(value) { - value = replaceVars(value, vars); + // Remove caret container on mouse up and on key up + tinymce.each('onMouseUp onKeyUp'.split(' '), function(name) { + ed[name].addToTop(function() { + removeCaretContainer(); + unmarkBogusCaretParents(); + }); + }); + + // Remove caret container on keydown and it's a backspace, enter or left/right arrow keys + ed.onKeyDown.addToTop(function(ed, e) { + var keyCode = e.keyCode; + + if (keyCode == 8 || keyCode == 37 || keyCode == 39) { + removeCaretContainer(getParentCaretContainer(selection.getStart())); + } - if (!compare_node || dom.hasClass(compare_node, value)) - dom.removeClass(node, value); + unmarkBogusCaretParents(); }); - // Check for non internal attributes - attrs = dom.getAttribs(node); - for (i = 0; i < attrs.length; i++) { - if (attrs[i].nodeName.indexOf('_') !== 0) - return FALSE; - } + // Remove bogus state if they got filled by contents using editor.selection.setContent + selection.onSetContent.add(unmarkBogusCaretParents); + + self._hasCaretEvents = true; } - // Remove the inline child if it's empty for example or - if (format.remove != 'none') { - removeNode(node, format); - return TRUE; + // Do apply or remove caret format + if (type == "apply") { + applyCaretFormat(); + } else { + removeCaretFormat(); } }; - function removeNode(node, format) { - var parentNode = node.parentNode, rootBlockElm; - - if (format.block) { - if (!forcedRootBlock) { - function find(node, next, inc) { - node = getNonWhiteSpaceSibling(node, next, inc); + function moveStart(rng) { + var container = rng.startContainer, + offset = rng.startOffset, isAtEndOfText, + walker, node, nodes, tmpNode; - return !node || (node.nodeName == 'BR' || isBlock(node)); - }; + // Convert text node into index if possible + if (container.nodeType == 3 && offset >= container.nodeValue.length) { + // Get the parent container location and walk from there + offset = nodeIndex(container); + container = container.parentNode; + isAtEndOfText = true; + } - // Append BR elements if needed before we remove the block - if (isBlock(node) && !isBlock(parentNode)) { - if (!find(node, FALSE) && !find(node.firstChild, TRUE, 1)) - node.insertBefore(dom.create('br'), node.firstChild); + // 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, dom.getParent(container, dom.isBlock)); + + // If offset is at end of the parent node walk to the next one + if (offset > nodes.length - 1 || isAtEndOfText) + 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); - if (!find(node, TRUE) && !find(node.lastChild, FALSE, 1)) - node.appendChild(dom.create('br')); - } - } else { - // Wrap the block in a forcedRootBlock if we are at the root of document - if (parentNode == dom.getRoot()) { - if (!format.list_block || !isEq(node, format.list_block)) { - each(tinymce.grep(node.childNodes), function(node) { - if (isValid(forcedRootBlock, node.nodeName.toLowerCase())) { - if (!rootBlockElm) - rootBlockElm = wrap(node, forcedRootBlock); - else - rootBlockElm.appendChild(node); - } else - rootBlockElm = 0; - }); - } + return; } } } + }; + }; +})(tinymce); - // 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; +tinymce.onAddEditor.add(function(tinymce, ed) { + var filters, fontSizes, dom, settings = ed.settings; - dom.remove(node, 1); - }; + function replaceWithSpan(node, styles) { + tinymce.each(styles, function(value, name) { + if (value) + dom.setStyle(node, name, value); + }); - function getNonWhiteSpaceSibling(node, next, inc) { - if (node) { - next = next ? 'nextSibling' : 'previousSibling'; + dom.rename(node, 'span'); + }; - for (node = inc ? node : node[next]; node; node = node[next]) { - if (node.nodeType == 1 || !isWhiteSpaceNode(node)) - return node; - } - } - }; + function convert(editor, params) { + dom = editor.dom; - function isBookmarkNode(node) { - return node && node.nodeType == 1 && node.getAttribute('data-mce-type') == 'bookmark'; - }; + if (settings.convert_fonts_to_spans) { + tinymce.each(dom.select('font,u,strike', params.node), function(node) { + filters[node.nodeName.toLowerCase()](ed.dom, node); + }); + } + }; - function mergeSiblings(prev, next) { - var marker, sibling, tmpSibling; + if (settings.inline_styles) { + fontSizes = tinymce.explode(settings.font_size_legacy_values); - function compareElements(node1, node2) { - // Not the same name - if (node1.nodeName != node2.nodeName) - return FALSE; + filters = { + font : function(dom, node) { + replaceWithSpan(node, { + backgroundColor : node.style.backgroundColor, + color : node.color, + fontFamily : node.face, + fontSize : fontSizes[parseInt(node.size, 10) - 1] + }); + }, - function getAttribs(node) { - var attribs = {}; + u : function(dom, node) { + replaceWithSpan(node, { + textDecoration : 'underline' + }); + }, - each(dom.getAttribs(node), function(attr) { - var name = attr.nodeName.toLowerCase(); + strike : function(dom, node) { + replaceWithSpan(node, { + textDecoration : 'line-through' + }); + } + }; - // Don't compare internal attributes or style - if (name.indexOf('_') !== 0 && name !== 'style') - attribs[name] = dom.getAttrib(node, name); - }); + ed.onPreProcess.add(convert); + ed.onSetContent.add(convert); - return attribs; - }; + ed.onInit.add(function() { + ed.selection.onSetContent.add(convert); + }); + } +}); - function compareObjects(obj1, obj2) { - var value, name; +(function(tinymce) { + var TreeWalker = tinymce.dom.TreeWalker; + + tinymce.EnterKey = function(editor) { + var dom = editor.dom, selection = editor.selection, settings = editor.settings, undoManager = editor.undoManager, nonEmptyElementsMap = editor.schema.getNonEmptyElements(); + + function handleEnterKey(evt) { + var rng = selection.getRng(true), tmpRng, editableRoot, container, offset, parentBlock, documentMode, shiftKey, + newBlock, fragment, containerBlock, parentBlockName, containerBlockName, newBlockName, isAfterLastNodeInContainer; + + // Returns true if the block can be split into two blocks or not + function canSplitBlock(node) { + return node && + dom.isBlock(node) && + !/^(TD|TH|CAPTION|FORM)$/.test(node.nodeName) && + !/^(fixed|absolute)/i.test(node.style.position) && + dom.getContentEditable(node) !== "true"; + }; - for (name in obj1) { - // Obj1 has item obj2 doesn't have - if (obj1.hasOwnProperty(name)) { - value = obj2[name]; + // Renders empty block on IE + function renderBlockOnIE(block) { + var oldRng; - // Obj2 doesn't have obj1 item - if (value === undefined) - return FALSE; + if (tinymce.isIE && dom.isBlock(block)) { + oldRng = selection.getRng(); + block.appendChild(dom.create('span', null, '\u00a0')); + selection.select(block); + block.lastChild.outerHTML = ''; + selection.setRng(oldRng); + } + }; - // Obj2 item has a different value - if (obj1[name] != value) - return FALSE; + // Remove the first empty inline element of the block so this:

    x

    becomes this:

    x

    + function trimInlineElementsOnLeftSideOfBlock(block) { + var node = block, firstChilds = [], i; - // Delete similar value - delete obj2[name]; - } + // Find inner most first child ex:

    *

    + while (node = node.firstChild) { + if (dom.isBlock(node)) { + return; } - // Check if obj 2 has something obj 1 doesn't have - for (name in obj2) { - // Obj2 has item obj1 doesn't have - if (obj2.hasOwnProperty(name)) - return FALSE; + if (node.nodeType == 1 && !nonEmptyElementsMap[node.nodeName.toLowerCase()]) { + firstChilds.push(node); } + } - return TRUE; - }; + i = firstChilds.length; + while (i--) { + node = firstChilds[i]; + if (!node.hasChildNodes() || (node.firstChild == node.lastChild && node.firstChild.nodeValue === '')) { + dom.remove(node); + } else { + // Remove
    see #5381 + if (node.nodeName == "A" && (node.innerText || node.textContent) === ' ') { + dom.remove(node); + } + } + } + }; + + // Moves the caret to a suitable position within the root for example in the first non pure whitespace text node or before an image + function moveToCaretPosition(root) { + var walker, node, rng, y, viewPort, lastNode = root, tempElm; - // Attribs are not the same - if (!compareObjects(getAttribs(node1), getAttribs(node2))) - return FALSE; + rng = dom.createRng(); - // Styles are not the same - if (!compareObjects(dom.parseStyle(dom.getAttrib(node1, 'style')), dom.parseStyle(dom.getAttrib(node2, 'style')))) - return FALSE; + if (root.hasChildNodes()) { + walker = new TreeWalker(root, root); - return TRUE; - }; + while (node = walker.current()) { + if (node.nodeType == 3) { + rng.setStart(node, 0); + rng.setEnd(node, 0); + break; + } - // Check if next/prev exists and that they are elements - if (prev && next) { - function findElementSibling(node, sibling_name) { - for (sibling = node; sibling; sibling = sibling[sibling_name]) { - if (sibling.nodeType == 3 && sibling.nodeValue.length !== 0) - return node; + if (nonEmptyElementsMap[node.nodeName.toLowerCase()]) { + rng.setStartBefore(node); + rng.setEndBefore(node); + break; + } - if (sibling.nodeType == 1 && !isBookmarkNode(sibling)) - return sibling; + lastNode = node; + node = walker.next(); } - return node; - }; - - // If previous sibling is empty then jump over it - prev = findElementSibling(prev, 'previousSibling'); - next = findElementSibling(next, 'nextSibling'); - - // Compare next and previous nodes - if (compareElements(prev, next)) { - // Append nodes between - for (sibling = prev.nextSibling; sibling && sibling != next;) { - tmpSibling = sibling; - sibling = sibling.nextSibling; - prev.appendChild(tmpSibling); + if (!node) { + rng.setStart(lastNode, 0); + rng.setEnd(lastNode, 0); } + } else { + if (root.nodeName == 'BR') { + if (root.nextSibling && dom.isBlock(root.nextSibling)) { + // Trick on older IE versions to render the caret before the BR between two lists + if (!documentMode || documentMode < 9) { + tempElm = dom.create('br'); + root.parentNode.insertBefore(tempElm, root); + } - // Remove next node - dom.remove(next); + rng.setStartBefore(root); + rng.setEndBefore(root); + } else { + rng.setStartAfter(root); + rng.setEndAfter(root); + } + } else { + rng.setStart(root, 0); + rng.setEnd(root, 0); + } + } - // Move children into prev node - each(tinymce.grep(next.childNodes), function(node) { - prev.appendChild(node); - }); + selection.setRng(rng); - return prev; - } - } + // Remove tempElm created for old IE:s + dom.remove(tempElm); - return next; - }; + viewPort = dom.getViewPort(editor.getWin()); - function isTextBlock(name) { - return /^(h[1-6]|p|div|pre|address|dl|dt|dd)$/.test(name); - }; + // scrollIntoView seems to scroll the parent window in most browsers now including FF 3.0b4 so it's time to stop using it and do it our selfs + y = dom.getPos(root).y; + if (y < viewPort.y || y + 25 > viewPort.y + viewPort.h) { + editor.getWin().scrollTo(0, y < viewPort.y ? y : y - viewPort.h + 25); // Needs to be hardcoded to roughly one line of text if a huge text block is broken into two blocks + } + }; - function getContainer(rng, start) { - var container, offset, lastIdx, walker; + // Creates a new block element by cloning the current one or creating a new one if the name is specified + // This function will also copy any text formatting from the parent block and add it to the new one + function createNewBlock(name) { + var node = container, block, clonedNode, caretNode; - container = rng[start ? 'startContainer' : 'endContainer']; - offset = rng[start ? 'startOffset' : 'endOffset']; + block = name || parentBlockName == "TABLE" ? dom.create(name || newBlockName) : parentBlock.cloneNode(false); + caretNode = block; - if (container.nodeType == 1) { - lastIdx = container.childNodes.length - 1; + // Clone any parent styles + if (settings.keep_styles !== false) { + do { + if (/^(SPAN|STRONG|B|EM|I|FONT|STRIKE|U)$/.test(node.nodeName)) { + // Never clone a caret containers + if (node.id == '_mce_caret') { + continue; + } - if (!start && offset) - offset--; + clonedNode = node.cloneNode(false); + dom.setAttrib(clonedNode, 'id', ''); // Remove ID since it needs to be document unique - container = container.childNodes[offset > lastIdx ? lastIdx : offset]; - } + if (block.hasChildNodes()) { + clonedNode.appendChild(block.firstChild); + block.appendChild(clonedNode); + } else { + caretNode = clonedNode; + block.appendChild(clonedNode); + } + } + } while (node = node.parentNode); + } - // If start text node is excluded then walk to the next node - if (container.nodeType === 3 && start && offset >= container.nodeValue.length) { - container = new TreeWalker(container, ed.getBody()).next() || container; - } + // BR is needed in empty blocks on non IE browsers + if (!tinymce.isIE) { + caretNode.innerHTML = '
    '; + } - // If end text node is excluded then walk to the previous node - if (container.nodeType === 3 && !start && offset == 0) { - container = new TreeWalker(container, ed.getBody()).prev() || container; - } + return block; + }; - return container; - }; + // Returns true/false if the caret is at the start/end of the parent block element + function isCaretAtStartOrEndOfBlock(start) { + var walker, node, name; - function performCaretAction(type, name, vars) { - var invisibleChar, caretContainerId = '_mce_caret', debug = ed.settings.caret_debug; + // Caret is in the middle of a text node like "a|b" + if (container.nodeType == 3 && (start ? offset > 0 : offset < container.nodeValue.length)) { + return false; + } - // Setup invisible character use zero width space on Gecko since it doesn't change the heigt of the container - invisibleChar = tinymce.isGecko ? '\u200B' : INVISIBLE_CHAR; + // If after the last element in block node edge case for #5091 + if (container.parentNode == parentBlock && isAfterLastNodeInContainer && !start) { + return true; + } - // Creates a caret container bogus element - function createCaretContainer(fill) { - var caretContainer = dom.create('span', {id: caretContainerId, 'data-mce-bogus': true, style: debug ? 'color:red' : ''}); + // If the caret if before the first element in parentBlock + if (start && container.nodeType == 1 && container == parentBlock.firstChild) { + return true; + } - if (fill) { - caretContainer.appendChild(ed.getDoc().createTextNode(invisibleChar)); + // Caret can be before/after a table + if (container.nodeName === "TABLE" || (container.previousSibling && container.previousSibling.nodeName == "TABLE")) { + return (isAfterLastNodeInContainer && !start) || (!isAfterLastNodeInContainer && start); } - return caretContainer; - }; + // Walk the DOM and look for text nodes or non empty elements + walker = new TreeWalker(container, parentBlock); + + // If caret is in beginning or end of a text block then jump to the next/previous node + if (container.nodeType == 3) { + if (start && offset == 0) { + walker.prev(); + } else if (!start && offset == container.nodeValue.length) { + walker.next(); + } + } - function isCaretContainerEmpty(node, nodes) { - while (node) { - if ((node.nodeType === 3 && node.nodeValue !== invisibleChar) || node.childNodes.length > 1) { + while (node = walker.current()) { + if (node.nodeType === 1) { + // Ignore bogus elements + if (!node.getAttribute('data-mce-bogus')) { + // Keep empty elements like but not trailing br:s like

    text|

    + name = node.nodeName.toLowerCase(); + if (nonEmptyElementsMap[name] && name !== 'br') { + return false; + } + } + } else if (node.nodeType === 3 && !/^[ \t\r\n]*$/.test(node.nodeValue)) { return false; } - // Collect nodes - if (nodes && node.nodeType === 1) { - nodes.push(node); + if (start) { + walker.prev(); + } else { + walker.next(); } - - node = node.firstChild; } return true; }; - - // Returns any parent caret container element - function getParentCaretContainer(node) { - while (node) { - if (node.id === caretContainerId) { - return node; + + // Wraps any text nodes or inline elements in the specified forced root block name + function wrapSelfAndSiblingsInDefaultBlock(container, offset) { + var newBlock, parentBlock, startNode, node, next, blockName = newBlockName || 'P'; + + // Not in a block element or in a table cell or caption + parentBlock = dom.getParent(container, dom.isBlock); + if (!parentBlock || !canSplitBlock(parentBlock)) { + parentBlock = parentBlock || editableRoot; + + if (!parentBlock.hasChildNodes()) { + newBlock = dom.create(blockName); + parentBlock.appendChild(newBlock); + rng.setStart(newBlock, 0); + rng.setEnd(newBlock, 0); + return newBlock; } - node = node.parentNode; - } - }; + // Find parent that is the first child of parentBlock + node = container; + while (node.parentNode != parentBlock) { + node = node.parentNode; + } - // Finds the first text node in the specified node - function findFirstTextNode(node) { - var walker; + // Loop left to find start node start wrapping at + while (node && !dom.isBlock(node)) { + startNode = node; + node = node.previousSibling; + } - if (node) { - walker = new TreeWalker(node, node); + if (startNode) { + newBlock = dom.create(blockName); + startNode.parentNode.insertBefore(newBlock, startNode); - for (node = walker.current(); node; node = walker.next()) { - if (node.nodeType === 3) { - return node; + // Start wrapping until we hit a block + node = startNode; + while (node && !dom.isBlock(node)) { + next = node.nextSibling; + newBlock.appendChild(node); + node = next; } + + // Restore range to it's past location + rng.setStart(container, offset); + rng.setEnd(container, offset); } } - }; - // Removes the caret container for the specified node or all on the current document - function removeCaretContainer(node, move_caret) { - var child, rng; + return container; + }; - if (!node) { - node = getParentCaretContainer(selection.getStart()); + // Inserts a block or br before/after or in the middle of a split list of the LI is empty + function handleEmptyListItem() { + function isFirstOrLastLi(first) { + var node = containerBlock[first ? 'firstChild' : 'lastChild']; - if (!node) { - while (node = dom.get(caretContainerId)) { - removeCaretContainer(node, false); + // Find first/last element since there might be whitespace there + while (node) { + if (node.nodeType == 1) { + break; } + + node = node[first ? 'nextSibling' : 'previousSibling']; } + + return node === parentBlock; + }; + + newBlock = newBlockName ? createNewBlock(newBlockName) : dom.create('BR'); + + if (isFirstOrLastLi(true) && isFirstOrLastLi()) { + // Is first and last list item then replace the OL/UL with a text block + dom.replace(newBlock, containerBlock); + } else if (isFirstOrLastLi(true)) { + // First LI in list then remove LI and add text block before list + containerBlock.parentNode.insertBefore(newBlock, containerBlock); + } else if (isFirstOrLastLi()) { + // Last LI in list then temove LI and add text block after list + dom.insertAfter(newBlock, containerBlock); + renderBlockOnIE(newBlock); } else { - rng = selection.getRng(true); + // Middle LI in list the split the list and insert a text block in the middle + // Extract after fragment and insert it after the current block + tmpRng = rng.cloneRange(); + tmpRng.setStartAfter(parentBlock); + tmpRng.setEndAfter(containerBlock); + fragment = tmpRng.extractContents(); + dom.insertAfter(fragment, containerBlock); + dom.insertAfter(newBlock, containerBlock); + } + + dom.remove(parentBlock); + moveToCaretPosition(newBlock); + undoManager.add(); + }; - if (isCaretContainerEmpty(node)) { - if (move_caret !== false) { - rng.setStartBefore(node); - rng.setEndBefore(node); - } + // Walks the parent block to the right and look for BR elements + function hasRightSideBr() { + var walker = new TreeWalker(container, parentBlock), node; - dom.remove(node); - } else { - child = findFirstTextNode(node); - child = child.deleteData(0, 1); - dom.remove(node, 1); + while (node = walker.current()) { + if (node.nodeName == 'BR') { + return true; } - selection.setRng(rng); + node = walker.next(); } - }; + } - // Applies formatting to the caret postion - function applyCaretFormat() { - var rng, caretContainer, textNode, offset, bookmark, container, text; + // Inserts a BR element if the forced_root_block option is set to false or empty string + function insertBr() { + var brElm, extraBr, marker; + + if (container && container.nodeType == 3 && offset >= container.nodeValue.length) { + // Insert extra BR element at the end block elements + if (!tinymce.isIE && !hasRightSideBr()) { + brElm = dom.create('br'); + rng.insertNode(brElm); + rng.setStartAfter(brElm); + rng.setEndAfter(brElm); + extraBr = true; + } + } - rng = selection.getRng(true); - offset = rng.startOffset; - container = rng.startContainer; - text = container.nodeValue; + brElm = dom.create('br'); + rng.insertNode(brElm); - caretContainer = getParentCaretContainer(selection.getStart()); - if (caretContainer) { - textNode = findFirstTextNode(caretContainer); + // Rendering modes below IE8 doesn't display BR elements in PRE unless we have a \n before it + if (tinymce.isIE && parentBlockName == 'PRE' && (!documentMode || documentMode < 8)) { + brElm.parentNode.insertBefore(dom.doc.createTextNode('\r'), brElm); } - // Expand to word is caret is in the middle of a text node and the char before/after is a alpha numeric character - if (text && offset > 0 && offset < text.length && /\w/.test(text.charAt(offset)) && /\w/.test(text.charAt(offset - 1))) { - // Get bookmark of caret position - bookmark = selection.getBookmark(); + // Insert temp marker and scroll to that + marker = dom.create('span', {}, ' '); + brElm.parentNode.insertBefore(marker, brElm); + selection.scrollIntoView(marker); + dom.remove(marker); - // Collapse bookmark range (WebKit) - rng.collapse(true); + if (!extraBr) { + rng.setStartAfter(brElm); + rng.setEndAfter(brElm); + } else { + rng.setStartBefore(brElm); + rng.setEndBefore(brElm); + } - // Expand the range to the closest word and split it at those points - rng = expandRng(rng, get(name)); - rng = rangeUtils.split(rng); + selection.setRng(rng); + undoManager.add(); + }; - // Apply the format to the range - apply(name, vars, rng); + // Trims any linebreaks at the beginning of node user for example when pressing enter in a PRE element + function trimLeadingLineBreaks(node) { + do { + if (node.nodeType === 3) { + node.nodeValue = node.nodeValue.replace(/^[\r\n]+/, ''); + } - // Move selection back to caret position - selection.moveToBookmark(bookmark); - } else { - if (!caretContainer || textNode.nodeValue !== invisibleChar) { - caretContainer = createCaretContainer(true); - textNode = caretContainer.firstChild; + node = node.firstChild; + } while (node); + }; - rng.insertNode(caretContainer); - offset = 1; + function getEditableRoot(node) { + var root = dom.getRoot(), parent, editableRoot; - apply(name, vars, caretContainer); - } else { - apply(name, vars, caretContainer); + // Get all parents until we hit a non editable parent or the root + parent = node; + while (parent !== root && dom.getContentEditable(parent) !== "false") { + if (dom.getContentEditable(parent) === "true") { + editableRoot = parent; } - // Move selection to text node - selection.setCursorLocation(textNode, offset); + parent = parent.parentNode; } + + return parent !== root ? editableRoot : root; }; - function removeCaretFormat() { - var rng = selection.getRng(true), container, offset, bookmark, - hasContentAfter, node, formatNode, parents = [], i, caretContainer; + // Adds a BR at the end of blocks that only contains an IMG or INPUT since these might be floated and then they won't expand the block + function addBrToBlockIfNeeded(block) { + var lastChild; - container = rng.startContainer; - offset = rng.startOffset; - node = container; + // IE will render the blocks correctly other browsers needs a BR + if (!tinymce.isIE) { + block.normalize(); // Remove empty text nodes that got left behind by the extract - if (container.nodeType == 3) { - if (offset != container.nodeValue.length || container.nodeValue === invisibleChar) { - hasContentAfter = true; + // Check if the block is empty or contains a floated last child + lastChild = block.lastChild; + if (!lastChild || (/^(left|right)$/gi.test(dom.getStyle(lastChild, 'float', true)))) { + dom.add(block, 'br'); } - - node = node.parentNode; } + }; - while (node) { - if (matchNode(node, name, vars)) { - formatNode = node; - break; - } + // Delete any selected contents + if (!rng.collapsed) { + editor.execCommand('Delete'); + return; + } - if (node.nextSibling) { - hasContentAfter = true; - } + // Event is blocked by some other handler for example the lists plugin + if (evt.isDefaultPrevented()) { + return; + } - parents.push(node); - node = node.parentNode; + // Setup range items and newBlockName + container = rng.startContainer; + offset = rng.startOffset; + newBlockName = (settings.force_p_newlines ? 'p' : '') || settings.forced_root_block; + newBlockName = newBlockName ? newBlockName.toUpperCase() : ''; + documentMode = dom.doc.documentMode; + shiftKey = evt.shiftKey; + + // Resolve node index + if (container.nodeType == 1 && container.hasChildNodes()) { + isAfterLastNodeInContainer = offset > container.childNodes.length - 1; + container = container.childNodes[Math.min(offset, container.childNodes.length - 1)] || container; + if (isAfterLastNodeInContainer && container.nodeType == 3) { + offset = container.nodeValue.length; + } else { + offset = 0; } + } - // Node doesn't have the specified format - if (!formatNode) { - return; - } + // Get editable root node normaly the body element but sometimes a div or span + editableRoot = getEditableRoot(container); - // Is there contents after the caret then remove the format on the element - if (hasContentAfter) { - // Get bookmark of caret position - bookmark = selection.getBookmark(); + // If there is no editable root then enter is done inside a contentEditable false element + if (!editableRoot) { + return; + } - // Collapse bookmark range (WebKit) - rng.collapse(true); + undoManager.beforeChange(); - // Expand the range to the closest word and split it at those points - rng = expandRng(rng, get(name), true); - rng = rangeUtils.split(rng); + // If editable root isn't block nor the root of the editor + if (!dom.isBlock(editableRoot) && editableRoot != dom.getRoot()) { + if (!newBlockName || shiftKey) { + insertBr(); + } - // Remove the format from the range - remove(name, vars, rng); + return; + } - // Move selection back to caret position - selection.moveToBookmark(bookmark); - } else { - caretContainer = createCaretContainer(); + // Wrap the current node and it's sibling in a default block if it's needed. + // for example this text|text2 will become this

    text|text2

    + // This won't happen if root blocks are disabled or the shiftKey is pressed + if ((newBlockName && !shiftKey) || (!newBlockName && shiftKey)) { + container = wrapSelfAndSiblingsInDefaultBlock(container, offset); + } - node = caretContainer; - for (i = parents.length - 1; i >= 0; i--) { - node.appendChild(parents[i].cloneNode(false)); - node = node.firstChild; - } + // Find parent block and setup empty block paddings + parentBlock = dom.getParent(container, dom.isBlock); + containerBlock = parentBlock ? dom.getParent(parentBlock.parentNode, dom.isBlock) : null; - // Insert invisible character into inner most format element - node.appendChild(dom.doc.createTextNode(invisibleChar)); - node = node.firstChild; + // Setup block names + parentBlockName = parentBlock ? parentBlock.nodeName.toUpperCase() : ''; // IE < 9 & HTML5 + containerBlockName = containerBlock ? containerBlock.nodeName.toUpperCase() : ''; // IE < 9 & HTML5 - // Insert caret container after the formated node - dom.insertAfter(caretContainer, formatNode); + // Enter inside block contained within a LI then split or insert before/after LI + if (containerBlockName == 'LI' && !evt.ctrlKey) { + parentBlock = containerBlock; + parentBlockName = containerBlockName; + } - // Move selection to text node - selection.setCursorLocation(node, 1); + // Handle enter in LI + if (parentBlockName == 'LI') { + if (!newBlockName && shiftKey) { + insertBr(); + return; } - }; - - // Mark current caret container elements as bogus when getting the contents so we don't end up with empty elements - ed.onBeforeGetContent.addToTop(function() { - var nodes = [], i; - if (isCaretContainerEmpty(getParentCaretContainer(selection.getStart()), nodes)) { - // Mark children - i = nodes.length; - while (i--) { - dom.setAttrib(nodes[i], 'data-mce-bogus', '1'); + // Handle enter inside an empty list item + if (dom.isEmpty(parentBlock)) { + // Let the list plugin or browser handle nested lists for now + if (/^(UL|OL|LI)$/.test(containerBlock.parentNode.nodeName)) { + return false; } - } - }); - - // Remove caret container on mouse up and on key up - tinymce.each('onMouseUp onKeyUp'.split(' '), function(name) { - ed[name].addToTop(function() { - removeCaretContainer(); - }); - }); - - // Remove caret container on keydown and it's a backspace, enter or left/right arrow keys - ed.onKeyDown.addToTop(function(ed, e) { - var keyCode = e.keyCode; - if (keyCode == 8 || keyCode == 37 || keyCode == 39) { - removeCaretContainer(getParentCaretContainer(selection.getStart())); + handleEmptyListItem(); + return; } - }); + } - // Do apply or remove caret format - if (type == "apply") { - applyCaretFormat(); + // Don't split PRE tags but insert a BR instead easier when writing code samples etc + if (parentBlockName == 'PRE' && settings.br_in_pre !== false) { + if (!shiftKey) { + insertBr(); + return; + } } else { - removeCaretFormat(); + // If no root block is configured then insert a BR by default or if the shiftKey is pressed + if ((!newBlockName && !shiftKey && parentBlockName != 'LI') || (newBlockName && shiftKey)) { + insertBr(); + return; + } } - }; - }; -})(tinymce); -tinymce.onAddEditor.add(function(tinymce, ed) { - var filters, fontSizes, dom, settings = ed.settings; - - if (settings.inline_styles) { - fontSizes = tinymce.explode(settings.font_size_legacy_values); - - function replaceWithSpan(node, styles) { - tinymce.each(styles, function(value, name) { - if (value) - dom.setStyle(node, name, value); - }); - - dom.rename(node, 'span'); - }; - - filters = { - font : function(dom, node) { - replaceWithSpan(node, { - backgroundColor : node.style.backgroundColor, - color : node.color, - fontFamily : node.face, - fontSize : fontSizes[parseInt(node.size) - 1] - }); - }, + // Default block name if it's not configured + newBlockName = newBlockName || 'P'; - u : function(dom, node) { - replaceWithSpan(node, { - textDecoration : 'underline' - }); - }, + // Insert new block before/after the parent block depending on caret location + if (isCaretAtStartOrEndOfBlock()) { + // If the caret is at the end of a header we produce a P tag after it similar to Word unless we are in a hgroup + if (/^(H[1-6]|PRE)$/.test(parentBlockName) && containerBlockName != 'HGROUP') { + newBlock = createNewBlock(newBlockName); + } else { + newBlock = createNewBlock(); + } - strike : function(dom, node) { - replaceWithSpan(node, { - textDecoration : 'line-through' - }); - } - }; + // Split the current container block element if enter is pressed inside an empty inner block element + if (settings.end_container_on_empty_block && canSplitBlock(containerBlock) && dom.isEmpty(parentBlock)) { + // Split container block for example a BLOCKQUOTE at the current blockParent location for example a P + newBlock = dom.split(containerBlock, parentBlock); + } else { + dom.insertAfter(newBlock, parentBlock); + } - function convert(editor, params) { - dom = editor.dom; + moveToCaretPosition(newBlock); + } else if (isCaretAtStartOrEndOfBlock(true)) { + // Insert new block before + newBlock = parentBlock.parentNode.insertBefore(createNewBlock(), parentBlock); + renderBlockOnIE(newBlock); + } else { + // Extract after fragment and insert it after the current block + tmpRng = rng.cloneRange(); + tmpRng.setEndAfter(parentBlock); + fragment = tmpRng.extractContents(); + trimLeadingLineBreaks(fragment); + newBlock = fragment.firstChild; + dom.insertAfter(fragment, parentBlock); + trimInlineElementsOnLeftSideOfBlock(newBlock); + addBrToBlockIfNeeded(parentBlock); + moveToCaretPosition(newBlock); + } + + dom.setAttrib(newBlock, 'id', ''); // Remove ID since it needs to be document unique + undoManager.add(); + } - if (settings.convert_fonts_to_spans) { - tinymce.each(dom.select('font,u,strike', params.node), function(node) { - filters[node.nodeName.toLowerCase()](ed.dom, node); - }); + editor.onKeyDown.add(function(ed, evt) { + if (evt.keyCode == 13) { + if (handleEnterKey(evt) !== false) { + evt.preventDefault(); + } } - }; - - ed.onPreProcess.add(convert); - ed.onSetContent.add(convert); - - ed.onInit.add(function() { - ed.selection.onSetContent.add(convert); }); - } -}); + }; +})(tinymce); -- cgit v1.2.3