From f55779fd831029f764c885bf1fd026a7e94f08eb Mon Sep 17 00:00:00 2001 From: friendica Date: Tue, 20 Mar 2012 20:47:31 -0700 Subject: update tinymce to 3.5b2 to fix issues with FF 11 and pasting into code blocks --- library/tinymce/jscripts/tiny_mce/tiny_mce_src.js | 20360 +++++++++++--------- 1 file changed, 11779 insertions(+), 8581 deletions(-) mode change 100755 => 100644 library/tinymce/jscripts/tiny_mce/tiny_mce_src.js (limited to 'library/tinymce/jscripts/tiny_mce/tiny_mce_src.js') diff --git a/library/tinymce/jscripts/tiny_mce/tiny_mce_src.js b/library/tinymce/jscripts/tiny_mce/tiny_mce_src.js old mode 100755 new mode 100644 index 9db8d18fe..42f01a58c --- a/library/tinymce/jscripts/tiny_mce/tiny_mce_src.js +++ b/library/tinymce/jscripts/tiny_mce/tiny_mce_src.js @@ -1,13 +1,13 @@ (function(win) { var whiteSpaceRe = /^\s*|\s*$/g, - undefined; + undefined, isRegExpBroken = 'B'.replace(/A(.)|B/, '$1') === '$1'; var tinymce = { majorVersion : '3', - minorVersion : '3.7', + minorVersion : '5b2', - releaseDate : '2010-06-10', + releaseDate : '2012-03-15', _init : function() { var t = this, d = document, na = navigator, ua = na.userAgent, i, nl, n, base, p, v; @@ -20,6 +20,12 @@ t.isIE6 = t.isIE && /MSIE [56]/.test(ua); + t.isIE7 = t.isIE && /MSIE [7]/.test(ua); + + t.isIE8 = t.isIE && /MSIE [8]/.test(ua); + + t.isIE9 = t.isIE && /MSIE [9]/.test(ua); + t.isGecko = !t.isWebKit && /Gecko/.test(ua); t.isMac = ua.indexOf('Mac') != -1; @@ -27,6 +33,8 @@ t.isAir = /adobeair/i.test(ua); t.isIDevice = /(iPad|iPhone)/.test(ua); + + t.isIOS5 = t.isIDevice && ua.match(/AppleWebKit\/(\d*)/)[1]>=534; // TinyMCE .NET webcontrol might be setting the values for TinyMCE if (win.tinyMCEPreInit) { @@ -52,7 +60,7 @@ } function getBase(n) { - if (n.src && /tiny_mce(|_gzip|_jquery|_prototype)(_dev|_src)?.js/.test(n.src)) { + if (n.src && /tiny_mce(|_gzip|_jquery|_prototype|_full)(_dev|_src)?.js/.test(n.src)) { if (/_(src|dev)\.js/g.test(n.src)) t.suffix = '_src'; @@ -103,6 +111,24 @@ return typeof(o) == t; }, + makeMap : function(items, delim, map) { + var i; + + items = items || []; + delim = delim || ','; + + if (typeof(items) == "string") + items = items.split(delim); + + map = map || {}; + + i = items.length; + while (i--) + map[items[i]] = {}; + + return map; + }, + each : function(o, cb, s) { var n, l; @@ -185,7 +211,7 @@ return (s ? '' + s : '').replace(whiteSpaceRe, ''); }, - create : function(s, p) { + create : function(s, p, root) { var t = this, sp, ns, cn, scn, c, de = 0; // Parse : : @@ -193,7 +219,7 @@ cn = s[3].match(/(^|\.)(\w+)$/i)[2]; // Class name // Create namespace for new class - ns = t.createNS(s[3].replace(/\.\w+$/, '')); + ns = t.createNS(s[3].replace(/\.\w+$/, ''), root); // Class already exists if (ns[cn]) @@ -428,6 +454,29 @@ return u + v; return u.replace('#', v + '#'); + }, + + // Fix function for IE 9 where regexps isn't working correctly + // Todo: remove me once MS fixes the bug + _replace : function(find, replace, str) { + // On IE9 we have to fake $x replacement + if (isRegExpBroken) { + return str.replace(find, function() { + var val = replace, args = arguments, i; + + for (i = 0; i < args.length - 2; i++) { + if (args[i] === undefined) { + val = val.replace(new RegExp('\\$' + i, 'g'), ''); + } else { + val = val.replace(new RegExp('\\$' + i, 'g'), args[i]); + } + } + + return val; + }); + } + + return str.replace(find, replace); } }; @@ -437,7 +486,11 @@ // Expose tinymce namespace to the global namespace (window) win.tinymce = win.tinyMCE = tinymce; -})(window); + + // Describe the different namespaces + + })(window); + tinymce.create('tinymce.util.Dispatcher', { @@ -482,7 +535,7 @@ tinymce.create('tinymce.util.Dispatcher', { // And this is also more efficient for (i = 0; i 0 ? a : [c.scope]); if (s === false) break; @@ -498,7 +551,7 @@ tinymce.create('tinymce.util.Dispatcher', { tinymce.create('tinymce.util.URI', { URI : function(u, s) { - var t = this, o, a, b; + var t = this, o, a, b, base_url; // Trim whitespace u = tinymce.trim(u); @@ -506,8 +559,9 @@ tinymce.create('tinymce.util.Dispatcher', { // Default settings s = t.settings = s || {}; - // Strange app protocol or local anchor - if (/^(mailto|tel|news|javascript|about|data):/i.test(u) || /^\s*#/.test(u)) { + // Strange app protocol that isn't http/https or local anchor + // For example: mailto,skype,tel etc. + if (/^([\w\-]+):([^\/]{2})/i.test(u) || /^\s*#/.test(u)) { t.source = u; return; } @@ -517,12 +571,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)) - u = (s.base_uri.protocol || 'http') + '://mce_host' + t.toAbsPath(s.base_uri.path, 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]; @@ -805,9 +861,11 @@ tinymce.create('tinymce.util.Dispatcher', { }); })(); -tinymce.create('static tinymce.util.JSON', { - serialize : function(o) { - var i, v, s = tinymce.util.JSON.serialize, t; +(function() { + function serialize(o, quote) { + var i, v, t; + + quote = quote || '"'; if (o == null) return 'null'; @@ -817,7 +875,11 @@ tinymce.create('static tinymce.util.JSON', { if (t == 'string') { v = '\bb\tt\nn\ff\rr\""\'\'\\\\'; - return '"' + o.replace(/([\u0080-\uFFFF\x00-\x1f\"])/g, function(a, b) { + return quote + o.replace(/([\u0080-\uFFFF\x00-\x1f\"\'\\])/g, function(a, b) { + // Make sure single quotes never get encoded inside double quotes for JSON compatibility + if (quote === '"' && a === "'") + return a; + i = v.indexOf(b); if (i + 1) @@ -826,37 +888,44 @@ tinymce.create('static tinymce.util.JSON', { a = b.charCodeAt().toString(16); return '\\u' + '0000'.substring(a.length) + a; - }) + '"'; + }) + quote; } if (t == 'object') { if (o.hasOwnProperty && o instanceof Array) { for (i=0, v = '['; i 0 ? ',' : '') + s(o[i]); + v += (i > 0 ? ',' : '') + serialize(o[i], quote); return v + ']'; } v = '{'; - for (i in o) - v += typeof o[i] != 'function' ? (v.length > 1 ? ',"' : '"') + i + '":' + s(o[i]) : ''; + for (i in o) { + if (o.hasOwnProperty(i)) { + v += typeof o[i] != 'function' ? (v.length > 1 ? ',' + quote : quote) + i + quote +':' + serialize(o[i], quote) : ''; + } + } return v + '}'; } return '' + o; - }, + }; - parse : function(s) { - try { - return eval('(' + s + ')'); - } catch (ex) { - // Ignore + tinymce.util.JSON = { + serialize: serialize, + + parse: function(s) { + try { + return eval('(' + s + ')'); + } catch (ex) { + // Ignore + } } - } - }); + }; +})(); tinymce.create('static tinymce.util.XHR', { send : function(o) { @@ -948,7 +1017,8 @@ tinymce.create('static tinymce.util.XHR', { }; o.error = function(ty, x) { - ecb.call(o.error_scope || o.scope, ty, x); + if (ecb) + ecb.call(o.error_scope || o.scope, ty, x); }; o.data = JSON.serialize({ @@ -970,5862 +1040,8219 @@ tinymce.create('static tinymce.util.XHR', { } }); }()); +(function(tinymce){ + tinymce.VK = { + BACKSPACE: 8, + DELETE: 46, + DOWN: 40, + ENTER: 13, + LEFT: 37, + RIGHT: 39, + SPACEBAR: 32, + TAB: 9, + UP: 38, + + modifierPressed: function (e) { + return e.shiftKey || e.ctrlKey || e.altKey; + } + } +})(tinymce); + (function(tinymce) { - // Shorten names - var each = tinymce.each, - is = tinymce.is, - isWebKit = tinymce.isWebKit, - isIE = tinymce.isIE, - blockRe = /^(H[1-6R]|P|DIV|ADDRESS|PRE|FORM|T(ABLE|BODY|HEAD|FOOT|H|R|D)|LI|OL|UL|CAPTION|BLOCKQUOTE|CENTER|DL|DT|DD|DIR|FIELDSET|NOSCRIPT|MENU|ISINDEX|SAMP)$/, - boolAttrs = makeMap('checked,compact,declare,defer,disabled,ismap,multiple,nohref,noresize,noshade,nowrap,readonly,selected'), - mceAttribs = makeMap('src,href,style,coords,shape'), - encodedChars = {'&' : '&', '"' : '"', '<' : '<', '>' : '>'}, - encodeCharsRe = /[<>&\"]/g, - simpleSelectorRe = /^([a-z0-9],?)+$/i, - tagRegExp = /<(\w+)((?:\s+\w+(?:\s*=\s*(?:(?:"[^"]*")|(?:'[^']*')|[^>\s]+))?)*)(\s*\/?)>/g, - attrRegExp = /(\w+)(?:\s*=\s*(?:(?:"((?:\\.|[^"])*)")|(?:'((?:\\.|[^'])*)')|([^>\s]+)))?/g; + var VK = tinymce.VK, BACKSPACE = VK.BACKSPACE, DELETE = VK.DELETE; - function makeMap(str) { - var map = {}, i; + function setEditorCommandState(editor, cmd, state) { + try { + editor.getDoc().execCommand(cmd, false, state); + } catch (ex) { + // Ignore + } + } - str = str.split(','); - for (i = str.length; i >= 0; i--) - map[str[i]] = 1; + function cleanupStylesWhenDeleting(ed) { + var dom = ed.dom, selection = ed.selection; - return map; - }; + ed.onKeyDown.add(function(ed, e) { + var rng, blockElm, node, clonedSpan, isDelete; - 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 (e.isDefaultPrevented()) { + return; + } - DOMUtils : function(d, s) { - var t = this, globalStyle; + isDelete = e.keyCode == DELETE; + if ((isDelete || e.keyCode == BACKSPACE) && !VK.modifierPressed(e)) { + e.preventDefault(); + rng = selection.getRng(); - t.doc = d; - t.win = window; - t.files = {}; - t.cssFlicker = false; - t.counter = 0; - t.boxModel = !tinymce.isIE || d.compatMode == "CSS1Compat"; - t.stdMode = d.documentMode === 8; + // Find root block + blockElm = dom.getParent(rng.startContainer, dom.isBlock); - t.settings = s = tinymce.extend({ - keep_values : false, - hex_colors : 1, - process_html : 1 - }, s); + // On delete clone the root span of the next block element + if (isDelete) + blockElm = dom.getNext(blockElm, dom.isBlock); - // Fix IE6SP2 flicker and check it failed for pre SP2 - if (tinymce.isIE6) { - try { - d.execCommand('BackgroundImageCache', false, true); - } catch (e) { - t.cssFlicker = true; + // 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; + + if (node && node.nodeName === 'SPAN') { + clonedSpan = node.cloneNode(false); + } } - } - // Build styles list - if (s.valid_styles) { - t._styles = {}; + // Do the backspace/delete action + ed.getDoc().execCommand(isDelete ? 'ForwardDelete' : 'Delete', false, null); + + // 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(); - // Convert styles into a rule list - each(s.valid_styles, function(value, key) { - t._styles[key] = tinymce.explode(value); + if (clonedSpan) { + dom.replace(clonedSpan.cloneNode(false), span, true); + } else { + dom.remove(span, true); + } + + // Restore the selection + selection.moveToBookmark(bm); }); } + }); + }; - tinymce.addUnload(t.destroy, t); - }, + function emptyEditorWhenDeleting(ed) { + function serializeRng(rng) { + var body = ed.dom.create("body"); + var contents = rng.cloneContents(); + body.appendChild(contents); + return ed.selection.serializer.serialize(body, {format: 'html'}); + } - getRoot : function() { - var t = this, s = t.settings; + function allContentsSelected(rng) { + var selection = serializeRng(rng); - return (s && t.get(s.root_element)) || t.doc.body; - }, + var allRng = ed.dom.createRng(); + allRng.selectNode(ed.getBody()); - getViewPort : function(w) { - var d, b; + var allSelection = serializeRng(allRng); + return selection === allSelection; + } - w = !w ? this.win : w; - d = w.document; - b = this.boxModel ? d.documentElement : d.body; + ed.onKeyDown.addToTop(function(ed, e) { + var keyCode = e.keyCode; + if (keyCode == DELETE || keyCode == BACKSPACE) { + var rng = ed.selection.getRng(true); + if (!rng.collapsed && allContentsSelected(rng)) { + ed.setContent('', {format : 'raw'}); + ed.nodeChanged(); + e.preventDefault(); + } + } + }); + }; - // 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 inputMethodFocus(ed) { + ed.dom.bind(ed.getDoc(), 'focusin', function() { + ed.selection.setRng(ed.selection.getRng()); + }); + }; - getRect : function(e) { - var p, t = this, sr; + function removeHrOnBackspace(ed) { + ed.onKeyDown.add(function(ed, e) { + if (e.keyCode === BACKSPACE) { + if (ed.selection.isCollapsed() && ed.selection.getRng(true).startOffset === 0) { + var node = ed.selection.getNode(); + var previousSibling = node.previousSibling; + if (previousSibling && previousSibling.nodeName && previousSibling.nodeName.toLowerCase() === "hr") { + ed.dom.remove(previousSibling); + tinymce.dom.Event.cancel(e); + } + } + } + }) + } - e = t.get(e); - p = t.getPos(e); - sr = t.getSize(e); + function focusBody(ed) { + // Fix for a focus bug in FF 3.x where the body element + // wouldn't get proper focus if the user clicked on the HTML element + if (!Range.prototype.getClientRects) { // Detect getClientRects got introduced in FF 4 + ed.onMouseDown.add(function(ed, e) { + if (e.target.nodeName === "HTML") { + var body = ed.getBody(); + + // Blur the body it's focused but not correctly focused + body.blur(); + + // Refocus the body after a little while + setTimeout(function() { + body.focus(); + }, 0); + } + }); + } + }; - return { - x : p.x, - y : p.y, - w : sr.w, - h : sr.h - }; - }, + function selectControlElements(ed) { + ed.onClick.add(function(ed, e) { + e = e.target; - getSize : function(e) { - var t = this, w, h; + // 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); - e = t.get(e); - w = t.getStyle(e, 'width'); - h = t.getStyle(e, 'height'); + if (e.nodeName == 'A' && ed.dom.hasClass(e, 'mceItemAnchor')) + ed.selection.select(e); - // Non pixel value, then force offset/clientWidth - if (w.indexOf('px') === -1) - w = 0; + ed.nodeChanged(); + }); + }; - // Non pixel value, then force offset/clientWidth - if (h.indexOf('px') === -1) - h = 0; + function removeStylesWhenDeletingAccrossBlockElements(ed) { + var selection = ed.selection, dom = ed.dom; - return { - w : parseInt(w) || e.offsetWidth || e.clientWidth, - h : parseInt(h) || e.offsetHeight || e.clientHeight - }; - }, + function getAttributeApplyFunction() { + var template = dom.getAttribs(selection.getStart().cloneNode(false)); - getParent : function(n, f, r) { - return this.getParents(n, f, r, false); - }, + return function() { + var target = selection.getStart(); - getParents : function(n, f, r, c) { - var t = this, na, se = t.settings, o = []; + if (target !== ed.getBody()) { + dom.setAttrib(target, "style", null); - n = t.get(n); - c = c === undefined; + tinymce.each(template, function(attr) { + target.setAttributeNode(attr.cloneNode(true)); + }); + } + }; + } - if (se.strict_root) - r = r || t.getRoot(); + function isSelectionAcrossElements() { + return !selection.isCollapsed() && selection.getStart() != selection.getEnd(); + } - // Wrap node name as func - if (is(f, 'string')) { - na = f; + function blockEvent(ed, e) { + e.preventDefault(); + return false; + } - if (f === '*') { - f = function(n) {return n.nodeType == 1;}; - } else { - f = function(n) { - return t.is(n, na); - }; - } + ed.onKeyPress.add(function(ed, e) { + var applyAttributes; + + if ((e.keyCode == 8 || e.keyCode == 46) && isSelectionAcrossElements()) { + applyAttributes = getAttributeApplyFunction(); + ed.getDoc().execCommand('delete', false, null); + applyAttributes(); + e.preventDefault(); + return false; } + }); - while (n) { - if (n == r || !n.nodeType || n.nodeType === 9) - break; + dom.bind(ed.getDoc(), 'cut', function(e) { + var applyAttributes; - if (!f || f(n)) { - if (c) - o.push(n); - else - return n; - } + if (isSelectionAcrossElements()) { + applyAttributes = getAttributeApplyFunction(); + ed.onKeyUp.addToTop(blockEvent); - n = n.parentNode; + setTimeout(function() { + applyAttributes(); + ed.onKeyUp.remove(blockEvent); + }, 0); } + }); + } + + /* + function removeStylesOnPTagsInheritedFromHeadingTag(ed) { + ed.onKeyDown.add(function(ed, event) { + function checkInHeadingTag(ed) { + var currentNode = ed.selection.getNode(); + var headingTags = 'h1,h2,h3,h4,h5,h6'; + return ed.dom.is(currentNode, headingTags) || ed.dom.getParent(currentNode, headingTags) !== null; + } + + if (event.keyCode === VK.ENTER && !VK.modifierPressed(event) && checkInHeadingTag(ed)) { + setTimeout(function() { + var currentNode = ed.selection.getNode(); + if (ed.dom.is(currentNode, 'p')) { + ed.dom.setAttrib(currentNode, 'style', null); + // While tiny's content is correct after this method call, the content shown is not representative of it and needs to be 'repainted' + ed.execCommand('mceCleanup'); + } + }, 0); + } + }); + } + */ - return c ? o : null; - }, - - get : function(e) { - var n; - - if (e && this.doc && typeof(e) == 'string') { - n = e; - e = this.doc.getElementById(e); + function selectionChangeNodeChanged(ed) { + var lastRng, selectionTimer; - // 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]; + ed.dom.bind(ed.getDoc(), 'selectionchange', function() { + if (selectionTimer) { + clearTimeout(selectionTimer); + selectionTimer = 0; } - return e; - }, + selectionTimer = window.setTimeout(function() { + var rng = ed.selection.getRng(); - getNext : function(node, selector) { - return this._findSib(node, selector, 'nextSibling'); - }, + // Compare the ranges to see if it was a real change or not + if (!lastRng || !tinymce.dom.RangeUtils.compareRanges(rng, lastRng)) { + ed.nodeChanged(); + lastRng = rng; + } + }, 50); + }); + } - getPrev : function(node, selector) { - return this._findSib(node, selector, 'previousSibling'); - }, + function ensureBodyHasRoleApplication(ed) { + document.body.setAttribute("role", "application"); + } + + function disableBackspaceIntoATable(ed) { + ed.onKeyDown.add(function(ed, e) { + if (e.keyCode === BACKSPACE) { + if (ed.selection.isCollapsed() && ed.selection.getRng(true).startOffset === 0) { + var previousSibling = ed.selection.getNode().previousSibling; + if (previousSibling && previousSibling.nodeName && previousSibling.nodeName.toLowerCase() === "table") { + return tinymce.dom.Event.cancel(e); + } + } + } + }) + } + function addNewLinesBeforeBrInPre(editor) { + var documentMode = editor.getDoc().documentMode; - select : function(pa, s) { - var t = this; + // IE8+ rendering mode does the right thing with BR in PRE + if (documentMode && documentMode > 7) { + return; + } - return tinymce.dom.Sizzle(pa, t.get(s) || t.get(t.settings.root_element) || t.doc, []); - }, - - is : function(n, selector) { - var i; - - // 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; - - // Simple selector just elements - if (simpleSelectorRe.test(selector)) { - selector = selector.toLowerCase().split(/,/); - n = n.nodeName.toLowerCase(); + // 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(editor, 'RespectVisibilityInDesign', true); + editor.dom.addClass(editor.getBody(), 'mceHideBrInPre'); + + // Adds a \n before all BR elements in PRE to get them visual + editor.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'; + } + } + } + }); - for (i = selector.length - 1; i >= 0; i--) { - if (selector[i] == n) - return true; + // Removes any \n before BR elements in PRE since other browsers and in contentEditable=false mode they will be visible + editor.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$/, ''); } + } + } + }); + } - return false; + tinymce.create('tinymce.util.Quirks', { + Quirks: function(ed) { + // All browsers + disableBackspaceIntoATable(ed); + + // WebKit + if (tinymce.isWebKit) { + cleanupStylesWhenDeleting(ed); + emptyEditorWhenDeleting(ed); + inputMethodFocus(ed); + selectControlElements(ed); + + // iOS + if (tinymce.isIDevice) { + selectionChangeNodeChanged(ed); } } - return tinymce.dom.Sizzle.matches(selector, n.nodeType ? [n] : n).length > 0; - }, + // IE + if (tinymce.isIE) { + removeHrOnBackspace(ed); + emptyEditorWhenDeleting(ed); + ensureBodyHasRoleApplication(ed); + //removeStylesOnPTagsInheritedFromHeadingTag(ed) + addNewLinesBeforeBrInPre(ed); + } + // Gecko + if (tinymce.isGecko) { + removeHrOnBackspace(ed); + focusBody(ed); + removeStylesWhenDeletingAccrossBlockElements(ed); + } + } + }); +})(tinymce); - add : function(p, n, a, h, c) { - var t = this; +(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" + }; - return this.run(p, function(p) { - var e, k; + // Raw entities + baseEntities = { + '\"' : '"', // Needs to be escaped since the YUI compressor would otherwise break the code + "'" : ''', + '<' : '<', + '>' : '>', + '&' : '&' + }; - e = is(n, 'string') ? t.doc.createElement(n) : n; - t.setAttribs(e, a); + // Reverse lookup table for raw entities + reverseEntities = { + '<' : '<', + '>' : '>', + '&' : '&', + '"' : '"', + ''' : "'" + }; - if (h) { - if (h.nodeType) - e.appendChild(h); - else - t.setHTML(e, h); - } + // Decodes text by using the browser + function nativeDecode(text) { + var elm; - return !c ? p.appendChild(e) : e; - }); - }, + elm = document.createElement("div"); + elm.innerHTML = text; - create : function(n, a, h) { - return this.add(this.doc.createElement(n), n, a, h, 1); - }, + return elm.textContent || elm.innerText || text; + }; - createHTML : function(n, a, h) { - var o = '', t = this, k; + // Build a two way lookup table for the entities + function buildEntitiesLookup(items, radix) { + var i, chr, entity, lookup = {}; - o += '<' + n; + if (items) { + items = items.split(','); + radix = radix || 10; - for (k in a) { - if (a.hasOwnProperty(k)) - o += ' ' + k + '="' + t.encode(a[k]) + '"'; + // 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; + } } - if (tinymce.is(h)) - return o + '>' + h + ''; + return lookup; + } + }; - return o + ' />'; + // 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,' + + '6i,Ograve,6j,Oacute,6k,Ocirc,6l,Otilde,6m,Ouml,6n,times,6o,Oslash,6p,Ugrave,6q,Uacute,' + + '6r,Ucirc,6s,Uuml,6t,Yacute,6u,THORN,6v,szlig,70,agrave,71,aacute,72,acirc,73,atilde,74,auml,' + + '75,aring,76,aelig,77,ccedil,78,egrave,79,eacute,7a,ecirc,7b,euml,7c,igrave,7d,iacute,7e,icirc,' + + '7f,iuml,7g,eth,7h,ntilde,7i,ograve,7j,oacute,7k,ocirc,7l,otilde,7m,ouml,7n,divide,7o,oslash,' + + '7p,ugrave,7q,uacute,7r,ucirc,7s,uuml,7t,yacute,7u,thorn,7v,yuml,ci,fnof,sh,Alpha,si,Beta,' + + 'sj,Gamma,sk,Delta,sl,Epsilon,sm,Zeta,sn,Eta,so,Theta,sp,Iota,sq,Kappa,sr,Lambda,ss,Mu,' + + 'st,Nu,su,Xi,sv,Omicron,t0,Pi,t1,Rho,t3,Sigma,t4,Tau,t5,Upsilon,t6,Phi,t7,Chi,t8,Psi,' + + 't9,Omega,th,alpha,ti,beta,tj,gamma,tk,delta,tl,epsilon,tm,zeta,tn,eta,to,theta,tp,iota,' + + 'tq,kappa,tr,lambda,ts,mu,tt,nu,tu,xi,tv,omicron,u0,pi,u1,rho,u2,sigmaf,u3,sigma,u4,tau,' + + 'u5,upsilon,u6,phi,u7,chi,u8,psi,u9,omega,uh,thetasym,ui,upsih,um,piv,812,bull,816,hellip,' + + '81i,prime,81j,Prime,81u,oline,824,frasl,88o,weierp,88h,image,88s,real,892,trade,89l,alefsym,' + + '8cg,larr,8ch,uarr,8ci,rarr,8cj,darr,8ck,harr,8dl,crarr,8eg,lArr,8eh,uArr,8ei,rArr,8ej,dArr,' + + '8ek,hArr,8g0,forall,8g2,part,8g3,exist,8g5,empty,8g7,nabla,8g8,isin,8g9,notin,8gb,ni,8gf,prod,' + + '8gh,sum,8gi,minus,8gn,lowast,8gq,radic,8gt,prop,8gu,infin,8h0,ang,8h7,and,8h8,or,8h9,cap,8ha,cup,' + + '8hb,int,8hk,there4,8hs,sim,8i5,cong,8i8,asymp,8j0,ne,8j1,equiv,8j4,le,8j5,ge,8k2,sub,8k3,sup,8k4,' + + 'nsub,8k6,sube,8k7,supe,8kl,oplus,8kn,otimes,8l5,perp,8m5,sdot,8o8,lceil,8o9,rceil,8oa,lfloor,8ob,' + + '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); + + tinymce.html = tinymce.html || {}; + + tinymce.html.Entities = { + encodeRaw : function(text, attr) { + return text.replace(attr ? attrsCharsRegExp : textCharsRegExp, function(chr) { + return baseEntities[chr] || chr; + }); }, - remove : function(node, keep_children) { - return this.run(node, function(node) { - var parent, child; + encodeAllRaw : function(text) { + return ('' + text).replace(rawCharsRegExp, function(chr) { + return baseEntities[chr] || chr; + }); + }, - parent = node.parentNode; + encodeNumeric : function(text, attr) { + return text.replace(attr ? attrsCharsRegExp : textCharsRegExp, function(chr) { + // Multi byte sequence convert it to a single entity + if (chr.length > 1) + return '&#' + (((chr.charCodeAt(0) - 0xD800) * 0x400) + (chr.charCodeAt(1) - 0xDC00) + 0x10000) + ';'; - if (!parent) - return null; + return baseEntities[chr] || '&#' + chr.charCodeAt(0) + ';'; + }); + }, - 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); - } - } + encodeNamed : function(text, attr, entities) { + entities = entities || namedEntities; - return parent.removeChild(node); + return text.replace(attr ? attrsCharsRegExp : textCharsRegExp, function(chr) { + return baseEntities[chr] || entities[chr] || chr; }); }, - setStyle : function(n, na, v) { - var t = this; - - return t.run(n, function(e) { - var s, i; + getEncodeFunc : function(name, entities) { + var Entities = tinymce.html.Entities; - s = e.style; + entities = buildEntitiesLookup(entities) || namedEntities; - // Camelcase it, if needed - na = na.replace(/-(\D)/g, function(a, b){ - return b.toUpperCase(); + function encodeNamedAndNumeric(text, attr) { + return text.replace(attr ? attrsCharsRegExp : textCharsRegExp, function(chr) { + return baseEntities[chr] || entities[chr] || '&#' + chr.charCodeAt(0) + ';' || chr; }); + }; - // Default px suffix on these - if (t.pixelStyles.test(na) && (tinymce.is(v, 'number') || /^[\-0-9\.]+$/.test(v))) - v += 'px'; + function encodeCustomNamed(text, attr) { + return Entities.encodeNamed(text, attr, entities); + }; - switch (na) { - case 'opacity': - // IE specific opacity - if (isIE) { - s.filter = v === '' ? '' : "alpha(opacity=" + (v * 100) + ")"; + // Replace + with , to be compatible with previous TinyMCE versions + name = tinymce.makeMap(name.replace(/\+/g, ',')); - if (!n.currentStyle || !n.currentStyle.hasLayout) - s.display = 'inline-block'; - } + // Named and numeric encoder + if (name.named && name.numeric) + return encodeNamedAndNumeric; - // Fix for older browsers - s[na] = s['-moz-opacity'] = s['-khtml-opacity'] = v || ''; - break; + // Named encoder + if (name.named) { + // Custom names + if (entities) + return encodeCustomNamed; - case 'float': - isIE ? s.styleFloat = v : s.cssFloat = v; - break; - - default: - s[na] = v || ''; - } + return Entities.encodeNamed; + } - // Force update of the style data - if (t.settings.update_styles) - t.setAttrib(e, '_mce_style'); - }); - }, + // Numeric + if (name.numeric) + return Entities.encodeNumeric; - getStyle : function(n, na, c) { - n = this.get(n); + // Raw encoder + return Entities.encodeRaw; + }, - if (!n) - return false; + decode : function(text) { + return text.replace(entityRegExp, function(all, numeric, value) { + if (numeric) { + value = parseInt(value, numeric.length === 2 ? 16 : 10); - // Gecko - if (this.doc.defaultView && c) { - // Remove camelcase - na = na.replace(/[A-Z]/g, function(a){ - return '-' + a; - }); + // Support upper UTF + if (value > 0xFFFF) { + value -= 0x10000; - try { - return this.doc.defaultView.getComputedStyle(n, null).getPropertyValue(na); - } catch (ex) { - // Old safari might fail - return null; + return String.fromCharCode(0xD800 + (value >> 10), 0xDC00 + (value & 0x3FF)); + } else + return asciiMap[value] || String.fromCharCode(value); } - } - // Camelcase it, if needed - na = na.replace(/-(\D)/g, function(a, b){ - return b.toUpperCase(); + return reverseEntities[all] || namedEntities[all] || nativeDecode(all); }); + } + }; +})(tinymce); - if (na == 'float') - na = isIE ? 'styleFloat' : 'cssFloat'; +tinymce.html.Styles = function(settings, schema) { + var rgbRegExp = /rgb\s*\(\s*([0-9]+)\s*,\s*([0-9]+)\s*,\s*([0-9]+)\s*\)/gi, + urlOrStrRegExp = /(?:url(?:(?:\(\s*\"([^\"]+)\"\s*\))|(?:\(\s*\'([^\']+)\'\s*\))|(?:\(\s*([^)\s]+)\s*\))))|(?:\'([^\']+)\')|(?:\"([^\"]+)\")/gi, + styleRegExp = /\s*([^:]+):\s*([^;]+);?/g, + trimRightRegExp = /\s+$/, + urlColorRegExp = /rgb/, + undef, i, encodingLookup = {}, encodingItems; - // IE & Opera - if (n.currentStyle && c) - return n.currentStyle[na]; + settings = settings || {}; - return n.style[na]; - }, + encodingItems = '\\" \\\' \\; \\: ; : \uFEFF'.split(' '); + for (i = 0; i < encodingItems.length; i++) { + encodingLookup[encodingItems[i]] = '\uFEFF' + i; + encodingLookup['\uFEFF' + i] = encodingItems[i]; + } - setStyles : function(e, o) { - var t = this, s = t.settings, ol; + function toHex(match, r, g, b) { + function hex(val) { + val = parseInt(val).toString(16); - ol = s.update_styles; - s.update_styles = 0; + return val.length > 1 ? val : '0' + val; // 0 -> 00 + }; - each(o, function(v, n) { - t.setStyle(e, n, v); - }); + return '#' + hex(r) + hex(g) + hex(b); + }; - // Update style info - s.update_styles = ol; - if (s.update_styles) - t.setAttrib(e, s.cssText); + return { + toHex : function(color) { + return color.replace(rgbRegExp, toHex); }, - setAttrib : function(e, n, v) { - var t = this; + parse : function(css) { + var styles = {}, matches, name, value, isEncoded, urlConverter = settings.url_converter, urlConverterScope = settings.url_converter_scope || this; - // Whats the point - if (!e || !n) - return; + function compress(prefix, suffix) { + var top, right, bottom, left; - // Strict XML mode - if (t.settings.strict) - n = n.toLowerCase(); + // Get values and check it it needs compressing + top = styles[prefix + '-top' + suffix]; + if (!top) + return; - return this.run(e, function(e) { - var s = t.settings; + right = styles[prefix + '-right' + suffix]; + if (top != right) + return; - switch (n) { - case "style": - if (!is(v, 'string')) { - each(v, function(v, n) { - t.setStyle(e, n, v); - }); + bottom = styles[prefix + '-bottom' + suffix]; + if (right != bottom) + return; - return; - } + left = styles[prefix + '-left' + suffix]; + if (bottom != left) + return; - // No mce_style for elements with these since they might get resized by the user - if (s.keep_values) { - if (v && !t._isRes(v)) - e.setAttribute('_mce_style', v, 2); - else - e.removeAttribute('_mce_style', 2); - } + // Compress + styles[prefix + suffix] = left; + delete styles[prefix + '-top' + suffix]; + delete styles[prefix + '-right' + suffix]; + delete styles[prefix + '-bottom' + suffix]; + delete styles[prefix + '-left' + suffix]; + }; - e.style.cssText = v; - break; + function canCompress(key) { + var value = styles[key], i; - case "class": - e.className = v || ''; // Fix IE null bug - break; + if (!value || value.indexOf(' ') < 0) + return; - 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); + value = value.split(' '); + i = value.length; + while (i--) { + if (value[i] !== value[0]) + return false; + } - t.setAttrib(e, '_mce_' + n, v, 2); - } + styles[key] = value[0]; - break; - - case "shape": - e.setAttribute('_mce_style', v); - break; - } + return true; + }; - if (is(v) && v !== null && v.length !== 0) - e.setAttribute(n, '' + v, 2); - else - e.removeAttribute(n, 2); - }); - }, + function compress2(target, a, b, c) { + if (!canCompress(a)) + return; - setAttribs : function(e, o) { - var t = this; + if (!canCompress(b)) + return; - return this.run(e, function(e) { - each(o, function(v, n) { - t.setAttrib(e, n, v); - }); - }); - }, + if (!canCompress(c)) + return; - getAttrib : function(e, n, dv) { - var v, t = this; + // Compress + styles[target] = styles[a] + ' ' + styles[b] + ' ' + styles[c]; + delete styles[a]; + delete styles[b]; + delete styles[c]; + }; - e = t.get(e); + // Encodes the specified string by replacing all \" \' ; : with _ + function encode(str) { + isEncoded = true; - if (!e || e.nodeType !== 1) - return false; + return encodingLookup[str]; + }; - if (!is(dv)) - dv = ''; + // Decodes the specified string by replacing all _ with it's original value \" \' etc + // It will also decode the \" \' if keep_slashes is set to fale or omitted + function decode(str, keep_slashes) { + if (isEncoded) { + str = str.replace(/\uFEFF[0-9]/g, function(str) { + return encodingLookup[str]; + }); + } - // Try the mce variant for these - if (/^(src|href|style|coords|shape)$/.test(n)) { - v = e.getAttribute("_mce_" + n); + if (!keep_slashes) + str = str.replace(/\\([\'\";:])/g, "$1"); - if (v) - return v; + return str; } - if (isIE && t.props[n]) { - v = e[t.props[n]]; - v = v && v.nodeValue ? v.nodeValue : v; - } + if (css) { + // Encode \" \' % and ; and : inside strings so they don't interfere with the style parsing + css = css.replace(/\\[\"\';:\uFEFF]/g, encode).replace(/\"[^\"]+\"|\'[^\']+\'/g, function(str) { + return str.replace(/[;:]/g, encode); + }); - if (!v) - v = e.getAttribute(n, 2); + // Parse styles + while (matches = styleRegExp.exec(css)) { + name = matches[1].replace(trimRightRegExp, '').toLowerCase(); + value = matches[2].replace(trimRightRegExp, ''); - // 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; + if (name && value.length > 0) { + // Opera will produce 700 instead of bold in their style values + if (name === 'font-weight' && value === '700') + value = 'bold'; + else if (name === 'color' || name === 'background-color') // Lowercase colors like RED + value = value.toLowerCase(); - return v ? n : ''; - } + // Convert RGB colors to HEX + value = value.replace(rgbRegExp, toHex); - // Inner input elements will override attributes on form elements - if (e.nodeName === "FORM" && e.getAttributeNode(n)) - return e.getAttributeNode(n).nodeValue; + // Convert URLs and force them into url('value') format + value = value.replace(urlOrStrRegExp, function(match, url, url2, url3, str, str2) { + str = str || str2; - if (n === 'style') { - v = v || e.style.cssText; + if (str) { + str = decode(str); - if (v) { - v = t.serializeStyle(t.parseStyle(v), e.nodeName); + // Force strings into single quote format + return "'" + str.replace(/\'/g, "\\'") + "'"; + } - if (t.settings.keep_values && !t._isRes(v)) - e.setAttribute('_mce_style', v); + 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, "\\'") + "')"; + }); + + styles[name] = isEncoded ? decode(value, true) : value; + } + + styleRegExp.lastIndex = matches.index + matches[0].length; } + + // Compress the styles to reduce it's size for example IE will expand styles + compress("border", ""); + compress("border", "-width"); + compress("border", "-color"); + compress("border", "-style"); + compress("padding", ""); + compress("margin", ""); + compress2('border', 'border-width', 'border-style', 'border-color'); + + // Remove pointless border, IE produces these + if (styles.border === 'medium none') + delete styles.border; } - // Remove Apple and WebKit stuff - if (isWebKit && n === "class" && v) - v = v.replace(/(apple|webkit)\-[a-z\-]+/gi, ''); + return styles; + }, - // Handle IE issues - if (isIE) { - switch (n) { - case 'rowspan': - case 'colspan': - // IE returns 1 as default value - if (v === 1) - v = ''; + serialize : function(styles, element_name) { + var css = '', name, value; - break; + function serializeStyles(name) { + var styleList, i, l, value; - case 'size': - // IE returns +0 as default value for size - if (v === '+0' || v === 20 || v === 0) - v = ''; + styleList = schema.styles[name]; + if (styleList) { + for (i = 0, l = styleList.length; i < l; i++) { + name = styleList[i]; + value = styles[name]; - break; + if (value !== undef && value.length > 0) + css += (css.length > 0 ? ' ' : '') + name + ': ' + value + ';'; + } + } + }; - case 'width': - case 'height': - case 'vspace': - case 'checked': - case 'disabled': - case 'readonly': - if (v === 0) - v = ''; + // Serialize styles according to schema + if (element_name && schema && schema.styles) { + // Serialize global styles and element specific styles + serializeStyles('*'); + serializeStyles(element_name); + } else { + // Output the styles in the order they are inside the object + for (name in styles) { + value = styles[name]; - break; + if (value !== undef && value.length > 0) + css += (css.length > 0 ? ' ' : '') + name + ': ' + value + ';'; + } + } - case 'hspace': - // IE returns -1 as default value - if (v === -1) - v = ''; + return css; + } + }; +}; - break; +(function(tinymce) { + var mapCache = {}, makeMap = tinymce.makeMap, each = tinymce.each; - case 'maxlength': - case 'tabindex': - // IE returns default value - if (v === 32768 || v === 2147483647 || v === '32768') - v = ''; + function split(str, delim) { + return str.split(delim || ','); + }; - break; + function unpack(lookup, data) { + var key, elements = {}; - case 'multiple': - case 'compact': - case 'noshade': - case 'nowrap': - if (v === 65535) - return n; + function replace(value) { + return value.replace(/[A-Z]+/g, function(key) { + return replace(lookup[key]); + }); + }; - return dv; + // Unpack lookup + for (key in lookup) { + if (lookup.hasOwnProperty(key)) + lookup[key] = replace(lookup[key]); + } - case 'shape': - v = v.toLowerCase(); - break; + // Unpack and parse data into object map + replace(data).replace(/#/g, '#text').replace(/(\w+)\[([^\]]+)\]\[([^\]]*)\]/g, function(str, name, attributes, children) { + attributes = split(attributes, '|'); - default: - // IE has odd anonymous function for event attributes - if (n.indexOf('on') === 0 && v) - v = ('' + v).replace(/^function\s+\w+\(\)\s+\{\s+(.*)\s+\}$/, '$1'); - } + elements[name] = { + attributes : makeMap(attributes), + attributesOrder : attributes, + children : makeMap(children, '|', {'#comment' : {}}) } + }); - return (v !== undefined && v !== null && v !== '') ? '' + v : dv; - }, + return elements; + }; - getPos : function(n, ro) { - var t = this, x = 0, y = 0, e, d = t.doc, r; + 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', + 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', + 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][C]' + + '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]' + + '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|checked|disabled|form|formaction|formenctype|formmethod|formnovalidate|formtarget|height|list|max|maxlength|min|multiple|pattern|placeholder|readonly|required|size|src|step|width|files|value][]' + + '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|summary][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]' + ); + } - n = t.get(n); - ro = ro || d.body; + return html5; + }; - if (n) { - // Use getBoundingClientRect on IE, Opera has it but it's not perfect - if (isIE && !t.stdMode) { - n = n.getBoundingClientRect(); - e = t.boxModel ? d.documentElement : d.body; - x = t.getStyle(t.select('html')[0], 'borderWidth'); // Remove border - x = (x == 'medium' || t.boxModel && !t.isIE6) && 2 || x; - n.top += t.win.self != t.win.top ? 2 : 0; // IE adds some strange extra cord if used in a frameset + 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 {x : n.left + e.scrollLeft - x, y : n.top + e.scrollTop - x}; - } + return html4; + }; - r = n; - while (r && r != ro && r.nodeType) { - x += r.offsetLeft || 0; - y += r.offsetTop || 0; - r = r.offsetParent; - } + tinymce.html.Schema = function(settings) { + var self = this, elements = {}, children = {}, patternElements = [], validStyles, schemaItems; + var whiteSpaceElementsMap, selfClosingElementsMap, shortEndedElementsMap, boolAttrMap, blockElementsMap, nonEmptyElementsMap, customElementsMap = {}; - r = n.parentNode; - while (r && r != ro && r.nodeType) { - x -= r.scrollLeft || 0; - y -= r.scrollTop || 0; - r = r.parentNode; + // 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 {x : x, y : y}; - }, + return value; + }; - parseStyle : function(st) { - var t = this, s = t.settings, o = {}; + settings = settings || {}; + schemaItems = settings.schema == "html5" ? getHTML5() : getHTML4(); - if (!st) - return o; + // Allow all elements and attributes if verify_html is set to false + if (settings.verify_html === false) + settings.valid_elements = '*[*]'; - function compress(p, s, ot) { - var t, r, b, l; + // Build styles list + if (settings.valid_styles) { + validStyles = {}; - // Get values and check it it needs compressing - t = o[p + '-top' + s]; - if (!t) - return; + // Convert styles into a rule list + each(settings.valid_styles, function(value, key) { + validStyles[key] = tinymce.explode(value); + }); + } - r = o[p + '-right' + s]; - if (t != r) - return; + // Setup map objects + whiteSpaceElementsMap = createLookupTable('whitespace_elements', 'pre script style textarea'); + selfClosingElementsMap = createLookupTable('self_closing_elements', 'colgroup dd dt li options 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'); + 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); + blockElementsMap = createLookupTable('block_elements', '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 aside nav'); + + // Converts a wildcard expression string to a regexp for example *a will become /.*a/. + function patternToRegExp(str) { + return new RegExp('^' + str.replace(/([?+*])/g, '.$1') + '$'); + }; - b = o[p + '-bottom' + s]; - if (r != b) - return; + // Parses the specified valid_elements string and adds to the current rules + // This function is a bit hard to read since it's heavily optimized for speed + 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 = /^([#+-])?([^\[\/]+)(?:\/([^\[]+))?(?:\[([^\]]+)\])?$/, + attrRuleRegExp = /^([!\-])?(\w+::\w+|[^=:<]+)?(?:([=:<])(.*))?$/, + hasPatternsRegExp = /[*?+]/; + + if (valid_elements) { + // Split valid elements into an array with rules + valid_elements = split(valid_elements); + + if (elements['@']) { + globalAttributes = elements['@'].attributes; + globalAttributesOrder = elements['@'].attributesOrder; + } - l = o[p + '-left' + s]; - if (b != l) - return; + // Loop all rules + for (ei = 0, el = valid_elements.length; ei < el; ei++) { + // Parse element rule + matches = elementRuleRegExp.exec(valid_elements[ei]); + if (matches) { + // Setup local names for matches + prefix = matches[1]; + elementName = matches[2]; + outputName = matches[3]; + attrData = matches[4]; + + // Create new attributes and attributesOrder + attributes = {}; + attributesOrder = []; + + // Create the new element + element = { + attributes : attributes, + attributesOrder : attributesOrder + }; - // Compress - o[ot] = l; - delete o[p + '-top' + s]; - delete o[p + '-right' + s]; - delete o[p + '-bottom' + s]; - delete o[p + '-left' + s]; - }; + // Padd empty elements prefix + if (prefix === '#') + element.paddEmpty = true; - function compress2(ta, a, b, c) { - var t; + // Remove empty elements prefix + if (prefix === '-') + element.removeEmpty = true; - t = o[a]; - if (!t) - return; + // Copy attributes from global rule into current rule + if (globalAttributes) { + for (key in globalAttributes) + attributes[key] = globalAttributes[key]; - t = o[b]; - if (!t) - return; + attributesOrder.push.apply(attributesOrder, globalAttributesOrder); + } - t = o[c]; - if (!t) - return; + // Attributes defined + if (attrData) { + attrData = split(attrData, '|'); + for (ai = 0, al = attrData.length; ai < al; ai++) { + matches = attrRuleRegExp.exec(attrData[ai]); + if (matches) { + attr = {}; + attrType = matches[1]; + attrName = matches[2].replace(/::/g, ':'); + prefix = matches[3]; + value = matches[4]; + + // Required + if (attrType === '!') { + element.attributesRequired = element.attributesRequired || []; + element.attributesRequired.push(attrName); + attr.required = true; + } - // Compress - o[ta] = o[a] + ' ' + o[b] + ' ' + o[c]; - delete o[a]; - delete o[b]; - delete o[c]; - }; + // Denied from global + if (attrType === '-') { + delete attributes[attrName]; + attributesOrder.splice(tinymce.inArray(attributesOrder, attrName), 1); + continue; + } - st = st.replace(/&(#?[a-z0-9]+);/g, '&$1_MCE_SEMI_'); // Protect entities + // Default value + if (prefix) { + // Default value + if (prefix === '=') { + element.attributesDefault = element.attributesDefault || []; + element.attributesDefault.push({name: attrName, value: value}); + attr.defaultValue = value; + } - each(st.split(';'), function(v) { - var sv, ur = []; + // Forced value + if (prefix === ':') { + element.attributesForced = element.attributesForced || []; + element.attributesForced.push({name: attrName, value: value}); + attr.forcedValue = value; + } - if (v) { - v = v.replace(/_MCE_SEMI_/g, ';'); // Restore entities - v = v.replace(/url\([^\)]+\)/g, function(v) {ur.push(v);return 'url(' + ur.length + ')';}); - v = v.split(':'); - sv = tinymce.trim(v[1]); - sv = sv.replace(/url\(([^\)]+)\)/g, function(a, b) {return ur[parseInt(b) - 1];}); - - sv = sv.replace(/rgb\([^\)]+\)/g, function(v) { - return t.toHex(v); - }); + // Required values + if (prefix === '<') + attr.validValues = makeMap(value, '?'); + } - if (s.url_converter) { - sv = sv.replace(/url\([\'\"]?([^\)\'\"]+)[\'\"]?\)/g, function(x, c) { - return 'url(' + s.url_converter.call(s.url_converter_scope || t, t.decode(c), 'style', null) + ')'; - }); - } + // Check for attribute patterns + if (hasPatternsRegExp.test(attrName)) { + element.attributePatterns = element.attributePatterns || []; + attr.pattern = patternToRegExp(attrName); + element.attributePatterns.push(attr); + } else { + // Add attribute to order list if it doesn't already exist + if (!attributes[attrName]) + attributesOrder.push(attrName); - o[tinymce.trim(v[0]).toLowerCase()] = sv; - } - }); + attributes[attrName] = attr; + } + } + } + } - compress("border", "", "border"); - compress("border", "-width", "border-width"); - compress("border", "-color", "border-color"); - compress("border", "-style", "border-style"); - compress("padding", "", "padding"); - compress("margin", "", "margin"); - compress2('border', 'border-width', 'border-style', 'border-color'); + // Global rule, store away these for later usage + if (!globalAttributes && elementName == '@') { + globalAttributes = attributes; + globalAttributesOrder = attributesOrder; + } - if (isIE) { - // Remove pointless border - if (o.border == 'medium none') - o.border = ''; + // Handle substitute elements such as b/strong + if (outputName) { + element.outputName = elementName; + elements[outputName] = element; + } + + // Add pattern or exact element + if (hasPatternsRegExp.test(elementName)) { + element.pattern = patternToRegExp(elementName); + patternElements.push(element); + } else + elements[elementName] = element; + } + } } + }; - return o; - }, + function setValidElements(valid_elements) { + elements = {}; + patternElements = []; - serializeStyle : function(o, name) { - var t = this, s = ''; + addValidElements(valid_elements); - function add(v, k) { - if (k && v) { - // Remove browser specific styles like -moz- or -webkit- - if (k.indexOf('-') === 0) - return; + each(schemaItems, function(element, name) { + children[name] = element.children; + }); + }; - switch (k) { - case 'font-weight': - // Opera will output bold as 700 - if (v == 700) - v = 'bold'; + // Adds custom non HTML elements to the schema + function addCustomElements(custom_elements) { + var customElementRegExp = /^(~)?(.+)$/; - break; + if (custom_elements) { + each(split(custom_elements), function(rule) { + var matches = customElementRegExp.exec(rule), + inline = matches[1] === '~', + cloneName = inline ? 'span' : 'div', + name = matches[2]; - case 'color': - case 'background-color': - v = v.toLowerCase(); - break; - } + children[name] = children[cloneName]; + customElementsMap[name] = cloneName; - s += (s ? ' ' : '') + k + ': ' + v + ';'; - } - }; + // If it's not marked as inline then add it to valid block elements + if (!inline) + blockElementsMap[name] = {}; - // Validate style output - if (name && t._styles) { - each(t._styles['*'], function(name) { - add(o[name], name); + // Add custom elements at span/div positions + each(children, function(element, child) { + if (element[cloneName]) + element[name] = element[cloneName]; + }); }); + } + }; + + // Adds valid children to the schema object + function addValidChildren(valid_children) { + var childRuleRegExp = /^([+\-]?)(\w+)\[([^\]]+)\]$/; + + if (valid_children) { + each(split(valid_children), function(rule) { + var matches = childRuleRegExp.exec(rule), parent, prefix; + + if (matches) { + prefix = matches[1]; + + // Add/remove items from default + if (prefix) + parent = children[matches[2]]; + else + parent = children[matches[2]] = {'#comment' : {}}; + + parent = children[matches[2]]; - each(t._styles[name.toLowerCase()], function(name) { - add(o[name], name); + each(split(matches[3], '|'), function(child) { + if (prefix === '-') + delete parent[child]; + else + parent[child] = {}; + }); + } }); - } else - each(o, add); + } + }; - return s; - }, + function getElementRule(name) { + var element = elements[name], i; - loadCSS : function(u) { - var t = this, d = t.doc, head; + // Exact match found + if (element) + return element; - if (!u) - u = ''; + // No exact match then try the patterns + i = patternElements.length; + while (i--) { + element = patternElements[i]; - head = t.select('head')[0]; + if (element.pattern.test(name)) + return element; + } + }; - each(u.split(','), function(u) { - var link; + if (!settings.valid_elements) { + // 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 + }; - if (t.files[u]) - return; + children[name] = element.children; + }); - t.files[u] = true; - link = t.create('link', {rel : 'stylesheet', href : tinymce._addVer(u)}); + // 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]; + }); + } - // 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) { - link.onload = function() { - d.recalc(); - link.onload = null; - }; + // 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,strong,em,b,i'), function(name) { + if (elements[name]) { + elements[name].removeEmpty = true; } + }); - head.appendChild(link); + // Padd these by default + each(split('p,h1,h2,h3,h4,h5,h6,th,td,pre,div,address,caption'), function(name) { + elements[name].paddEmpty = true; }); - }, + } else + setValidElements(settings.valid_elements); - addClass : function(e, c) { - return this.run(e, function(e) { - var o; + addCustomElements(settings.custom_elements); + addValidChildren(settings.valid_children); + addValidElements(settings.extended_valid_elements); - if (!c) - return 0; + // Todo: Remove this when we fix list handling to be valid + addValidChildren('+ol[ul|ol],+ul[ul|ol]'); - if (this.hasClass(e, c)) - return e.className; + // Delete invalid elements + if (settings.invalid_elements) { + tinymce.each(tinymce.explode(settings.invalid_elements), function(item) { + if (elements[item]) + delete elements[item]; + }); + } - o = this.removeClass(e, c); + // If the user didn't allow span only allow internal spans + if (!getElementRule('span')) + addValidElements('span[!data-mce-type|*]'); - return e.className = (o != '' ? (o + ' ') : '') + c; - }); - }, + self.children = children; - removeClass : function(e, c) { - var t = this, re; + self.styles = validStyles; - return t.run(e, function(e) { - var v; + self.getBoolAttrs = function() { + return boolAttrMap; + }; - if (t.hasClass(e, c)) { - if (!re) - re = new RegExp("(^|\\s+)" + c + "(\\s+|$)", "g"); + self.getBlockElements = function() { + return blockElementsMap; + }; - v = e.className.replace(re, ' '); - v = tinymce.trim(v != ' ' ? v : ''); + self.getShortEndedElements = function() { + return shortEndedElementsMap; + }; - e.className = v; + self.getSelfClosingElements = function() { + return selfClosingElementsMap; + }; - // Empty class attr - if (!v) { - e.removeAttribute('class'); - e.removeAttribute('className'); - } + self.getNonEmptyElements = function() { + return nonEmptyElementsMap; + }; - return v; - } + self.getWhiteSpaceElements = function() { + return whiteSpaceElementsMap; + }; - return e.className; - }); - }, + self.isValidChild = function(name, child) { + var parent = children[name]; - hasClass : function(n, c) { - n = this.get(n); + return !!(parent && parent[child]); + }; - if (!n || !c) - return false; + self.getElementRule = getElementRule; - return (' ' + n.className + ' ').indexOf(' ' + c + ' ') !== -1; - }, + self.getCustomElements = function() { + return customElementsMap; + }; - show : function(e) { - return this.setStyle(e, 'display', 'block'); - }, + self.addValidElements = addValidElements; - hide : function(e) { - return this.setStyle(e, 'display', 'none'); - }, + self.setValidElements = setValidElements; - isHidden : function(e) { - e = this.get(e); + self.addCustomElements = addCustomElements; - return !e || e.style.display == 'none' || this.getStyle(e, 'display') == 'none'; - }, + self.addValidChildren = addValidChildren; + }; +})(tinymce); - uniqueId : function(p) { - return (!p ? 'mce_' : p) + (this.counter++); - }, +(function(tinymce) { + tinymce.html.SaxParser = function(settings, schema) { + var self = this, noop = function() {}; - setHTML : function(e, h) { - var t = this; + settings = settings || {}; + self.schema = schema = schema || new tinymce.html.Schema(); - return this.run(e, function(e) { - var x, i, nl, n, p, x; + if (settings.fix_self_closing !== false) + settings.fix_self_closing = true; - h = t.processHTML(h); + // Add handler functions from settings and setup default handlers + tinymce.each('comment cdata text start end pi doctype'.split(' '), function(name) { + if (name) + self[name] = settings[name] || noop; + }); - if (isIE) { - function set() { - // Remove all child nodes - while (e.firstChild) - e.firstChild.removeNode(); + self.parse = function(html) { + var self = this, matches, index = 0, value, endRegExp, stack = [], attrList, i, text, name, isInternalElement, removeInternalElements, + shortEndedElements, fillAttrsMap, isShortEnded, validate, elementRule, isValidElement, attr, attribsValue, invalidPrefixRegExp, + validAttributesMap, validAttributePatterns, attributesRequired, attributesDefault, attributesForced, selfClosing, + tokenRegExp, attrRegExp, specialElements, attrValue, idCount = 0, decode = tinymce.html.Entities.decode, fixSelfClosing, isIE; - try { - // IE will remove comments from the beginning - // unless you padd the contents with something - e.innerHTML = '
' + h; - e.removeChild(e.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 - - // Create new div with HTML contents and a BR infront to keep comments - x = t.create('div'); - x.innerHTML = '
' + h; - - // Add all children from div to target - each (x.childNodes, function(n, i) { - // Skip br element - if (i) - e.appendChild(n); - }); - } - }; + function processEndTag(name) { + var pos, i; - // IE has a serious bug when it comes to paragraphs it can produce an invalid - // DOM tree if contents like this

  • Item 1

is inserted - // It seems to be that IE doesn't like a root block element placed inside another root block element - if (t.settings.fix_ie_paragraphs) - h = h.replace(/

<\/p>|]+)><\/p>|/gi, ' 

'); + // Find position of parent of the same type + pos = stack.length; + while (pos--) { + if (stack[pos].name === name) + break; + } - set(); + // Found parent + if (pos >= 0) { + // Close all the open elements + for (i = stack.length - 1; i >= pos; i--) { + name = stack[i]; - if (t.settings.fix_ie_paragraphs) { - // Check for odd paragraphs this is a sign of a broken DOM - nl = e.getElementsByTagName("p"); - for (i = nl.length - 1, x = 0; i >= 0; i--) { - n = nl[i]; + if (name.valid) + self.end(name.name); + } - if (!n.hasChildNodes()) { - if (!n._mce_keep) { - x = 1; // Is broken - break; - } + // Remove the open elements from the stack + stack.length = pos; + } + }; - n.removeAttribute('_mce_keep'); - } - } - } + // Precompile RegExps and map objects + tokenRegExp = new RegExp('<(?:' + + '(?:!--([\\w\\W]*?)-->)|' + // Comment + '(?:!\\[CDATA\\[([\\w\\W]*?)\\]\\]>)|' + // CDATA + '(?:!DOCTYPE([\\w\\W]*?)>)|' + // DOCTYPE + '(?:\\?([^\\s\\/<>]+) ?([\\w\\W]*?)[?/]>)|' + // PI + '(?:\\/([^>]+)>)|' + // End element + '(?:([A-Za-z0-9\-\:]+)((?:\\s+[^"\'>]+(?:(?:"[^"]*")|(?:\'[^\']*\')|[^>]*))*|\\/|\\s+)>)' + // Start element + ')', 'g'); + + attrRegExp = /([\w:\-]+)(?:\s*=\s*(?:(?:\"((?:\\.|[^\"])*)\")|(?:\'((?:\\.|[^\'])*)\')|([^>\s]+)))?/g; + specialElements = { + 'script' : /<\/script[^>]*>/gi, + 'style' : /<\/style[^>]*>/gi, + 'noscript' : /<\/noscript[^>]*>/gi + }; - // Time to fix the madness IE left us - if (x) { - // So if we replace the p elements with divs and mark them and then replace them back to paragraphs - // after we use innerHTML we can fix the DOM tree - h = h.replace(/

]+)>|

/ig, '

'); - h = h.replace(/<\/p>/gi, '
'); + // Setup lookup tables for empty elements and boolean attributes + shortEndedElements = schema.getShortEndedElements(); + selfClosing = schema.getSelfClosingElements(); + fillAttrsMap = schema.getBoolAttrs(); + validate = settings.validate; + removeInternalElements = settings.remove_internals; + fixSelfClosing = settings.fix_self_closing; + isIE = tinymce.isIE; + invalidPrefixRegExp = /^:/; + + while (matches = tokenRegExp.exec(html)) { + // Text + if (index < matches.index) + self.text(decode(html.substr(index, matches.index - index))); + + if (value = matches[6]) { // End element + value = value.toLowerCase(); + + // IE will add a ":" in front of elements it doesn't understand like custom elements or HTML5 elements + if (isIE && invalidPrefixRegExp.test(value)) + value = value.substr(1); + + processEndTag(value); + } else if (value = matches[7]) { // Start element + value = value.toLowerCase(); + + // IE will add a ":" in front of elements it doesn't understand like custom elements or HTML5 elements + if (isIE && invalidPrefixRegExp.test(value)) + value = value.substr(1); + + isShortEnded = value in shortEndedElements; + + // Is self closing tag for example an
  • after an open
  • + if (fixSelfClosing && selfClosing[value] && stack.length > 0 && stack[stack.length - 1].name === value) + processEndTag(value); + + // Validate element + if (!validate || (elementRule = schema.getElementRule(value))) { + isValidElement = true; + + // Grab attributes map and patters when validation is enabled + if (validate) { + validAttributesMap = elementRule.attributes; + validAttributePatterns = elementRule.attributePatterns; + } - // Set the new HTML with DIVs - set(); + // Parse attributes + if (attribsValue = matches[8]) { + isInternalElement = attribsValue.indexOf('data-mce-type') !== -1; // Check if the element is an internal element - // Replace all DIV elements with the _mce_tmp attibute back to paragraphs - // This is needed since IE has a annoying bug see above for details - // This is a slow process but it has to be done. :( - if (t.settings.fix_ie_paragraphs) { - nl = e.getElementsByTagName("DIV"); - for (i = nl.length - 1; i >= 0; i--) { - n = nl[i]; + // If the element has internal attributes then remove it if we are told to do so + if (isInternalElement && removeInternalElements) + isValidElement = false; - // Is it a temp div - if (n._mce_tmp) { - // Create new paragraph - p = t.doc.createElement('p'); + attrList = []; + attrList.map = {}; - // Copy all attributes - n.cloneNode(false).outerHTML.replace(/([a-z0-9\-_]+)=/gi, function(a, b) { - var v; + attribsValue.replace(attrRegExp, function(match, name, value, val2, val3) { + var attrRule, i; - if (b !== '_mce_tmp') { - v = n.getAttribute(b); + name = name.toLowerCase(); + value = name in fillAttrsMap ? name : decode(value || val2 || val3 || ''); // Handle boolean attribute than value attribute - if (!v && b === 'class') - v = n.className; + // Validate name and value + if (validate && !isInternalElement && name.indexOf('data-') !== 0) { + attrRule = validAttributesMap[name]; - p.setAttribute(b, v); + // Find rule by pattern matching + if (!attrRule && validAttributePatterns) { + i = validAttributePatterns.length; + while (i--) { + attrRule = validAttributePatterns[i]; + if (attrRule.pattern.test(name)) + break; } - }); - // Append all children to new paragraph - for (x = 0; x]+)\/>|/gi, ''); // Force open + // Handle required attributes + if (attributesRequired) { + i = attributesRequired.length; + while (i--) { + if (attributesRequired[i] in attrList.map) + break; + } - // Store away src and href in _mce_src and mce_href since browsers mess them up - if (s.keep_values) { - // Wrap scripts and styles in comments for serialization purposes - if (/)/g, '\n'); - s = s.replace(/^[\r\n]*|[\r\n]*$/g, ''); - s = s.replace(/^\s*(\/\/\s*|\]\]>|-->|\]\]-->)\s*$/g, ''); + // None of the required attributes where found + if (i === -1) + isValidElement = false; + } - return s; - }; + // Invalidate element if it's marked as bogus + if (attrList.map['data-mce-bogus']) + isValidElement = false; + } - // Wrap the script contents in CDATA and keep them from executing - h = h.replace(/]+|)>([\s\S]*?)<\/script>/gi, function(v, attribs, text) { - // Force type attribute - if (!attribs) - attribs = ' type="text/javascript"'; + if (isValidElement) + self.start(value, attrList, isShortEnded); + } else + isValidElement = false; - // Convert the src attribute of the scripts - attribs = attribs.replace(/src=\"([^\"]+)\"?/i, function(a, url) { - if (s.url_converter) - url = t.encode(s.url_converter.call(s.url_converter_scope || t, t.decode(url), 'src', 'script')); + // Treat script, noscript and style a bit different since they may include code that looks like elements + if (endRegExp = specialElements[value]) { + endRegExp.lastIndex = index = matches.index + matches[0].length; - return '_mce_src="' + url + '"'; - }); + if (matches = endRegExp.exec(html)) { + if (isValidElement) + text = html.substr(index, matches.index - index); - // Wrap text contents - if (tinymce.trim(text)) { - codeBlocks.push(trim(text)); - text = ''; + index = matches.index + matches[0].length; + } else { + text = html.substr(index); + index = html.length; } - return '' + text + ''; - }); + if (isValidElement && text.length > 0) + self.text(text, true); - // Wrap style elements - h = h.replace(/]+|)>([\s\S]*?)<\/style>/gi, function(v, attribs, text) { - // Wrap text contents - if (text) { - codeBlocks.push(trim(text)); - text = ''; - } + if (isValidElement) + self.end(value); - return '' + text + ''; - }); + tokenRegExp.lastIndex = index; + continue; + } - // Wrap noscript elements - h = h.replace(/]+|)>([\s\S]*?)<\/noscript>/g, function(v, attribs, text) { - return ''; - }); + // Push value on to stack + if (!isShortEnded) { + if (!attribsValue || attribsValue.indexOf('/') != attribsValue.length - 1) + stack.push({name: value, valid: isValidElement}); + else if (isValidElement) + self.end(value); + } + } else if (value = matches[1]) { // Comment + self.comment(value); + } else if (value = matches[2]) { // CDATA + self.cdata(value); + } else if (value = matches[3]) { // DOCTYPE + self.doctype(value); + } else if (value = matches[4]) { // PI + self.pi(value, matches[5]); } - h = h.replace(//g, ''); - - // This function processes the attributes in the HTML string to force boolean - // attributes to the attr="attr" format and convert style, src and href to _mce_ versions - function processTags(html) { - return html.replace(tagRegExp, function(match, elm_name, attrs, end) { - return '<' + elm_name + attrs.replace(attrRegExp, function(match, name, value, val2, val3) { - var mceValue; + index = matches.index + matches[0].length; + } - name = name.toLowerCase(); - value = value || val2 || val3 || ""; + // Text + if (index < html.length) + self.text(decode(html.substr(index))); - // Treat boolean attributes - if (boolAttrs[name]) { - // false or 0 is treated as a missing attribute - if (value === 'false' || value === '0') - return; + // Close any open elements + for (i = stack.length - 1; i >= 0; i--) { + value = stack[i]; - return name + '="' + name + '"'; - } + if (value.valid) + self.end(value.name); + } + }; + } +})(tinymce); - // Is attribute one that needs special treatment - if (mceAttribs[name] && attrs.indexOf('_mce_' + name) == -1) { - mceValue = t.decode(value); +(function(tinymce) { + var whiteSpaceRegExp = /^[ \t\r\n]*$/, typeLookup = { + '#text' : 3, + '#comment' : 8, + '#cdata' : 4, + '#pi' : 7, + '#doctype' : 10, + '#document-fragment' : 11 + }; - // Convert URLs to relative/absolute ones - if (s.url_converter && (name == "src" || name == "href")) - mceValue = s.url_converter.call(s.url_converter_scope || t, mceValue, name, elm_name); + // Walks the tree left/right + function walk(node, root_node, prev) { + var sibling, parent, startName = prev ? 'lastChild' : 'firstChild', siblingName = prev ? 'prev' : 'next'; - // Process styles lowercases them and compresses them - if (name == 'style') - mceValue = t.serializeStyle(t.parseStyle(mceValue), name); + // Walk into nodes if it has a start + if (node[startName]) + return node[startName]; - return name + '="' + value + '"' + ' _mce_' + name + '="' + t.encode(mceValue) + '"'; - } + // Return the sibling if it has one + if (node !== root_node) { + sibling = node[siblingName]; - return match; - }) + end + '>'; - }); - }; + if (sibling) + return sibling; - h = processTags(h); + // Walk up the parents to look for siblings + for (parent = node.parent; parent && parent !== root_node; parent = parent.parent) { + sibling = parent[siblingName]; - // Restore script blocks - h = h.replace(/MCE_SCRIPT:([0-9]+)/g, function(val, idx) { - return codeBlocks[idx]; - }); + if (sibling) + return sibling; } + } + }; - return h; - }, - - getOuterHTML : function(e) { - var d; + function Node(name, type) { + this.name = name; + this.type = type; - e = this.get(e); + if (type === 1) { + this.attributes = []; + this.attributes.map = {}; + } + } - if (!e) - return null; + tinymce.extend(Node.prototype, { + replace : function(node) { + var self = this; - if (e.outerHTML !== undefined) - return e.outerHTML; + if (node.parent) + node.remove(); - d = (e.ownerDocument || this.doc).createElement("body"); - d.appendChild(e.cloneNode(true)); + self.insert(node, self); + self.remove(); - return d.innerHTML; + return self; }, - setOuterHTML : function(e, h, d) { - var t = this; - - function setHTML(e, h, d) { - var n, tp; + attr : function(name, value) { + var self = this, attrs, i, undef; - tp = d.createElement("body"); - tp.innerHTML = h; + if (typeof name !== "string") { + for (i in name) + self.attr(i, name[i]); - n = tp.lastChild; - while (n) { - t.insertAfter(n.cloneNode(true), e); - n = n.previousSibling; - } + return self; + } - t.remove(e); - }; + if (attrs = self.attributes) { + if (value !== undef) { + // Remove attribute + if (value === null) { + if (name in attrs.map) { + delete attrs.map[name]; - return this.run(e, function(e) { - e = t.get(e); + i = attrs.length; + while (i--) { + if (attrs[i].name === name) { + attrs = attrs.splice(i, 1); + return self; + } + } + } - // Only set HTML on elements - if (e.nodeType == 1) { - d = d || e.ownerDocument || t.doc; + return self; + } - 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); + // Set attribute + if (name in attrs.map) { + // Set attribute + i = attrs.length; + while (i--) { + if (attrs[i].name === name) { + attrs[i].value = value; + break; + } } } else - setHTML(e, h, d); + attrs.push({name: name, value: value}); + + attrs.map[name] = value; + + return self; + } else { + return attrs.map[name]; } - }); + } }, - decode : function(s) { - var e, n, v; + clone : function() { + var self = this, clone = new Node(self.name, self.type), i, l, selfAttrs, selfAttr, cloneAttrs; - // Look for entities to decode - if (/&[\w#]+;/.test(s)) { - // Decode the entities using a div element not super efficient but less code - e = this.doc.createElement("div"); - e.innerHTML = s; - n = e.firstChild; - v = ''; + // Clone element attributes + if (selfAttrs = self.attributes) { + cloneAttrs = []; + cloneAttrs.map = {}; - if (n) { - do { - v += n.nodeValue; - } while (n = n.nextSibling); + for (i = 0, l = selfAttrs.length; i < l; i++) { + selfAttr = selfAttrs[i]; + + // Clone everything except id + if (selfAttr.name !== 'id') { + cloneAttrs[cloneAttrs.length] = {name: selfAttr.name, value: selfAttr.value}; + cloneAttrs.map[selfAttr.name] = selfAttr.value; + } } - return v || s; + clone.attributes = cloneAttrs; } - return s; - }, + clone.value = self.value; + clone.shortEnded = self.shortEnded; - encode : function(str) { - return ('' + str).replace(encodeCharsRe, function(chr) { - return encodedChars[chr]; - }); + return clone; }, - insertAfter : function(node, reference_node) { - reference_node = this.get(reference_node); - - return this.run(node, function(node) { - var parent, nextSibling; - - parent = reference_node.parentNode; - nextSibling = reference_node.nextSibling; + wrap : function(wrapper) { + var self = this; - if (nextSibling) - parent.insertBefore(node, nextSibling); - else - parent.appendChild(node); + self.parent.insert(wrapper, self); + wrapper.append(self); - return node; - }); + return self; }, - isBlock : function(n) { - if (n.nodeType && n.nodeType !== 1) - return false; + unwrap : function() { + var self = this, node, next; - n = n.nodeName || n; + for (node = self.firstChild; node; ) { + next = node.next; + self.insert(node, self, true); + node = next; + } - return blockRe.test(n); + self.remove(); }, - replace : function(n, o, k) { - var t = this; + remove : function() { + var self = this, parent = self.parent, next = self.next, prev = self.prev; - if (is(o, 'array')) - n = n.cloneNode(true); + if (parent) { + if (parent.firstChild === self) { + parent.firstChild = next; - return t.run(o, function(o) { - if (k) { - each(tinymce.grep(o.childNodes), function(c) { - n.appendChild(c); - }); + if (next) + next.prev = null; + } else { + prev.next = next; } - return o.parentNode.replaceChild(n, o); - }); + if (parent.lastChild === self) { + parent.lastChild = prev; + + if (prev) + prev.next = null; + } else { + next.prev = prev; + } + + self.parent = self.next = self.prev = null; + } + + return self; }, - rename : function(elm, name) { - var t = this, newElm; + append : function(node) { + var self = this, last; - if (elm.nodeName != name.toUpperCase()) { - // Rename block element - newElm = t.create(name); + if (node.parent) + node.remove(); - // Copy attribs to new block - each(t.getAttribs(elm), function(attr_node) { - t.setAttrib(newElm, attr_node.nodeName, t.getAttrib(elm, attr_node.nodeName)); - }); + last = self.lastChild; + if (last) { + last.next = node; + node.prev = last; + self.lastChild = node; + } else + self.lastChild = self.firstChild = node; - // Replace block - t.replace(newElm, elm, 1); - } + node.parent = self; - return newElm || elm; + return node; }, - findCommonAncestor : function(a, b) { - var ps = a, pe; + insert : function(node, ref_node, before) { + var parent; - while (ps) { - pe = b; + if (node.parent) + node.remove(); - while (pe && ps != pe) - pe = pe.parentNode; + parent = ref_node.parent || this; - if (ps == pe) - break; + if (before) { + if (ref_node === parent.firstChild) + parent.firstChild = node; + else + ref_node.prev.next = node; - ps = ps.parentNode; + node.prev = ref_node.prev; + node.next = ref_node; + ref_node.prev = node; + } else { + if (ref_node === parent.lastChild) + parent.lastChild = node; + else + ref_node.next.prev = node; + + node.next = ref_node.next; + node.prev = ref_node; + ref_node.next = node; } - if (!ps && a.ownerDocument) - return a.ownerDocument.documentElement; + node.parent = parent; - return ps; + return node; }, - toHex : function(s) { - var c = /^\s*rgb\s*?\(\s*?([0-9]+)\s*?,\s*?([0-9]+)\s*?,\s*?([0-9]+)\s*?\)\s*$/i.exec(s); + getAll : function(name) { + var self = this, node, collection = []; - function hex(s) { - s = parseInt(s).toString(16); + for (node = self.firstChild; node; node = walk(node, self)) { + if (node.name === name) + collection.push(node); + } - return s.length > 1 ? s : '0' + s; // 0 -> 00 - }; + return collection; + }, - if (c) { - s = '#' + hex(c[1]) + hex(c[2]) + hex(c[3]); + empty : function() { + var self = this, nodes, i, node; - return s; + // Remove all children + if (self.firstChild) { + nodes = []; + + // Collect the children + for (node = self.firstChild; node; node = walk(node, self)) + nodes.push(node); + + // Remove the children + i = nodes.length; + while (i--) { + node = nodes[i]; + node.parent = node.firstChild = node.lastChild = node.next = node.prev = null; + } } - return s; - }, + self.firstChild = self.lastChild = null; - getClasses : function() { - var t = this, cl = [], i, lo = {}, f = t.settings.class_filter, ov; + return self; + }, - if (t.classes) - return t.classes; + isEmpty : function(elements) { + var self = this, node = self.firstChild, i, name; - function addClasses(s) { - // IE style imports - each(s.imports, function(r) { - addClasses(r); - }); - - 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, ""); - - // Is internal or it doesn't contain a class - if (/\.mce/.test(v) || !/\.[\w\-]+$/.test(v)) - return; - - // Remove everything but class name - ov = v; - v = v.replace(/.*\.([a-z0-9_\-]+).*/i, '$1'); - - // Filter classes - if (f && !(v = f(v, ov))) - return; + if (node) { + do { + if (node.type === 1) { + // Ignore bogus elements + if (node.attributes.map['data-mce-bogus']) + continue; - if (!lo[v]) { - cl.push({'class' : v}); - lo[v] = 1; - } - }); - } - break; + // Keep empty elements like + if (elements[node.name]) + return false; - // Import - case 3: - addClasses(r.styleSheet); - break; + // Keep elements with data attributes or name attribute like + i = node.attributes.length; + while (i--) { + name = node.attributes[i].name; + if (name === "name" || name.indexOf('data-') === 0) + return false; + } } - }); - }; - try { - each(t.doc.styleSheets, addClasses); - } catch (ex) { - // Ignore + // Keep comments + if (node.type === 8) + return false; + + // Keep non whitespace text nodes + if ((node.type === 3 && !whiteSpaceRegExp.test(node.value))) + return false; + } while (node = walk(node, self)); } - if (cl.length > 0) - t.classes = cl; - - return cl; + return true; }, - run : function(e, f, s) { - var t = this, o; + walk : function(prev) { + return walk(this, null, prev); + } + }); - if (t.doc && typeof(e) === 'string') - e = t.get(e); + tinymce.extend(Node, { + create : function(name, attrs) { + var node, attrName; - if (!e) - return false; + // Create node + node = new Node(name, typeLookup[name] || 1); - s = s || this; - if (!e.nodeType && (e.length || e.length === 0)) { - o = []; + // Add attributes if needed + if (attrs) { + for (attrName in attrs) + node.attr(attrName, attrs[attrName]); + } - each(e, function(e, i) { - if (e) { - if (typeof(e) == 'string') - e = t.doc.getElementById(e); + return node; + } + }); - o.push(f.call(s, e, i)); - } - }); + tinymce.html.Node = Node; +})(tinymce); - return o; - } +(function(tinymce) { + var Node = tinymce.html.Node; - return f.call(s, e); - }, + tinymce.html.DomParser = function(settings, schema) { + var self = this, nodeFilters = {}, attributeFilters = [], matchedNodes = {}, matchedAttributes = {}; - getAttribs : function(n) { - var o; + settings = settings || {}; + settings.validate = "validate" in settings ? settings.validate : true; + settings.root_name = settings.root_name || 'body'; + self.schema = schema = schema || new tinymce.html.Schema(); - n = this.get(n); + function fixInvalidChildren(nodes) { + var ni, node, parent, parents, newParent, currentNode, tempNode, childNode, i, + childClone, nonEmptyElements, nonSplitableElements, sibling, nextNode; - if (!n) - return []; + nonSplitableElements = tinymce.makeMap('tr,td,th,tbody,thead,tfoot,table'); + nonEmptyElements = schema.getNonEmptyElements(); - if (isIE) { - o = []; + for (ni = 0; ni < nodes.length; ni++) { + node = nodes[ni]; - // Object will throw exception in IE - if (n.nodeName == 'OBJECT') - return n.attributes; + // Already removed + if (!node.parent) + continue; - // 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'}); + // Get list of all parent nodes until we find a valid parent to stick the child into + parents = [node]; + for (parent = node.parent; parent && !schema.isValidChild(parent.name, node.name) && !nonSplitableElements[parent.name]; parent = parent.parent) + parents.push(parent); - // 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}); - }); + // Found a suitable parent + if (parent && parents.length > 1) { + // Reverse the array since it makes looping easier + parents.reverse(); - return o; - } + // Clone the related parent and insert that after the moved node + newParent = currentNode = self.filterNode(parents[0].clone()); - return n.attributes; - }, + // Start cloning and moving children on the left side of the target node + for (i = 0; i < parents.length - 1; i++) { + if (schema.isValidChild(currentNode.name, parents[i].name)) { + tempNode = self.filterNode(parents[i].clone()); + currentNode.append(tempNode); + } else + tempNode = currentNode; - destroy : function(s) { - var t = this; + for (childNode = parents[i].firstChild; childNode && childNode != parents[i + 1]; ) { + nextNode = childNode.next; + tempNode.append(childNode); + childNode = nextNode; + } - if (t.events) - t.events.destroy(); + currentNode = tempNode; + } - t.win = t.doc = t.root = t.events = null; + if (!newParent.isEmpty(nonEmptyElements)) { + parent.insert(newParent, parents[0], true); + parent.insert(node, newParent); + } else { + parent.insert(node, parents[0], true); + } - // Manual destroy then remove unload handler - if (!s) - tinymce.removeUnload(t.destroy); - }, + // Check if the element is empty by looking through it's contents and special treatment for


    + parent = parents[0]; + if (parent.isEmpty(nonEmptyElements) || parent.firstChild === parent.lastChild && parent.firstChild.name === 'br') { + parent.empty().remove(); + } + } else if (node.parent) { + // If it's an LI try to find a UL/OL for it or wrap it + if (node.name === 'li') { + sibling = node.prev; + if (sibling && (sibling.name === 'ul' || sibling.name === 'ul')) { + sibling.append(node); + continue; + } - createRng : function() { - var d = this.doc; + sibling = node.next; + if (sibling && (sibling.name === 'ul' || sibling.name === 'ul')) { + sibling.insert(node, sibling.firstChild, true); + continue; + } - return d.createRange ? d.createRange() : new tinymce.dom.Range(this); - }, + node.wrap(self.filterNode(new Node('ul', 1))); + continue; + } - nodeIndex : function(node, normalized) { - var idx = 0, lastNodeType, lastNode, nodeType; + // Try wrapping the element in a DIV + if (schema.isValidChild(node.parent.name, 'div') && schema.isValidChild('div', node.name)) { + node.wrap(self.filterNode(new Node('div', 1))); + } else { + // We failed wrapping it, then remove or unwrap it + if (node.name === 'style' || node.name === 'script') + node.empty().remove(); + else + node.unwrap(); + } + } + } + }; - if (node) { - for (lastNodeType = node.nodeType, node = node.previousSibling, lastNode = node; node; node = node.previousSibling) { - nodeType = node.nodeType; + self.filterNode = function(node) { + var i, name, list; - // Normalize text nodes - if (normalized && nodeType == 3) { - if (nodeType == lastNodeType || !node.nodeValue.length) - continue; - } + // Run element filters + if (name in nodeFilters) { + list = matchedNodes[name]; - idx++; - lastNodeType = nodeType; + if (list) + list.push(node); + else + matchedNodes[name] = [node]; + } + + // Run attribute filters + i = attributeFilters.length; + while (i--) { + name = attributeFilters[i].name; + + if (name in node.attributes.map) { + list = matchedAttributes[name]; + + if (list) + list.push(node); + else + matchedAttributes[name] = [node]; } } - return idx; - }, + return node; + }; - split : function(pe, e, re) { - var t = this, r = t.createRng(), bef, aft, pa; + self.addNodeFilter = function(name, callback) { + tinymce.each(tinymce.explode(name), function(name) { + var list = nodeFilters[name]; - // 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; + if (!list) + nodeFilters[name] = list = []; - if (node.nodeType == 1 && node.getAttribute('_mce_type') == 'bookmark') - return; + list.push(callback); + }); + }; - for (i = children.length - 1; i >= 0; i--) - trim(children[i]); + self.addAttributeFilter = function(name, callback) { + tinymce.each(tinymce.explode(name), function(name) { + var i; - if (node.nodeType != 9) { - // Keep non whitespace text nodes - if (node.nodeType == 3 && node.nodeValue.length > 0) + for (i = 0; i < attributeFilters.length; i++) { + if (attributeFilters[i].name === name) { + attributeFilters[i].callbacks.push(callback); return; + } + } - if (node.nodeType == 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('_mce_type') == 'bookmark') - node.parentNode.insertBefore(children[0], node); + attributeFilters.push({name: name, callbacks: [callback]}); + }); + }; - // Keep non empty elements or img, hr etc - if (children.length || /^(br|hr|input|img)$/i.test(node.nodeName)) - return; + self.parse = function(html, args) { + var parser, rootNode, node, nodes, i, l, fi, fl, list, name, validate, + blockElements, startWhiteSpaceRegExp, invalidChildren = [], isInWhiteSpacePreservedElement, + endWhiteSpaceRegExp, allWhiteSpaceRegExp, whiteSpaceElements, children, nonEmptyElements, rootBlockName; + + args = args || {}; + matchedNodes = {}; + matchedAttributes = {}; + blockElements = tinymce.extend(tinymce.makeMap('script,style,head,html,body,title,meta,param'), schema.getBlockElements()); + nonEmptyElements = schema.getNonEmptyElements(); + children = schema.children; + validate = settings.validate; + rootBlockName = "forced_root_block" in args ? args.forced_root_block : settings.forced_root_block; + + whiteSpaceElements = schema.getWhiteSpaceElements(); + startWhiteSpaceRegExp = /^[ \t\r\n]+/; + endWhiteSpaceRegExp = /[ \t\r\n]+$/; + allWhiteSpaceRegExp = /[ \t\r\n]+/g; + + function addRootBlocks() { + var node = rootNode.firstChild, next, rootBlockNode; + + while (node) { + next = node.next; + + if (node.type == 3 || (node.type == 1 && node.name !== 'p' && !blockElements[node.name] && !node.attr('data-mce-type'))) { + if (!rootBlockNode) { + // Create a new root block element + rootBlockNode = createNode(rootBlockName, 1); + rootNode.insert(rootBlockNode, node); + rootBlockNode.append(node); + } else + rootBlockNode.append(node); + } else { + rootBlockNode = null; } - t.remove(node); + node = next; + }; + }; + + function createNode(name, type) { + var node = new Node(name, type), list; + + if (name in nodeFilters) { + list = matchedNodes[name]; + + if (list) + list.push(node); + else + matchedNodes[name] = [node]; } return node; }; - if (pe && e) { - // Get before chunk - r.setStart(pe.parentNode, t.nodeIndex(pe)); - r.setEnd(e.parentNode, t.nodeIndex(e)); - bef = r.extractContents(); - - // 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(); + function removeWhitespaceBefore(node) { + var textNode, textVal, sibling; - // Insert before chunk - pa = pe.parentNode; - pa.insertBefore(trim(bef), pe); + for (textNode = node.prev; textNode && textNode.type === 3; ) { + textVal = textNode.value.replace(endWhiteSpaceRegExp, ''); - // Insert middle chunk - if (re) - pa.replaceChild(re, e); - else - pa.insertBefore(e, pe); + if (textVal.length > 0) { + textNode.value = textVal; + textNode = textNode.prev; + } else { + sibling = textNode.prev; + textNode.remove(); + textNode = sibling; + } + } + }; - // Insert after chunk - pa.insertBefore(trim(aft), pe); - t.remove(pe); + parser = new tinymce.html.SaxParser({ + validate : validate, + fix_self_closing : !validate, // Let the DOM parser handle
  • in
  • or

    in

    for better results - return re || e; - } - }, + cdata: function(text) { + node.append(createNode('#cdata', 4)).value = text; + }, - bind : function(target, name, func, scope) { - var t = this; + text: function(text, raw) { + var textNode; - if (!t.events) - t.events = new tinymce.dom.EventUtils(); + // Trim all redundant whitespace on non white space elements + if (!isInWhiteSpacePreservedElement) { + text = text.replace(allWhiteSpaceRegExp, ' '); - return t.events.add(target, name, func, scope || this); - }, + if (node.lastChild && blockElements[node.lastChild.name]) + text = text.replace(startWhiteSpaceRegExp, ''); + } - unbind : function(target, name, func) { - var t = this; + // Do we need to create the node + if (text.length !== 0) { + textNode = createNode('#text', 3); + textNode.raw = !!raw; + node.append(textNode).value = text; + } + }, - if (!t.events) - t.events = new tinymce.dom.EventUtils(); + comment: function(text) { + node.append(createNode('#comment', 8)).value = text; + }, - return t.events.remove(target, name, func); - }, + pi: function(name, text) { + node.append(createNode(name, 7)).value = text; + removeWhitespaceBefore(node); + }, + doctype: function(text) { + var newNode; + + newNode = node.append(createNode('#doctype', 10)); + newNode.value = text; + removeWhitespaceBefore(node); + }, - _findSib : function(node, selector, name) { - var t = this, f = selector; + start: function(name, attrs, empty) { + var newNode, attrFiltersLen, elementRule, textNode, attrName, text, sibling, parent; - if (node) { - // If expression make a function of it using is - if (is(f, 'string')) { - f = function(node) { - return t.is(node, selector); - }; - } + elementRule = validate ? schema.getElementRule(name) : {}; + if (elementRule) { + newNode = createNode(elementRule.outputName || name, 1); + newNode.attributes = attrs; + newNode.shortEnded = empty; - // Loop all siblings - for (node = node[name]; node; node = node[name]) { - if (f(node)) - return node; - } - } + node.append(newNode); - return null; - }, + // Check if node is valid child of the parent node is the child is + // unknown we don't collect it since it's probably a custom element + parent = children[node.name]; + if (parent && children[newNode.name] && !parent[newNode.name]) + invalidChildren.push(newNode); - _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); - } + attrFiltersLen = attributeFilters.length; + while (attrFiltersLen--) { + attrName = attributeFilters[attrFiltersLen].name; - /* - walk : function(n, f, s) { - var d = this.doc, w; + if (attrName in attrs.map) { + list = matchedAttributes[attrName]; - if (d.createTreeWalker) { - w = d.createTreeWalker(n, NodeFilter.SHOW_TEXT, null, false); - - while ((n = w.nextNode()) != null) - f.call(s || this, n); - } else - tinymce.walk(n, f, 'childNodes', s); - } - */ + if (list) + list.push(newNode); + else + matchedAttributes[attrName] = [newNode]; + } + } - /* - toRGB : function(s) { - var c = /^\s*?#([0-9A-F]{2})([0-9A-F]{1,2})([0-9A-F]{2})?\s*?$/.exec(s); + // Trim whitespace before block + if (blockElements[name]) + removeWhitespaceBefore(newNode); - if (c) { - // #FFF -> #FFFFFF - if (!is(c[3])) - c[3] = c[2] = c[1]; + // Change current node if the element wasn't empty i.e not
    or + if (!empty) + node = newNode; - return "rgb(" + parseInt(c[1], 16) + "," + parseInt(c[2], 16) + "," + parseInt(c[3], 16) + ")"; - } + // Check if we are inside a whitespace preserved element + if (!isInWhiteSpacePreservedElement && whiteSpaceElements[name]) { + isInWhiteSpacePreservedElement = true; + } + } + }, - return s; - } - */ - }); + end: function(name) { + var textNode, elementRule, text, sibling, tempNode; - tinymce.DOM = new tinymce.dom.DOMUtils(document, {process_html : 0}); -})(tinymce); + elementRule = validate ? schema.getElementRule(name) : {}; + if (elementRule) { + if (blockElements[name]) { + if (!isInWhiteSpacePreservedElement) { + // Trim whitespace at beginning of block + for (textNode = node.firstChild; textNode && textNode.type === 3; ) { + text = textNode.value.replace(startWhiteSpaceRegExp, ''); -(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; + if (text.length > 0) { + textNode.value = text; + textNode = textNode.next; + } else { + sibling = textNode.next; + textNode.remove(); + textNode = sibling; + } + } - extend(t, { - // Inital states - startContainer : doc, - startOffset : 0, - endContainer : doc, - endOffset : 0, - collapsed : TRUE, - commonAncestorContainer : doc, + // Trim whitespace at end of block + for (textNode = node.lastChild; textNode && textNode.type === 3; ) { + text = textNode.value.replace(endWhiteSpaceRegExp, ''); - // Range constants - START_TO_START : 0, - START_TO_END : 1, - END_TO_END : 2, - END_TO_START : 3, + if (text.length > 0) { + textNode.value = text; + textNode = textNode.prev; + } else { + sibling = textNode.prev; + textNode.remove(); + textNode = sibling; + } + } + } - // 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 - }); + // Trim start white space + textNode = node.prev; + if (textNode && textNode.type === 3) { + text = textNode.value.replace(startWhiteSpaceRegExp, ''); - function setStart(n, o) { - _setEndPoint(TRUE, n, o); - }; + if (text.length > 0) + textNode.value = text; + else + textNode.remove(); + } + } - function setEnd(n, o) { - _setEndPoint(FALSE, n, o); - }; + // Check if we exited a whitespace preserved element + if (isInWhiteSpacePreservedElement && whiteSpaceElements[name]) { + isInWhiteSpacePreservedElement = false; + } - function setStartBefore(n) { - setStart(n.parentNode, nodeIndex(n)); - }; + // Handle empty nodes + if (elementRule.removeEmpty || elementRule.paddEmpty) { + if (node.isEmpty(nonEmptyElements)) { + if (elementRule.paddEmpty) + node.empty().append(new Node('#text', '3')).value = '\u00a0'; + else { + // Leave nodes that have a name like + if (!node.attributes.map.name) { + tempNode = node.parent; + node.empty().remove(); + node = tempNode; + return; + } + } + } + } - function setStartAfter(n) { - setStart(n.parentNode, nodeIndex(n) + 1); - }; + node = node.parent; + } + } + }, schema); - function setEndBefore(n) { - setEnd(n.parentNode, nodeIndex(n)); - }; + rootNode = node = new Node(args.context || settings.root_name, 11); - function setEndAfter(n) { - setEnd(n.parentNode, nodeIndex(n) + 1); - }; + parser.parse(html); - 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]; + // Fix invalid children or report invalid children in a contextual parsing + if (validate && invalidChildren.length) { + if (!args.context) + fixInvalidChildren(invalidChildren); + else + args.invalid = true; } - t.collapsed = TRUE; - }; - - function selectNode(n) { - setStartBefore(n); - setEndAfter(n); - }; + // Wrap nodes in the root into block elements if the root is body + if (rootBlockName && rootNode.name == 'body') + addRootBlocks(); - function selectNodeContents(n) { - setStart(n, 0); - setEnd(n, n.nodeType === 1 ? n.childNodes.length : n.nodeValue.length); - }; + // Run filters only when the contents is valid + if (!args.invalid) { + // Run node filters + for (name in matchedNodes) { + list = nodeFilters[name]; + nodes = matchedNodes[name]; - function compareBoundaryPoints(h, r) { - var sc = t[START_CONTAINER], so = t[START_OFFSET], ec = t[END_CONTAINER], eo = t[END_OFFSET]; + // Remove already removed children + fi = nodes.length; + while (fi--) { + if (!nodes[fi].parent) + nodes.splice(fi, 1); + } - // Check START_TO_START - if (h === 0) - return _compareBoundaryPoints(sc, so, sc, so); + for (i = 0, l = list.length; i < l; i++) + list[i](nodes, name, args); + } - // Check START_TO_END - if (h === 1) - return _compareBoundaryPoints(sc, so, ec, eo); + // Run attribute filters + for (i = 0, l = attributeFilters.length; i < l; i++) { + list = attributeFilters[i]; - // Check END_TO_END - if (h === 2) - return _compareBoundaryPoints(ec, eo, ec, eo); + if (list.name in matchedAttributes) { + nodes = matchedAttributes[list.name]; - // Check END_TO_START - if (h === 3) - return _compareBoundaryPoints(ec, eo, sc, so); - }; + // Remove already removed children + fi = nodes.length; + while (fi--) { + if (!nodes[fi].parent) + nodes.splice(fi, 1); + } - function deleteContents() { - _traverse(DELETE); - }; + for (fi = 0, fl = list.callbacks.length; fi < fl; fi++) + list.callbacks[fi](nodes, list.name, args); + } + } + } - function extractContents() { - return _traverse(EXTRACT); + return rootNode; }; - function cloneContents() { - return _traverse(CLONE); - }; + // Remove
    at end of block elements Gecko and WebKit injects BR elements to + // make it possible to place the caret inside empty blocks. This logic tries to remove + // 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; + + // Remove brs from body element as well + blockElements.body = 1; + + // Must loop forwards since it will otherwise remove all brs in

    a


    + for (i = 0; i < l; i++) { + node = nodes[i]; + 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 + // excluding bookmarks since they are invisible + prev = node.prev; + while (prev) { + prevName = prev.name; + + // Ignore bookmarks + if (prevName !== "span" || prev.attr('data-mce-type') !== 'bookmark') { + // Found a non BR element + if (prevName !== "br") + break; + + // Found another br it's a

    structure then don't remove anything + if (prevName === 'br') { + node = null; + break; + } + } - function insertNode(n) { - var startContainer = this[START_CONTAINER], - startOffset = this[START_OFFSET], nn, o; + prev = prev.prev; + } - // 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); + if (node) { + node.remove(); + + // Is the parent to be considered empty after we removed the BR + if (parent.isEmpty(nonEmptyElements)) { + elementRule = schema.getElementRule(parent.name); + + // 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'; + } + } + } + } } - } else { - // Insert element node - if (startContainer.childNodes.length > 0) - o = startContainer.childNodes[startOffset]; + }); + } + } +})(tinymce); - if (o) - startContainer.insertBefore(n, o); - else - startContainer.appendChild(n); - } - }; +tinymce.html.Writer = function(settings) { + var html = [], indent, indentBefore, indentAfter, encode, htmlOutput; - function surroundContents(n) { - var f = t.extractContents(); + settings = settings || {}; + indent = settings.indent; + indentBefore = tinymce.makeMap(settings.indent_before || ''); + indentAfter = tinymce.makeMap(settings.indent_after || ''); + encode = tinymce.html.Entities.getEncodeFunc(settings.entity_encoding || 'raw', settings.entities); + htmlOutput = settings.element_format == "html"; - t.insertNode(n); - n.appendChild(f); - t.selectNode(n); - }; + return { + start: function(name, attrs, empty) { + var i, l, attr, value; - 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 - }); - }; + if (indent && indentBefore[name] && html.length > 0) { + value = html[html.length - 1]; - // Private methods + if (value.length > 0 && value !== '\n') + html.push('\n'); + } - function _getSelectedNode(container, offset) { - var child; + html.push('<', name); - if (container.nodeType == 3 /* TEXT_NODE */) - return container; + if (attrs) { + for (i = 0, l = attrs.length; i < l; i++) { + attr = attrs[i]; + html.push(' ', attr.name, '="', encode(attr.value, true), '"'); + } + } - if (offset < 0) - return container; + if (!empty || htmlOutput) + html[html.length] = '>'; + else + html[html.length] = ' />'; - child = container.firstChild; - while (child && offset > 0) { - --offset; - child = child.nextSibling; - } + if (empty && indent && indentAfter[name] && html.length > 0) { + value = html[html.length - 1]; - if (child) - return child; + if (value.length > 0 && value !== '\n') + html.push('\n'); + } + }, - return container; - }; + end: function(name) { + var value; - function _isCollapsed() { - return (t[START_CONTAINER] == t[END_CONTAINER] && t[START_OFFSET] == t[END_OFFSET]); - }; + /*if (indent && indentBefore[name] && html.length > 0) { + value = html[html.length - 1]; - function _compareBoundaryPoints(containerA, offsetA, containerB, offsetB) { - var c, offsetC, n, cmnRoot, childA, childB; + if (value.length > 0 && value !== '\n') + html.push('\n'); + }*/ - // 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 + html.push(''); - if (offsetA < offsetB) - return -1; // before + if (indent && indentAfter[name] && html.length > 0) { + value = html[html.length - 1]; - return 1; // after + if (value.length > 0 && value !== '\n') + html.push('\n'); } + }, - // 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; + text: function(text, raw) { + if (text.length > 0) + html[html.length] = raw ? text : encode(text); + }, - if (c) { - offsetC = 0; - n = containerA.firstChild; - - while (n != c && offsetC < offsetA) { - offsetC++; - n = n.nextSibling; - } - - if (offsetA <= offsetC) - return -1; // before - - return 1; // after - } + cdata: function(text) { + html.push(''); + }, - // 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; - } + comment: function(text) { + html.push(''); + }, - if (c) { - offsetC = 0; - n = containerB.firstChild; + pi: function(name, text) { + if (text) + html.push(''); + else + html.push(''); - while (n != c && offsetC < offsetB) { - offsetC++; - n = n.nextSibling; - } + if (indent) + html.push('\n'); + }, - if (offsetC < offsetB) - return -1; // before + doctype: function(text) { + html.push('', indent ? '\n' : ''); + }, - return 1; // after - } + reset: function() { + html.length = 0; + }, - // 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; + getContent: function() { + return html.join('').replace(/\n$/, ''); + } + }; +}; - while (childA && childA.parentNode != cmnRoot) - childA = childA.parentNode; +(function(tinymce) { + tinymce.html.Serializer = function(settings, schema) { + var self = this, writer = new tinymce.html.Writer(settings); - if (!childA) - childA = cmnRoot; + settings = settings || {}; + settings.validate = "validate" in settings ? settings.validate : true; - childB = containerB; - while (childB && childB.parentNode != cmnRoot) - childB = childB.parentNode; + self.schema = schema = schema || new tinymce.html.Schema(); + self.writer = writer; - if (!childB) - childB = cmnRoot; + self.serialize = function(node) { + var handlers, validate; - if (childA == childB) - return 0; // equal + validate = settings.validate; - n = cmnRoot.firstChild; - while (n) { - if (n == childA) - return -1; // before + handlers = { + // #text + 3: function(node, raw) { + writer.text(node.value, node.raw); + }, - if (n == childB) - return 1; // after + // #comment + 8: function(node) { + writer.comment(node.value); + }, - n = n.nextSibling; - } - }; + // Processing instruction + 7: function(node) { + writer.pi(node.name, node.value); + }, - function _setEndPoint(st, n, o) { - var ec, sc; + // Doctype + 10: function(node) { + writer.doctype(node.value); + }, - if (st) { - t[START_CONTAINER] = n; - t[START_OFFSET] = o; - } else { - t[END_CONTAINER] = n; - t[END_OFFSET] = o; - } + // CDATA + 4: function(node) { + writer.cdata(node.value); + }, - // 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; + // Document fragment + 11: function(node) { + if ((node = node.firstChild)) { + do { + walk(node); + } while (node = node.next); + } + } + }; - sc = t[START_CONTAINER]; - while (sc.parentNode) - sc = sc.parentNode; + writer.reset(); - 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); + function walk(node) { + var handler = handlers[node.type], name, isEmpty, attrs, attrName, attrValue, sortedAttrs, i, l, elementRule; - t.collapsed = _isCollapsed(); - t.commonAncestorContainer = dom.findCommonAncestor(t[START_CONTAINER], t[END_CONTAINER]); - }; + if (!handler) { + name = node.name; + isEmpty = node.shortEnded; + attrs = node.attributes; - function _traverse(how) { - var c, endContainerDepth = 0, startContainerDepth = 0, p, depthDiff, startNode, endNode, sp, ep; + // Sort attributes + if (validate && attrs && attrs.length > 1) { + sortedAttrs = []; + sortedAttrs.map = {}; - if (t[START_CONTAINER] == t[END_CONTAINER]) - return _traverseSameContainer(how); + elementRule = schema.getElementRule(node.name); + for (i = 0, l = elementRule.attributesOrder.length; i < l; i++) { + attrName = elementRule.attributesOrder[i]; - for (c = t[END_CONTAINER], p = c.parentNode; p; c = p, p = p.parentNode) { - if (p == t[START_CONTAINER]) - return _traverseCommonStartContainer(c, how); + if (attrName in attrs.map) { + attrValue = attrs.map[attrName]; + sortedAttrs.map[attrName] = attrValue; + sortedAttrs.push({name: attrName, value: attrValue}); + } + } - ++endContainerDepth; - } + for (i = 0, l = attrs.length; i < l; i++) { + attrName = attrs[i].name; - for (c = t[START_CONTAINER], p = c.parentNode; p; c = p, p = p.parentNode) { - if (p == t[END_CONTAINER]) - return _traverseCommonEndContainer(c, how); + if (!(attrName in sortedAttrs.map)) { + attrValue = attrs.map[attrName]; + sortedAttrs.map[attrName] = attrValue; + sortedAttrs.push({name: attrName, value: attrValue}); + } + } - ++startContainerDepth; - } + attrs = sortedAttrs; + } - depthDiff = startContainerDepth - endContainerDepth; + writer.start(node.name, attrs, isEmpty); - startNode = t[START_CONTAINER]; - while (depthDiff > 0) { - startNode = startNode.parentNode; - depthDiff--; - } + if (!isEmpty) { + if ((node = node.firstChild)) { + do { + walk(node); + } while (node = node.next); + } - endNode = t[END_CONTAINER]; - while (depthDiff < 0) { - endNode = endNode.parentNode; - depthDiff++; + writer.end(name); + } + } else + handler(node); } - // 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; - } + // Serialize element and treat all non elements as fragments + if (node.type == 1 && !settings.inner) + walk(node); + else + handlers[11](node); - return _traverseCommonAncestors(startNode, endNode, how); + return writer.getContent(); }; + } +})(tinymce); - function _traverseSameContainer(how) { - var frag, s, sub, n, cnt, sibling, xferNode; +// JSLint defined globals +/*global tinymce:false, window:false */ - if (how != DELETE) - frag = doc.createDocumentFragment(); +tinymce.dom = {}; - // If selection is empty, just return the fragment - if (t[START_OFFSET] == t[END_OFFSET]) - return frag; +(function(namespace, expando) { + var w3cEventModel = !!document.addEventListener; - // 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]); + function addEvent(target, name, callback, capture) { + if (target.addEventListener) { + target.addEventListener(name, callback, capture || false); + } else if (target.attachEvent) { + target.attachEvent('on' + name, callback); + } + } - // 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]); + function removeEvent(target, name, callback, capture) { + if (target.removeEventListener) { + target.removeEventListener(name, callback, capture || false); + } else if (target.detachEvent) { + target.detachEvent('on' + name, callback); + } + } - // Nothing is partially selected, so collapse to start point - t.collapse(TRUE); - } + function fix(original_event, data) { + var name, event = data || {}; - if (how == DELETE) - return; + // Dummy function that gets replaced on the delegation state functions + function returnFalse() { + return false; + } - frag.appendChild(doc.createTextNode(sub)); - return frag; - } + // Dummy function that gets replaced on the delegation state functions + function returnTrue() { + return true; + } - // Copy nodes between the start/end offsets. - n = _getSelectedNode(t[START_CONTAINER], t[START_OFFSET]); - cnt = t[END_OFFSET] - t[START_OFFSET]; + // 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]; + } + } - while (cnt > 0) { - sibling = n.nextSibling; - xferNode = _traverseFullySelected(n, how); + // Normalize target IE uses srcElement + if (!event.target) { + event.target = event.srcElement || document; + } - if (frag) - frag.appendChild( xferNode ); + // Add preventDefault method + event.preventDefault = function() { + event.isDefaultPrevented = returnTrue; - --cnt; - n = sibling; + // Execute preventDefault on the original event object + if (original_event) { + if (original_event.preventDefault) { + original_event.preventDefault(); + } else { + original_event.returnValue = false; // IE + } } - - // Nothing is partially selected, so collapse to start point - if (how != CLONE) - t.collapse(TRUE); - - return frag; }; - function _traverseCommonStartContainer(endAncestor, how) { - var frag, n, endIdx, cnt, sibling, xferNode; + // Add stopPropagation + event.stopPropagation = function() { + event.isPropagationStopped = returnTrue; - if (how != DELETE) - frag = doc.createDocumentFragment(); + // Execute stopPropagation on the original event object + if (original_event) { + if (original_event.stopPropagation) { + original_event.stopPropagation(); + } else { + original_event.cancelBubble = true; // IE + } + } + }; - n = _traverseRightBoundary(endAncestor, how); + // Add stopImmediatePropagation + event.stopImmediatePropagation = function() { + event.isImmediatePropagationStopped = returnTrue; + event.stopPropagation(); + }; - if (frag) - frag.appendChild(n); + // Add event delegation states + if (!event.isDefaultPrevented) { + event.isDefaultPrevented = returnFalse; + event.isPropagationStopped = returnFalse; + event.isImmediatePropagationStopped = returnFalse; + } - endIdx = nodeIndex(endAncestor); - cnt = endIdx - t[START_OFFSET]; + return event; + } - if (cnt <= 0) { - // Collapse to just before the endAncestor, which - // is partially selected. - if (how != CLONE) { - t.setEndBefore(endAncestor); - t.collapse(FALSE); - } + function bindOnReady(win, callback, event_utils) { + var doc = win.document, event = {type: 'ready'}; - return frag; + // Gets called when the DOM is ready + function readyHandler() { + if (!event_utils.domLoaded) { + event_utils.domLoaded = true; + callback(event); } + } - n = endAncestor.previousSibling; - while (cnt > 0) { - sibling = n.previousSibling; - xferNode = _traverseFullySelected(n, how); + // 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(); + } + }); - if (frag) - frag.insertBefore(xferNode, frag.firstChild); + // 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; + } - --cnt; - n = sibling; + readyHandler(); + })(); } + } - // Collapse to just before the endAncestor, which - // is partially selected. - if (how != CLONE) { - t.setEndBefore(endAncestor); - t.collapse(FALSE); - } + // Fallback if any of the above methods should fail for some odd reason + addEvent(win, 'load', readyHandler); + } - return frag; - }; + function EventUtils(proxy) { + var self = this, events = {}, count, isFocusBlurBound, hasFocusIn, hasMouseEnterLeave, mouseEnterLeave; - function _traverseCommonEndContainer(startAncestor, how) { - var frag, startIdx, n, cnt, sibling, xferNode; + hasMouseEnterLeave = "onmouseenter" in document.documentElement; + hasFocusIn = "onfocusin" in document.documentElement; + mouseEnterLeave = {mouseenter: 'mouseover', mouseleave: 'mouseout'}; + count = 1; - if (how != DELETE) - frag = doc.createDocumentFragment(); + // State if the DOMContentLoaded was executed or not + self.domLoaded = false; + self.events = events; - n = _traverseLeftBoundary(startAncestor, how); - if (frag) - frag.appendChild(n); + function executeHandlers(evt, id) { + var callbackList, i, l, callback; - startIdx = nodeIndex(startAncestor); - ++startIdx; // Because we already traversed it.... + 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(); + } - 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; - } - - if (how != CLONE) { - t.setStartAfter(startAncestor); - t.collapse(TRUE); + // Should we stop propagation to immediate listeners + if (evt.isImmediatePropagationStopped()) { + return; + } + } } + } - return frag; - }; - - function _traverseCommonAncestors(startAncestor, endAncestor, how) { - var n, frag, commonParent, startOffset, endOffset, cnt, sibling, nextSibling; - - if (how != DELETE) - frag = doc.createDocumentFragment(); - - n = _traverseLeftBoundary(startAncestor, how); - if (frag) - frag.appendChild(n); - - commonParent = startAncestor.parentNode; - startOffset = nodeIndex(startAncestor); - endOffset = nodeIndex(endAncestor); - ++startOffset; - - cnt = endOffset - startOffset; - sibling = startAncestor.nextSibling; - - while (cnt > 0) { - nextSibling = sibling.nextSibling; - n = _traverseFullySelected(sibling, how); - - if (frag) - frag.appendChild(n); + self.bind = function(target, names, callback, scope) { + var id, callbackList, i, name, fakeName, nativeHandler, capture, win = window; - sibling = nextSibling; - --cnt; + // Native event handler function patches the event and executes the callbacks for the expando + function defaultNativeHandler(evt) { + executeHandlers(fix(evt || win.event), id); } - n = _traverseRightBoundary(endAncestor, how); - - if (frag) - frag.appendChild(n); - - if (how != CLONE) { - t.setStartAfter(startAncestor); - t.collapse(TRUE); + // Don't bind to text nodes or comments + if (!target || target.nodeType === 3 || target.nodeType === 8) { + return; } - return frag; - }; - - 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); - - parent = next.parentNode; - clonedParent = _traverseNode(parent, FALSE, FALSE, how); - - while (parent) { - while (next) { - prevSibling = next.previousSibling; - clonedChild = _traverseNode(next, isFullySelected, FALSE, how); - - if (how != DELETE) - clonedParent.insertBefore(clonedChild, clonedParent.firstChild); + // Create or get events id for the target + if (!target[expando]) { + id = count++; + target[expando] = id; + events[id] = {}; + } else { + id = target[expando]; - isFullySelected = TRUE; - next = prevSibling; + if (!events[id]) { + events[id] = {}; } + } - if (parent == root) - return clonedParent; - - next = parent.previousSibling; - parent = parent.parentNode; - - clonedGrandParent = _traverseNode(parent, FALSE, FALSE, how); + // Setup the specified scope or use the target as a default + scope = scope || target; - if (how != DELETE) - clonedGrandParent.appendChild(clonedParent); + // 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; - clonedParent = clonedGrandParent; - } - }; + // Use ready instead of DOMContentLoaded + if (name === "DOMContentLoaded") { + name = "ready"; + } - function _traverseLeftBoundary(root, how) { - var next = _getSelectedNode(t[START_CONTAINER], t[START_OFFSET]), isFullySelected = next != t[START_CONTAINER], parent, clonedParent, nextSibling, clonedChild, clonedGrandParent; + // DOM is already ready + if ((self.domLoaded || target.readyState == 'complete') && name === "ready") { + self.domLoaded = true; + callback.call(scope, fix({type: name})); + continue; + } - if (next == root) - return _traverseNode(next, isFullySelected, TRUE, how); + // Handle mouseenter/mouseleaver + if (!hasMouseEnterLeave) { + fakeName = mouseEnterLeave[name]; - parent = next.parentNode; - clonedParent = _traverseNode(parent, FALSE, TRUE, how); + if (fakeName) { + nativeHandler = function(evt) { + var current, related; - while (parent) { - while (next) { - nextSibling = next.nextSibling; - clonedChild = _traverseNode(next, isFullySelected, TRUE, how); + current = evt.currentTarget; + related = evt.relatedTarget; - if (how != DELETE) - clonedParent.appendChild(clonedChild); + // 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; + } + } - isFullySelected = TRUE; - next = nextSibling; + // Fire fake event + if (!related) { + evt = fix(evt || win.event); + evt.type = evt.type === 'mouseout' ? 'mouseleave' : 'mouseenter'; + evt.target = current; + executeHandlers(evt, id); + } + }; + } } - if (parent == root) - return clonedParent; - - next = parent.nextSibling; - parent = parent.parentNode; - - clonedGrandParent = _traverseNode(parent, FALSE, TRUE, how); + // 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); + }; + } - if (how != DELETE) - clonedGrandParent.appendChild(clonedParent); + // 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; + + // Add the nativeHandler to the callback list so that we can later unbind it + callbackList.nativeHandler = nativeHandler; + if (!w3cEventModel) { + callbackList.proxyHandler = proxy(id); + } - clonedParent = clonedGrandParent; + // 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}); + } } - }; - function _traverseNode(n, isFullySelected, isLeft, how) { - var txtValue, newNodeValue, oldNodeValue, offset, newNode; + target = callbackList = 0; // Clean memory for IE - if (isFullySelected) - return _traverseFullySelected(n, how); + return callback; + }; - if (n.nodeType == 3 /* TEXT_NODE */) { - txtValue = n.nodeValue; + self.unbind = function(target, names, callback) { + var id, callbackList, i, ci, name, eventMap; + + // Don't bind to text nodes or comments + if (!target || target.nodeType === 3 || target.nodeType === 8) { + return self; + } + + // Unbind event or events if the target has the expando + id = target[expando]; + if (id) { + eventMap = events[id]; + + // 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); + } + } + } - if (isLeft) { - offset = t[START_OFFSET]; - newNodeValue = txtValue.substring(offset); - oldNodeValue = txtValue.substring(0, offset); + // 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 { - offset = t[END_OFFSET]; - newNodeValue = txtValue.substring(0, offset); - oldNodeValue = txtValue.substring(offset); - } + // 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); + } - if (how != CLONE) - n.nodeValue = oldNodeValue; + eventMap = {}; + } - if (how == DELETE) - return; + // Check if object is empty, if it isn't then we won't remove the expando map + for (name in eventMap) { + return self; + } - newNode = n.cloneNode(FALSE); - newNode.nodeValue = newNodeValue; + // Delete event object + delete events[id]; - return newNode; + // 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; + } } - if (how == DELETE) - return; - - return n.cloneNode(FALSE); + return self; }; - function _traverseFullySelected(n, how) { - if (how != DELETE) - return how == CLONE ? n.cloneNode(TRUE) : n; - - n.parentNode.removeChild(n); - }; - }; + self.fire = function(target, name, args) { + var id, event; - ns.Range = Range; -})(tinymce.dom); + // Don't bind to text nodes or comments + if (!target || target.nodeType === 3 || target.nodeType === 8) { + return self; + } -(function() { - function Selection(selection) { - var t = this, invisibleChar = '\uFEFF', range, lastIERng, dom = selection.dom, TRUE = true, FALSE = false; + // Build event object by patching the args + event = fix(null, args); + event.type = name; - // Returns a W3C DOM compatible range object by using the IE Range API - function getRange() { - var ieRange = selection.getRng(), domRange = dom.createRng(), element, collapsed; + do { + // Found an expando that means there is listeners to execute + id = target[expando]; + if (id) { + executeHandlers(event, id); + } - // 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; + // Walk up the DOM + target = target.parentNode || target.ownerDocument || target.defaultView || target.parentWindow; + } while (target && !event.isPropagationStopped()); - // Handle control selection or text selection of a image - if (ieRange.item || !element.hasChildNodes()) { - domRange.setStart(element.parentNode, dom.nodeIndex(element)); - domRange.setEnd(domRange.startContainer, domRange.startOffset + 1); + return self; + }; - return domRange; + 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; } - collapsed = selection.isCollapsed(); + // Unbind any element on the specificed target + if (target[expando]) { + unbind(target); + } - function findEndPoint(start) { - var marker, container, offset, nodes, startIndex = 0, endIndex, index, parent, checkRng, position; + // 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; + } - // Setup temp range and collapse it - checkRng = ieRange.duplicate(); - checkRng.collapse(start); + // Remove events from each child element + if (target && target.getElementsByTagName) { + unbind(target); - // Create marker and insert it at the end of the endpoints parent - marker = dom.create('a'); - parent = checkRng.parentElement(); + children = target.getElementsByTagName('*'); + i = children.length; + while (i--) { + target = children[i]; - // If parent doesn't have any children then set the container to that parent and the index to 0 - if (!parent.hasChildNodes()) { - domRange[start ? 'setStart' : 'setEnd'](parent, 0); - return; + if (target[expando]) { + unbind(target); + } } + } - parent.appendChild(marker); - checkRng.moveToElementText(marker); - position = ieRange.compareEndPoints(start ? 'StartToStart' : 'EndToEnd', checkRng); - if (position > 0) { - // The position is after the end of the parent element. - // This is the case where IE puts the caret to the left edge of a table. - domRange[start ? 'setStartAfter' : 'setEndAfter'](parent); - dom.remove(marker); - return; - } + return self; + }; - // Setup node list and endIndex - nodes = tinymce.grep(parent.childNodes); - endIndex = nodes.length - 1; - // Perform a binary search for the position - while (startIndex <= endIndex) { - index = Math.floor((startIndex + endIndex) / 2); - - // Insert marker and check it's position relative to the selection - parent.insertBefore(marker, nodes[index]); - checkRng.moveToElementText(marker); - position = ieRange.compareEndPoints(start ? 'StartToStart' : 'EndToEnd', checkRng); - if (position > 0) { - // Marker is to the right - startIndex = index + 1; - } else if (position < 0) { - // Marker is to the left - endIndex = index - 1; - } else { - // Maker is where we are - found = true; - break; - } - } + self.callNativeHandler = function(id, evt) { + if (events) { + events[id][evt.type].nativeHandler(evt); + } + }; - // Setup container - container = position > 0 || index == 0 ? marker.nextSibling : marker.previousSibling; + self.destory = function() { + events = {}; + }; - // Handle element selection - if (container.nodeType == 1) { - dom.remove(marker); + // Legacy function calls - // Find offset and container - offset = dom.nodeIndex(container); - container = container.parentNode; + self.add = function(target, events, func, scope) { + // Old API supported direct ID assignment + if (typeof(target) === "string") { + target = document.getElementById(target); + } - // Move the offset if we are setting the end or the position is after an element - if (!start || index > 0) - offset++; - } else { - // Calculate offset within text node - if (position > 0 || index == 0) { - checkRng.setEndPoint(start ? 'StartToStart' : 'EndToEnd', ieRange); - offset = checkRng.text.length; - } else { - checkRng.setEndPoint(start ? 'StartToStart' : 'EndToEnd', ieRange); - offset = container.nodeValue.length - checkRng.text.length; - } + // Old API supported multiple targets + if (target && target instanceof Array) { + var i = target; - dom.remove(marker); + while (i--) { + self.add(target[i], events, func, scope); } - domRange[start ? 'setStart' : 'setEnd'](container, offset); - }; - - // Find start point - findEndPoint(true); + return; + } - // Find end point if needed - if (!collapsed) - findEndPoint(); + // Old API called ready init + if (events === "init") { + events = "ready"; + } - return domRange; + return self.bind(target, events instanceof Array ? events.join(' ') : events, func, scope); }; - this.addRange = function(rng) { - var ieRng, ieRng2, doc = selection.dom.doc, body = doc.body, startPos, endPos, sc, so, ec, eo, marker, lastIndex, skipStart, skipEnd; + self.remove = function(target, events, func) { + // Old API supported direct ID assignment + if (typeof(target) === "string") { + target = document.getElementById(target); + } - this.destroy(); + // Old API supported multiple targets + if (target instanceof Array) { + var i = target; - // Setup some shorter versions - sc = rng.startContainer; - so = rng.startOffset; - ec = rng.endContainer; - eo = rng.endOffset; - ieRng = body.createTextRange(); + while (i--) { + self.remove(target[i], events, func, scope); + } - // If document selection move caret to first node in document - if (sc == doc || ec == doc) { - ieRng = body.createTextRange(); - ieRng.collapse(); - ieRng.select(); - return; + return self; } - // If child index resolve it - if (sc.nodeType == 1 && sc.hasChildNodes()) { - lastIndex = sc.childNodes.length - 1; - - // Index is higher that the child count then we need to jump over the start container - if (so > lastIndex) { - skipStart = 1; - sc = sc.childNodes[lastIndex]; - } else - sc = sc.childNodes[so]; + return self.unbind(target, events instanceof Array ? events.join(' ') : events, func); + }; - // Child was text node then move offset to start of it - if (sc.nodeType == 3) - so = 0; + self.clear = function(target) { + // Old API supported direct ID assignment + if (typeof(target) === "string") { + target = document.getElementById(target); } - // If child index resolve it - if (ec.nodeType == 1 && ec.hasChildNodes()) { - lastIndex = ec.childNodes.length - 1; - - if (eo == 0) { - skipEnd = 1; - ec = ec.childNodes[0]; - } else { - ec = ec.childNodes[Math.min(lastIndex, eo - 1)]; + return self.clean(target); + }; - // Child was text node then move offset to end of text node - if (ec.nodeType == 3) - eo = ec.nodeValue.length; - } + self.cancel = function(e) { + if (e) { + self.prevent(e); + self.stop(e); } - // Single element selection - if (sc == ec && sc.nodeType == 1) { - // Make control selection for some elements - if (/^(IMG|TABLE)$/.test(sc.nodeName) && so != eo) { - ieRng = body.createControlRange(); - ieRng.addElement(sc); - } else { - ieRng = body.createTextRange(); + return false; + }; - // Padd empty elements with invisible character - if (!sc.hasChildNodes() && sc.canHaveHTML) - sc.innerHTML = invisibleChar; + self.prevent = function(e) { + e.preventDefault(); - // Select element contents - ieRng.moveToElementText(sc); + return false; + }; - // If it's only containing a padding remove it so the caret remains - if (sc.innerHTML == invisibleChar) { - ieRng.collapse(TRUE); - sc.removeChild(sc.firstChild); - } - } + self.stop = function(e) { + e.stopPropagation(); - if (so == eo) - ieRng.collapse(eo <= rng.endContainer.childNodes.length - 1); + return false; + }; + } - ieRng.select(); - ieRng.scrollIntoView(); - return; - } + namespace.EventUtils = EventUtils; - // Create range and marker - ieRng = body.createTextRange(); - marker = doc.createElement('span'); - marker.innerHTML = ' '; - - // Set start of range to startContainer/startOffset - if (sc.nodeType == 3) { - // Insert marker after/before startContainer - if (skipStart) - dom.insertAfter(marker, sc); - else - sc.parentNode.insertBefore(marker, sc); + namespace.Event = new EventUtils(function(id) { + return function(evt) { + tinymce.dom.Event.callNativeHandler(id, evt); + }; + }); - // Select marker the caret to offset position - ieRng.moveToElementText(marker); - marker.parentNode.removeChild(marker); + // Bind ready event when tinymce script is loaded + namespace.Event.bind(window, 'ready', function() {}); - // Move if we need to, moving it 0 characters actually moves it! - if (so > 0) - ieRng.move('character', so); - } else { - ieRng.moveToElementText(sc); + namespace = 0; +})(tinymce.dom, 'data-mce-expando'); // Namespace and expando - if (skipStart) - ieRng.collapse(FALSE); - } +(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 same text container then we can do a more simple move - if (sc == ec && sc.nodeType == 3) { - try { - ieRng.moveEnd('character', eo - so); - ieRng.select(); - ieRng.scrollIntoView(); - } catch (ex) { - // Some times a Runtime error of the 800a025e type gets thrown - // especially when the caret is placed before a table. - // This is a somewhat strange location for the caret. - // TODO: Find a better solution for this would possible require a rewrite of the setRng method - } + 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" + }, - return; - } + DOMUtils : function(d, s) { + var t = this, globalStyle, name, blockElementsMap; - // Set end of range to endContainer/endOffset - ieRng2 = body.createTextRange(); - if (ec.nodeType == 3) { - // Insert marker after/before startContainer - ec.parentNode.insertBefore(marker, ec); + 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"); - // Move selection to end marker and move caret to end offset - ieRng2.moveToElementText(marker); - marker.parentNode.removeChild(marker); - ieRng2.move('character', eo); - ieRng.setEndPoint('EndToStart', ieRng2); - } else { - ieRng2.moveToElementText(ec); - ieRng2.collapse(!!skipEnd); - ieRng.setEndPoint('EndToEnd', ieRng2); + 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); + + // Fix IE6SP2 flicker and check it failed for pre SP2 + if (tinymce.isIE6) { + try { + d.execCommand('BackgroundImageCache', false, true); + } catch (e) { + t.cssFlicker = true; + } } - ieRng.select(); - ieRng.scrollIntoView(); - }; + 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() : {}; - this.getRangeAt = function() { - // Setup new range if the cache is empty - if (!range || !tinymce.dom.RangeUtils.compareRanges(lastIERng, selection.getRng())) { - range = getRange(); + t.isBlock = function(node) { + // This function is called in module pattern style since it might be executed with the wrong this scope + var type = node.nodeType; - // Store away text range for next call - lastIERng = selection.getRng(); - } + // If it's a node then check the type and use the nodeName + if (type) + return !!(type === 1 && blockElementsMap[node.nodeName]); - // IE will say that the range is equal then produce an invalid argument exception - // if you perform specific operations in a keyup event. For example Ctrl+Del. - // This hack will invalidate the range cache if the exception occurs - try { - range.startContainer.nextSibling; - } catch (ex) { - range = getRange(); - lastIERng = null; - } + return !!blockElementsMap[node]; + }; + }, - // Return cached range - return range; - }; + fixDoc: function(doc) { + var settings = this.settings, name; - this.destroy = function() { - // Destroy cached range and last IE range to avoid memory leaks - lastIERng = range = null; - }; + 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); + }); - // IE has an issue where you can't select/move the caret by clicking outside the body if the document is in standards mode - if (selection.dom.boxModel) { - (function() { - var doc = dom.doc, body = doc.body, started, startRng; + // Create all custom elements + for (name in settings.schema.getCustomElements()) { + doc.createElement(name); + } + } + }, - // Make HTML element unselectable since we are going to handle selection by hand - doc.documentElement.unselectable = TRUE; + clone: function(node, deep) { + var self = this, clone, doc; - // Return range from point or null if it failed - function rngFromPoint(x, y) { - var rng = body.createTextRange(); + // TODO: Add feature detection here in the future + if (!isIE || node.nodeType !== 1 || deep) { + return node.cloneNode(deep); + } - try { - rng.moveToPoint(x, y); - } catch (ex) { - // IE sometimes throws and exception, so lets just ignore it - rng = null; - } + doc = self.doc; - return rng; - }; + // Make a HTML5 safe shallow copy + if (!deep) { + clone = doc.createElement(node.nodeName); - // Fires while the selection is changing - function selectionChange(e) { - var pointRng; + // Copy attribs + each(self.getAttribs(node), function(attr) { + self.setAttrib(clone, attr.nodeName, self.getAttrib(node, attr.nodeName)); + }); - // Check if the button is down or not - if (e.button) { - // Create range from mouse position - pointRng = rngFromPoint(e.x, e.y); + return clone; + } +/* + // Setup HTML5 patched document fragment + if (!self.frag) { + self.frag = doc.createDocumentFragment(); + self.fixDoc(self.frag); + } - if (pointRng) { - // Check if pointRange is before/after selection then change the endPoint - if (pointRng.compareEndPoints('StartToStart', startRng) > 0) - pointRng.setEndPoint('StartToStart', startRng); - else - pointRng.setEndPoint('EndToEnd', startRng); + // 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; + }, - pointRng.select(); - } - } else - endSelection(); - } + getRoot : function() { + var t = this, s = t.settings; - // Removes listeners - function endSelection() { - dom.unbind(doc, 'mouseup', endSelection); - dom.unbind(doc, 'mousemove', selectionChange); - started = 0; - }; + return (s && t.get(s.root_element)) || t.doc.body; + }, - // Detect when user selects outside BODY - dom.bind(doc, 'mousedown', function(e) { - if (e.target.nodeName === 'HTML') { - if (started) - endSelection(); + getViewPort : function(w) { + var d, b; - started = 1; + w = !w ? this.win : w; + d = w.document; + b = this.boxModel ? d.documentElement : d.body; - // Setup start position - startRng = rngFromPoint(e.x, e.y); - if (startRng) { - // Listen for selection change events - dom.bind(doc, 'mouseup', endSelection); - dom.bind(doc, 'mousemove', selectionChange); + // 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 + }; + }, - startRng.select(); - } - } - }); - })(); - } - }; + getRect : function(e) { + var p, t = this, sr; - // Expose the selection object - tinymce.dom.TridentSelection = Selection; -})(); + e = t.get(e); + p = t.getPos(e); + sr = t.getSize(e); + return { + x : p.x, + y : p.y, + w : sr.w, + h : sr.h + }; + }, -/* - * 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(){ + getSize : function(e) { + var t = this, w, h; -var chunker = /((?:\((?:\([^()]+\)|[^()]+)+\)|\[(?:\[[^\[\]]*\]|['"][^'"]*['"]|[^\[\]'"]+)+\]|\\.|[^ >+~,(\[\\]+)+|[>+~])(\s*,\s*)?((?:.|\r|\n)*)/g, - done = 0, - toString = Object.prototype.toString, - hasDuplicate = false, - baseHasDuplicate = true; + e = t.get(e); + w = t.getStyle(e, 'width'); + h = t.getStyle(e, 'height'); -// 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; -}); + // Non pixel value, then force offset/clientWidth + if (w.indexOf('px') === -1) + w = 0; -var Sizzle = function(selector, context, results, seed) { - results = results || []; - context = context || document; + // Non pixel value, then force offset/clientWidth + if (h.indexOf('px') === -1) + h = 0; - var origContext = context; + return { + w : parseInt(w) || e.offsetWidth || e.clientWidth, + h : parseInt(h) || e.offsetHeight || e.clientHeight + }; + }, - if ( context.nodeType !== 1 && context.nodeType !== 9 ) { - return []; - } - - if ( !selector || typeof selector !== "string" ) { - return results; - } + getParent : function(n, f, r) { + return this.getParents(n, f, r, false); + }, - 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); + getParents : function(n, f, r, c) { + var t = this, na, se = t.settings, o = []; - if ( m ) { - soFar = m[3]; - - parts.push( m[1] ); - - if ( m[2] ) { - extra = m[3]; - break; - } - } - } while ( m ); + n = t.get(n); + c = c === undefined; - 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 ); + if (se.strict_root) + r = r || t.getRoot(); - while ( parts.length ) { - selector = parts.shift(); + // Wrap node name as func + if (is(f, 'string')) { + na = f; - if ( Expr.relative[ selector ] ) { - selector += parts.shift(); + if (f === '*') { + f = function(n) {return n.nodeType == 1;}; + } else { + f = function(n) { + return t.is(n, na); + }; } - - set = posProcess( selector, set ); } - } - } 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; + while (n) { + if (n == r || !n.nodeType || n.nodeType === 9) + break; - if ( parts.length > 0 ) { - checkSet = makeArray(set); - } else { - prune = false; + if (!f || f(n)) { + if (c) + o.push(n); + else + return n; + } + + n = n.parentNode; } - while ( parts.length ) { - cur = parts.pop(); - pop = cur; + return c ? o : null; + }, - if ( !Expr.relative[ cur ] ) { - cur = ""; - } else { - pop = parts.pop(); - } + get : function(e) { + var n; - if ( pop == null ) { - pop = context; - } + if (e && this.doc && typeof(e) == 'string') { + n = e; + e = this.doc.getElementById(e); - Expr.relative[ cur ]( checkSet, pop, contextXML ); + // 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]; } - } else { - checkSet = parts = []; - } - } - if ( !checkSet ) { - checkSet = set; - } + return e; + }, - if ( !checkSet ) { - Sizzle.error( cur || selector ); - } + getNext : function(node, selector) { + return this._findSib(node, selector, 'nextSibling'); + }, - 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 ); - } + getPrev : function(node, selector) { + return this._findSib(node, selector, 'previousSibling'); + }, - if ( extra ) { - Sizzle( extra, origContext, results, seed ); - Sizzle.uniqueSort( results ); - } - return results; -}; + select : function(pa, s) { + var t = this; -Sizzle.uniqueSort = function(results){ - if ( sortOrder ) { - hasDuplicate = baseHasDuplicate; - results.sort(sortOrder); + return tinymce.dom.Sizzle(pa, t.get(s) || t.get(t.settings.root_element) || t.doc, []); + }, - if ( hasDuplicate ) { - for ( var i = 1; i < results.length; i++ ) { - if ( results[i] === results[i-1] ) { - results.splice(i--, 1); + is : function(n, selector) { + var i; + + // 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; + + // Simple selector just elements + if (simpleSelectorRe.test(selector)) { + selector = selector.toLowerCase().split(/,/); + n = n.nodeName.toLowerCase(); + + for (i = selector.length - 1; i >= 0; i--) { + if (selector[i] == n) + return true; + } + + return false; } } - } - } - return results; -}; + return tinymce.dom.Sizzle.matches(selector, n.nodeType ? [n] : n).length > 0; + }, -Sizzle.matches = function(expr, set){ - return Sizzle(expr, null, null, set); -}; -Sizzle.find = function(expr, context, isXML){ - var set; + add : function(p, n, a, h, c) { + var t = this; - if ( !expr ) { - return []; - } + return this.run(p, function(p) { + var e, k; - 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); + e = is(n, 'string') ? t.doc.createElement(n) : n; + t.setAttribs(e, a); - 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; + if (h) { + if (h.nodeType) + e.appendChild(h); + else + t.setHTML(e, h); } - } - } - } - if ( !set ) { - set = context.getElementsByTagName("*"); - } - - return {set: set, expr: expr}; -}; + return !c ? p.appendChild(e) : e; + }); + }, -Sizzle.filter = function(expr, set, inplace, not){ - var old = expr, result = [], curLoop = set, match, anyFound, - isXMLFilter = set && set[0] && Sizzle.isXML(set[0]); + create : function(n, a, h) { + return this.add(this.doc.createElement(n), n, a, h, 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; + createHTML : function(n, a, h) { + var o = '', t = this, k; - match.splice(1,1); + o += '<' + n; - if ( left.substr( left.length - 1 ) === "\\" ) { - continue; - } + for (k in a) { + if (a.hasOwnProperty(k)) + o += ' ' + k + '="' + t.encode(a[k]) + '"'; + } - if ( curLoop === result ) { - result = []; - } + // 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 + ''; - if ( Expr.preFilter[ type ] ) { - match = Expr.preFilter[ type ]( match, curLoop, inplace, result, not, isXMLFilter ); + return o + ' />'; + }, - if ( !match ) { - anyFound = found = true; - } else if ( match === true ) { - continue; - } - } + remove : function(node, keep_children) { + return this.run(node, function(node) { + var child, parent = node.parentNode; - if ( match ) { - for ( var i = 0; (item = curLoop[i]) != null; i++ ) { - if ( item ) { - found = filter( item, match, i, curLoop ); - var pass = not ^ !!found; + if (!parent) + return null; - if ( inplace && found != null ) { - if ( pass ) { - anyFound = true; - } else { - curLoop[i] = false; - } - } else if ( pass ) { - result.push( item ); - anyFound = true; - } - } + 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); } } - if ( found !== undefined ) { - if ( !inplace ) { - curLoop = result; - } + return parent.removeChild(node); + }); + }, - expr = expr.replace( Expr.match[ type ], "" ); + setStyle : function(n, na, v) { + var t = this; - if ( !anyFound ) { - return []; - } + return t.run(n, function(e) { + var s, i; - break; - } - } - } + s = e.style; - // Improper expression - if ( expr === old ) { - if ( anyFound == null ) { - Sizzle.error( expr ); - } else { - break; - } - } + // Camelcase it, if needed + na = na.replace(/-(\D)/g, function(a, b){ + return b.toUpperCase(); + }); - old = expr; - } + // Default px suffix on these + if (t.pixelStyles.test(na) && (tinymce.is(v, 'number') || /^[\-0-9\.]+$/.test(v))) + v += 'px'; - return curLoop; -}; + switch (na) { + case 'opacity': + // IE specific opacity + if (isIE) { + s.filter = v === '' ? '' : "alpha(opacity=" + (v * 100) + ")"; -Sizzle.error = function( msg ) { - throw "Syntax error, unrecognized expression: " + msg; -}; + if (!n.currentStyle || !n.currentStyle.hasLayout) + s.display = 'inline-block'; + } -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; - - 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 ) {} + // Fix for older browsers + s[na] = s['-moz-opacity'] = s['-khtml-opacity'] = v || ''; + break; - checkSet[i] = isPartStrNotTag || elem && elem.nodeName.toLowerCase() === part ? - elem || false : - elem === part; + case 'float': + isIE ? s.styleFloat = v : s.cssFloat = v; + break; + + default: + s[na] = v || ''; } - } - if ( isPartStrNotTag ) { - Sizzle.filter( part, checkSet, true ); - } + // Force update of the style data + if (t.settings.update_styles) + t.setAttrib(e, 'data-mce-style'); + }); }, - ">": function(checkSet, part){ - var isPartStr = typeof part === "string", - elem, i = 0, l = checkSet.length; - if ( isPartStr && !/\W/.test(part) ) { - part = part.toLowerCase(); + getStyle : function(n, na, c) { + n = this.get(n); - 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 : - elem.parentNode === part; - } - } + if (!n) + return; - if ( isPartStr ) { - Sizzle.filter( part, checkSet, true ); + // Gecko + if (this.doc.defaultView && c) { + // Remove camelcase + na = na.replace(/[A-Z]/g, function(a){ + return '-' + a; + }); + + try { + return this.doc.defaultView.getComputedStyle(n, null).getPropertyValue(na); + } catch (ex) { + // Old safari might fail + return null; } } - }, - "": function(checkSet, part, isXML){ - var doneName = done++, checkFn = dirCheck, nodeCheck; - if ( typeof part === "string" && !/\W/.test(part) ) { - part = part.toLowerCase(); - nodeCheck = part; - checkFn = dirNodeCheck; - } + // Camelcase it, if needed + na = na.replace(/-(\D)/g, function(a, b){ + return b.toUpperCase(); + }); - checkFn("parentNode", part, doneName, checkSet, nodeCheck, isXML); + if (na == 'float') + na = isIE ? 'styleFloat' : 'cssFloat'; + + // IE & Opera + if (n.currentStyle && c) + return n.currentStyle[na]; + + return n.style ? n.style[na] : undefined; }, - "~": function(checkSet, part, isXML){ - var doneName = done++, checkFn = dirCheck, nodeCheck; - if ( typeof part === "string" && !/\W/.test(part) ) { - part = part.toLowerCase(); - nodeCheck = part; - checkFn = dirNodeCheck; - } + setStyles : function(e, o) { + var t = this, s = t.settings, ol; - checkFn("previousSibling", part, doneName, checkSet, nodeCheck, isXML); - } - }, - find: { - ID: function(match, context, isXML){ - if ( typeof context.getElementById !== "undefined" && !isXML ) { - var m = context.getElementById(match[1]); - return m ? [m] : []; - } + ol = s.update_styles; + s.update_styles = 0; + + each(o, function(v, n) { + t.setStyle(e, n, v); + }); + + // Update style info + s.update_styles = ol; + if (s.update_styles) + t.setAttrib(e, s.cssText); }, - NAME: function(match, context){ - if ( typeof context.getElementsByName !== "undefined" ) { - var ret = [], results = context.getElementsByName(match[1]); - for ( var i = 0, l = results.length; i < l; i++ ) { - if ( results[i].getAttribute("name") === match[1] ) { - ret.push( results[i] ); - } + 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)); } - - return ret.length === 0 ? null : ret; - } + }); }, - TAG: function(match, context){ - return context.getElementsByTagName(match[1]); - } - }, - preFilter: { - CLASS: function(match, curLoop, inplace, result, not, isXML){ - match = " " + match[1].replace(/\\/g, "") + " "; - if ( isXML ) { - return match; - } + setAttrib : function(e, n, v) { + var t = this; - 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 ( !inplace ) { - result.push( elem ); - } - } else if ( inplace ) { - curLoop[i] = false; + // Whats the point + if (!e || !n) + return; + + // Strict XML mode + if (t.settings.strict) + n = n.toLowerCase(); + + return this.run(e, function(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); + }); + + return; + } + + // No mce_style for elements with these since they might get resized by the user + if (s.keep_values) { + if (v && !t._isRes(v)) + e.setAttribute('data-mce-style', v, 2); + else + e.removeAttribute('data-mce-style', 2); + } + + e.style.cssText = v; + break; + + case "class": + e.className = v || ''; // Fix IE null bug + break; + + case "src": + case "href": + if (s.keep_values) { + if (s.url_converter) + v = s.url_converter.call(s.url_converter_scope || t, v, n, e); + + t.setAttrib(e, 'data-mce-' + n, v, 2); + } + + break; + + case "shape": + e.setAttribute('data-mce-style', v); + break; } } - } + if (is(v) && v !== null && v.length !== 0) + e.setAttribute(n, '' + v, 2); + else + e.removeAttribute(n, 2); - return false; - }, - ID: function(match){ - return match[1].replace(/\\/g, ""); + // fire onChangeAttrib event for attributes that have changed + if (tinyMCE.activeEditor && originalValue != v) { + var ed = tinyMCE.activeEditor; + ed.onSetAttrib.dispatch(ed, e, n, v); + } + }); }, - TAG: function(match, curLoop){ - return match[1].toLowerCase(); + + setAttribs : function(e, o) { + var t = this; + + return this.run(e, function(e) { + each(o, function(v, n) { + t.setAttrib(e, n, v); + }); + }); }, - CHILD: function(match){ - if ( match[1] === "nth" ) { - // parse equations like 'even', 'odd', '5', '2n', '3n+2', '4n-1', '-n+6' - var test = /(-?)(\d*)n((?:\+|-)?\d*)/.exec( - match[2] === "even" && "2n" || match[2] === "odd" && "2n+1" || - !/\D/.test( match[2] ) && "0n+" + match[2] || match[2]); - // calculate the numbers (first)n+(last) including if they are negative - match[2] = (test[1] + (test[2] || 1)) - 0; - match[3] = test[3] - 0; - } + getAttrib : function(e, n, dv) { + var v, t = this, undef; - // TODO: Move to normal caching system - match[0] = done++; + e = t.get(e); - return match; - }, - ATTR: function(match, curLoop, inplace, result, not, isXML){ - var name = match[1].replace(/\\/g, ""); - - if ( !isXML && Expr.attrMap[name] ) { - match[1] = Expr.attrMap[name]; + if (!e || e.nodeType !== 1) + return dv === undef ? false : dv; + + if (!is(dv)) + dv = ''; + + // Try the mce variant for these + if (/^(src|href|style|coords|shape)$/.test(n)) { + v = e.getAttribute("data-mce-" + n); + + if (v) + return v; } - if ( match[2] === "~=" ) { - match[4] = " " + match[4] + " "; + if (isIE && t.props[n]) { + v = e[t.props[n]]; + v = v && v.nodeValue ? v.nodeValue : v; } - return match; - }, - 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; + if (!v) + v = e.getAttribute(n, 2); + + // 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; + + return v ? n : ''; + } + + // Inner input elements will override attributes on form elements + if (e.nodeName === "FORM" && e.getAttributeNode(n)) + return e.getAttributeNode(n).nodeValue; + + if (n === 'style') { + v = v || e.style.cssText; + + if (v) { + v = t.serializeStyle(t.parseStyle(v), e.nodeName); + + if (t.settings.keep_values && !t._isRes(v)) + e.setAttribute('data-mce-style', v); } - } else if ( Expr.match.POS.test( match[0] ) || Expr.match.CHILD.test( match[0] ) ) { - return true; } - - return match; - }, - POS: function(match){ - match.unshift( true ); - return match; - } - }, - filters: { - enabled: function(elem){ - return elem.disabled === false && elem.type !== "hidden"; + + // Remove Apple and WebKit stuff + if (isWebKit && n === "class" && v) + v = v.replace(/(apple|webkit)\-[a-z\-]+/gi, ''); + + // Handle IE issues + if (isIE) { + switch (n) { + case 'rowspan': + case 'colspan': + // IE returns 1 as default value + if (v === 1) + v = ''; + + break; + + case 'size': + // IE returns +0 as default value for size + if (v === '+0' || v === 20 || v === 0) + v = ''; + + break; + + case 'width': + case 'height': + case 'vspace': + case 'checked': + case 'disabled': + case 'readonly': + if (v === 0) + v = ''; + + break; + + case 'hspace': + // IE returns -1 as default value + if (v === -1) + v = ''; + + break; + + case 'maxlength': + case 'tabindex': + // IE returns default value + if (v === 32768 || v === 2147483647 || v === '32768') + v = ''; + + break; + + case 'multiple': + case 'compact': + case 'noshade': + case 'nowrap': + if (v === 65535) + return n; + + return dv; + + case 'shape': + v = v.toLowerCase(); + break; + + 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 (v !== undef && v !== null && v !== '') ? '' + v : dv; }, - disabled: function(elem){ - return elem.disabled === true; + + getPos : function(n, ro) { + var t = this, x = 0, y = 0, e, d = t.doc, r; + + n = t.get(n); + ro = ro || d.body; + + 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; + + // 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; + + return {x : x, y : y}; + } + + r = n; + while (r && r != ro && r.nodeType) { + x += r.offsetLeft || 0; + y += r.offsetTop || 0; + r = r.offsetParent; + } + + r = n.parentNode; + while (r && r != ro && r.nodeType) { + x -= r.scrollLeft || 0; + y -= r.scrollTop || 0; + r = r.parentNode; + } + } + + return {x : x, y : y}; }, - checked: function(elem){ - return elem.checked === true; + + parseStyle : function(st) { + return this.styles.parse(st); }, - selected: function(elem){ - // Accessing this property makes selected-by-default - // options in Safari work properly - elem.parentNode.selectedIndex; - return elem.selected === true; + + serializeStyle : function(o, name) { + return this.styles.serialize(o, name); }, - parent: function(elem){ - return !!elem.firstChild; + + loadCSS : function(u) { + var t = this, d = t.doc, head; + + if (!u) + u = ''; + + head = t.select('head')[0]; + + each(u.split(','), function(u) { + var link; + + if (t.files[u]) + return; + + t.files[u] = true; + link = t.create('link', {rel : 'stylesheet', href : tinymce._addVer(u)}); + + // 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(); + + link.onload = null; + }; + } + + head.appendChild(link); + }); }, - empty: function(elem){ - return !elem.firstChild; + + addClass : function(e, c) { + return this.run(e, function(e) { + var o; + + if (!c) + return 0; + + if (this.hasClass(e, c)) + return e.className; + + o = this.removeClass(e, c); + + return e.className = (o != '' ? (o + ' ') : '') + c; + }); }, - has: function(elem, i, match){ - return !!Sizzle( match[3], elem ).length; + + removeClass : function(e, c) { + var t = this, re; + + return t.run(e, function(e) { + var v; + + if (t.hasClass(e, c)) { + if (!re) + re = new RegExp("(^|\\s+)" + c + "(\\s+|$)", "g"); + + v = e.className.replace(re, ' '); + v = tinymce.trim(v != ' ' ? v : ''); + + e.className = v; + + // Empty class attr + if (!v) { + e.removeAttribute('class'); + e.removeAttribute('className'); + } + + return v; + } + + return e.className; + }); }, - header: function(elem){ - return (/h\d/i).test( elem.nodeName ); + + hasClass : function(n, c) { + n = this.get(n); + + if (!n || !c) + return false; + + return (' ' + n.className + ' ').indexOf(' ' + c + ' ') !== -1; }, - text: function(elem){ - return "text" === elem.type; + + show : function(e) { + return this.setStyle(e, 'display', 'block'); }, - radio: function(elem){ - return "radio" === elem.type; + + hide : function(e) { + return this.setStyle(e, 'display', 'none'); }, - checkbox: function(elem){ - return "checkbox" === elem.type; - }, - file: function(elem){ - return "file" === elem.type; - }, - password: function(elem){ - return "password" === elem.type; - }, - submit: function(elem){ - return "submit" === elem.type; - }, - image: function(elem){ - return "image" === elem.type; - }, - reset: function(elem){ - return "reset" === elem.type; - }, - button: function(elem){ - return "button" === elem.type || elem.nodeName.toLowerCase() === "button"; + + isHidden : function(e) { + e = this.get(e); + + return !e || e.style.display == 'none' || this.getStyle(e, 'display') == 'none'; }, - input: function(elem){ - return (/input|select|textarea|button/i).test(elem.nodeName); - } - }, - setFilters: { - first: function(elem, i){ - return i === 0; + + uniqueId : function(p) { + return (!p ? 'mce_' : p) + (this.counter++); }, - last: function(elem, i, match, array){ - return i === array.length - 1; + + setHTML : function(element, html) { + var self = this; + + 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); + + 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 + + // Create new div with HTML contents and a BR infront to keep comments + element = self.create('div'); + element.innerHTML = '
    ' + html; + + // 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; + + return html; + }); }, - even: function(elem, i){ - return i % 2 === 0; + + getOuterHTML : function(elm) { + var doc, self = this; + + elm = self.get(elm); + + if (!elm) + return null; + + if (elm.nodeType === 1 && self.hasOuterHTML) + return elm.outerHTML; + + doc = (elm.ownerDocument || self.doc).createElement("body"); + doc.appendChild(elm.cloneNode(true)); + + return doc.innerHTML; }, - odd: function(elem, i){ - return i % 2 === 1; + + setOuterHTML : function(e, h, d) { + var t = this; + + function setHTML(e, h, d) { + var n, tp; + + tp = d.createElement("body"); + tp.innerHTML = h; + + n = tp.lastChild; + while (n) { + t.insertAfter(n.cloneNode(true), e); + n = n.previousSibling; + } + + 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); + } + }); }, - lt: function(elem, i, match){ - return i < match[3] - 0; + + decode : Entities.decode, + + encode : Entities.encodeAllRaw, + + insertAfter : function(node, reference_node) { + reference_node = this.get(reference_node); + + return this.run(node, function(node) { + var parent, nextSibling; + + parent = reference_node.parentNode; + nextSibling = reference_node.nextSibling; + + if (nextSibling) + parent.insertBefore(node, nextSibling); + else + parent.appendChild(node); + + return node; + }); }, - gt: function(elem, i, match){ - return i > match[3] - 0; + + 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); + }); }, - nth: function(elem, i, match){ - return match[3] - 0 === i; + + rename : function(elm, name) { + var t = this, newElm; + + if (elm.nodeName != name.toUpperCase()) { + // Rename block element + newElm = t.create(name); + + // Copy attribs to new block + each(t.getAttribs(elm), function(attr_node) { + t.setAttrib(newElm, attr_node.nodeName, t.getAttrib(elm, attr_node.nodeName)); + }); + + // Replace block + t.replace(newElm, elm, 1); + } + + return newElm || elm; }, - 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 ]; - if ( filter ) { - return filter( elem, i, match, array ); - } else if ( name === "contains" ) { - return (elem.textContent || elem.innerText || Sizzle.getText([ elem ]) || "").indexOf(match[3]) >= 0; - } else if ( name === "not" ) { - var not = match[3]; + findCommonAncestor : function(a, b) { + var ps = a, pe; - for ( var j = 0, l = not.length; j < l; j++ ) { - if ( not[j] === elem ) { - return false; - } - } + while (ps) { + pe = b; - return true; - } else { - Sizzle.error( "Syntax error, unrecognized expression: " + name ); + while (pe && ps != pe) + pe = pe.parentNode; + + if (ps == pe) + break; + + ps = ps.parentNode; } + + if (!ps && a.ownerDocument) + return a.ownerDocument.documentElement; + + return ps; }, - 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; - } - } - if ( type === "first" ) { - return true; - } - node = elem; - case 'last': - while ( (node = node.nextSibling) ) { - if ( node.nodeType === 1 ) { - return false; - } - } - return true; - case 'nth': - var 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; - for ( node = parent.firstChild; node; node = node.nextSibling ) { - if ( node.nodeType === 1 ) { - node.nodeIndex = ++count; - } - } - parent.sizcache = doneName; - } - - var diff = elem.nodeIndex - last; - if ( first === 0 ) { - return diff === 0; - } else { - return ( diff % first === 0 && diff / first >= 0 ); - } + 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 hex(s) { + s = parseInt(s).toString(16); + + return s.length > 1 ? s : '0' + s; // 0 -> 00 + }; + + if (c) { + s = '#' + hex(c[1]) + hex(c[2]) + hex(c[3]); + + return s; } + + return s; }, - ID: function(elem, match){ - return elem.nodeType === 1 && elem.getAttribute("id") === match; + + getClasses : function() { + var t = this, cl = [], i, lo = {}, f = t.settings.class_filter, ov; + + if (t.classes) + return t.classes; + + function addClasses(s) { + // IE style imports + each(s.imports, function(r) { + addClasses(r); + }); + + 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, ""); + + // Is internal or it doesn't contain a class + if (/\.mce/.test(v) || !/\.[\w\-]+$/.test(v)) + return; + + // Remove everything but class name + ov = v; + v = tinymce._replace(/.*\.([a-z0-9_\-]+).*/i, '$1', v); + + // Filter classes + if (f && !(v = f(v, ov))) + return; + + if (!lo[v]) { + cl.push({'class' : v}); + lo[v] = 1; + } + }); + } + break; + + // Import + case 3: + addClasses(r.styleSheet); + break; + } + }); + }; + + try { + each(t.doc.styleSheets, addClasses); + } catch (ex) { + // Ignore + } + + if (cl.length > 0) + t.classes = cl; + + return cl; }, - TAG: function(elem, match){ - return (match === "*" && elem.nodeType === 1) || elem.nodeName.toLowerCase() === match; + + run : function(e, f, s) { + var t = this, o; + + if (t.doc && typeof(e) === 'string') + e = t.get(e); + + if (!e) + return false; + + s = s || this; + if (!e.nodeType && (e.length || e.length === 0)) { + o = []; + + each(e, function(e, i) { + if (e) { + if (typeof(e) == 'string') + e = t.doc.getElementById(e); + + o.push(f.call(s, e, i)); + } + }); + + return o; + } + + return f.call(s, e); }, - CLASS: function(elem, match){ - return (" " + (elem.className || elem.getAttribute("class")) + " ") - .indexOf( match ) > -1; + + getAttribs : function(n) { + var o; + + n = this.get(n); + + if (!n) + return []; + + if (isIE) { + o = []; + + // 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; + } + + return n.attributes; }, - ATTR: function(elem, match){ - var name = match[1], - result = Expr.attrHandle[ name ] ? - Expr.attrHandle[ name ]( elem ) : - elem[ name ] != null ? - elem[ name ] : - elem.getAttribute( name ), - value = result + "", - type = match[2], - check = match[4]; - return result == null ? - type === "!=" : - type === "=" ? - value === check : - type === "*=" ? - value.indexOf(check) >= 0 : - type === "~=" ? - (" " + value + " ").indexOf(check) >= 0 : - !check ? - value && result !== false : - type === "!=" ? - value !== check : - type === "^=" ? - value.indexOf(check) === 0 : - type === "$=" ? - value.substr(value.length - check.length) === check : - type === "|=" ? - value === check || value.substr(0, check.length + 1) === check + "-" : - false; + isEmpty : function(node, elements) { + var self = this, i, attributes, type, walker, name, brCount = 0; + + node = node.firstChild; + if (node) { + walker = new tinymce.dom.TreeWalker(node, node.parentNode); + elements = elements || self.schema ? self.schema.getNonEmptyElements() : null; + + do { + type = node.nodeType; + + if (type === 1) { + // Ignore bogus elements + if (node.getAttribute('data-mce-bogus')) + continue; + + // 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; + } + + return false; + } + + // 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; + } + } + + // 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()); + } + + return brCount <= 1; }, - POS: function(elem, match, i, array){ - var name = match[2], filter = Expr.setFilters[ name ]; - if ( filter ) { - return filter( elem, i, match, array ); + destroy : function(s) { + var t = this; + + t.win = t.doc = t.root = t.events = t.frag = null; + + // Manual destroy then remove unload handler + if (!s) + tinymce.removeUnload(t.destroy); + }, + + createRng : function() { + var d = this.doc; + + return d.createRange ? d.createRange() : new tinymce.dom.Range(this); + }, + + nodeIndex : function(node, normalized) { + var idx = 0, lastNodeType, lastNode, nodeType; + + if (node) { + for (lastNodeType = node.nodeType, node = node.previousSibling, lastNode = node; node; node = node.previousSibling) { + nodeType = node.nodeType; + + // Normalize text nodes + if (normalized && nodeType == 3) { + if (nodeType == lastNodeType || !node.nodeValue.length) + continue; + } + idx++; + lastNodeType = nodeType; + } } - } - } -}; -var origPOS = Expr.match.POS, - fescape = function(all, num){ - return "\\" + (num - 0 + 1); - }; + return idx; + }, + + split : function(pe, e, re) { + var t = this, r = t.createRng(), bef, aft, pa; + + // 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 surroundedBySpans(node) { + var previousIsSpan = node.previousSibling && node.previousSibling.nodeName == 'SPAN'; + var nextIsSpan = node.nextSibling && node.nextSibling.nodeName == 'SPAN'; + return previousIsSpan && nextIsSpan; + } + + if (type == 1 && node.getAttribute('data-mce-type') == 'bookmark') + return; + + for (i = children.length - 1; i >= 0; i--) + trim(children[i]); + + 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); + + // Keep non empty elements or img, hr etc + if (children.length || /^(br|hr|input|img)$/i.test(node.nodeName)) + return; + } + + t.remove(node); + } + + return node; + }; + + if (pe && e) { + // Get before chunk + r.setStart(pe.parentNode, t.nodeIndex(pe)); + r.setEnd(e.parentNode, t.nodeIndex(e)); + bef = r.extractContents(); + + // 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(); + + // Insert before chunk + pa = pe.parentNode; + pa.insertBefore(trim(bef), pe); + + // Insert middle chunk + if (re) + pa.replaceChild(re, e); + else + pa.insertBefore(e, pe); + + // Insert after chunk + pa.insertBefore(trim(aft), pe); + t.remove(pe); + + return re || e; + } + }, + + bind : function(target, name, func, scope) { + return this.events.add(target, name, func, scope || this); + }, + + unbind : function(target, name, func) { + return this.events.remove(target, name, func); + }, + + fire : function(target, name, evt) { + return this.events.fire(target, name, evt); + }, + + + _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); + }; + } + + // Loop all siblings + for (node = node[name]; node; node = node[name]) { + if (f(node)) + return node; + } + } + + return null; + }, + + _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); + } + + /* + walk : function(n, f, s) { + var d = this.doc, w; + + if (d.createTreeWalker) { + w = d.createTreeWalker(n, NodeFilter.SHOW_TEXT, null, false); + + while ((n = w.nextNode()) != null) + f.call(s || this, n); + } else + tinymce.walk(n, f, 'childNodes', s); + } + */ + + /* + toRGB : function(s) { + var c = /^\s*?#([0-9A-F]{2})([0-9A-F]{1,2})([0-9A-F]{2})?\s*?$/.exec(s); + + if (c) { + // #FFF -> #FFFFFF + if (!is(c[3])) + c[3] = c[2] = c[1]; + + return "rgb(" + parseInt(c[1], 16) + "," + parseInt(c[2], 16) + "," + parseInt(c[3], 16) + ")"; + } + + return s; + } + */ + }); + + tinymce.DOM = new tinymce.dom.DOMUtils(document, {process_html : 0}); +})(tinymce); + +(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; + + extend(t, { + // Inital states + startContainer : doc, + startOffset : 0, + endContainer : doc, + endOffset : 0, + collapsed : TRUE, + commonAncestorContainer : doc, + + // Range constants + START_TO_START : 0, + START_TO_END : 1, + END_TO_END : 2, + END_TO_START : 3, + + // 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 + }); + + function createDocumentFragment() { + return doc.createDocumentFragment(); + }; + + function setStart(n, o) { + _setEndPoint(TRUE, n, o); + }; + + function setEnd(n, o) { + _setEndPoint(FALSE, n, o); + }; + + function setStartBefore(n) { + setStart(n.parentNode, nodeIndex(n)); + }; + + function setStartAfter(n) { + setStart(n.parentNode, nodeIndex(n) + 1); + }; + + function setEndBefore(n) { + setEnd(n.parentNode, nodeIndex(n)); + }; + + function setEndAfter(n) { + setEnd(n.parentNode, nodeIndex(n) + 1); + }; + + 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]; + } + + t.collapsed = TRUE; + }; + + function selectNode(n) { + setStartBefore(n); + setEndAfter(n); + }; + + function selectNodeContents(n) { + setStart(n, 0); + setEnd(n, n.nodeType === 1 ? n.childNodes.length : n.nodeValue.length); + }; + + 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; + + // 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); + }; + + function deleteContents() { + _traverse(DELETE); + }; + + function extractContents() { + return _traverse(EXTRACT); + }; + + function cloneContents() { + return _traverse(CLONE); + }; + + function insertNode(n) { + var startContainer = this[START_CONTAINER], + startOffset = this[START_OFFSET], nn, o; + + // 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); + } + }; + + function surroundContents(n) { + var f = t.extractContents(); + + t.insertNode(n); + n.appendChild(f); + t.selectNode(n); + }; + + 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 + }); + }; + + // Private methods + + function _getSelectedNode(container, offset) { + var child; + + if (container.nodeType == 3 /* TEXT_NODE */) + return container; + + if (offset < 0) + return container; + + child = container.firstChild; + while (child && offset > 0) { + --offset; + child = child.nextSibling; + } + + if (child) + return child; + + return container; + }; + + function _isCollapsed() { + return (t[START_CONTAINER] == t[END_CONTAINER] && t[START_OFFSET] == t[END_OFFSET]); + }; + + 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 (offsetA < offsetB) + return -1; // before + + return 1; // after + } + + // 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 (c) { + offsetC = 0; + n = containerA.firstChild; + + while (n != c && offsetC < offsetA) { + offsetC++; + n = n.nextSibling; + } + + if (offsetA <= offsetC) + return -1; // before + + return 1; // after + } + + // 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; + } + + if (c) { + offsetC = 0; + n = containerB.firstChild; + + while (n != c && offsetC < offsetB) { + offsetC++; + n = n.nextSibling; + } + + if (offsetC < offsetB) + return -1; // before + + return 1; // after + } + + // 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; + + while (childA && childA.parentNode != cmnRoot) + childA = childA.parentNode; + + if (!childA) + childA = cmnRoot; + + childB = containerB; + while (childB && childB.parentNode != cmnRoot) + childB = childB.parentNode; + + if (!childB) + childB = cmnRoot; + + if (childA == childB) + return 0; // equal + + n = cmnRoot.firstChild; + while (n) { + if (n == childA) + return -1; // before + + if (n == childB) + return 1; // after + + n = n.nextSibling; + } + }; + + 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; + } + + // 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; + + 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; + + 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); + + ++endContainerDepth; + } + + for (c = t[START_CONTAINER], p = c.parentNode; p; c = p, p = p.parentNode) { + if (p == t[END_CONTAINER]) + return _traverseCommonEndContainer(c, how); + + ++startContainerDepth; + } + + depthDiff = startContainerDepth - endContainerDepth; + + startNode = t[START_CONTAINER]; + while (depthDiff > 0) { + startNode = startNode.parentNode; + depthDiff--; + } + + endNode = t[END_CONTAINER]; + while (depthDiff < 0) { + endNode = endNode.parentNode; + depthDiff++; + } + + // 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; + } + + return _traverseCommonAncestors(startNode, endNode, how); + }; + + function _traverseSameContainer(how) { + var frag, s, sub, n, cnt, sibling, xferNode, start, len; + + if (how != DELETE) + frag = createDocumentFragment(); + + // If selection is empty, just return the fragment + if (t[START_OFFSET] == t[END_OFFSET]) + return frag; + + // 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]); + + // 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); + } + + // Nothing is partially selected, so collapse to start point + t.collapse(TRUE); + } + + if (how == DELETE) + return; + + if (sub.length > 0) { + frag.appendChild(doc.createTextNode(sub)); + } + + return frag; + } + + // Copy nodes between the start/end offsets. + n = _getSelectedNode(t[START_CONTAINER], t[START_OFFSET]); + cnt = t[END_OFFSET] - t[START_OFFSET]; + + while (n && cnt > 0) { + sibling = n.nextSibling; + xferNode = _traverseFullySelected(n, how); + + if (frag) + frag.appendChild( xferNode ); + + --cnt; + n = sibling; + } + + // Nothing is partially selected, so collapse to start point + if (how != CLONE) + t.collapse(TRUE); + + return frag; + }; + + function _traverseCommonStartContainer(endAncestor, how) { + var frag, n, endIdx, cnt, sibling, xferNode; + + if (how != DELETE) + frag = createDocumentFragment(); + + n = _traverseRightBoundary(endAncestor, how); + + if (frag) + frag.appendChild(n); + + endIdx = nodeIndex(endAncestor); + cnt = endIdx - t[START_OFFSET]; + + if (cnt <= 0) { + // Collapse to just before the endAncestor, which + // is partially selected. + if (how != CLONE) { + t.setEndBefore(endAncestor); + t.collapse(FALSE); + } + + return frag; + } + + n = endAncestor.previousSibling; + while (cnt > 0) { + sibling = n.previousSibling; + xferNode = _traverseFullySelected(n, how); + + if (frag) + frag.insertBefore(xferNode, frag.firstChild); + + --cnt; + n = sibling; + } + + // Collapse to just before the endAncestor, which + // is partially selected. + if (how != CLONE) { + t.setEndBefore(endAncestor); + t.collapse(FALSE); + } + + return frag; + }; + + function _traverseCommonEndContainer(startAncestor, how) { + var frag, startIdx, n, cnt, sibling, xferNode; + + if (how != DELETE) + frag = createDocumentFragment(); + + n = _traverseLeftBoundary(startAncestor, how); + if (frag) + frag.appendChild(n); + + startIdx = nodeIndex(startAncestor); + ++startIdx; // Because we already traversed it + + cnt = t[END_OFFSET] - startIdx; + n = startAncestor.nextSibling; + while (n && cnt > 0) { + sibling = n.nextSibling; + xferNode = _traverseFullySelected(n, how); + + if (frag) + frag.appendChild(xferNode); + + --cnt; + n = sibling; + } + + if (how != CLONE) { + t.setStartAfter(startAncestor); + t.collapse(TRUE); + } + + return frag; + }; + + function _traverseCommonAncestors(startAncestor, endAncestor, how) { + var n, frag, commonParent, startOffset, endOffset, cnt, sibling, nextSibling; + + if (how != DELETE) + frag = createDocumentFragment(); + + n = _traverseLeftBoundary(startAncestor, how); + if (frag) + frag.appendChild(n); + + commonParent = startAncestor.parentNode; + startOffset = nodeIndex(startAncestor); + endOffset = nodeIndex(endAncestor); + ++startOffset; + + cnt = endOffset - startOffset; + sibling = startAncestor.nextSibling; + + while (cnt > 0) { + nextSibling = sibling.nextSibling; + n = _traverseFullySelected(sibling, how); + + if (frag) + frag.appendChild(n); + + sibling = nextSibling; + --cnt; + } + + n = _traverseRightBoundary(endAncestor, how); + + if (frag) + frag.appendChild(n); + + if (how != CLONE) { + t.setStartAfter(startAncestor); + t.collapse(TRUE); + } + + return frag; + }; + + 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); + + parent = next.parentNode; + clonedParent = _traverseNode(parent, FALSE, FALSE, how); + + while (parent) { + while (next) { + prevSibling = next.previousSibling; + clonedChild = _traverseNode(next, isFullySelected, FALSE, how); + + if (how != DELETE) + clonedParent.insertBefore(clonedChild, clonedParent.firstChild); + + isFullySelected = TRUE; + next = prevSibling; + } + + if (parent == root) + return clonedParent; + + next = parent.previousSibling; + parent = parent.parentNode; + + clonedGrandParent = _traverseNode(parent, FALSE, FALSE, how); + + if (how != DELETE) + clonedGrandParent.appendChild(clonedParent); + + clonedParent = clonedGrandParent; + } + }; + + function _traverseLeftBoundary(root, how) { + var next = _getSelectedNode(t[START_CONTAINER], t[START_OFFSET]), isFullySelected = next != t[START_CONTAINER], parent, clonedParent, nextSibling, clonedChild, clonedGrandParent; + + if (next == root) + return _traverseNode(next, isFullySelected, TRUE, how); + + parent = next.parentNode; + clonedParent = _traverseNode(parent, FALSE, TRUE, how); + + while (parent) { + while (next) { + nextSibling = next.nextSibling; + clonedChild = _traverseNode(next, isFullySelected, TRUE, how); + + if (how != DELETE) + clonedParent.appendChild(clonedChild); + + isFullySelected = TRUE; + next = nextSibling; + } + + if (parent == root) + return clonedParent; + + next = parent.nextSibling; + parent = parent.parentNode; + + clonedGrandParent = _traverseNode(parent, FALSE, TRUE, how); -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) ); -} + if (how != DELETE) + clonedGrandParent.appendChild(clonedParent); -var makeArray = function(array, results) { - array = Array.prototype.slice.call( array, 0 ); + clonedParent = clonedGrandParent; + } + }; - if ( results ) { - results.push.apply( results, array ); - return results; - } - - return array; -}; + function _traverseNode(n, isFullySelected, isLeft, how) { + var txtValue, newNodeValue, oldNodeValue, offset, newNode; -// Perform a simple check to determine if the browser is capable of -// converting a NodeList to an array using builtin methods. -// Also verifies that the returned array holds DOM nodes -// (which is not the case in the Blackberry browser) -try { - Array.prototype.slice.call( document.documentElement.childNodes, 0 )[0].nodeType; + if (isFullySelected) + return _traverseFullySelected(n, how); -// Provide a fallback method if it does not work -} catch(e){ - makeArray = function(array, results) { - var ret = results || [], i = 0; + if (n.nodeType == 3 /* TEXT_NODE */) { + txtValue = n.nodeValue; - 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] ); + 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); } - } - } - return ret; - }; -} + if (how != CLONE) + n.nodeValue = oldNodeValue; -var sortOrder; + if (how == DELETE) + return; -if ( document.documentElement.compareDocumentPosition ) { - sortOrder = function( a, b ) { - if ( !a.compareDocumentPosition || !b.compareDocumentPosition ) { - if ( a == b ) { - hasDuplicate = true; - } - return a.compareDocumentPosition ? -1 : 1; - } + newNode = dom.clone(n, FALSE); + newNode.nodeValue = newNodeValue; - var ret = a.compareDocumentPosition(b) & 4 ? -1 : a === b ? 0 : 1; - if ( ret === 0 ) { - hasDuplicate = true; - } - return ret; - }; -} else if ( "sourceIndex" in document.documentElement ) { - sortOrder = function( a, b ) { - if ( !a.sourceIndex || !b.sourceIndex ) { - if ( a == b ) { - hasDuplicate = true; + return newNode; } - return a.sourceIndex ? -1 : 1; - } - var ret = a.sourceIndex - b.sourceIndex; - if ( ret === 0 ) { - hasDuplicate = true; - } - 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; - } + if (how == DELETE) + return; - 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; - } - return ret; - }; -} + return dom.clone(n, FALSE); + }; -// Utility function for retreiving the text value of an array of DOM nodes -Sizzle.getText = function( elems ) { - var ret = "", elem; + function _traverseFullySelected(n, how) { + if (how != DELETE) + return how == CLONE ? dom.clone(n, TRUE) : n; - for ( var i = 0; elems[i]; i++ ) { - elem = elems[i]; + n.parentNode.removeChild(n); + }; + }; - // Get the text from text nodes and CDATA nodes - if ( elem.nodeType === 3 || elem.nodeType === 4 ) { - ret += elem.nodeValue; + ns.Range = Range; +})(tinymce.dom); - // Traverse everything else, except comment nodes - } else if ( elem.nodeType !== 8 ) { - ret += Sizzle.getText( elem.childNodes ); - } - } +(function() { + function Selection(selection) { + var self = this, dom = selection.dom, TRUE = true, FALSE = false; - return ret; -}; + function getPosition(rng, start) { + var checkRng, startIndex = 0, endIndex, inside, + children, child, offset, index, position = -1, parent; -// 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(); - form.innerHTML = ""; + // Setup test range, collapse it and get the parent + checkRng = rng.duplicate(); + checkRng.collapse(start); + parent = checkRng.parentElement(); - // Inject it into the root element, check its status, and remove it quickly - var root = document.documentElement; - root.insertBefore( form, root.firstChild ); + // Check if the selection is within the right document + if (parent.ownerDocument !== selection.dom.doc) + return; - // 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){ - 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 : []; + // IE will report non editable elements as it's parent so look for an editable one + while (parent.contentEditable === "false") { + parent = parent.parentNode; } - }; - Expr.filter.ID = function(elem, match){ - var node = typeof elem.getAttributeNode !== "undefined" && elem.getAttributeNode("id"); - return elem.nodeType === 1 && node && node.nodeValue === match; - }; - } + // If parent doesn't have any children then return that we are inside the element + if (!parent.hasChildNodes()) { + return {node : parent, inside : 1}; + } - root.removeChild( form ); - root = form = null; // release memory in IE -})(); + // Setup node list and endIndex + children = parent.children; + endIndex = children.length - 1; -(function(){ - // Check to see if the browser returns only elements - // when doing getElementsByTagName("*") + // Perform a binary search for the position + while (startIndex <= endIndex) { + index = Math.floor((startIndex + endIndex) / 2); - // Create a fake element - var div = document.createElement("div"); - div.appendChild( document.createComment("") ); + // Move selection to node and compare the ranges + child = children[index]; + checkRng.moveToElementText(child); + position = checkRng.compareEndPoints(start ? 'StartToStart' : 'EndToEnd', rng); - // Make sure no comments are found - if ( div.getElementsByTagName("*").length > 0 ) { - Expr.find.TAG = function(match, context){ - var results = context.getElementsByTagName(match[1]); + // Before/after or an exact match + if (position > 0) { + endIndex = index - 1; + } else if (position < 0) { + startIndex = index + 1; + } else { + return {node : child}; + } + } - // Filter out possible comments - if ( match[1] === "*" ) { - var tmp = []; + // 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); - for ( var i = 0; results[i]; i++ ) { - if ( results[i].nodeType === 1 ) { - tmp.push( results[i] ); + // 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; } + + offset++; } + } else { + // Child position is after the selection endpoint + checkRng.collapse(true); - results = tmp; + // 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; + } + + offset++; + } } - return results; + return {node : child, position : position, offset : offset, inside : inside}; }; - } - // 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); - }; - } + // 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; - div = null; // release memory in IE -})(); + // 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; -if ( document.querySelectorAll ) { - (function(){ - var oldSizzle = Sizzle, div = document.createElement("div"); - div.innerHTML = "

    "; + collapsed = selection.isCollapsed(); - // Safari can't handle uppercase or unicode characters when - // in quirks mode. - if ( div.querySelectorAll && div.querySelectorAll(".TEST").length === 0 ) { - return; - } - - Sizzle = function(query, context, extra, seed){ - context = context || document; + // Handle control selection + if (ieRange.item) { + domRange.setStart(element.parentNode, dom.nodeIndex(element)); + domRange.setEnd(domRange.startContainer, domRange.startOffset + 1); - // 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){} + return domRange; } - - return oldSizzle(query, context, extra, seed); - }; - for ( var prop in oldSizzle ) { - Sizzle[ prop ] = oldSizzle[ prop ]; - } + function findEndPoint(start) { + var endPoint = getPosition(ieRange, start), container, offset, textNodeOffset = 0, sibling, undef, nodeValue; + + container = endPoint.node; + offset = endPoint.offset; + + if (endPoint.inside && !container.hasChildNodes()) { + domRange[start ? 'setStart' : 'setEnd'](container, 0); + return; + } + + if (offset === undef) { + domRange[start ? 'setStartBefore' : 'setEndAfter'](container); + return; + } + + if (endPoint.position < 0) { + sibling = endPoint.inside ? container.firstChild : container.nextSibling; + + 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); - div = null; // release memory in IE - })(); -} + return; + } -(function(){ - var div = document.createElement("div"); + // Find the text node and offset + while (sibling) { + nodeValue = sibling.nodeValue; + textNodeOffset += nodeValue.length; - div.innerHTML = "
    "; + // We are at or passed the position we where looking for + if (textNodeOffset >= offset) { + container = sibling; + textNodeOffset -= offset; + textNodeOffset = nodeValue.length - textNodeOffset; + break; + } - // Opera can't find a second classname (in 9.6) - // Also, make sure that getElementsByClassName actually exists - if ( !div.getElementsByClassName || div.getElementsByClassName("e").length === 0 ) { - return; - } + sibling = sibling.nextSibling; + } + } else { + // Find the text node and offset + sibling = container.previousSibling; - // Safari caches class attributes, doesn't catch changes (in 3.2) - div.lastChild.className = "e"; + if (!sibling) + return domRange[start ? 'setStartBefore' : 'setEndBefore'](container); - 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]); - } - }; + // 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); - div = null; // release memory in IE -})(); + return; + } -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 (sibling) { + textNodeOffset += sibling.nodeValue.length; - while ( elem ) { - if ( elem.sizcache === doneName ) { - match = checkSet[elem.sizset]; - break; - } + // We are at or passed the position we where looking for + if (textNodeOffset >= offset) { + container = sibling; + textNodeOffset -= offset; + break; + } - if ( elem.nodeType === 1 && !isXML ){ - elem.sizcache = doneName; - elem.sizset = i; + sibling = sibling.previousSibling; + } } - if ( elem.nodeName.toLowerCase() === cur ) { - match = elem; - break; - } + domRange[start ? 'setStart' : 'setEnd'](container, textNodeOffset); + }; - elem = elem[dir]; + 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 } - checkSet[i] = match; - } - } -} + return domRange; + }; -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; + this.getBookmark = function(type) { + var rng = selection.getRng(), start, end, bookmark = {}; - while ( elem ) { - if ( elem.sizcache === doneName ) { - match = checkSet[elem.sizset]; - break; - } + function getIndexes(node) { + var node, parent, root, children, i, indexes = []; - if ( elem.nodeType === 1 ) { - if ( !isXML ) { - elem.sizcache = doneName; - elem.sizset = i; - } - if ( typeof cur !== "string" ) { - if ( elem === cur ) { - match = true; + 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; } - - } else if ( Sizzle.filter( cur, [elem] ).length > 0 ) { - match = elem; - break; } + + node = parent; + parent = parent.parentNode; } - elem = elem[dir]; + 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))}; } - checkSet[i] = match; - } - } -} + return bookmark; + }; -Sizzle.contains = document.compareDocumentPosition ? function(a, b){ - return !!(a.compareDocumentPosition(b) & 16); -} : function(a, b){ - return a !== b && (a.contains ? a.contains(b) : true); -}; + this.moveToBookmark = function(bookmark) { + var rng, body = dom.doc.body; -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; -}; + function resolveIndexes(indexes) { + var node, i, idx, children; -var posProcess = function(selector, context){ - var tmpSet = [], later = "", match, - root = context.nodeType ? [context] : context; + node = dom.getRoot(); + for (i = indexes.length - 1; i >= 0; i--) { + children = node.children; + idx = indexes[i]; - // 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, "" ); - } + if (idx <= children.length - 1) { + node = children[idx]; + } + } - selector = Expr.relative[selector] ? selector + "*" : selector; + return node; + }; + + function setBookmarkEndPoint(start) { + var endPoint = bookmark[start ? 'start' : 'end'], moveLeft, moveRng, undef; - for ( var i = 0, l = root.length; i < l; i++ ) { - Sizzle( selector, root[i], tmpSet ); - } + if (endPoint) { + moveLeft = endPoint.position > 0; - return Sizzle.filter( later, tmpSet ); -}; + moveRng = body.createTextRange(); + moveRng.moveToElementText(resolveIndexes(endPoint.indexes)); -// EXPOSE + offset = endPoint.offset; + if (offset !== undef) { + moveRng.collapse(endPoint.inside || moveLeft); + moveRng.moveStart('character', moveLeft ? -offset : offset); + } else + moveRng.collapse(start); -window.tinymce.dom.Sizzle = Sizzle; + 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(); + } + } + }; -(function(tinymce) { - // Shorten names - var each = tinymce.each, DOM = tinymce.DOM, isIE = tinymce.isIE, isWebKit = tinymce.isWebKit, Event; + this.addRange = function(rng) { + var ieRng, ctrlRng, startContainer, startOffset, endContainer, endOffset, doc = selection.dom.doc, body = doc.body; - tinymce.create('tinymce.dom.EventUtils', { - EventUtils : function() { - this.inits = []; - this.events = []; - }, + function setEndPoint(start) { + var container, offset, marker, tmpRng, nodes; - add : function(o, n, f, s) { - var cb, t = this, el = t.events, r; + marker = dom.create('a'); + container = start ? startContainer : endContainer; + offset = start ? startOffset : endOffset; + tmpRng = ieRng.duplicate(); - if (n instanceof Array) { - r = []; + if (container == doc || container == doc.documentElement) { + container = body; + offset = 0; + } - each(n, function(n) { - r.push(t.add(o, n, f, s)); - }); + 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; - return r; + 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); + } } - // Handle array - if (o && o.hasOwnProperty && o instanceof Array) { - r = []; + // Setup some shorter versions + startContainer = rng.startContainer; + startOffset = rng.startOffset; + endContainer = rng.endContainer; + endOffset = rng.endOffset; + ieRng = body.createTextRange(); - each(o, function(o) { - o = DOM.get(o); - r.push(t.add(o, n, f, s)); - }); + // 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 (!startContainer.hasChildNodes()) { + startContainer.innerHTML = '\uFEFF\uFEFF'; + ieRng.moveToElementText(startContainer.lastChild); + ieRng.select(); + dom.doc.selection.clear(); + startContainer.innerHTML = ''; + return; + } - return r; + if (startOffset == endOffset - 1) { + try { + ctrlRng = body.createControlRange(); + ctrlRng.addElement(startContainer.childNodes[startOffset]); + ctrlRng.select(); + return; + } catch (ex) { + // Ignore + } + } } - o = DOM.get(o); + // Set start/end point of selection + setEndPoint(true); + setEndPoint(); - if (!o) - return; + // Select the new range and scroll it into view + ieRng.select(); + }; - // Setup event callback - cb = function(e) { - // Is all events disabled - if (t.disabled) - return; + // Expose range method + this.getRangeAt = getRange; + }; - e = e || window.event; + // Expose the selection object + tinymce.dom.TridentSelection = Selection; +})(); - // 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); - } +/* + * 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(){ - if (!s) - return f(e); +var chunker = /((?:\((?:\([^()]+\)|[^()]+)+\)|\[(?:\[[^\[\]]*\]|['"][^'"]*['"]|[^\[\]'"]+)+\]|\\.|[^ >+~,(\[\\]+)+|[>+~])(\s*,\s*)?((?:.|\r|\n)*)/g, + done = 0, + toString = Object.prototype.toString, + hasDuplicate = false, + baseHasDuplicate = true; - return f.call(s, e); - }; +// 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; +}); - if (n == 'unload') { - tinymce.unloads.unshift({func : cb}); - return cb; - } +var Sizzle = function(selector, context, results, seed) { + results = results || []; + context = context || document; - if (n == 'init') { - if (t.domLoaded) - cb(); - else - t.inits.push(cb); + var origContext = context; - return cb; - } + if ( context.nodeType !== 1 && context.nodeType !== 9 ) { + return []; + } + + if ( !selector || typeof selector !== "string" ) { + return results; + } - // Store away listener reference - el.push({ - obj : o, - name : n, - func : f, - cfunc : cb, - scope : s - }); + 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); - t._add(o, n, cb); + if ( m ) { + soFar = m[3]; + + parts.push( m[1] ); + + if ( m[2] ) { + extra = m[3]; + break; + } + } + } while ( m ); - return f; - }, + 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 ); - remove : function(o, n, f) { - var t = this, a = t.events, s = false, r; + while ( parts.length ) { + selector = parts.shift(); - // Handle array - if (o && o.hasOwnProperty && o instanceof Array) { - r = []; + if ( Expr.relative[ selector ] ) { + selector += parts.shift(); + } + + set = posProcess( selector, set ); + } + } + } 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]; + } - each(o, function(o) { - o = DOM.get(o); - r.push(t.remove(o, n, f)); - }); + 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; - return r; + if ( parts.length > 0 ) { + checkSet = makeArray(set); + } else { + prune = false; } - o = DOM.get(o); + while ( parts.length ) { + cur = parts.pop(); + pop = cur; - 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; + if ( !Expr.relative[ cur ] ) { + cur = ""; + } else { + pop = parts.pop(); } - }); - return s; - }, + if ( pop == null ) { + pop = context; + } - clear : function(o) { - var t = this, a = t.events, i, e; + Expr.relative[ cur ]( checkSet, pop, contextXML ); + } + } else { + checkSet = parts = []; + } + } - if (o) { - o = DOM.get(o); + if ( !checkSet ) { + checkSet = set; + } - for (i = a.length - 1; i >= 0; i--) { - e = a[i]; + if ( !checkSet ) { + Sizzle.error( cur || selector ); + } - if (e.obj === o) { - t._remove(e.obj, e.name, e.cfunc); - e.obj = e.cfunc = null; - a.splice(i, 1); - } + 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] ); } } - }, - - cancel : function(e) { - if (!e) - return false; - - this.stop(e); - - return this.prevent(e); - }, - - stop : function(e) { - if (e.stopPropagation) - e.stopPropagation(); - else - e.cancelBubble = true; - - return false; - }, - - prevent : function(e) { - if (e.preventDefault) - e.preventDefault(); - else - e.returnValue = false; - - return false; - }, - - destroy : function() { - var t = this; - - each(t.events, function(e, i) { - t._remove(e.obj, e.name, e.cfunc); - e.obj = e.cfunc = null; - }); - - t.events = []; - t = null; - }, - - _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; - }, - - _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 { + for ( i = 0; checkSet[i] != null; i++ ) { + if ( checkSet[i] && checkSet[i].nodeType === 1 ) { + results.push( set[i] ); } } - }, - - _pageInit : function(win) { - var t = this; - - // Keep it from running more than once - if (t.domLoaded) - return; - - t.domLoaded = true; + } + } else { + makeArray( checkSet, results ); + } - each(t.inits, function(c) { - c(); - }); + if ( extra ) { + Sizzle( extra, origContext, results, seed ); + Sizzle.uniqueSort( results ); + } - t.inits = []; - }, + return results; +}; - _wait : function(win) { - var t = this, doc = win.document; +Sizzle.uniqueSort = function(results){ + if ( sortOrder ) { + hasDuplicate = baseHasDuplicate; + results.sort(sortOrder); - // No need since the document is already loaded - if (win.tinyMCE_GZ && tinyMCE_GZ.loaded) { - t.domLoaded = 1; - return; + if ( hasDuplicate ) { + for ( var i = 1; i < results.length; i++ ) { + if ( results[i] === results[i-1] ) { + results.splice(i--, 1); + } } + } + } - // Use IE method - if (doc.attachEvent) { - doc.attachEvent("onreadystatechange", function() { - if (doc.readyState === "complete") { - doc.detachEvent("onreadystatechange", arguments.callee); - t._pageInit(win); - } - }); - - if (doc.documentElement.doScroll && win == win.top) { - (function() { - if (t.domLoaded) - return; + return results; +}; - try { - // If IE is used, use the trick by Diego Perini - // http://javascript.nwbox.com/IEContentLoaded/ - doc.documentElement.doScroll("left"); - } catch (ex) { - setTimeout(arguments.callee, 0); - return; - } +Sizzle.matches = function(expr, set){ + return Sizzle(expr, null, null, set); +}; - t._pageInit(win); - })(); - } - } else if (doc.addEventListener) { - t._add(win, 'DOMContentLoaded', function() { - t._pageInit(win); - }); - } +Sizzle.find = function(expr, context, isXML){ + var set; - t._add(win, 'load', function() { - t._pageInit(win); - }); - }, + if ( !expr ) { + return []; + } - _stoppers : { - preventDefault : function() { - this.returnValue = 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); - stopPropagation : function() { - this.cancelBubble = true; + 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; + } } } - }); - - Event = tinymce.dom.Event = new tinymce.dom.EventUtils(); - - // 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) { - var t = this, dom, el; + if ( !set ) { + set = context.getElementsByTagName("*"); + } - t.settings = settings = settings || {}; - t.id = id; - t.dom = dom = settings.dom || tinymce.DOM; + return {set: set, expr: expr}; +}; - // Only IE leaks DOM references, this is a lot faster - if (!tinymce.isIE) - el = dom.get(t.id); +Sizzle.filter = function(expr, set, inplace, not){ + var old = expr, result = [], curLoop = set, match, anyFound, + isXMLFilter = set && set[0] && Sizzle.isXML(set[0]); - tinymce.each( - ('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; + 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; - for (i = 0; i < arguments.length; i++) - a.push(arguments[i]); + match.splice(1,1); - a = dom[k].apply(dom, a); - t.update(k); + if ( left.substr( left.length - 1 ) === "\\" ) { + continue; + } - return a; - }; - }); + if ( curLoop === result ) { + result = []; + } - tinymce.extend(t, { - on : function(n, f, s) { - return tinymce.dom.Event.add(t.id, n, f, s); - }, + if ( Expr.preFilter[ type ] ) { + match = Expr.preFilter[ type ]( match, curLoop, inplace, result, not, isXMLFilter ); - getXY : function() { - return { - x : parseInt(t.getStyle('left')), - y : parseInt(t.getStyle('top')) - }; - }, + if ( !match ) { + anyFound = found = true; + } else if ( match === true ) { + continue; + } + } - getSize : function() { - var n = dom.get(t.id); + if ( match ) { + for ( var i = 0; (item = curLoop[i]) != null; i++ ) { + if ( item ) { + found = filter( item, match, i, curLoop ); + var pass = not ^ !!found; - return { - w : parseInt(t.getStyle('width') || n.clientWidth), - h : parseInt(t.getStyle('height') || n.clientHeight) - }; - }, + if ( inplace && found != null ) { + if ( pass ) { + anyFound = true; + } else { + curLoop[i] = false; + } + } else if ( pass ) { + result.push( item ); + anyFound = true; + } + } + } + } - moveTo : function(x, y) { - t.setStyles({left : x, top : y}); - }, + if ( found !== undefined ) { + if ( !inplace ) { + curLoop = result; + } - moveBy : function(x, y) { - var p = t.getXY(); + expr = expr.replace( Expr.match[ type ], "" ); - t.moveTo(p.x + x, p.y + y); - }, + if ( !anyFound ) { + return []; + } - resizeTo : function(w, h) { - t.setStyles({width : w, height : h}); - }, + break; + } + } + } - resizeBy : function(w, h) { - var s = t.getSize(); + // Improper expression + if ( expr === old ) { + if ( anyFound == null ) { + Sizzle.error( expr ); + } else { + break; + } + } - t.resizeTo(s.w + w, s.h + h); - }, + old = expr; + } - update : function(k) { - var b; + return curLoop; +}; - if (tinymce.isIE6 && settings.blocker) { - k = k || ''; +Sizzle.error = function( msg ) { + throw "Syntax error, unrecognized expression: " + msg; +}; - // Ignore getters - if (k.indexOf('get') === 0 || k.indexOf('has') === 0 || k.indexOf('is') === 0) - return; +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; - // Remove blocker on remove - if (k == 'remove') { - dom.remove(t.blocker); - return; - } + if ( isTag ) { + part = part.toLowerCase(); + } - if (!t.blocker) { - t.blocker = dom.uniqueId(); - b = dom.add(settings.container || dom.getRoot(), 'iframe', {id : t.blocker, style : 'position:absolute;', frameBorder : 0, src : 'javascript:""'}); - dom.setStyle(b, 'opacity', 0); - } else - b = dom.get(t.blocker); + for ( var i = 0, l = checkSet.length, elem; i < l; i++ ) { + if ( (elem = checkSet[i]) ) { + while ( (elem = elem.previousSibling) && elem.nodeType !== 1 ) {} - dom.setStyles(b, { - left : t.getStyle('left', 1), - top : t.getStyle('top', 1), - width : t.getStyle('width', 1), - height : t.getStyle('height', 1), - display : t.getStyle('display', 1), - zIndex : parseInt(t.getStyle('zIndex', 1) || 0) - 1 - }); + checkSet[i] = isPartStrNotTag || elem && elem.nodeName.toLowerCase() === part ? + elem || false : + elem === part; } } - }); - }; -})(tinymce); - -(function(tinymce) { - function trimNl(s) { - return s.replace(/[\n\r]+/g, ''); - }; - // Shorten names - var is = tinymce.is, isIE = tinymce.isIE, each = tinymce.each; - - tinymce.create('tinymce.dom.Selection', { - Selection : function(dom, win, serializer) { - var t = this; - - t.dom = dom; - t.win = win; - t.serializer = serializer; + if ( isPartStrNotTag ) { + Sizzle.filter( part, checkSet, true ); + } + }, + ">": function(checkSet, part){ + var isPartStr = typeof part === "string", + elem, i = 0, l = checkSet.length; - // Add events - each([ - 'onBeforeSetContent', - 'onBeforeGetContent', - 'onSetContent', - 'onGetContent' - ], function(e) { - t[e] = new tinymce.util.Dispatcher(t); - }); + if ( isPartStr && !/\W/.test(part) ) { + part = part.toLowerCase(); - // No W3C Range support - if (!t.win.getSelection) - t.tridentSel = new tinymce.dom.TridentSelection(t); + 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 : + elem.parentNode === part; + } + } - // Prevent leaks - tinymce.addUnload(t.destroy, t); + if ( isPartStr ) { + Sizzle.filter( part, checkSet, true ); + } + } }, + "": function(checkSet, part, isXML){ + var doneName = done++, checkFn = dirCheck, nodeCheck; - getContent : function(s) { - var t = this, r = t.getRng(), e = t.dom.create("body"), se = t.getSel(), wb, wa, n; - - s = s || {}; - wb = wa = ''; - s.get = true; - s.format = s.format || 'html'; - t.onBeforeGetContent.dispatch(t, s); + if ( typeof part === "string" && !/\W/.test(part) ) { + part = part.toLowerCase(); + nodeCheck = part; + checkFn = dirNodeCheck; + } - if (s.format == 'text') - return t.isCollapsed() ? '' : (r.text || (se.toString ? se.toString() : '')); + checkFn("parentNode", part, doneName, checkSet, nodeCheck, isXML); + }, + "~": function(checkSet, part, isXML){ + var doneName = done++, checkFn = dirCheck, nodeCheck; - if (r.cloneContents) { - n = r.cloneContents(); + if ( typeof part === "string" && !/\W/.test(part) ) { + part = part.toLowerCase(); + nodeCheck = part; + checkFn = dirNodeCheck; + } - if (n) - e.appendChild(n); - } else if (is(r.item) || is(r.htmlText)) - e.innerHTML = r.item ? r.item(0).outerHTML : r.htmlText; - else - e.innerHTML = r.toString(); + checkFn("previousSibling", part, doneName, checkSet, nodeCheck, isXML); + } + }, + find: { + ID: function(match, context, isXML){ + if ( typeof context.getElementById !== "undefined" && !isXML ) { + var m = context.getElementById(match[1]); + return m ? [m] : []; + } + }, + NAME: function(match, context){ + if ( typeof context.getElementsByName !== "undefined" ) { + var ret = [], results = context.getElementsByName(match[1]); - // Keep whitespace before and after - if (/^\s/.test(e.innerHTML)) - wb = ' '; + for ( var i = 0, l = results.length; i < l; i++ ) { + if ( results[i].getAttribute("name") === match[1] ) { + ret.push( results[i] ); + } + } - if (/\s+$/.test(e.innerHTML)) - wa = ' '; + return ret.length === 0 ? null : ret; + } + }, + TAG: function(match, context){ + return context.getElementsByTagName(match[1]); + } + }, + preFilter: { + CLASS: function(match, curLoop, inplace, result, not, isXML){ + match = " " + match[1].replace(/\\/g, "") + " "; - s.getInner = true; + if ( isXML ) { + return match; + } - s.content = t.isCollapsed() ? '' : wb + t.serializer.serialize(e, s) + wa; - t.onGetContent.dispatch(t, s); + 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 ( !inplace ) { + result.push( elem ); + } + } else if ( inplace ) { + curLoop[i] = false; + } + } + } - return s.content; + return false; + }, + ID: function(match){ + return match[1].replace(/\\/g, ""); }, + TAG: function(match, curLoop){ + return match[1].toLowerCase(); + }, + CHILD: function(match){ + if ( match[1] === "nth" ) { + // parse equations like 'even', 'odd', '5', '2n', '3n+2', '4n-1', '-n+6' + var test = /(-?)(\d*)n((?:\+|-)?\d*)/.exec( + match[2] === "even" && "2n" || match[2] === "odd" && "2n+1" || + !/\D/.test( match[2] ) && "0n+" + match[2] || match[2]); - setContent : function(h, s) { - var t = this, r = t.getRng(), c, d = t.win.document; + // calculate the numbers (first)n+(last) including if they are negative + match[2] = (test[1] + (test[2] || 1)) - 0; + match[3] = test[3] - 0; + } - s = s || {format : 'html'}; - s.set = true; - h = s.content = t.dom.processHTML(h); + // TODO: Move to normal caching system + match[0] = done++; - // Dispatch before set content event - t.onBeforeSetContent.dispatch(t, s); - h = s.content; + return match; + }, + ATTR: function(match, curLoop, inplace, result, not, isXML){ + var name = match[1].replace(/\\/g, ""); + + if ( !isXML && Expr.attrMap[name] ) { + match[1] = Expr.attrMap[name]; + } - if (r.insertNode) { - // Make caret marker since insertNode places the caret in the beginning of text after insert - h += '_'; + if ( match[2] === "~=" ) { + match[4] = " " + match[4] + " "; + } - // Delete and insert new node - - if (r.startContainer == d && r.endContainer == d) { - // WebKit will fail if the body is empty since the range is then invalid and it can't insert contents - d.body.innerHTML = h; + return match; + }, + 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 { - r.deleteContents(); - if (d.body.childNodes.length == 0) { - d.body.innerHTML = h; - } else { - r.insertNode(r.createContextualFragment(h)); + var ret = Sizzle.filter(match[3], curLoop, inplace, true ^ not); + if ( !inplace ) { + result.push.apply( result, ret ); } + return false; } - - // Move to caret marker - c = t.dom.get('__caret'); - // Make sure we wrap it compleatly, Opera fails with a simple select call - r = d.createRange(); - r.setStartBefore(c); - r.setEndBefore(c); - t.setRng(r); - - // Remove the caret position - t.dom.remove('__caret'); - } else { - if (r.item) { - // Delete content and get caret text selection - d.execCommand('Delete', false, null); - r = t.getRng(); - } - - r.pasteHTML(h); + } else if ( Expr.match.POS.test( match[0] ) || Expr.match.CHILD.test( match[0] ) ) { + return true; } - - // Dispatch set content event - t.onSetContent.dispatch(t, s); + + return match; }, + POS: function(match){ + match.unshift( true ); + return match; + } + }, + filters: { + enabled: function(elem){ + return elem.disabled === false && elem.type !== "hidden"; + }, + disabled: function(elem){ + return elem.disabled === true; + }, + checked: function(elem){ + return elem.checked === true; + }, + selected: function(elem){ + // Accessing this property makes selected-by-default + // options in Safari work properly + elem.parentNode.selectedIndex; + return elem.selected === true; + }, + parent: function(elem){ + return !!elem.firstChild; + }, + empty: function(elem){ + return !elem.firstChild; + }, + has: function(elem, i, match){ + return !!Sizzle( match[3], elem ).length; + }, + header: function(elem){ + return (/h\d/i).test( elem.nodeName ); + }, + text: function(elem){ + return "text" === elem.type; + }, + radio: function(elem){ + return "radio" === elem.type; + }, + checkbox: function(elem){ + return "checkbox" === elem.type; + }, + file: function(elem){ + return "file" === elem.type; + }, + password: function(elem){ + return "password" === elem.type; + }, + submit: function(elem){ + return "submit" === elem.type; + }, + image: function(elem){ + return "image" === elem.type; + }, + reset: function(elem){ + return "reset" === elem.type; + }, + button: function(elem){ + return "button" === elem.type || elem.nodeName.toLowerCase() === "button"; + }, + input: function(elem){ + return (/input|select|textarea|button/i).test(elem.nodeName); + } + }, + setFilters: { + first: function(elem, i){ + return i === 0; + }, + last: function(elem, i, match, array){ + return i === array.length - 1; + }, + even: function(elem, i){ + return i % 2 === 0; + }, + odd: function(elem, i){ + return i % 2 === 1; + }, + lt: function(elem, i, match){ + return i < match[3] - 0; + }, + gt: function(elem, i, match){ + return i > match[3] - 0; + }, + nth: function(elem, i, match){ + return match[3] - 0 === i; + }, + 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 ]; - getStart : function() { - var rng = this.getRng(), startElement, parentElement, checkRng, node; - - if (rng.duplicate || rng.item) { - // Control selection, return first item - if (rng.item) - return rng.item(0); - - // Get start element - checkRng = rng.duplicate(); - checkRng.collapse(1); - startElement = checkRng.parentElement(); + if ( filter ) { + return filter( elem, i, match, array ); + } else if ( name === "contains" ) { + return (elem.textContent || elem.innerText || Sizzle.getText([ elem ]) || "").indexOf(match[3]) >= 0; + } else if ( name === "not" ) { + var not = match[3]; - // Check if range parent is inside the start element, then return the inner parent element - // This will fix issues when a single element is selected, IE would otherwise return the wrong start element - parentElement = node = rng.parentElement(); - while (node = node.parentNode) { - if (node == startElement) { - startElement = parentElement; - break; + for ( var j = 0, l = not.length; j < l; j++ ) { + if ( not[j] === elem ) { + return false; } } - // If start element is body element try to move to the first child if it exists - if (startElement && startElement.nodeName == 'BODY') - return startElement.firstChild || startElement; - - return startElement; + return true; } else { - startElement = rng.startContainer; - - if (startElement.nodeType == 1 && startElement.hasChildNodes()) - startElement = startElement.childNodes[Math.min(startElement.childNodes.length - 1, rng.startOffset)]; - - if (startElement && startElement.nodeType == 3) - return startElement.parentNode; - - return startElement; + Sizzle.error( "Syntax error, unrecognized expression: " + 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; + } + } + if ( type === "first" ) { + return true; + } + node = elem; + case 'last': + while ( (node = node.nextSibling) ) { + if ( node.nodeType === 1 ) { + return false; + } + } + return true; + case 'nth': + var first = match[2], last = match[3]; - getEnd : function() { - var t = this, r = t.getRng(), e, eo; - - if (r.duplicate || r.item) { - if (r.item) - return r.item(0); - - r = r.duplicate(); - r.collapse(0); - e = r.parentElement(); - - if (e && e.nodeName == 'BODY') - return e.lastChild || e; - - return e; - } else { - e = r.endContainer; - eo = r.endOffset; - - if (e.nodeType == 1 && e.hasChildNodes()) - e = e.childNodes[eo > 0 ? eo - 1 : eo]; - - if (e && e.nodeType == 3) - return e.parentNode; - - return e; + if ( first === 1 && last === 0 ) { + return true; + } + + var doneName = match[0], + parent = elem.parentNode; + + if ( parent && (parent.sizcache !== doneName || !elem.nodeIndex) ) { + var count = 0; + for ( node = parent.firstChild; node; node = node.nextSibling ) { + if ( node.nodeType === 1 ) { + node.nodeIndex = ++count; + } + } + parent.sizcache = doneName; + } + + var diff = elem.nodeIndex - last; + if ( first === 0 ) { + return diff === 0; + } else { + return ( diff % first === 0 && diff / first >= 0 ); + } } }, + 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; + }, + CLASS: function(elem, match){ + return (" " + (elem.className || elem.getAttribute("class")) + " ") + .indexOf( match ) > -1; + }, + ATTR: function(elem, match){ + var name = match[1], + result = Expr.attrHandle[ name ] ? + Expr.attrHandle[ name ]( elem ) : + elem[ name ] != null ? + elem[ name ] : + elem.getAttribute( name ), + value = result + "", + type = match[2], + check = match[4]; - getBookmark : function(type, normalized) { - var t = this, dom = t.dom, rng, rng2, id, collapsed, name, element, index, chr = '\uFEFF', styles; - - function findIndex(name, element) { - var index = 0; - - each(dom.select(name), function(node, i) { - if (node == element) - index = i; - }); + return result == null ? + type === "!=" : + type === "=" ? + value === check : + type === "*=" ? + value.indexOf(check) >= 0 : + type === "~=" ? + (" " + value + " ").indexOf(check) >= 0 : + !check ? + value && result !== false : + type === "!=" ? + value !== check : + type === "^=" ? + value.indexOf(check) === 0 : + type === "$=" ? + value.substr(value.length - check.length) === check : + type === "|=" ? + value === check || value.substr(0, check.length + 1) === check + "-" : + false; + }, + POS: function(elem, match, i, array){ + var name = match[2], filter = Expr.setFilters[ name ]; - return index; - }; + if ( filter ) { + return filter( elem, i, match, array ); + } + } + } +}; - if (type == 2) { - function getLocation() { - var rng = t.getRng(true), root = dom.getRoot(), bookmark = {}; +var origPOS = Expr.match.POS, + fescape = function(all, num){ + return "\\" + (num - 0 + 1); + }; - function getPoint(rng, start) { - var container = rng[start ? 'startContainer' : 'endContainer'], - offset = rng[start ? 'startOffset' : 'endOffset'], point = [], node, childNodes, after = 0; +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) ); +} - if (container.nodeType == 3) { - if (normalized) { - for (node = container.previousSibling; node && node.nodeType == 3; node = node.previousSibling) - offset += node.nodeValue.length; - } +var makeArray = function(array, results) { + array = Array.prototype.slice.call( array, 0 ); - point.push(offset); - } else { - childNodes = container.childNodes; + if ( results ) { + results.push.apply( results, array ); + return results; + } + + return array; +}; - if (offset >= childNodes.length && childNodes.length) { - after = 1; - offset = Math.max(0, childNodes.length - 1); - } +// Perform a simple check to determine if the browser is capable of +// converting a NodeList to an array using builtin methods. +// Also verifies that the returned array holds DOM nodes +// (which is not the case in the Blackberry browser) +try { + Array.prototype.slice.call( document.documentElement.childNodes, 0 )[0].nodeType; - point.push(t.dom.nodeIndex(childNodes[offset], normalized) + after); - } +// Provide a fallback method if it does not work +} catch(e){ + makeArray = function(array, results) { + var ret = results || [], i = 0; - for (; container && container != root; container = container.parentNode) - point.push(t.dom.nodeIndex(container, normalized)); + 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] ); + } + } + } - return point; - }; + return ret; + }; +} - bookmark.start = getPoint(rng, true); +var sortOrder; - if (!t.isCollapsed()) - bookmark.end = getPoint(rng); +if ( document.documentElement.compareDocumentPosition ) { + sortOrder = function( a, b ) { + if ( !a.compareDocumentPosition || !b.compareDocumentPosition ) { + if ( a == b ) { + hasDuplicate = true; + } + return a.compareDocumentPosition ? -1 : 1; + } - return bookmark; - }; + var ret = a.compareDocumentPosition(b) & 4 ? -1 : a === b ? 0 : 1; + if ( ret === 0 ) { + hasDuplicate = true; + } + return ret; + }; +} else if ( "sourceIndex" in document.documentElement ) { + sortOrder = function( a, b ) { + if ( !a.sourceIndex || !b.sourceIndex ) { + if ( a == b ) { + hasDuplicate = true; + } + return a.sourceIndex ? -1 : 1; + } - return getLocation(); + var ret = a.sourceIndex - b.sourceIndex; + if ( ret === 0 ) { + hasDuplicate = true; + } + 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; + } - // Handle simple range - if (type) - return {rng : t.getRng()}; + 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; + } + return ret; + }; +} - rng = t.getRng(); - id = dom.uniqueId(); - collapsed = tinyMCE.activeEditor.selection.isCollapsed(); - styles = 'overflow:hidden;line-height:0px'; +// Utility function for retreiving the text value of an array of DOM nodes +Sizzle.getText = function( elems ) { + var ret = "", elem; - // Explorer method - if (rng.duplicate || rng.item) { - // Text selection - if (!rng.item) { - rng2 = rng.duplicate(); + for ( var i = 0; elems[i]; i++ ) { + elem = elems[i]; - // Insert start marker - rng.collapse(); - rng.pasteHTML('' + chr + ''); + // Get the text from text nodes and CDATA nodes + if ( elem.nodeType === 3 || elem.nodeType === 4 ) { + ret += elem.nodeValue; - // Insert end marker - if (!collapsed) { - rng2.collapse(false); - rng2.pasteHTML('' + chr + ''); - } - } else { - // Control selection - element = rng.item(0); - name = element.nodeName; + // Traverse everything else, except comment nodes + } else if ( elem.nodeType !== 8 ) { + ret += Sizzle.getText( elem.childNodes ); + } + } - return {name : name, index : findIndex(name, element)}; - } - } else { - element = t.getNode(); - name = element.nodeName; - if (name == 'IMG') - return {name : name, index : findIndex(name, element)}; + return ret; +}; - // W3C method - rng2 = rng.cloneRange(); +// 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(); + form.innerHTML = ""; - // Insert end marker - if (!collapsed) { - rng2.collapse(false); - rng2.insertNode(dom.create('span', {_mce_type : "bookmark", id : id + '_end', style : styles}, chr)); - } + // Inject it into the root element, check its status, and remove it quickly + var root = document.documentElement; + root.insertBefore( form, root.firstChild ); - rng.collapse(true); - rng.insertNode(dom.create('span', {_mce_type : "bookmark", id : id + '_start', style : styles}, chr)); + // 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){ + 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 : []; } + }; - t.moveToBookmark({id : id, keep : 1}); + Expr.filter.ID = function(elem, match){ + var node = typeof elem.getAttributeNode !== "undefined" && elem.getAttributeNode("id"); + return elem.nodeType === 1 && node && node.nodeValue === match; + }; + } - return {id : id}; - }, + root.removeChild( form ); + root = form = null; // release memory in IE +})(); - moveToBookmark : function(bookmark) { - var t = this, dom = t.dom, marker1, marker2, rng, root, startContainer, endContainer, startOffset, endOffset; +(function(){ + // Check to see if the browser returns only elements + // when doing getElementsByTagName("*") - // Clear selection cache - if (t.tridentSel) - t.tridentSel.destroy(); + // Create a fake element + var div = document.createElement("div"); + div.appendChild( document.createComment("") ); - if (bookmark) { - if (bookmark.start) { - rng = dom.createRng(); - root = dom.getRoot(); + // Make sure no comments are found + if ( div.getElementsByTagName("*").length > 0 ) { + Expr.find.TAG = function(match, context){ + var results = context.getElementsByTagName(match[1]); - function setEndPoint(start) { - var point = bookmark[start ? 'start' : 'end'], i, node, offset, children; + // Filter out possible comments + if ( match[1] === "*" ) { + var tmp = []; - if (point) { - // Find container node - for (node = root, i = point.length - 1; i >= 1; i--) { - children = node.childNodes; + for ( var i = 0; results[i]; i++ ) { + if ( results[i].nodeType === 1 ) { + tmp.push( results[i] ); + } + } - if (children.length) - node = children[point[i]]; - } + results = tmp; + } - // Set offset within container node - if (start) - rng.setStart(node, point[0]); - else - rng.setEnd(node, point[0]); - } - }; + return results; + }; + } - setEndPoint(true); - setEndPoint(); + // 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); + }; + } - t.setRng(rng); - } else if (bookmark.id) { - function restoreEndPoint(suffix) { - var marker = dom.get(bookmark.id + '_' + suffix), node, idx, next, prev, keep = bookmark.keep; + div = null; // release memory in IE +})(); - if (marker) { - node = marker.parentNode; +if ( document.querySelectorAll ) { + (function(){ + var oldSizzle = Sizzle, div = document.createElement("div"); + div.innerHTML = "

    "; - if (suffix == 'start') { - if (!keep) { - idx = dom.nodeIndex(marker); - } else { - node = marker.firstChild; - idx = 1; - } + // Safari can't handle uppercase or unicode characters when + // in quirks mode. + if ( div.querySelectorAll && div.querySelectorAll(".TEST").length === 0 ) { + return; + } + + Sizzle = function(query, context, extra, seed){ + context = context || document; - startContainer = endContainer = node; - startOffset = endOffset = idx; - } else { - if (!keep) { - idx = dom.nodeIndex(marker); - } else { - node = marker.firstChild; - idx = 1; - } + // 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){} + } + + return oldSizzle(query, context, extra, seed); + }; - endContainer = node; - endOffset = idx; - } + for ( var prop in oldSizzle ) { + Sizzle[ prop ] = oldSizzle[ prop ]; + } - if (!keep) { - prev = marker.previousSibling; - next = marker.nextSibling; + div = null; // release memory in IE + })(); +} - // Remove all marker text nodes - each(tinymce.grep(marker.childNodes), function(node) { - if (node.nodeType == 3) - node.nodeValue = node.nodeValue.replace(/\uFEFF/g, ''); - }); +(function(){ + var div = document.createElement("div"); - // 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); + div.innerHTML = "
    "; - // If siblings are text nodes then merge them - if (prev && next && prev.nodeType == next.nodeType && prev.nodeType == 3) { - idx = prev.nodeValue.length; - prev.appendData(next.nodeValue); - dom.remove(next); + // Opera can't find a second classname (in 9.6) + // Also, make sure that getElementsByClassName actually exists + if ( !div.getElementsByClassName || div.getElementsByClassName("e").length === 0 ) { + return; + } - if (suffix == 'start') { - startContainer = endContainer = prev; - startOffset = endOffset = idx; - } else { - endContainer = prev; - endOffset = idx; - } - } - } - } - }; + // Safari caches class attributes, doesn't catch changes (in 3.2) + div.lastChild.className = "e"; - function addBogus(node) { - // Adds a bogus BR element for empty block elements - // on non IE browsers just to have a place to put the caret - if (!isIE && dom.isBlock(node) && !node.innerHTML) - node.innerHTML = '
    '; + 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]); + } + }; - return node; - }; + div = null; // release memory in IE +})(); - // Restore start/end points - restoreEndPoint('start'); - restoreEndPoint('end'); +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; - rng = dom.createRng(); - rng.setStart(addBogus(startContainer), startOffset); - rng.setEnd(addBogus(endContainer), endOffset); - t.setRng(rng); - } else if (bookmark.name) { - t.select(dom.select(bookmark.name)[bookmark.index]); - } else if (bookmark.rng) - t.setRng(bookmark.rng); - } - }, + while ( elem ) { + if ( elem.sizcache === doneName ) { + match = checkSet[elem.sizset]; + break; + } - select : function(node, content) { - var t = this, dom = t.dom, rng = dom.createRng(), idx; + if ( elem.nodeType === 1 && !isXML ){ + elem.sizcache = doneName; + elem.sizset = i; + } - idx = dom.nodeIndex(node); - rng.setStart(node.parentNode, idx); - rng.setEnd(node.parentNode, idx + 1); + if ( elem.nodeName.toLowerCase() === cur ) { + match = elem; + break; + } - // Find first/last text node or BR element - if (content) { - function setPoint(node, start) { - var walker = new tinymce.dom.TreeWalker(node, node); + elem = elem[dir]; + } - 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); + checkSet[i] = match; + } + } +} - return; - } +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; - // BR element - if (node.nodeName == 'BR') { - if (start) - rng.setStartBefore(node); - else - rng.setEndBefore(node); + while ( elem ) { + if ( elem.sizcache === doneName ) { + match = checkSet[elem.sizset]; + break; + } - return; + if ( elem.nodeType === 1 ) { + if ( !isXML ) { + elem.sizcache = doneName; + elem.sizset = i; + } + if ( typeof cur !== "string" ) { + if ( elem === cur ) { + match = true; + break; } - } while (node = (start ? walker.next() : walker.prev())); - }; - setPoint(node, 1); - setPoint(node); + } else if ( Sizzle.filter( cur, [elem] ).length > 0 ) { + match = elem; + break; + } + } + + elem = elem[dir]; } - t.setRng(rng); + checkSet[i] = match; + } + } +} - return node; - }, +Sizzle.contains = document.compareDocumentPosition ? function(a, b){ + return !!(a.compareDocumentPosition(b) & 16); +} : function(a, b){ + return a !== b && (a.contains ? a.contains(b) : true); +}; - isCollapsed : function() { - var t = this, r = t.getRng(), s = t.getSel(); +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; +}; - if (!r || r.item) - return false; +var posProcess = function(selector, context){ + var tmpSet = [], later = "", match, + root = context.nodeType ? [context] : context; - if (r.compareEndPoints) - return r.compareEndPoints('StartToEnd', r) === 0; + // 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, "" ); + } - return !s || r.collapsed; - }, + selector = Expr.relative[selector] ? selector + "*" : selector; - collapse : function(b) { - var t = this, r = t.getRng(), n; + for ( var i = 0, l = root.length; i < l; i++ ) { + Sizzle( selector, root[i], tmpSet ); + } - // Control range on IE - if (r.item) { - n = r.item(0); - r = this.win.document.body.createTextRange(); - r.moveToElementText(n); - } + return Sizzle.filter( later, tmpSet ); +}; - r.collapse(!!b); - t.setRng(r); - }, +// EXPOSE - getSel : function() { - var t = this, w = this.win; +window.tinymce.dom.Sizzle = Sizzle; - return w.getSelection ? w.getSelection() : w.document.selection; - }, +})(); - getRng : function(w3c) { - var t = this, s, r; - // Found tridentSel object then we need to use that one - if (w3c && t.tridentSel) - return t.tridentSel.getRangeAt(0); +(function(tinymce) { + tinymce.dom.Element = function(id, settings) { + var t = this, dom, el; - try { - if (s = t.getSel()) - r = s.rangeCount > 0 ? s.getRangeAt(0) : (s.createRange ? s.createRange() : t.win.document.createRange()); - } catch (ex) { - // IE throws unspecified error here if TinyMCE is placed in a frame/iframe - } + t.settings = settings = settings || {}; + t.id = id; + t.dom = dom = settings.dom || tinymce.DOM; - // 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 = t.win.document.createRange ? t.win.document.createRange() : t.win.document.body.createTextRange(); + // Only IE leaks DOM references, this is a lot faster + if (!tinymce.isIE) + el = dom.get(t.id); - if (t.selectedRange && t.explicitRange) { - if (r.compareBoundaryPoints(r.START_TO_START, t.selectedRange) === 0 && r.compareBoundaryPoints(r.END_TO_END, t.selectedRange) === 0) { - // Safari, Opera and Chrome only ever select text which causes the range to change. - // This lets us use the originally set range if the selection hasn't been changed by the user. - r = t.explicitRange; - } else { - t.selectedRange = null; - t.explicitRange = null; - } - } - return r; - }, + tinymce.each( + ('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; - setRng : function(r) { - var s, t = this; - - if (!t.tridentSel) { - s = t.getSel(); + for (i = 0; i < arguments.length; i++) + a.push(arguments[i]); - if (s) { - t.explicitRange = r; - s.removeAllRanges(); - s.addRange(r); - t.selectedRange = s.getRangeAt(0); - } - } else { - // Is W3C Range - if (r.cloneRange) { - t.tridentSel.addRange(r); - return; - } + a = dom[k].apply(dom, a); + t.update(k); - // Is IE specific range - try { - r.select(); - } catch (ex) { - // Needed for some odd IE bug #1843306 - } - } - }, + return a; + }; + }); - setNode : function(n) { - var t = this; + tinymce.extend(t, { + on : function(n, f, s) { + return tinymce.dom.Event.add(t.id, n, f, s); + }, - t.setContent(t.dom.getOuterHTML(n)); + getXY : function() { + return { + x : parseInt(t.getStyle('left')), + y : parseInt(t.getStyle('top')) + }; + }, - return n; - }, + getSize : function() { + var n = dom.get(t.id); - getNode : function() { - var t = this, rng = t.getRng(), sel = t.getSel(), elm; + return { + w : parseInt(t.getStyle('width') || n.clientWidth), + h : parseInt(t.getStyle('height') || n.clientHeight) + }; + }, - if (rng.setStart) { - // Range maybe lost after the editor is made visible again - if (!rng) - return t.dom.getRoot(); + moveTo : function(x, y) { + t.setStyles({left : x, top : y}); + }, - elm = rng.commonAncestorContainer; + moveBy : function(x, y) { + var p = t.getXY(); - // Handle selection a image or other control like element such as anchors - if (!rng.collapsed) { - if (rng.startContainer == rng.endContainer) { - if (rng.startOffset - rng.endOffset < 2) { - if (rng.startContainer.hasChildNodes()) - elm = rng.startContainer.childNodes[rng.startOffset]; - } - } + t.moveTo(p.x + x, p.y + y); + }, - // 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) - return sel.anchorNode.childNodes[sel.anchorOffset]; - } + resizeTo : function(w, h) { + t.setStyles({width : w, height : h}); + }, - if (elm && elm.nodeType == 3) - return elm.parentNode; + resizeBy : function(w, h) { + var s = t.getSize(); - return elm; - } + t.resizeTo(s.w + w, s.h + h); + }, - return rng.item ? rng.item(0) : rng.parentElement(); - }, + update : function(k) { + var b; - getSelectedBlocks : function(st, en) { - var t = this, dom = t.dom, sb, eb, n, bl = []; + if (tinymce.isIE6 && settings.blocker) { + k = k || ''; - sb = dom.getParent(st || t.getStart(), dom.isBlock); - eb = dom.getParent(en || t.getEnd(), dom.isBlock); + // Ignore getters + if (k.indexOf('get') === 0 || k.indexOf('has') === 0 || k.indexOf('is') === 0) + return; - if (sb) - bl.push(sb); + // Remove blocker on remove + if (k == 'remove') { + dom.remove(t.blocker); + return; + } - if (sb && eb && sb != eb) { - n = sb; + if (!t.blocker) { + t.blocker = dom.uniqueId(); + b = dom.add(settings.container || dom.getRoot(), 'iframe', {id : t.blocker, style : 'position:absolute;', frameBorder : 0, src : 'javascript:""'}); + dom.setStyle(b, 'opacity', 0); + } else + b = dom.get(t.blocker); - while ((n = n.nextSibling) && n != eb) { - if (dom.isBlock(n)) - bl.push(n); + dom.setStyles(b, { + left : t.getStyle('left', 1), + top : t.getStyle('top', 1), + width : t.getStyle('width', 1), + height : t.getStyle('height', 1), + display : t.getStyle('display', 1), + zIndex : parseInt(t.getStyle('zIndex', 1) || 0) - 1 + }); } } + }); + }; +})(tinymce); - if (eb && sb != eb) - bl.push(eb); +(function(tinymce) { + function trimNl(s) { + return s.replace(/[\n\r]+/g, ''); + }; - return bl; - }, + // Shorten names + var is = tinymce.is, isIE = tinymce.isIE, each = tinymce.each; - destroy : function(s) { + tinymce.create('tinymce.dom.Selection', { + Selection : function(dom, win, serializer) { var t = this; - t.win = null; + t.dom = dom; + t.win = win; + t.serializer = serializer; - if (t.tridentSel) - t.tridentSel.destroy(); + // Add events + each([ + 'onBeforeSetContent', - // Manual destroy then remove unload handler - if (!s) - tinymce.removeUnload(t.destroy); - } - }); -})(tinymce); + 'onBeforeGetContent', -(function(tinymce) { - tinymce.create('tinymce.dom.XMLWriter', { - node : null, - - XMLWriter : function(s) { - // Get XML document - function getXML() { - var i = document.implementation; - - if (!i || !i.createDocument) { - // Try IE objects - try {return new ActiveXObject('MSXML2.DOMDocument');} catch (ex) {} - try {return new ActiveXObject('Microsoft.XmlDom');} catch (ex) {} - } else - return i.createDocument('', '', null); - }; + 'onSetContent', - this.doc = getXML(); - - // Since Opera and WebKit doesn't escape > into > we need to do it our self to normalize the output for all browsers - this.valid = tinymce.isOpera || tinymce.isWebKit; + 'onGetContent' + ], function(e) { + t[e] = new tinymce.util.Dispatcher(t); + }); + + // No W3C Range support + if (!t.win.getSelection) + t.tridentSel = new tinymce.dom.TridentSelection(t); + + if (tinymce.isIE && dom.boxModel) + this._fixIESelection(); + + // Prevent leaks + tinymce.addUnload(t.destroy, t); + }, - this.reset(); + setCursorLocation: function(node, offset) { + var t = this; var r = t.dom.createRng(); + r.setStart(node, offset); + r.setEnd(node, offset); + t.setRng(r); + t.collapse(false); }, + getContent : function(s) { + var t = this, r = t.getRng(), e = t.dom.create("body"), se = t.getSel(), wb, wa, n; + + s = s || {}; + wb = wa = ''; + s.get = true; + s.format = s.format || 'html'; + s.forced_root_block = ''; + t.onBeforeGetContent.dispatch(t, s); - reset : function() { - var t = this, d = t.doc; + if (s.format == 'text') + return t.isCollapsed() ? '' : (r.text || (se.toString ? se.toString() : '')); - if (d.firstChild) - d.removeChild(d.firstChild); + if (r.cloneContents) { + n = r.cloneContents(); - t.node = d.appendChild(d.createElement("html")); - }, + if (n) + e.appendChild(n); + } else if (is(r.item) || is(r.htmlText)) { + // IE will produce invalid markup if elements are present that + // it doesn't understand like custom elements or HTML5 elements. + // Adding a BR in front of the contents and then remoiving it seems to fix it though. + e.innerHTML = '
    ' + (r.item ? r.item(0).outerHTML : r.htmlText); + e.removeChild(e.firstChild); + } else + e.innerHTML = r.toString(); - writeStartElement : function(n) { - var t = this; + // Keep whitespace before and after + if (/^\s/.test(e.innerHTML)) + wb = ' '; - t.node = t.node.appendChild(t.doc.createElement(n)); - }, + if (/\s+$/.test(e.innerHTML)) + wa = ' '; - writeAttribute : function(n, v) { - if (this.valid) - v = v.replace(/>/g, '%MCGT%'); + s.getInner = true; - this.node.setAttribute(n, v); - }, + s.content = t.isCollapsed() ? '' : wb + t.serializer.serialize(e, s) + wa; + t.onGetContent.dispatch(t, s); - writeEndElement : function() { - this.node = this.node.parentNode; + return s.content; }, - writeFullEndElement : function() { - var t = this, n = t.node; + setContent : function(content, args) { + var self = this, rng = self.getRng(), caretNode, doc = self.win.document, frag, temp; - n.appendChild(t.doc.createTextNode("")); - t.node = n.parentNode; - }, + args = args || {format : 'html'}; + args.set = true; + content = args.content = content; - writeText : function(v) { - if (this.valid) - v = v.replace(/>/g, '%MCGT%'); + // Dispatch before set content event + if (!args.no_events) + self.onBeforeSetContent.dispatch(self, args); - this.node.appendChild(this.doc.createTextNode(v)); - }, + content = args.content; - writeCDATA : function(v) { - this.node.appendChild(this.doc.createCDATASection(v)); - }, + if (rng.insertNode) { + // Make caret marker since insertNode places the caret in the beginning of text after insert + content += '_'; - writeComment : function(v) { - // Fix for bug #2035694 - if (tinymce.isIE) - v = v.replace(/^\-|\-$/g, ' '); + // Delete and insert new node + if (rng.startContainer == doc && rng.endContainer == doc) { + // WebKit will fail if the body is empty since the range is then invalid and it can't insert contents + doc.body.innerHTML = content; + } else { + rng.deleteContents(); - this.node.appendChild(this.doc.createComment(v.replace(/\-\-/g, ' '))); - }, + if (doc.body.childNodes.length == 0) { + doc.body.innerHTML = content; + } else { + // createContextualFragment doesn't exists in IE 9 DOMRanges + if (rng.createContextualFragment) { + rng.insertNode(rng.createContextualFragment(content)); + } else { + // Fake createContextualFragment call in IE 9 + frag = doc.createDocumentFragment(); + temp = doc.createElement('div'); - getContent : function() { - var h; + frag.appendChild(temp); + temp.outerHTML = content; - h = this.doc.xml || new XMLSerializer().serializeToString(this.doc); - h = h.replace(/<\?[^?]+\?>||<\/html>||]+>/g, ''); - h = h.replace(/ ?\/>/g, ' />'); + rng.insertNode(frag); + } + } + } - if (this.valid) - h = h.replace(/\%MCGT%/g, '>'); + // Move to caret marker + caretNode = self.dom.get('__caret'); - return h; - } - }); -})(tinymce); + // Make sure we wrap it compleatly, Opera fails with a simple select call + rng = doc.createRange(); + rng.setStartBefore(caretNode); + rng.setEndBefore(caretNode); + self.setRng(rng); -(function(tinymce) { - tinymce.create('tinymce.dom.StringWriter', { - str : null, - tags : null, - count : 0, - settings : null, - indent : null, - - StringWriter : function(s) { - this.settings = tinymce.extend({ - indent_char : ' ', - indentation : 0 - }, s); + // Remove the caret position + self.dom.remove('__caret'); - this.reset(); - }, + try { + self.setRng(rng); + } catch (ex) { + // Might fail on Opera for some odd reason + } + } else { + if (rng.item) { + // Delete content and get caret text selection + doc.execCommand('Delete', false, null); + rng = self.getRng(); + } - reset : function() { - this.indent = ''; - this.str = ""; - this.tags = []; - this.count = 0; - }, + // Explorer removes spaces from the beginning of pasted contents + if (/^\s+/.test(content)) { + rng.pasteHTML('_' + content); + self.dom.remove('__mce_tmp'); + } else + rng.pasteHTML(content); + } - writeStartElement : function(n) { - this._writeAttributesEnd(); - this.writeRaw('<' + n); - this.tags.push(n); - this.inAttr = true; - this.count++; - this.elementCount = this.count; + // Dispatch set content event + if (!args.no_events) + self.onSetContent.dispatch(self, args); }, - writeAttribute : function(n, v) { - var t = this; + getStart : function() { + var rng = this.getRng(), startElement, parentElement, checkRng, node; - t.writeRaw(" " + t.encode(n) + '="' + t.encode(v) + '"'); - }, + if (rng.duplicate || rng.item) { + // Control selection, return first item + if (rng.item) + return rng.item(0); - writeEndElement : function() { - var n; + // Get start element + checkRng = rng.duplicate(); + checkRng.collapse(1); + startElement = checkRng.parentElement(); - if (this.tags.length > 0) { - n = this.tags.pop(); + // Check if range parent is inside the start element, then return the inner parent element + // This will fix issues when a single element is selected, IE would otherwise return the wrong start element + parentElement = node = rng.parentElement(); + while (node = node.parentNode) { + if (node == startElement) { + startElement = parentElement; + break; + } + } - if (this._writeAttributesEnd(1)) - this.writeRaw(''); + return startElement; + } else { + startElement = rng.startContainer; - if (this.settings.indentation > 0) - this.writeRaw('\n'); - } - }, + if (startElement.nodeType == 1 && startElement.hasChildNodes()) + startElement = startElement.childNodes[Math.min(startElement.childNodes.length - 1, rng.startOffset)]; - writeFullEndElement : function() { - if (this.tags.length > 0) { - this._writeAttributesEnd(); - this.writeRaw(''); + if (startElement && startElement.nodeType == 3) + return startElement.parentNode; - if (this.settings.indentation > 0) - this.writeRaw('\n'); + return startElement; } }, - writeText : function(v) { - this._writeAttributesEnd(); - this.writeRaw(this.encode(v)); - this.count++; - }, - - writeCDATA : function(v) { - this._writeAttributesEnd(); - this.writeRaw(''); - this.count++; - }, + getEnd : function() { + var t = this, r = t.getRng(), e, eo; - writeComment : function(v) { - this._writeAttributesEnd(); - this.writeRaw(''); - this.count++; - }, + if (r.duplicate || r.item) { + if (r.item) + return r.item(0); - writeRaw : function(v) { - this.str += v; - }, + r = r.duplicate(); + r.collapse(0); + e = r.parentElement(); - encode : function(s) { - return s.replace(/[<>&"]/g, function(v) { - switch (v) { - case '<': - return '<'; + if (e && e.nodeName == 'BODY') + return e.lastChild || e; - case '>': - return '>'; + return e; + } else { + e = r.endContainer; + eo = r.endOffset; - case '&': - return '&'; + if (e.nodeType == 1 && e.hasChildNodes()) + e = e.childNodes[eo > 0 ? eo - 1 : eo]; - case '"': - return '"'; - } + if (e && e.nodeType == 3) + return e.parentNode; - return v; - }); + return e; + } }, - getContent : function() { - return this.str; - }, + getBookmark : function(type, normalized) { + var t = this, dom = t.dom, rng, rng2, id, collapsed, name, element, index, chr = '\uFEFF', styles; - _writeAttributesEnd : function(s) { - if (!this.inAttr) - return; + function findIndex(name, element) { + var index = 0; - this.inAttr = false; + each(dom.select(name), function(node, i) { + if (node == element) + index = i; + }); - if (s && this.elementCount == this.count) { - this.writeRaw(' />'); - return false; - } + return index; + }; - this.writeRaw('>'); + if (type == 2) { + function getLocation() { + var rng = t.getRng(true), root = dom.getRoot(), bookmark = {}; - return true; - } - }); -})(tinymce); + function getPoint(rng, start) { + var container = rng[start ? 'startContainer' : 'endContainer'], + offset = rng[start ? 'startOffset' : 'endOffset'], point = [], node, childNodes, after = 0; -(function(tinymce) { - // Shorten names - var extend = tinymce.extend, each = tinymce.each, Dispatcher = tinymce.util.Dispatcher, isIE = tinymce.isIE, isGecko = tinymce.isGecko; + if (container.nodeType == 3) { + if (normalized) { + for (node = container.previousSibling; node && node.nodeType == 3; node = node.previousSibling) + offset += node.nodeValue.length; + } - function wildcardToRE(s) { - return s.replace(/([?+*])/g, '.$1'); - }; + point.push(offset); + } else { + childNodes = container.childNodes; - tinymce.create('tinymce.dom.Serializer', { - Serializer : function(s) { - var t = this; + if (offset >= childNodes.length && childNodes.length) { + after = 1; + offset = Math.max(0, childNodes.length - 1); + } - t.key = 0; - t.onPreProcess = new Dispatcher(t); - t.onPostProcess = new Dispatcher(t); + point.push(t.dom.nodeIndex(childNodes[offset], normalized) + after); + } - try { - t.writer = new tinymce.dom.XMLWriter(); - } catch (ex) { - // IE might throw exception if ActiveX is disabled so we then switch to the slightly slower StringWriter - t.writer = new tinymce.dom.StringWriter(); - } + for (; container && container != root; container = container.parentNode) + point.push(t.dom.nodeIndex(container, normalized)); - // Default settings - t.settings = s = extend({ - dom : tinymce.DOM, - valid_nodes : 0, - node_filter : 0, - attr_filter : 0, - invalid_attrs : /^(_mce_|_moz_|sizset|sizcache)/, - closed : /^(br|hr|input|meta|img|link|param|area)$/, - entity_encoding : 'named', - entities : '160,nbsp,161,iexcl,162,cent,163,pound,164,curren,165,yen,166,brvbar,167,sect,168,uml,169,copy,170,ordf,171,laquo,172,not,173,shy,174,reg,175,macr,176,deg,177,plusmn,178,sup2,179,sup3,180,acute,181,micro,182,para,183,middot,184,cedil,185,sup1,186,ordm,187,raquo,188,frac14,189,frac12,190,frac34,191,iquest,192,Agrave,193,Aacute,194,Acirc,195,Atilde,196,Auml,197,Aring,198,AElig,199,Ccedil,200,Egrave,201,Eacute,202,Ecirc,203,Euml,204,Igrave,205,Iacute,206,Icirc,207,Iuml,208,ETH,209,Ntilde,210,Ograve,211,Oacute,212,Ocirc,213,Otilde,214,Ouml,215,times,216,Oslash,217,Ugrave,218,Uacute,219,Ucirc,220,Uuml,221,Yacute,222,THORN,223,szlig,224,agrave,225,aacute,226,acirc,227,atilde,228,auml,229,aring,230,aelig,231,ccedil,232,egrave,233,eacute,234,ecirc,235,euml,236,igrave,237,iacute,238,icirc,239,iuml,240,eth,241,ntilde,242,ograve,243,oacute,244,ocirc,245,otilde,246,ouml,247,divide,248,oslash,249,ugrave,250,uacute,251,ucirc,252,uuml,253,yacute,254,thorn,255,yuml,402,fnof,913,Alpha,914,Beta,915,Gamma,916,Delta,917,Epsilon,918,Zeta,919,Eta,920,Theta,921,Iota,922,Kappa,923,Lambda,924,Mu,925,Nu,926,Xi,927,Omicron,928,Pi,929,Rho,931,Sigma,932,Tau,933,Upsilon,934,Phi,935,Chi,936,Psi,937,Omega,945,alpha,946,beta,947,gamma,948,delta,949,epsilon,950,zeta,951,eta,952,theta,953,iota,954,kappa,955,lambda,956,mu,957,nu,958,xi,959,omicron,960,pi,961,rho,962,sigmaf,963,sigma,964,tau,965,upsilon,966,phi,967,chi,968,psi,969,omega,977,thetasym,978,upsih,982,piv,8226,bull,8230,hellip,8242,prime,8243,Prime,8254,oline,8260,frasl,8472,weierp,8465,image,8476,real,8482,trade,8501,alefsym,8592,larr,8593,uarr,8594,rarr,8595,darr,8596,harr,8629,crarr,8656,lArr,8657,uArr,8658,rArr,8659,dArr,8660,hArr,8704,forall,8706,part,8707,exist,8709,empty,8711,nabla,8712,isin,8713,notin,8715,ni,8719,prod,8721,sum,8722,minus,8727,lowast,8730,radic,8733,prop,8734,infin,8736,ang,8743,and,8744,or,8745,cap,8746,cup,8747,int,8756,there4,8764,sim,8773,cong,8776,asymp,8800,ne,8801,equiv,8804,le,8805,ge,8834,sub,8835,sup,8836,nsub,8838,sube,8839,supe,8853,oplus,8855,otimes,8869,perp,8901,sdot,8968,lceil,8969,rceil,8970,lfloor,8971,rfloor,9001,lang,9002,rang,9674,loz,9824,spades,9827,clubs,9829,hearts,9830,diams,338,OElig,339,oelig,352,Scaron,353,scaron,376,Yuml,710,circ,732,tilde,8194,ensp,8195,emsp,8201,thinsp,8204,zwnj,8205,zwj,8206,lrm,8207,rlm,8211,ndash,8212,mdash,8216,lsquo,8217,rsquo,8218,sbquo,8220,ldquo,8221,rdquo,8222,bdquo,8224,dagger,8225,Dagger,8240,permil,8249,lsaquo,8250,rsaquo,8364,euro', - valid_elements : '*[*]', - extended_valid_elements : 0, - invalid_elements : 0, - fix_table_elements : 1, - fix_list_elements : true, - fix_content_duplication : true, - convert_fonts_to_spans : false, - font_size_classes : 0, - apply_source_formatting : 0, - indent_mode : 'simple', - indent_char : '\t', - indent_levels : 1, - remove_linebreaks : 1, - remove_redundant_brs : 1, - element_format : 'xhtml' - }, s); + return point; + }; - t.dom = s.dom; - t.schema = s.schema; + bookmark.start = getPoint(rng, true); - // Use raw entities if no entities are defined - if (s.entity_encoding == 'named' && !s.entities) - s.entity_encoding = 'raw'; + if (!t.isCollapsed()) + bookmark.end = getPoint(rng); - if (s.remove_redundant_brs) { - t.onPostProcess.add(function(se, o) { - // Remove single BR at end of block elements since they get rendered - o.content = o.content.replace(/(
    \s*)+<\/(p|h[1-6]|div|li)>/gi, function(a, b, c) { - // Check if it's a single element - if (/^
    \s*<\//.test(a)) - return ''; + return bookmark; + }; - return a; - }); - }); - } + if (t.tridentSel) + return t.tridentSel.getBookmark(type); - // Remove XHTML element endings i.e. produce crap :) XHTML is better - if (s.element_format == 'html') { - t.onPostProcess.add(function(se, o) { - o.content = o.content.replace(/<([^>]+) \/>/g, '<$1>'); - }); + return getLocation(); } - if (s.fix_list_elements) { - t.onPreProcess.add(function(se, o) { - var nl, x, a = ['ol', 'ul'], i, n, p, r = /^(OL|UL)$/, np; - - function prevNode(e, n) { - var a = n.split(','), i; + // Handle simple range + if (type) + return {rng : t.getRng()}; - while ((e = e.previousSibling) != null) { - for (i=0; i' + chr + ''); - for (i=0; i

    ] becomes

    ]

    + rng.moveToElementText(rng2.parentElement()); + if (rng.compareEndPoints('StartToEnd', rng2) == 0) + rng2.move('character', -1); - if (!np) { - np = t.dom.create('li'); - np.innerHTML = ' '; - np.appendChild(n); - p.insertBefore(np, p.firstChild); - } else - np.appendChild(n); - } + rng2.pasteHTML('' + chr + ''); } + } catch (ex) { + // IE might throw unspecified error so lets ignore it + return null; } - }); - } - - if (s.fix_table_elements) { - t.onPreProcess.add(function(se, o) { - // Since Opera will crash if you attach the node to a dynamic document we need to brrowser sniff a specific build - // so Opera users with an older version will have to live with less compaible output not much we can do here - if (!tinymce.isOpera || opera.buildNumber() >= 1767) { - each(t.dom.select('p table', o.node).reverse(), function(n) { - var parent = t.dom.getParent(n.parentNode, 'table,p'); - - if (parent.nodeName != 'TABLE') { - try { - t.dom.split(parent, n); - } catch (ex) { - // IE can sometimes fire an unknown runtime error so we just ignore it - } - } - }); - } - }); - } - }, - - setEntities : function(s) { - var t = this, a, i, l = {}, v; + } else { + // Control selection + element = rng.item(0); + name = element.nodeName; - // No need to setup more than once - if (t.entityLookup) - return; + return {name : name, index : findIndex(name, element)}; + } + } else { + element = t.getNode(); + name = element.nodeName; + if (name == 'IMG') + return {name : name, index : findIndex(name, element)}; - // Build regex and lookup array - a = s.split(','); - for (i = 0; i < a.length; i += 2) { - v = a[i]; + // Can't insert a node into the root of document WebKit defaults to document + if (rng.startContainer.nodeType == 9) { + return; + } - // Don't add default & " etc. - if (v == 34 || v == 38 || v == 60 || v == 62) - continue; + // W3C method + rng2 = rng.cloneRange(); - l[String.fromCharCode(a[i])] = a[i + 1]; + // Insert end marker + if (!collapsed) { + rng2.collapse(false); + rng2.insertNode(dom.create('span', {'data-mce-type' : "bookmark", id : id + '_end', style : styles}, chr)); + } - v = parseInt(a[i]).toString(16); + rng.collapse(true); + rng.insertNode(dom.create('span', {'data-mce-type' : "bookmark", id : id + '_start', style : styles}, chr)); } - t.entityLookup = l; - }, - - setRules : function(s) { - var t = this; - - t._setup(); - t.rules = {}; - t.wildRules = []; - t.validElements = {}; + t.moveToBookmark({id : id, keep : 1}); - return t.addRules(s); + return {id : id}; }, - addRules : function(s) { - var t = this, dr; - - if (!s) - return; - - t._setup(); - - each(s.split(','), function(s) { - var p = s.split(/\[|\]/), tn = p[0].split('/'), ra, at, wat, va = []; - - // Extend with default rules - if (dr) - at = tinymce.extend([], dr.attribs); + moveToBookmark : function(bookmark) { + var t = this, dom = t.dom, marker1, marker2, rng, root, startContainer, endContainer, startOffset, endOffset; - // Parse attributes - if (p.length > 1) { - each(p[1].split('|'), function(s) { - var ar = {}, i; + if (bookmark) { + if (bookmark.start) { + rng = dom.createRng(); + root = dom.getRoot(); - at = at || []; + function setEndPoint(start) { + var point = bookmark[start ? 'start' : 'end'], i, node, offset, children; - // Parse attribute rule - s = s.replace(/::/g, '~'); - s = /^([!\-])?([\w*.?~_\-]+|)([=:<])?(.+)?$/.exec(s); - s[2] = s[2].replace(/~/g, ':'); + if (point) { + offset = point[0]; - // Add required attributes - if (s[1] == '!') { - ra = ra || []; - ra.push(s[2]); - } + // Find container node + for (node = root, i = point.length - 1; i >= 1; i--) { + children = node.childNodes; - // Remove inherited attributes - if (s[1] == '-') { - for (i = 0; i children.length - 1) return; - } - } - } - switch (s[3]) { - // Add default attrib values - case '=': - ar.defaultVal = s[4] || ''; - break; + node = children[point[i]]; + } - // Add forced attrib values - case ':': - ar.forcedVal = s[4]; - break; + // Move text offset to best suitable location + if (node.nodeType === 3) + offset = Math.min(point[0], node.nodeValue.length); - // Add validation values - case '<': - ar.validVals = s[4].split('?'); - break; - } + // Move element offset to best suitable location + if (node.nodeType === 1) + offset = Math.min(point[0], node.childNodes.length); - if (/[*.?]/.test(s[2])) { - wat = wat || []; - ar.nameRE = new RegExp('^' + wildcardToRE(s[2]) + '$'); - wat.push(ar); - } else { - ar.name = s[2]; - at.push(ar); + // Set offset within container node + if (start) + rng.setStart(node, offset); + else + rng.setEnd(node, offset); } - va.push(s[2]); - }); - } - - // Handle element names - each(tn, function(s, i) { - var pr = s.charAt(0), x = 1, ru = {}; - - // Extend with default rule data - if (dr) { - if (dr.noEmpty) - ru.noEmpty = dr.noEmpty; + return true; + }; - if (dr.fullEnd) - ru.fullEnd = dr.fullEnd; + if (t.tridentSel) + return t.tridentSel.moveToBookmark(bookmark); - if (dr.padd) - ru.padd = dr.padd; + if (setEndPoint(true) && setEndPoint()) { + t.setRng(rng); } + } else if (bookmark.id) { + function restoreEndPoint(suffix) { + var marker = dom.get(bookmark.id + '_' + suffix), node, idx, next, prev, keep = bookmark.keep; - // Handle prefixes - switch (pr) { - case '-': - ru.noEmpty = true; - break; + if (marker) { + node = marker.parentNode; - case '+': - ru.fullEnd = true; - break; + if (suffix == 'start') { + if (!keep) { + idx = dom.nodeIndex(marker); + } else { + node = marker.firstChild; + idx = 1; + } - case '#': - ru.padd = true; - break; + startContainer = endContainer = node; + startOffset = endOffset = idx; + } else { + if (!keep) { + idx = dom.nodeIndex(marker); + } else { + node = marker.firstChild; + idx = 1; + } - default: - x = 0; - } + endContainer = node; + endOffset = idx; + } - tn[i] = s = s.substring(x); - t.validElements[s] = 1; + if (!keep) { + prev = marker.previousSibling; + next = marker.nextSibling; - // Add element name or element regex - if (/[*.?]/.test(tn[0])) { - ru.nameRE = new RegExp('^' + wildcardToRE(tn[0]) + '$'); - t.wildRules = t.wildRules || {}; - t.wildRules.push(ru); - } else { - ru.name = tn[0]; + // Remove all marker text nodes + each(tinymce.grep(marker.childNodes), function(node) { + if (node.nodeType == 3) + node.nodeValue = node.nodeValue.replace(/\uFEFF/g, ''); + }); - // Store away default rule - if (tn[0] == '@') - dr = ru; + // 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); - t.rules[s] = ru; - } + // 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; + } + } + } + } + }; - ru.attribs = at; + 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 (ra) - ru.requiredAttribs = ra; + return node; + }; - if (wat) { - // Build valid attributes regexp - s = ''; - each(va, function(v) { - if (s) - s += '|'; + // Restore start/end points + restoreEndPoint('start'); + restoreEndPoint('end'); - s += '(' + wildcardToRE(v) + ')'; - }); - ru.validAttribsRE = new RegExp('^' + s.toLowerCase() + '$'); - ru.wildAttribs = wat; + if (startContainer) { + rng = dom.createRng(); + rng.setStart(addBogus(startContainer), startOffset); + rng.setEnd(addBogus(endContainer), endOffset); + t.setRng(rng); } - }); - }); + } else if (bookmark.name) { + t.select(dom.select(bookmark.name)[bookmark.index]); + } else if (bookmark.rng) + t.setRng(bookmark.rng); + } + }, - // Build valid elements regexp - s = ''; - each(t.validElements, function(v, k) { - if (s) - s += '|'; + select : function(node, content) { + var t = this, dom = t.dom, rng = dom.createRng(), idx; - if (k != '@') - s += k; - }); - t.validElementsRE = new RegExp('^(' + wildcardToRE(s.toLowerCase()) + ')$'); + 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) { + 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); - //console.debug(t.validElementsRE.toString()); - //console.dir(t.rules); - //console.dir(t.wildRules); - }, + return; + } - findRule : function(n) { - var t = this, rl = t.rules, i, r; + // BR element + if (node.nodeName == 'BR') { + if (start) + rng.setStartBefore(node); + else + rng.setEndBefore(node); - t._setup(); + return; + } + } while (node = (start ? walker.next() : walker.prev())); + }; - // Exact match - r = rl[n]; - if (r) - return r; + setPoint(node, 1); + setPoint(node); + } - // Try wildcards - rl = t.wildRules; - for (i = 0; i < rl.length; i++) { - if (rl[i].nameRE.test(n)) - return rl[i]; + t.setRng(rng); } - return null; + return node; }, - findAttribRule : function(ru, n) { - var i, wa = ru.wildAttribs; - - for (i = 0; i < wa.length; i++) { - if (wa[i].nameRE.test(n)) - return wa[i]; - } + isCollapsed : function() { + var t = this, r = t.getRng(), s = t.getSel(); - return null; - }, + if (!r || r.item) + return false; - serialize : function(n, o) { - var h, t = this, doc, oldDoc, impl, selected; + if (r.compareEndPoints) + return r.compareEndPoints('StartToEnd', r) === 0; - t._setup(); - o = o || {}; - o.format = o.format || 'html'; - t.processObj = o; + return !s || r.collapsed; + }, - // IE looses the selected attribute on option elements so we need to store it - // See: http://support.microsoft.com/kb/829907 - if (isIE) { - selected = []; - each(n.getElementsByTagName('option'), function(n) { - var v = t.dom.getAttrib(n, 'selected'); + collapse : function(to_start) { + var self = this, rng = self.getRng(), node; - selected.push(v ? v : null); - }); + // Control range on IE + if (rng.item) { + node = rng.item(0); + rng = self.win.document.body.createTextRange(); + rng.moveToElementText(node); } - n = n.cloneNode(true); + rng.collapse(!!to_start); + self.setRng(rng); + }, - // IE looses the selected attribute on option elements so we need to restore it - if (isIE) { - each(n.getElementsByTagName('option'), function(n, i) { - t.dom.setAttrib(n, 'selected', selected[i]); - }); - } + getSel : function() { + var t = this, w = this.win; - // Nodes needs to be attached to something in WebKit/Opera - // Older builds of Opera crashes if you attach the node to an document created dynamically - // and since we can't feature detect a crash we need to sniff the acutal build number - // This fix will make DOM ranges and make Sizzle happy! - impl = n.ownerDocument.implementation; - if (impl.createHTMLDocument && (tinymce.isOpera && opera.buildNumber() >= 1767)) { - // Create an empty HTML document - doc = impl.createHTMLDocument(""); + return w.getSelection ? w.getSelection() : w.document.selection; + }, - // Add the element or it's children if it's a body element to the new document - each(n.nodeName == 'BODY' ? n.childNodes : [n], function(node) { - doc.body.appendChild(doc.importNode(node, true)); - }); + getRng : function(w3c) { + var t = this, s, r, elm, doc = t.win.document; - // Grab first child or body element for serialization - if (n.nodeName != 'BODY') - n = doc.body.firstChild; - else - n = doc.body; + // Found tridentSel object then we need to use that one + if (w3c && t.tridentSel) + return t.tridentSel.getRangeAt(0); + + try { + if (s = t.getSel()) + r = s.rangeCount > 0 ? s.getRangeAt(0) : (s.createRange ? s.createRange() : doc.createRange()); + } catch (ex) { + // IE throws unspecified error here if TinyMCE is placed in a frame/iframe + } - // set the new document in DOMUtils so createElement etc works - oldDoc = t.dom.doc; - t.dom.doc = doc; + // 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) { + elm = doc.selection.createRange().item(0); + r = doc.createRange(); + r.setStartBefore(elm); + r.setEndAfter(elm); } - t.key = '' + (parseInt(t.key) + 1); + // 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(); - // Pre process - if (!o.no_events) { - o.node = n; - t.onPreProcess.dispatch(t, o); + if (t.selectedRange && t.explicitRange) { + if (r.compareBoundaryPoints(r.START_TO_START, t.selectedRange) === 0 && r.compareBoundaryPoints(r.END_TO_END, t.selectedRange) === 0) { + // Safari, Opera and Chrome only ever select text which causes the range to change. + // This lets us use the originally set range if the selection hasn't been changed by the user. + r = t.explicitRange; + } else { + t.selectedRange = null; + t.explicitRange = null; + } } - // Serialize HTML DOM into a string - t.writer.reset(); - t._info = o; - t._serializeNode(n, o.getInner); + return r; + }, + + setRng : function(r) { + var s, t = this; - // Post process - o.content = t.writer.getContent(); + if (!t.tridentSel) { + s = t.getSel(); - // Restore the old document if it was changed - if (oldDoc) - t.dom.doc = oldDoc; + if (s) { + t.explicitRange = r; - if (!o.no_events) - t.onPostProcess.dispatch(t, o); + try { + s.removeAllRanges(); + } catch (ex) { + // IE9 might throw errors here don't know why + } - t._postProcess(o); - o.node = null; + s.addRange(r); + // 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) { + try { + t.tridentSel.addRange(r); + return; + } catch (ex) { + //IE9 throws an error here if called before selection is placed in the editor + } + } - return tinymce.trim(o.content); + // Is IE specific range + try { + r.select(); + } catch (ex) { + // Needed for some odd IE bug #1843306 + } + } }, - // Internal functions + setNode : function(n) { + var t = this; - _postProcess : function(o) { - var t = this, s = t.settings, h = o.content, sc = [], p; - - if (o.format == 'html') { - // Protect some elements - p = t._protect({ - content : h, - patterns : [ - {pattern : /(]*>)(.*?)(<\/script>)/g}, - {pattern : /(]*>)(.*?)(<\/noscript>)/g}, - {pattern : /(]*>)(.*?)(<\/style>)/g}, - {pattern : /(]*>)(.*?)(<\/pre>)/g, encode : 1}, - {pattern : /()/g} - ] - }); + t.setContent(t.dom.getOuterHTML(n)); - h = p.content; + return n; + }, - // Entity encode - if (s.entity_encoding !== 'raw') - h = t._encode(h); + getNode : function() { + var t = this, rng = t.getRng(), sel = t.getSel(), elm, start = rng.startContainer, end = rng.endContainer; - // Use BR instead of   padded P elements inside editor and use

     

    outside editor -/* if (o.set) - h = h.replace(/

    \s+( | |\u00a0|
    )\s+<\/p>/g, '


    '); - else - h = h.replace(/

    \s+( | |\u00a0|
    )\s+<\/p>/g, '

    $1

    ');*/ + // Range maybe lost after the editor is made visible again + if (!rng) + return t.dom.getRoot(); - // Since Gecko and Safari keeps whitespace in the DOM we need to - // remove it inorder to match other browsers. But I think Gecko and Safari is right. - // This process is only done when getting contents out from the editor. - if (!o.set) { - // We need to replace paragraph whitespace with an nbsp before indentation to keep the \u00a0 char - h = h.replace(/

    \s+<\/p>|]+)>\s+<\/p>/g, s.entity_encoding == 'numeric' ? ' 

    ' : ' 

    '); + if (rng.setStart) { + elm = rng.commonAncestorContainer; - if (s.remove_linebreaks) { - h = h.replace(/\r?\n|\r/g, ' '); - h = h.replace(/(<[^>]+>)\s+/g, '$1 '); - h = h.replace(/\s+(<\/[^>]+>)/g, ' $1'); - h = h.replace(/<(p|h[1-6]|blockquote|hr|div|table|tbody|tr|td|body|head|html|title|meta|style|pre|script|link|object) ([^>]+)>\s+/g, '<$1 $2>'); // Trim block start - h = h.replace(/<(p|h[1-6]|blockquote|hr|div|table|tbody|tr|td|body|head|html|title|meta|style|pre|script|link|object)>\s+/g, '<$1>'); // Trim block start - h = h.replace(/\s+<\/(p|h[1-6]|blockquote|hr|div|table|tbody|tr|td|body|head|html|title|meta|style|pre|script|link|object)>/g, ''); // Trim block end + // Handle selection a image or other control like element such as anchors + if (!rng.collapsed) { + if (rng.startContainer == rng.endContainer) { + if (rng.endOffset - rng.startOffset < 2) { + if (rng.startContainer.hasChildNodes()) + elm = rng.startContainer.childNodes[rng.startOffset]; + } } - // Simple indentation - if (s.apply_source_formatting && s.indent_mode == 'simple') { - // Add line breaks before and after block elements - h = h.replace(/<(\/?)(ul|hr|table|meta|link|tbody|tr|object|body|head|html|map)(|[^>]+)>\s*/g, '\n<$1$2$3>\n'); - h = h.replace(/\s*<(p|h[1-6]|blockquote|div|title|style|pre|script|td|li|area)(|[^>]+)>/g, '\n<$1$2>'); - h = h.replace(/<\/(p|h[1-6]|blockquote|div|title|style|pre|script|td|li)>\s*/g, '\n'); - h = h.replace(/\n\n/g, '\n'); + // 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) + // 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 { + start = start.parentNode; + } + if (rng.endOffset === 0) { + end = skipEmptyTextNodes(end.previousSibling, false); + } else { + end = end.parentNode; + } + + if (start && start === end) + return start; } } - h = t._unprotect(h, p); + if (elm && elm.nodeType == 3) + return elm.parentNode; - // Restore CDATA sections - h = h.replace(//g, ''); + return elm; + } - // Restore the \u00a0 character if raw mode is enabled - if (s.entity_encoding == 'raw') - h = h.replace(/

     <\/p>|]+)> <\/p>/g, '\u00a0

    '); + return rng.item ? rng.item(0) : rng.parentElement(); + }, - // Restore noscript elements - h = h.replace(/]+|)>([\s\S]*?)<\/noscript>/g, function(v, attribs, text) { - return '' + t.dom.decode(text.replace(//g, '')) + ''; - }); + getSelectedBlocks : function(st, en) { + var t = this, dom = t.dom, sb, eb, n, bl = []; + + sb = dom.getParent(st || t.getStart(), dom.isBlock); + eb = dom.getParent(en || t.getEnd(), dom.isBlock); + + if (sb) + bl.push(sb); + + if (sb && eb && sb != eb) { + n = sb; + + var walker = new tinymce.dom.TreeWalker(sb, dom.getRoot()); + while ((n = walker.next()) && n != eb) { + if (dom.isBlock(n)) + bl.push(n); + } } - o.content = h; + if (eb && sb != eb) + bl.push(eb); + + return bl; }, - _serializeNode : function(n, inner) { - var t = this, s = t.settings, w = t.writer, hc, el, cn, i, l, a, at, no, v, nn, ru, ar, iv, closed, keep, type; + normalize : function() { + var self = this, rng, normalized; - if (!s.node_filter || s.node_filter(n)) { - switch (n.nodeType) { - case 1: // Element - if (n.hasAttribute ? n.hasAttribute('_mce_bogus') : n.getAttribute('_mce_bogus')) - return; + // TODO: + // Retain selection direction. + // Lean left/right on Gecko for inline elements. + // Run this on mouse up/key up when the user manually moves the selection - iv = keep = false; - hc = n.hasChildNodes(); - nn = n.getAttribute('_mce_name') || n.nodeName.toLowerCase(); + // Normalize only on non IE browsers for now + if (tinymce.isIE) + return; - // Get internal type - type = n.getAttribute('_mce_type'); - if (type) { - if (!t._info.cleanup) { - iv = true; - return; - } else - keep = 1; - } + function normalizeEndPoint(start) { + var container, offset, walker, dom = self.dom, body = dom.getRoot(), node; - // Add correct prefix on IE - if (isIE) { - if (n.scopeName !== 'HTML' && n.scopeName !== 'html') - nn = n.scopeName + ':' + nn; - } + container = rng[(start ? 'start' : 'end') + 'Container']; + offset = rng[(start ? 'start' : 'end') + 'Offset']; - // Remove mce prefix on IE needed for the abbr element - if (nn.indexOf('mce:') === 0) - nn = nn.substring(4); + // If the container is a document move it to the body element + if (container.nodeType === 9) { + container = container.body; + offset = 0; + } - // Check if valid - if (!keep) { - if (!t.validElementsRE || !t.validElementsRE.test(nn) || (t.invalidElementsRE && t.invalidElementsRE.test(nn)) || inner) { - iv = true; - break; - } - } + // 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) { + // 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()) { + // 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); + do { + // Found a text node use that position + if (node.nodeType === 3) { + offset = start ? 0 : node.nodeValue.length - 1; + container = node; + normalized = true; + break; + } - if (isIE) { - // Fix IE content duplication (DOM can have multiple copies of the same node) - if (s.fix_content_duplication) { - if (n._mce_serialized == t.key) - return; + // Found a BR/IMG element that we can place the caret before + if (/^(BR|IMG)$/.test(node.nodeName)) { + offset = dom.nodeIndex(node); + container = node.parentNode; - n._mce_serialized = t.key; - } + // Put caret after image when moving the end point + if (node.nodeName == "IMG" && !start) { + offset++; + } - // IE sometimes adds a / infront of the node name - if (nn.charAt(0) == '/') - nn = nn.substring(1); - } else if (isGecko) { - // Ignore br elements - if (n.nodeName === 'BR' && n.getAttribute('type') == '_moz') - return; + normalized = true; + break; + } + } while (node = (start ? walker.next() : walker.prev())); } + } + } - // Check if valid child - if (s.validate_children) { - if (t.elementName && !t.schema.isValid(t.elementName, nn)) { - iv = true; - break; - } + // Set endpoint if it was normalized + if (normalized) + rng['set' + (start ? 'Start' : 'End')](container, offset); + }; - t.elementName = nn; - } + rng = self.getRng(); - ru = t.findRule(nn); - - // No valid rule for this element could be found then skip it - if (!ru) { - iv = true; - break; - } + // Normalize the end points + normalizeEndPoint(true); - nn = ru.name || nn; - closed = s.closed.test(nn); + if (!rng.collapsed) + normalizeEndPoint(); - // Skip empty nodes or empty node name in IE - if ((!hc && ru.noEmpty) || (isIE && !nn)) { - iv = true; - break; - } + // Set the selection if it was normalized + if (normalized) { + //console.log(self.dom.dumpRng(rng)); + self.setRng(rng); + } + }, - // Check required - if (ru.requiredAttribs) { - a = ru.requiredAttribs; + destroy : function(s) { + var t = this; - for (i = a.length - 1; i >= 0; i--) { - if (this.dom.getAttrib(n, a[i]) !== '') - break; - } + t.win = null; - // None of the required was there - if (i == -1) { - iv = true; - break; - } - } + // Manual destroy then remove unload handler + if (!s) + tinymce.removeUnload(t.destroy); + }, - w.writeStartElement(nn); + // 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; - // Add ordered attributes - if (ru.attribs) { - for (i=0, at = ru.attribs, l = at.length; i-1; i--) { - no = at[i]; + try { + rng.moveToPoint(x, y); + } catch (ex) { + // IE sometimes throws and exception, so lets just ignore it + rng = null; + } - if (no.specified) { - a = no.nodeName.toLowerCase(); + return rng; + }; - if (s.invalid_attrs.test(a) || !ru.validAttribsRE.test(a)) - continue; + // Fires while the selection is changing + function selectionChange(e) { + var pointRng; - ar = t.findAttribRule(ru, a); - v = t._getAttrib(n, ar, a); + // Check if the button is down or not + if (e.button) { + // Create range from mouse position + pointRng = rngFromPoint(e.x, e.y); - if (v !== null) - w.writeAttribute(a, v); - } - } - } + if (pointRng) { + // Check if pointRange is before/after selection then change the endPoint + if (pointRng.compareEndPoints('StartToStart', startRng) > 0) + pointRng.setEndPoint('StartToStart', startRng); + else + pointRng.setEndPoint('EndToEnd', startRng); - // Keep type attribute - if (type && keep) - w.writeAttribute('_mce_type', type); + pointRng.select(); + } + } else + endSelection(); + } - // Write text from script - if (nn === 'script' && tinymce.trim(n.innerHTML)) { - w.writeText('// '); // Padd it with a comment so it will parse on older browsers - w.writeCDATA(n.innerHTML.replace(/|<\[CDATA\[|\]\]>/g, '')); // Remove comments and cdata stuctures - hc = false; - break; - } + // Removes listeners + function endSelection() { + var rng = doc.selection.createRange(); - // Padd empty nodes with a   - if (ru.padd) { - // If it has only one bogus child, padd it anyway workaround for
    bug - if (hc && (cn = n.firstChild) && cn.nodeType === 1 && n.childNodes.length === 1) { - if (cn.hasAttribute ? cn.hasAttribute('_mce_bogus') : cn.getAttribute('_mce_bogus')) - w.writeText('\u00a0'); - } else if (!hc) - w.writeText('\u00a0'); // No children then padd it - } + // If the range is collapsed then use the last start range + if (startRng && !rng.item && rng.compareEndPoints('StartToEnd', rng) === 0) + startRng.select(); - break; + dom.unbind(doc, 'mouseup', endSelection); + dom.unbind(doc, 'mousemove', selectionChange); + startRng = started = 0; + }; - case 3: // Text - // Check if valid child - if (s.validate_children && t.elementName && !t.schema.isValid(t.elementName, '#text')) - return; + // Detect when user selects outside BODY + dom.bind(doc, ['mousedown', 'contextmenu'], function(e) { + if (e.target.nodeName === 'HTML') { + if (started) + endSelection(); - return w.writeText(n.nodeValue); + // Detect vertical scrollbar, since IE will fire a mousedown on the scrollbar and have target set as HTML + htmlElm = doc.documentElement; + if (htmlElm.scrollHeight > htmlElm.clientHeight) + return; - case 4: // CDATA - return w.writeCDATA(n.nodeValue); + started = 1; + // Setup start position + startRng = rngFromPoint(e.x, e.y); + if (startRng) { + // Listen for selection change events + dom.bind(doc, 'mouseup', endSelection); + dom.bind(doc, 'mousemove', selectionChange); - case 8: // Comment - return w.writeComment(n.nodeValue); + dom.win.focus(); + startRng.select(); + } } - } else if (n.nodeType == 1) - hc = n.hasChildNodes(); + }); + } + }); +})(tinymce); - if (hc && !closed) { - cn = n.firstChild; +(function(tinymce) { + tinymce.dom.Serializer = function(settings, dom, schema) { + var onPreProcess, onPostProcess, isIE = tinymce.isIE, each = tinymce.each, htmlParser; - while (cn) { - t._serializeNode(cn); - t.elementName = nn; - cn = cn.nextSibling; - } - } + // Support the old apply_source_formatting option + if (!settings.apply_source_formatting) + settings.indent = false; - // Write element end - if (!iv) { - if (!closed) - w.writeFullEndElement(); - else - w.writeEndElement(); - } - }, + // 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; - _protect : function(o) { - var t = this; + onPreProcess = new tinymce.util.Dispatcher(self); - o.items = o.items || []; + onPostProcess = new tinymce.util.Dispatcher(self); - function enc(s) { - return s.replace(/[\r\n\\]/g, function(c) { - if (c === '\n') - return '\\n'; - else if (c === '\\') - return '\\\\'; + htmlParser = new tinymce.html.DomParser(settings, schema); - return '\\r'; - }); - }; + // Convert move data-mce-src, data-mce-href and data-mce-style into nodes or process them if needed + htmlParser.addAttributeFilter('src,href,style', function(nodes, name) { + var i = nodes.length, node, value, internalName = 'data-mce-' + name, urlConverter = settings.url_converter, urlConverterScope = settings.url_converter_scope, undef; - function dec(s) { - return s.replace(/\\[\\rn]/g, function(c) { - if (c === '\\n') - return '\n'; - else if (c === '\\\\') - return '\\'; + while (i--) { + node = nodes[i]; - return '\r'; - }); - }; + value = node.attributes.map[internalName]; + if (value !== undef) { + // Set external name to internal value and remove internal + node.attr(name, value.length > 0 ? value : null); + node.attr(internalName, null); + } else { + // No internal attribute found then convert the value we have in the DOM + value = node.attributes.map[name]; - each(o.patterns, function(p) { - o.content = dec(enc(o.content).replace(p.pattern, function(x, a, b, c) { - b = dec(b); + if (name === "style") + value = dom.serializeStyle(dom.parseStyle(value), node.name); + else if (urlConverter) + value = urlConverter.call(urlConverterScope, value, name, node.name); - if (p.encode) - b = t._encode(b); + node.attr(name, value.length > 0 ? value : null); + } + } + }); - o.items.push(b); - return a + '' + c; - })); - }); + // Remove internal classes mceItem<..> + htmlParser.addAttributeFilter('class', function(nodes, name) { + var i = nodes.length, node, value; - return o; - }, + while (i--) { + node = nodes[i]; + value = node.attr('class').replace(/\s*mce(Item\w+|Selected)\s*/g, ''); + node.attr('class', value.length > 0 ? value : null); + } + }); - _unprotect : function(h, o) { - h = h.replace(/\)/g, '\n') + .replace(/^[\r\n]*|[\r\n]*$/g, '') + .replace(/^\s*(()?|\s*\/\/\s*\]\]>(-->)?|\/\/\s*(-->)?|\]\]>|\/\*\s*-->\s*\*\/|\s*-->\s*)\s*$/g, ''); + }; - if (v = l[a]) - a = '&' + v + ';'; + while (i--) { + node = nodes[i]; + value = node.firstChild ? node.firstChild.value : ''; - return a; - }); + if (name === "script") { + // Remove mce- prefix from script elements + node.attr('type', (node.attr('type') || 'text/javascript').replace(/^mce\-/, '')); + + if (value.length > 0) + node.firstChild.value = '// '; + } else { + if (value.length > 0) + node.firstChild.value = ''; } + } + }); - if (s.entity_encoding.indexOf('numeric') != -1) { - h = h.replace(/[\u007E-\uFFFF]/g, function(a) { - return '&#' + a.charCodeAt(0) + ';'; - }); + // Convert comments to cdata and handle protected comments + htmlParser.addNodeFilter('#comment', function(nodes, name) { + var i = nodes.length, node; + + while (i--) { + node = nodes[i]; + + if (node.value.indexOf('[CDATA[') === 0) { + node.name = '#cdata'; + node.type = 4; + node.value = node.value.replace(/^\[CDATA\[|\]\]$/g, ''); + } else if (node.value.indexOf('mce:protected ') === 0) { + node.name = "#text"; + node.type = 3; + node.raw = true; + node.value = unescape(node.value).substr(14); } } + }); - return h; - }, + htmlParser.addNodeFilter('xml:namespace,input', function(nodes, name) { + var i = nodes.length, node; - _setup : function() { - var t = this, s = this.settings; + while (i--) { + node = nodes[i]; + if (node.type === 7) + node.remove(); + else if (node.type === 1) { + if (name === "input" && !("type" in node.attributes.map)) + node.attr('type', 'text'); + } + } + }); - if (t.done) - return; + // Fix list elements, TODO: Replace this later + if (settings.fix_list_elements) { + htmlParser.addNodeFilter('ul,ol', function(nodes, name) { + var i = nodes.length, node, parentNode; - t.done = 1; + while (i--) { + node = nodes[i]; + parentNode = node.parent; - t.setRules(s.valid_elements); - t.addRules(s.extended_valid_elements); + if (parentNode.name === 'ul' || parentNode.name === 'ol') { + if (node.prev && node.prev.name === 'li') { + node.prev.append(node); + } + } + } + }); + } - if (s.invalid_elements) - t.invalidElementsRE = new RegExp('^(' + wildcardToRE(s.invalid_elements.replace(/,/g, '|').toLowerCase()) + ')$'); + // Remove internal data attributes + htmlParser.addAttributeFilter('data-mce-src,data-mce-href,data-mce-style', function(nodes, name) { + var i = nodes.length; - if (s.attrib_value_filter) - t.attribValueFilter = s.attribValueFilter; - }, + while (i--) { + nodes[i].attr(name, null); + } + }); - _getAttrib : function(n, a, na) { - var i, v; + // Return public methods + return { + schema : schema, - na = na || a.name; + addNodeFilter : htmlParser.addNodeFilter, - if (a.forcedVal && (v = a.forcedVal)) { - if (v === '{$uid}') - return this.dom.uniqueId(); + addAttributeFilter : htmlParser.addAttributeFilter, - return v; - } + onPreProcess : onPreProcess, - v = this.dom.getAttrib(n, na); + onPostProcess : onPostProcess, - switch (na) { - case 'rowspan': - case 'colspan': - // Whats the point? Remove usless attribute value - if (v == '1') - v = ''; + serialize : function(node, args) { + var impl, doc, oldDoc, htmlSerializer, content; - break; - } + // Explorer won't clone contents of script and style and the + // selected index of select elements are cleared on a clone operation. + if (isIE && dom.select('script,style,select,map').length > 0) { + content = node.innerHTML; + node = node.cloneNode(false); + dom.setHTML(node, content); + } else + node = node.cloneNode(true); + + // Nodes needs to be attached to something in WebKit/Opera + // Older builds of Opera crashes if you attach the node to an document created dynamically + // and since we can't feature detect a crash we need to sniff the acutal build number + // This fix will make DOM ranges and make Sizzle happy! + impl = node.ownerDocument.implementation; + if (impl.createHTMLDocument) { + // Create an empty HTML document + doc = impl.createHTMLDocument(""); + + // Add the element or it's children if it's a body element to the new document + each(node.nodeName == 'BODY' ? node.childNodes : [node], function(node) { + doc.body.appendChild(doc.importNode(node, true)); + }); - if (this.attribValueFilter) - v = this.attribValueFilter(na, v, n); + // Grab first child or body element for serialization + if (node.nodeName != 'BODY') + node = doc.body.firstChild; + else + node = doc.body; - if (a.validVals) { - for (i = a.validVals.length - 1; i >= 0; i--) { - if (v == a.validVals[i]) - break; + // set the new document in DOMUtils so createElement etc works + oldDoc = dom.doc; + dom.doc = doc; } - if (i == -1) - return null; - } + args = args || {}; + args.format = args.format || 'html'; - if (v === '' && typeof(a.defaultVal) != 'undefined') { - v = a.defaultVal; + // Pre process + if (!args.no_events) { + args.node = node; + onPreProcess.dispatch(self, args); + } - if (v === '{$uid}') - return this.dom.uniqueId(); + // Setup serializer + htmlSerializer = new tinymce.html.Serializer(settings, schema); - return v; - } else { - // Remove internal mceItemXX classes when content is extracted from editor - if (na == 'class' && this.processObj.get) - v = v.replace(/\s?mceItem\w+\s?/g, ''); - } + // Parse and serialize HTML + args.content = htmlSerializer.serialize( + htmlParser.parse(args.getInner ? node.innerHTML : tinymce.trim(dom.getOuterHTML(node), args), args) + ); - if (v === '') - return null; + // Replace all BOM characters for now until we can find a better solution + if (!args.cleanup) + args.content = args.content.replace(/\uFEFF|\u200B/g, ''); + // Post process + if (!args.no_events) + onPostProcess.dispatch(self, args); - return v; - } - }); -})(tinymce); + // Restore the old document if it was changed + if (oldDoc) + dom.doc = oldDoc; + + args.node = null; + + return args.content; + }, + addRules : function(rules) { + schema.addValidElements(rules); + }, + + setRules : function(rules) { + schema.setValidElements(rules); + } + }; + }; +})(tinymce); (function(tinymce) { tinymce.dom.ScriptLoader = function(settings) { var QUEUED = 0, @@ -6850,6 +9277,17 @@ window.tinymce.dom.Sizzle = Sizzle; callback(); }; + + function error() { + // Report the error so it's easier for people to spot loading errors + if (typeof(console) !== "undefined" && console.log) + console.log("Failed to load: " + url); + + // We can't mark it as done if there is a load error since + // A) We don't want to produce 404 errors on the server and + // B) the onerror event won't fire on all browsers. + // done(); + }; id = dom.uniqueId(); @@ -6859,7 +9297,7 @@ window.tinymce.dom.Sizzle = Sizzle; // If script is from same domain and we // use IE 6 then use XHR since it's more reliable - if (uri.host == loc.hostname && uri.port == loc.port && (uri.protocol + ':') == loc.protocol) { + if (uri.host == loc.hostname && uri.port == loc.port && (uri.protocol + ':') == loc.protocol && uri.protocol.toLowerCase() != 'file') { tinymce.util.XHR.send({ url : tinymce._addVer(uri.getURI()), success : function(content) { @@ -6874,7 +9312,9 @@ window.tinymce.dom.Sizzle = Sizzle; dom.remove(script); done(); - } + }, + + error : error }); return; @@ -6888,17 +9328,26 @@ window.tinymce.dom.Sizzle = Sizzle; src : tinymce._addVer(url) }); - // Add onload and readystate listeners - elm.onload = done; - elm.onreadystatechange = function() { - var state = elm.readyState; + // Add onload listener for non IE browsers since IE9 + // fires onload event before the script is parsed and executed + if (!tinymce.isIE) + elm.onload = done; - // Loaded state is passed on IE 6 however there - // are known issues with this method but we can't use - // XHR in a cross domain loading - if (state == 'complete' || state == 'loaded') - done(); - }; + // Add onerror event will get fired on some browsers but not all of them + elm.onerror = error; + + // Opera 9.60 doesn't seem to fire the onreadystate event at correctly + if (!tinymce.isOpera) { + elm.onreadystatechange = function() { + var state = elm.readyState; + + // Loaded state is passed on IE 6 however there + // are known issues with this method but we can't use + // XHR in a cross domain loading + if (state == 'complete' || state == 'loaded') + done(); + }; + } // Most browsers support this feature so we report errors // for those at least to help users track their missing plugins etc @@ -7046,164 +9495,10 @@ tinymce.dom.TreeWalker = function(start_node, root_node) { }; this.prev = function(shallow) { - return (node = findSibling(node, 'lastChild', 'lastSibling', shallow)); + return (node = findSibling(node, 'lastChild', 'previousSibling', shallow)); }; }; -(function() { - var transitional = {}; - - function unpack(lookup, data) { - var key; - - function replace(value) { - return value.replace(/[A-Z]+/g, function(key) { - return replace(lookup[key]); - }); - }; - - // Unpack lookup - for (key in lookup) { - if (lookup.hasOwnProperty(key)) - lookup[key] = replace(lookup[key]); - } - - // Unpack and parse data into object map - replace(data).replace(/#/g, '#text').replace(/(\w+)\[([^\]]+)\]/g, function(str, name, children) { - var i, map = {}; - - children = children.split(/\|/); - - for (i = children.length - 1; i >= 0; i--) - map[children[i]] = 1; - - transitional[name] = map; - }); - }; - - // This is the XHTML 1.0 transitional elements with it's children packed to reduce it's size - // we will later include the attributes here and use it as a default for valid elements but it - // requires us to rewrite the serializer engine - unpack({ - Z : '#|H|K|N|O|P', - Y : '#|X|form|R|Q', - X : 'p|T|div|U|W|isindex|fieldset|table', - W : 'pre|hr|blockquote|address|center|noframes', - 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' - }, 'script[]' + - 'style[]' + - 'object[#|param|X|form|a|H|K|N|O|Q]' + - 'param[]' + - 'p[S]' + - 'a[Z]' + - 'br[]' + - 'span[S]' + - 'bdo[S]' + - 'applet[#|param|X|form|a|H|K|N|O|Q]' + - 'h1[S]' + - 'img[]' + - 'map[X|form|Q|area]' + - 'h2[S]' + - 'iframe[#|X|form|a|H|K|N|O|Q]' + - 'h3[S]' + - 'tt[S]' + - 'i[S]' + - 'b[S]' + - 'u[S]' + - 's[S]' + - 'strike[S]' + - 'big[S]' + - 'small[S]' + - 'font[S]' + - 'basefont[]' + - 'em[S]' + - 'strong[S]' + - 'dfn[S]' + - 'code[S]' + - 'q[S]' + - 'samp[S]' + - 'kbd[S]' + - 'var[S]' + - 'cite[S]' + - 'abbr[S]' + - 'acronym[S]' + - 'sub[S]' + - 'sup[S]' + - 'input[]' + - 'select[optgroup|option]' + - 'optgroup[option]' + - 'option[]' + - 'textarea[]' + - 'label[S]' + - 'button[#|p|T|div|U|W|table|G|object|applet|img|map|K|N|Q]' + - 'h4[S]' + - 'ins[#|X|form|a|H|K|N|O|Q]' + - 'h5[S]' + - 'del[#|X|form|a|H|K|N|O|Q]' + - 'h6[S]' + - 'div[#|X|form|a|H|K|N|O|Q]' + - 'ul[li]' + - 'li[#|X|form|a|H|K|N|O|Q]' + - 'ol[li]' + - 'dl[dt|dd]' + - 'dt[S]' + - 'dd[#|X|form|a|H|K|N|O|Q]' + - 'menu[li]' + - 'dir[li]' + - 'pre[ZA]' + - 'hr[]' + - 'blockquote[#|X|form|a|H|K|N|O|Q]' + - 'address[S|p]' + - 'center[#|X|form|a|H|K|N|O|Q]' + - 'noframes[#|X|form|a|H|K|N|O|Q]' + - 'isindex[]' + - 'fieldset[#|legend|X|form|a|H|K|N|O|Q]' + - 'legend[S]' + - 'table[caption|col|colgroup|thead|tfoot|tbody|tr]' + - 'caption[S]' + - 'col[]' + - 'colgroup[col]' + - 'thead[tr]' + - 'tr[th|td]' + - 'th[#|X|form|a|H|K|N|O|Q]' + - 'form[#|X|a|H|K|N|O|Q]' + - 'noscript[#|X|form|a|H|K|N|O|Q]' + - 'td[#|X|form|a|H|K|N|O|Q]' + - 'tfoot[tr]' + - 'tbody[tr]' + - 'area[]' + - 'base[]' + - 'body[#|X|form|a|H|K|N|O|Q]' - ); - - tinymce.dom.Schema = function() { - var t = this, elements = transitional; - - t.isValid = function(name, child_name) { - var element = elements[name]; - - return !!(element && (!child_name || element[child_name])); - }; - }; -})(); (function(tinymce) { tinymce.dom.RangeUtils = function(dom) { var INVISIBLE_CHAR = '\uFEFF'; @@ -7227,6 +9522,24 @@ tinymce.dom.TreeWalker = function(start_node, root_node) { return; } + function exclude(nodes) { + var node; + + // First node is excluded + node = nodes[0]; + if (node.nodeType === 3 && node === startContainer && startOffset >= node.nodeValue.length) { + nodes.splice(0, 1); + } + + // Last node is excluded + node = nodes[nodes.length - 1]; + if (endOffset === 0 && nodes.length > 0 && node === endContainer && node.nodeType === 3) { + nodes.splice(nodes.length - 1, 1); + } + + return nodes; + }; + function collectSiblings(node, name, end_node) { var siblings = []; @@ -7256,7 +9569,7 @@ tinymce.dom.TreeWalker = function(start_node, root_node) { if (!next) siblings.reverse(); - callback(siblings); + callback(exclude(siblings)); } } }; @@ -7267,30 +9580,30 @@ tinymce.dom.TreeWalker = function(start_node, root_node) { // If index based end position then resolve it if (endContainer.nodeType == 1 && endContainer.hasChildNodes()) - endContainer = endContainer.childNodes[Math.min(startOffset == endOffset ? endOffset : endOffset - 1, endContainer.childNodes.length - 1)]; - - // Find common ancestor and end points - ancestor = dom.findCommonAncestor(startContainer, endContainer); + endContainer = endContainer.childNodes[Math.min(endOffset - 1, endContainer.childNodes.length - 1)]; // Same container if (startContainer == endContainer) - return callback([startContainer]); + return callback(exclude([startContainer])); + // Find common ancestor and end points + ancestor = dom.findCommonAncestor(startContainer, endContainer); + // Process left side for (node = startContainer; node; node = node.parentNode) { - if (node == endContainer) + if (node === endContainer) return walkBoundary(startContainer, ancestor, true); - if (node == ancestor) + if (node === ancestor) break; } // Process right side for (node = endContainer; node; node = node.parentNode) { - if (node == startContainer) + if (node === startContainer) return walkBoundary(endContainer, ancestor); - if (node == ancestor) + if (node === ancestor) break; } @@ -7309,48 +9622,46 @@ tinymce.dom.TreeWalker = function(start_node, root_node) { ); if (siblings.length) - callback(siblings); + callback(exclude(siblings)); // Walk right leaf walkBoundary(endContainer, endPoint); }; - /* this.split = function(rng) { + this.split = function(rng) { var startContainer = rng.startContainer, startOffset = rng.startOffset, endContainer = rng.endContainer, endOffset = rng.endOffset; function splitText(node, offset) { - if (offset == node.nodeValue.length) - node.appendData(INVISIBLE_CHAR); - - node = node.splitText(offset); - - if (node.nodeValue === INVISIBLE_CHAR) - node.nodeValue = ''; - - return node; + return node.splitText(offset); }; // Handle single text node - if (startContainer == endContainer) { - if (startContainer.nodeType == 3) { - if (startOffset != 0) - startContainer = endContainer = splitText(startContainer, startOffset); - - if (endOffset - startOffset != startContainer.nodeValue.length) - splitText(startContainer, endOffset - startOffset); + if (startContainer == endContainer && startContainer.nodeType == 3) { + if (startOffset > 0 && startOffset < startContainer.nodeValue.length) { + endContainer = splitText(startContainer, startOffset); + startContainer = endContainer.previousSibling; + + if (endOffset > startOffset) { + endOffset = endOffset - startOffset; + startContainer = endContainer = splitText(endContainer, endOffset).previousSibling; + endOffset = endContainer.nodeValue.length; + startOffset = 0; + } else { + endOffset = 0; + } } } else { // Split startContainer text node if needed - if (startContainer.nodeType == 3 && startOffset != 0) { + if (startContainer.nodeType == 3 && startOffset > 0 && startOffset < startContainer.nodeValue.length) { startContainer = splitText(startContainer, startOffset); startOffset = 0; } // Split endContainer text node if needed - if (endContainer.nodeType == 3 && endOffset != endContainer.nodeValue.length) { + if (endContainer.nodeType == 3 && endOffset > 0 && endOffset < endContainer.nodeValue.length) { endContainer = splitText(endContainer, endOffset).previousSibling; endOffset = endContainer.nodeValue.length; } @@ -7363,7 +9674,7 @@ tinymce.dom.TreeWalker = function(start_node, root_node) { endOffset : endOffset }; }; -*/ + }; tinymce.dom.RangeUtils.compareRanges = function(rng1, rng2) { @@ -7374,17 +9685,164 @@ tinymce.dom.TreeWalker = function(start_node, root_node) { if (rng1.item && rng2.item && rng1.item(0) === rng2.item(0)) return true; - // Both are text ranges and the range matches - if (rng1.isEqual && rng2.isEqual && rng2.isEqual(rng1)) - return true; - } else { - // Compare w3c ranges - return rng1.startContainer == rng2.startContainer && rng1.startOffset == rng2.startOffset; + // Both are text ranges and the range matches + if (rng1.isEqual && rng2.isEqual && rng2.isEqual(rng1)) + return true; + } else { + // Compare w3c ranges + return rng1.startContainer == rng2.startContainer && rng1.startOffset == rng2.startOffset; + } + } + + return false; + }; +})(tinymce); + +(function(tinymce) { + var Event = tinymce.dom.Event, each = tinymce.each; + + tinymce.create('tinymce.ui.KeyboardNavigation', { + KeyboardNavigation: function(settings, dom) { + var t = this, root = settings.root, items = settings.items, + enableUpDown = settings.enableUpDown, enableLeftRight = settings.enableLeftRight || !settings.enableUpDown, + excludeFromTabOrder = settings.excludeFromTabOrder, + itemFocussed, itemBlurred, rootKeydown, rootFocussed, focussedId; + + dom = dom || tinymce.DOM; + + itemFocussed = function(evt) { + focussedId = evt.target.id; + }; + + itemBlurred = function(evt) { + dom.setAttrib(evt.target.id, 'tabindex', '-1'); + }; + + rootFocussed = function(evt) { + var item = dom.get(focussedId); + dom.setAttrib(item, 'tabindex', '0'); + item.focus(); + }; + + t.focus = function() { + dom.get(focussedId).focus(); + }; + + t.destroy = function() { + each(items, function(item) { + dom.unbind(dom.get(item.id), 'focus', itemFocussed); + dom.unbind(dom.get(item.id), 'blur', itemBlurred); + }); + + dom.unbind(dom.get(root), 'focus', rootFocussed); + dom.unbind(dom.get(root), 'keydown', rootKeydown); + + items = dom = root = t.focus = itemFocussed = itemBlurred = rootKeydown = rootFocussed = null; + t.destroy = function() {}; + }; + + t.moveFocus = function(dir, evt) { + var idx = -1, controls = t.controls, newFocus; + + if (!focussedId) + return; + + each(items, function(item, index) { + if (item.id === focussedId) { + idx = index; + return false; + } + }); + + idx += dir; + if (idx < 0) { + idx = items.length - 1; + } else if (idx >= items.length) { + idx = 0; + } + + newFocus = items[idx]; + dom.setAttrib(focussedId, 'tabindex', '-1'); + dom.setAttrib(newFocus.id, 'tabindex', '0'); + dom.get(newFocus.id).focus(); + + if (settings.actOnFocus) { + settings.onAction(newFocus.id); + } + + if (evt) + Event.cancel(evt); + }; + + rootKeydown = function(evt) { + var DOM_VK_LEFT = 37, DOM_VK_RIGHT = 39, DOM_VK_UP = 38, DOM_VK_DOWN = 40, DOM_VK_ESCAPE = 27, DOM_VK_ENTER = 14, DOM_VK_RETURN = 13, DOM_VK_SPACE = 32; + + switch (evt.keyCode) { + case DOM_VK_LEFT: + if (enableLeftRight) t.moveFocus(-1); + break; + + case DOM_VK_RIGHT: + if (enableLeftRight) t.moveFocus(1); + break; + + case DOM_VK_UP: + if (enableUpDown) t.moveFocus(-1); + break; + + case DOM_VK_DOWN: + if (enableUpDown) t.moveFocus(1); + break; + + case DOM_VK_ESCAPE: + if (settings.onCancel) { + settings.onCancel(); + Event.cancel(evt); + } + break; + + case DOM_VK_ENTER: + case DOM_VK_RETURN: + case DOM_VK_SPACE: + if (settings.onAction) { + settings.onAction(focussedId); + Event.cancel(evt); + } + break; + } + }; + + // Set up state and listeners for each item. + each(items, function(item, idx) { + var tabindex; + + if (!item.id) { + item.id = dom.uniqueId('_mce_item_'); + } + + if (excludeFromTabOrder) { + dom.bind(item.id, 'blur', itemBlurred); + tabindex = '-1'; + } else { + tabindex = (idx === 0 ? '0' : '-1'); + } + + dom.setAttrib(item.id, 'tabindex', tabindex); + dom.bind(dom.get(item.id), 'focus', itemFocussed); + }); + + // Setup initial state for root element. + if (items[0]){ + focussedId = items[0].id; } - } - return false; - }; + dom.setAttrib(root, 'tabindex', '-1'); + + // Setup listeners for root element. + dom.bind(dom.get(root), 'focus', rootFocussed); + dom.bind(dom.get(root), 'keydown', rootKeydown); + } + }); })(tinymce); (function(tinymce) { @@ -7392,7 +9850,7 @@ tinymce.dom.TreeWalker = function(start_node, root_node) { var DOM = tinymce.DOM, is = tinymce.is; tinymce.create('tinymce.ui.Control', { - Control : function(id, s) { + Control : function(id, s, editor) { this.id = id; this.settings = s = s || {}; this.rendered = false; @@ -7401,22 +9859,23 @@ tinymce.dom.TreeWalker = function(start_node, root_node) { this.scope = s.scope || this; this.disabled = 0; this.active = 0; + this.editor = editor; + }, + + setAriaProperty : function(property, value) { + var element = DOM.get(this.id + '_aria') || DOM.get(this.id); + if (element) { + DOM.setAttrib(element, 'aria-' + property, !!value); + } + }, + + focus : function() { + DOM.get(this.id).focus(); }, setDisabled : function(s) { - var e; - if (s != this.disabled) { - e = DOM.get(this.id); - - // Add accessibility title for unavailable actions - if (e && this.settings.unavailable_prefix) { - if (s) { - this.prevTitle = e.title; - e.title = this.settings.unavailable_prefix + ": " + e.title; - } else - e.title = this.prevTitle; - } + this.setAriaProperty('disabled', s); this.setState('Disabled', s); this.setState('Enabled', !s); @@ -7432,6 +9891,7 @@ tinymce.dom.TreeWalker = function(start_node, root_node) { if (s != this.active) { this.setState('Active', s); this.active = s; + this.setAriaProperty('pressed', s); } }, @@ -7489,8 +9949,8 @@ tinymce.dom.TreeWalker = function(start_node, root_node) { }); })(tinymce); tinymce.create('tinymce.ui.Container:tinymce.ui.Control', { - Container : function(id, s) { - this.parent(id, s); + Container : function(id, s, editor) { + this.parent(id, s, editor); this.controls = []; @@ -7514,10 +9974,11 @@ tinymce.create('tinymce.ui.Separator:tinymce.ui.Control', { Separator : function(id, s) { this.parent(id, s); this.classPrefix = 'mceSeparator'; + this.setDisabled(true); }, renderHTML : function() { - return tinymce.DOM.createHTML('span', {'class' : this.classPrefix}); + return tinymce.DOM.createHTML('span', {'class' : this.classPrefix, role : 'separator', 'aria-orientation' : 'vertical', tabindex : '-1'}); } }); @@ -7532,6 +9993,7 @@ tinymce.create('tinymce.ui.Separator:tinymce.ui.Control', { setSelected : function(s) { this.setState('Selected', s); + this.setAriaProperty('checked', !!s); this.selected = s; }, @@ -7679,12 +10141,20 @@ tinymce.create('tinymce.ui.Separator:tinymce.ui.Control', { s['class'] = s['class'] || cs['class']; s.vp_offset_x = s.vp_offset_x || cs.vp_offset_x; s.vp_offset_y = s.vp_offset_y || cs.vp_offset_y; + s.keyboard_focus = cs.keyboard_focus; m = new tinymce.ui.DropMenu(s.id || DOM.uniqueId(), s); m.onAddItem.add(t.onAddItem.dispatch, t.onAddItem); return m; }, + + focus : function() { + var t = this; + if (t.keyboardNav) { + t.keyboardNav.focus(); + } + }, update : function() { var t = this, s = t.settings, tb = DOM.get('menu_' + t.id + '_tbl'), co = DOM.get('menu_' + t.id + '_co'), tw, th; @@ -7780,7 +10250,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 } }); @@ -7808,13 +10278,13 @@ tinymce.create('tinymce.ui.Separator:tinymce.ui.Control', { } }); } + + Event.add(co, 'keydown', t._keyHandler, t); t.onShowMenu.dispatch(t); - if (s.keyboard_focus) { - Event.add(co, 'keydown', t._keyHandler, t); - DOM.select('a', 'menu_' + t.id)[0].focus(); // Select first link - t._focusIdx = 0; + if (s.keyboard_focus) { + t._setupKeyboardNav(); } }, @@ -7824,6 +10294,7 @@ tinymce.create('tinymce.ui.Separator:tinymce.ui.Control', { if (!t.isMenuVisible) return; + if (t.keyboardNav) t.keyboardNav.destroy(); Event.remove(co, 'mouseover', t.mouseOverFunc); Event.remove(co, 'click', t.mouseClickFunc); Event.remove(co, 'keydown', t._keyHandler); @@ -7868,8 +10339,11 @@ tinymce.create('tinymce.ui.Separator:tinymce.ui.Control', { destroy : function() { var t = this, co = DOM.get('menu_' + t.id); + if (t.keyboardNav) t.keyboardNav.destroy(); Event.remove(co, 'mouseover', t.mouseOverFunc); + Event.remove(DOM.select('a', co), 'focus', t.mouseOverFunc); Event.remove(co, 'click', t.mouseClickFunc); + Event.remove(co, 'keydown', t._keyHandler); if (t.element) t.element.remove(); @@ -7880,432 +10354,203 @@ tinymce.create('tinymce.ui.Separator:tinymce.ui.Control', { renderNode : function() { var t = this, s = t.settings, n, tb, co, w; - w = DOM.create('div', {id : 'menu_' + t.id, 'class' : s['class'], 'style' : 'position:absolute;left:0;top:0;z-index:200000'}); - co = DOM.add(w, 'div', {id : 'menu_' + t.id + '_co', 'class' : t.classPrefix + (s['class'] ? ' ' + s['class'] : '')}); + w = DOM.create('div', {role: 'listbox', id : 'menu_' + t.id, 'class' : s['class'], 'style' : 'position:absolute;left:0;top:0;z-index:200000;outline:0'}); + if (t.settings.parent) { + DOM.setAttrib(w, 'aria-parent', 'menu_' + t.settings.parent.id); + } + co = DOM.add(w, 'div', {role: 'presentation', id : 'menu_' + t.id + '_co', 'class' : t.classPrefix + (s['class'] ? ' ' + s['class'] : '')}); t.element = new Element('menu_' + t.id, {blocker : 1, container : s.container}); if (s.menu_line) DOM.add(co, 'span', {'class' : t.classPrefix + 'Line'}); // n = DOM.add(co, 'div', {id : 'menu_' + t.id + '_co', 'class' : 'mceMenuContainer'}); - n = DOM.add(co, 'table', {id : 'menu_' + t.id + '_tbl', border : 0, cellPadding : 0, cellSpacing : 0}); - tb = DOM.add(n, 'tbody'); - - each(t.items, function(o) { - t._add(tb, o); - }); - - t.rendered = true; - - return w; - }, - - // Internal functions - - _keyHandler : function(e) { - var t = this, kc = e.keyCode; - - function focus(d) { - var i = t._focusIdx + d, e = DOM.select('a', 'menu_' + t.id)[i]; - - if (e) { - t._focusIdx = i; - e.focus(); - } - }; - - switch (kc) { - case 38: - focus(-1); // Select first link - return; - - case 40: - focus(1); - return; - - case 13: - return; - - case 27: - return this.hideMenu(); - } - }, - - _add : function(tb, o) { - var n, s = o.settings, a, ro, it, cp = this.classPrefix, ic; - - if (s.separator) { - ro = DOM.add(tb, 'tr', {id : o.id, 'class' : cp + 'ItemSeparator'}); - DOM.add(ro, 'td', {'class' : cp + 'ItemSeparator'}); - - if (n = ro.previousSibling) - DOM.addClass(n, 'mceLast'); - - return; - } - - n = ro = DOM.add(tb, 'tr', {id : o.id, 'class' : cp + 'Item ' + cp + 'ItemEnabled'}); - n = it = DOM.add(n, 'td'); - n = a = DOM.add(n, 'a', {href : 'javascript:;', onclick : "return false;", onmousedown : 'return false;'}); - - DOM.addClass(it, s['class']); -// n = DOM.add(n, 'span', {'class' : 'item'}); - - ic = DOM.add(n, 'span', {'class' : 'mceIcon' + (s.icon ? ' mce_' + s.icon : '')}); - - if (s.icon_src) - DOM.add(ic, 'img', {src : s.icon_src}); - - n = DOM.add(n, s.element || 'span', {'class' : 'mceText', title : o.settings.title}, o.settings.title); - - if (o.settings.style) - DOM.setAttrib(n, 'style', o.settings.style); - - if (tb.childNodes.length == 1) - DOM.addClass(ro, 'mceFirst'); - - if ((n = ro.previousSibling) && DOM.hasClass(n, cp + 'ItemSeparator')) - DOM.addClass(ro, 'mceFirst'); - - if (o.collapse) - DOM.addClass(ro, cp + 'ItemSub'); - - if (n = ro.previousSibling) - DOM.removeClass(n, 'mceLast'); - - DOM.addClass(ro, 'mceLast'); - } - }); -})(tinymce); -(function(tinymce) { - var DOM = tinymce.DOM; - - tinymce.create('tinymce.ui.Button:tinymce.ui.Control', { - Button : function(id, s) { - this.parent(id, s); - this.classPrefix = 'mceButton'; - }, - - renderHTML : function() { - var cp = this.classPrefix, s = this.settings, h, l; - - l = DOM.encode(s.label || ''); - h = ''; - - if (s.image) - h += '' + l + ''; - else - h += '' + (l ? '' + l + '' : '') + ''; - - return h; - }, - - postRender : function() { - var t = this, s = t.settings; - - tinymce.dom.Event.add(t.id, 'click', function(e) { - if (!t.isDisabled()) - return s.onclick.call(s.scope, e); - }); - } - }); -})(tinymce); - -(function(tinymce) { - var DOM = tinymce.DOM, Event = tinymce.dom.Event, each = tinymce.each, Dispatcher = tinymce.util.Dispatcher; - - tinymce.create('tinymce.ui.ListBox:tinymce.ui.Control', { - ListBox : function(id, s) { - var t = this; - - t.parent(id, s); - - t.items = []; - - t.onChange = new Dispatcher(t); - - t.onPostRender = new Dispatcher(t); - - t.onAdd = new Dispatcher(t); - - t.onRenderMenu = new tinymce.util.Dispatcher(this); - - t.classPrefix = 'mceListBox'; - }, - - select : function(va) { - var t = this, fv, f; - - if (va == undefined) - return t.selectByIndex(-1); - - // Is string or number make function selector - if (va && va.call) - f = va; - else { - f = function(v) { - return v == va; - }; - } - - // Do we need to do something? - if (va != t.selectedValue) { - // Find item - each(t.items, function(o, i) { - if (f(o.value)) { - fv = 1; - t.selectByIndex(i); - return false; - } - }); - - if (!fv) - t.selectByIndex(-1); - } - }, - - selectByIndex : function(idx) { - var t = this, e, o; - - if (idx != t.selectedIndex) { - e = DOM.get(t.id + '_text'); - o = t.items[idx]; - - if (o) { - t.selectedValue = o.value; - t.selectedIndex = idx; - DOM.setHTML(e, DOM.encode(o.title)); - DOM.removeClass(e, 'mceTitle'); - } else { - DOM.setHTML(e, DOM.encode(t.settings.title)); - DOM.addClass(e, 'mceTitle'); - t.selectedValue = t.selectedIndex = null; - } - - e = 0; - } - }, - - add : function(n, v, o) { - var t = this; - - o = o || {}; - o = tinymce.extend(o, { - title : n, - value : v - }); - - t.items.push(o); - t.onAdd.dispatch(t, o); - }, - - getLength : function() { - return this.items.length; - }, - - renderHTML : function() { - var h = '', t = this, s = t.settings, cp = t.classPrefix; - - h = ''; - h += ''; - h += ''; - h += '
    ' + DOM.createHTML('a', {id : t.id + '_text', href : 'javascript:;', 'class' : 'mceText', onclick : "return false;", onmousedown : 'return false;'}, DOM.encode(t.settings.title)) + '' + DOM.createHTML('a', {id : t.id + '_open', tabindex : -1, href : 'javascript:;', 'class' : 'mceOpen', onclick : "return false;", onmousedown : 'return false;'}, '') + '
    '; - - return h; - }, - - showMenu : function() { - var t = this, p1, p2, e = DOM.get(this.id), m; - - if (t.isDisabled() || t.items.length == 0) - return; - - if (t.menu && t.menu.isMenuVisible) - return t.hideMenu(); - - if (!t.isMenuRendered) { - t.renderMenu(); - t.isMenuRendered = true; - } - - p1 = DOM.getPos(this.settings.menu_container); - p2 = DOM.getPos(e); - - m = t.menu; - m.settings.offset_x = p2.x; - m.settings.offset_y = p2.y; - 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); + n = DOM.add(co, 'table', {role: 'presentation', id : 'menu_' + t.id + '_tbl', border : 0, cellPadding : 0, cellSpacing : 0}); + tb = DOM.add(n, 'tbody'); each(t.items, function(o) { - if (o.value === t.selectedValue) { - m.items[o.id].setSelected(1); - t.oldID = o.id; - } + t._add(tb, o); }); - m.showMenu(0, e.clientHeight); - - Event.add(DOM.doc, 'mousedown', t.hideMenu, t); - DOM.addClass(t.id, t.classPrefix + 'Selected'); + t.rendered = true; - //DOM.get(t.id + '_text').focus(); + return w; }, - hideMenu : function(e) { - var t = this; - - if (t.menu && t.menu.isMenuVisible) { - // Prevent double toogles by canceling the mouse click event to the button - if (e && e.type == "mousedown" && (e.target.id == t.id + '_text' || e.target.id == t.id + '_open')) - return; + // Internal functions + _setupKeyboardNav : function(){ + var contextMenu, menuItems, t=this; + 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({ + root: 'menu_' + t.id, + items: menuItems, + onCancel: function() { + t.hideMenu(); + }, + enableUpDown: true + }); + contextMenu.focus(); + }, - if (!e || !DOM.getParent(e.target, '.mceMenu')) { - DOM.removeClass(t.id, t.classPrefix + 'Selected'); - Event.remove(DOM.doc, 'mousedown', t.hideMenu, t); - t.menu.hideMenu(); - } + _keyHandler : function(evt) { + var t = this, e; + switch (evt.keyCode) { + case 37: // Left + if (t.settings.parent) { + t.hideMenu(); + t.settings.parent.focus(); + Event.cancel(evt); + } + break; + case 39: // Right + if (t.mouseOverFunc) + t.mouseOverFunc(evt); + break; } }, - renderMenu : function() { - var t = this, m; + _add : function(tb, o) { + var n, s = o.settings, a, ro, it, cp = this.classPrefix, ic; - m = t.settings.control_manager.createDropMenu(t.id + '_menu', { - menu_line : 1, - 'class' : t.classPrefix + 'Menu mceNoIcons', - max_width : 150, - max_height : 150 - }); + if (s.separator) { + ro = DOM.add(tb, 'tr', {id : o.id, 'class' : cp + 'ItemSeparator'}); + DOM.add(ro, 'td', {'class' : cp + 'ItemSeparator'}); - m.onHideMenu.add(t.hideMenu, t); + if (n = ro.previousSibling) + DOM.addClass(n, 'mceLast'); - m.add({ - title : t.settings.title, - 'class' : 'mceMenuItemTitle', - onclick : function() { - if (t.settings.onselect('') !== false) - t.select(''); // Must be runned after - } - }); + return; + } - each(t.items, function(o) { - // No value then treat it as a title - if (o.value === undefined) { - m.add({ - title : o.title, - 'class' : 'mceMenuItemTitle', - onclick : function() { - if (t.settings.onselect('') !== false) - t.select(''); // Must be runned after - } - }); - } else { - o.id = DOM.uniqueId(); - o.onclick = function() { - if (t.settings.onselect(o.value) !== false) - t.select(o.value); // Must be runned after - }; + n = ro = DOM.add(tb, 'tr', {id : o.id, 'class' : cp + 'Item ' + cp + 'ItemEnabled'}); + n = it = DOM.add(n, s.titleItem ? 'th' : 'td'); + n = a = DOM.add(n, 'a', {id: o.id + '_aria', role: s.titleItem ? 'presentation' : 'option', href : 'javascript:;', onclick : "return false;", onmousedown : 'return false;'}); - m.add(o); - } - }); + if (s.parent) { + DOM.setAttrib(a, 'aria-haspopup', 'true'); + DOM.setAttrib(a, 'aria-owns', 'menu_' + o.id); + } - t.onRenderMenu.dispatch(t, m); - t.menu = m; - }, + DOM.addClass(it, s['class']); +// n = DOM.add(n, 'span', {'class' : 'item'}); - postRender : function() { - var t = this, cp = t.classPrefix; + ic = DOM.add(n, 'span', {'class' : 'mceIcon' + (s.icon ? ' mce_' + s.icon : '')}); - Event.add(t.id, 'click', t.showMenu, t); - Event.add(t.id + '_text', 'focus', function() { - if (!t._focused) { - t.keyDownHandler = Event.add(t.id + '_text', 'keydown', function(e) { - var idx = -1, v, kc = e.keyCode; + if (s.icon_src) + DOM.add(ic, 'img', {src : s.icon_src}); - // Find current index - each(t.items, function(v, i) { - if (t.selectedValue == v.value) - idx = i; - }); + n = DOM.add(n, s.element || 'span', {'class' : 'mceText', title : o.settings.title}, o.settings.title); - // Move up/down - if (kc == 38) - v = t.items[idx - 1]; - else if (kc == 40) - v = t.items[idx + 1]; - else if (kc == 13) { - // Fake select on enter - v = t.selectedValue; - t.selectedValue = null; // Needs to be null to fake change - t.settings.onselect(v); - return Event.cancel(e); - } + if (o.settings.style) { + if (typeof o.settings.style == "function") + o.settings.style = o.settings.style(); - if (v) { - t.hideMenu(); - t.select(v.value); - } - }); - } + DOM.setAttrib(n, 'style', o.settings.style); + } - t._focused = 1; - }); - Event.add(t.id + '_text', 'blur', function() {Event.remove(t.id + '_text', 'keydown', t.keyDownHandler); t._focused = 0;}); + if (tb.childNodes.length == 1) + DOM.addClass(ro, 'mceFirst'); - // Old IE doesn't have hover on all elements - if (tinymce.isIE6 || !DOM.boxModel) { - Event.add(t.id, 'mouseover', function() { - if (!DOM.hasClass(t.id, cp + 'Disabled')) - DOM.addClass(t.id, cp + 'Hover'); - }); + if ((n = ro.previousSibling) && DOM.hasClass(n, cp + 'ItemSeparator')) + DOM.addClass(ro, 'mceFirst'); - Event.add(t.id, 'mouseout', function() { - if (!DOM.hasClass(t.id, cp + 'Disabled')) - DOM.removeClass(t.id, cp + 'Hover'); - }); - } + if (o.collapse) + DOM.addClass(ro, cp + 'ItemSub'); - t.onPostRender.dispatch(t, DOM.get(t.id)); + if (n = ro.previousSibling) + DOM.removeClass(n, 'mceLast'); + + DOM.addClass(ro, 'mceLast'); + } + }); +})(tinymce); +(function(tinymce) { + var DOM = tinymce.DOM; + + tinymce.create('tinymce.ui.Button:tinymce.ui.Control', { + Button : function(id, s, ed) { + this.parent(id, s, ed); + this.classPrefix = 'mceButton'; }, - destroy : function() { - this.parent(); + renderHTML : function() { + var cp = this.classPrefix, s = this.settings, h, l; - Event.clear(this.id + '_text'); - Event.clear(this.id + '_open'); + l = DOM.encode(s.label || ''); + h = ''; + if (s.image && !(this.editor &&this.editor.forcedHighContrastMode) ) + h += '' + DOM.encode(s.title) + '' + l; + else + h += '' + (l ? '' + l + '' : ''); + + h += ''; + h += ''; + return h; + }, + + postRender : function() { + 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()) { + // 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); + }); } }); })(tinymce); + (function(tinymce) { var DOM = tinymce.DOM, Event = tinymce.dom.Event, each = tinymce.each, Dispatcher = tinymce.util.Dispatcher; - tinymce.create('tinymce.ui.NativeListBox:tinymce.ui.ListBox', { - NativeListBox : function(id, s) { - this.parent(id, s); - this.classPrefix = 'mceNativeListBox'; - }, + tinymce.create('tinymce.ui.ListBox:tinymce.ui.Control', { + ListBox : function(id, s, ed) { + var t = this; - setDisabled : function(s) { - DOM.get(this.id).disabled = s; - }, + t.parent(id, s, ed); - isDisabled : function() { - return DOM.get(this.id).disabled; + t.items = []; + + t.onChange = new Dispatcher(t); + + t.onPostRender = new Dispatcher(t); + + t.onAdd = new Dispatcher(t); + + t.onRenderMenu = new tinymce.util.Dispatcher(this); + + t.classPrefix = 'mceListBox'; + t.marked = {}; }, select : function(va) { var t = this, fv, f; + t.marked = {}; + if (va == undefined) 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) { @@ -8330,4003 +10575,4041 @@ tinymce.create('tinymce.ui.Separator:tinymce.ui.Control', { }, selectByIndex : function(idx) { - DOM.get(this.id).selectedIndex = idx + 1; - this.selectedValue = this.items[idx] ? this.items[idx].value : null; - }, - - add : function(n, v, a) { - var o, t = this; + var t = this, e, o, label; - a = a || {}; - a.value = v; - - if (t.isRendered()) - DOM.add(DOM.get(this.id), 'option', a, n); - - o = { - title : n, - value : v, - attribs : a - }; - - t.items.push(o); - t.onAdd.dispatch(t, o); - }, - - getLength : function() { - return this.items.length; - }, - - renderHTML : function() { - var h, t = this; - - h = DOM.createHTML('option', {value : ''}, '-- ' + t.settings.title + ' --'); - - each(t.items, function(it) { - h += DOM.createHTML('option', {value : it.value}, it.title); - }); - - h = DOM.createHTML('select', {id : t.id, 'class' : 'mceNativeListBox'}, h); - - return h; - }, + t.marked = {}; - postRender : function() { - var t = this, ch; - - t.rendered = true; - - function onChange(e) { - var v = t.items[e.target.selectedIndex - 1]; - - if (v && (v = v.value)) { - t.onChange.dispatch(t, v); + if (idx != t.selectedIndex) { + e = DOM.get(t.id + '_text'); + label = DOM.get(t.id + '_voiceDesc'); + o = t.items[idx]; - if (t.settings.onselect) - t.settings.onselect(v); + if (o) { + t.selectedValue = o.value; + t.selectedIndex = idx; + DOM.setHTML(e, DOM.encode(o.title)); + DOM.setHTML(label, t.settings.title + " - " + o.title); + DOM.removeClass(e, 'mceTitle'); + DOM.setAttrib(t.id, 'aria-valuenow', o.title); + } else { + DOM.setHTML(e, DOM.encode(t.settings.title)); + DOM.setHTML(label, DOM.encode(t.settings.title)); + DOM.addClass(e, 'mceTitle'); + t.selectedValue = t.selectedIndex = null; + DOM.setAttrib(t.id, 'aria-valuenow', t.settings.title); } - }; - - Event.add(t.id, 'change', onChange); - - // Accessibility keyhandler - Event.add(t.id, 'keydown', function(e) { - var bf; + e = 0; + } + }, - Event.remove(t.id, 'change', ch); + mark : function(value) { + this.marked[value] = true; + }, - bf = Event.add(t.id, 'blur', function() { - Event.add(t.id, 'change', onChange); - Event.remove(t.id, 'blur', bf); - }); + add : function(n, v, o) { + var t = this; - if (e.keyCode == 13 || e.keyCode == 32) { - onChange(e); - return Event.cancel(e); - } + o = o || {}; + o = tinymce.extend(o, { + title : n, + value : v }); - t.onPostRender.dispatch(t, DOM.get(t.id)); - } - }); -})(tinymce); -(function(tinymce) { - var DOM = tinymce.DOM, Event = tinymce.dom.Event, each = tinymce.each; + t.items.push(o); + t.onAdd.dispatch(t, o); + }, - tinymce.create('tinymce.ui.MenuButton:tinymce.ui.Button', { - MenuButton : function(id, s) { - this.parent(id, s); + getLength : function() { + return this.items.length; + }, - this.onRenderMenu = new tinymce.util.Dispatcher(this); + renderHTML : function() { + var h = '', t = this, s = t.settings, cp = t.classPrefix; - s.menu_container = s.menu_container || DOM.doc.body; + h = ''; + h += ''; + h += ''; + h += ''; + + return h; }, showMenu : function() { - var t = this, p1, p2, e = DOM.get(t.id), m; + var t = this, p2, e = DOM.get(this.id), m; - if (t.isDisabled()) + if (t.isDisabled() || t.items.length == 0) return; + if (t.menu && t.menu.isMenuVisible) + return t.hideMenu(); + if (!t.isMenuRendered) { t.renderMenu(); t.isMenuRendered = true; } - if (t.isMenuVisible) - return t.hideMenu(); - - p1 = DOM.getPos(t.settings.menu_container); p2 = DOM.getPos(e); m = t.menu; m.settings.offset_x = p2.x; m.settings.offset_y = p2.y; - 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); - - Event.add(DOM.doc, 'mousedown', t.hideMenu, t); - t.setState('Selected', 1); + m.settings.keyboard_focus = !tinymce.isOpera; // Opera is buggy when it comes to auto focus - t.isMenuVisible = 1; - }, + // Select in menu + each(t.items, function(o) { + if (m.items[o.id]) { + m.items[o.id].setSelected(0); + } + }); - renderMenu : function() { - var t = this, m; + each(t.items, function(o) { + if (m.items[o.id] && t.marked[o.value]) { + m.items[o.id].setSelected(1); + } - m = t.settings.control_manager.createDropMenu(t.id + '_menu', { - menu_line : 1, - 'class' : this.classPrefix + 'Menu', - icons : t.settings.icons + if (o.value === t.selectedValue) { + m.items[o.id].setSelected(1); + } }); - m.onHideMenu.add(t.hideMenu, t); + m.showMenu(0, e.clientHeight); - t.onRenderMenu.dispatch(t, m); - t.menu = m; + Event.add(DOM.doc, 'mousedown', t.hideMenu, t); + DOM.addClass(t.id, t.classPrefix + 'Selected'); + + //DOM.get(t.id + '_text').focus(); }, hideMenu : function(e) { var t = this; - // Prevent double toogles by canceling the mouse click event to the button - if (e && e.type == "mousedown" && DOM.getParent(e.target, function(e) {return e.id === t.id || e.id === t.id + '_open';})) - return; + if (t.menu && t.menu.isMenuVisible) { + DOM.removeClass(t.id, t.classPrefix + 'Selected'); - if (!e || !DOM.getParent(e.target, '.mceMenu')) { - t.setState('Selected', 0); - Event.remove(DOM.doc, 'mousedown', t.hideMenu, t); - if (t.menu) + // Prevent double toogles by canceling the mouse click event to the button + if (e && e.type == "mousedown" && (e.target.id == t.id + '_text' || e.target.id == t.id + '_open')) + return; + + if (!e || !DOM.getParent(e.target, '.mceMenu')) { + DOM.removeClass(t.id, t.classPrefix + 'Selected'); + Event.remove(DOM.doc, 'mousedown', t.hideMenu, t); t.menu.hideMenu(); + } } - - t.isMenuVisible = 0; }, - postRender : function() { - var t = this, s = t.settings; - - Event.add(t.id, 'click', function() { - if (!t.isDisabled()) { - if (s.onclick) - s.onclick(t.value); + renderMenu : function() { + var t = this, m; - t.showMenu(); - } + m = t.settings.control_manager.createDropMenu(t.id + '_menu', { + menu_line : 1, + 'class' : t.classPrefix + 'Menu mceNoIcons', + max_width : 150, + max_height : 150 }); - } - }); -})(tinymce); - -(function(tinymce) { - var DOM = tinymce.DOM, Event = tinymce.dom.Event, each = tinymce.each; - - tinymce.create('tinymce.ui.SplitButton:tinymce.ui.MenuButton', { - SplitButton : function(id, s) { - this.parent(id, s); - this.classPrefix = 'mceSplitButton'; - }, - - renderHTML : function() { - var h, t = this, s = t.settings, h1; - h = ''; + m.onHideMenu.add(function() { + t.hideMenu(); + t.focus(); + }); - if (s.image) - h1 = DOM.createHTML('img ', {src : s.image, 'class' : 'mceAction ' + s['class']}); - else - h1 = DOM.createHTML('span', {'class' : 'mceAction ' + s['class']}, ''); + m.add({ + title : t.settings.title, + 'class' : 'mceMenuItemTitle', + onclick : function() { + if (t.settings.onselect('') !== false) + t.select(''); // Must be runned after + } + }); - h += '' + DOM.createHTML('a', {id : t.id + '_action', href : 'javascript:;', 'class' : 'mceAction ' + s['class'], onclick : "return false;", onmousedown : 'return false;', title : s.title}, h1) + ''; - - h1 = DOM.createHTML('span', {'class' : 'mceOpen ' + s['class']}); - h += '' + DOM.createHTML('a', {id : t.id + '_open', href : 'javascript:;', 'class' : 'mceOpen ' + s['class'], onclick : "return false;", onmousedown : 'return false;', title : s.title}, h1) + ''; + each(t.items, function(o) { + // No value then treat it as a title + if (o.value === undefined) { + m.add({ + title : o.title, + role : "option", + 'class' : 'mceMenuItemTitle', + onclick : function() { + if (t.settings.onselect('') !== false) + t.select(''); // Must be runned after + } + }); + } else { + o.id = DOM.uniqueId(); + o.role= "option"; + o.onclick = function() { + if (t.settings.onselect(o.value) !== false) + t.select(o.value); // Must be runned after + }; - h += ''; + m.add(o); + } + }); - return DOM.createHTML('table', {id : t.id, 'class' : 'mceSplitButton mceSplitButtonEnabled ' + s['class'], cellpadding : '0', cellspacing : '0', onmousedown : 'return false;', title : s.title}, h); + t.onRenderMenu.dispatch(t, m); + t.menu = m; }, postRender : function() { - var t = this, s = t.settings; - - if (s.onclick) { - Event.add(t.id + '_action', 'click', function() { - if (!t.isDisabled()) - s.onclick(t.value); - }); - } - - Event.add(t.id + '_open', 'click', t.showMenu, t); - Event.add(t.id + '_open', 'focus', function() {t._focused = 1;}); - Event.add(t.id + '_open', 'blur', function() {t._focused = 0;}); - - // Old IE doesn't have hover on all elements - if (tinymce.isIE6 || !DOM.boxModel) { - Event.add(t.id, 'mouseover', function() { - if (!DOM.hasClass(t.id, 'mceSplitButtonDisabled')) - DOM.addClass(t.id, 'mceSplitButtonHover'); - }); - - Event.add(t.id, 'mouseout', function() { - if (!DOM.hasClass(t.id, 'mceSplitButtonDisabled')) - DOM.removeClass(t.id, 'mceSplitButtonHover'); - }); - } - }, - - destroy : function() { - this.parent(); - - Event.clear(this.id + '_action'); - Event.clear(this.id + '_open'); - } - }); -})(tinymce); - -(function(tinymce) { - var DOM = tinymce.DOM, Event = tinymce.dom.Event, is = tinymce.is, each = tinymce.each; - - tinymce.create('tinymce.ui.ColorSplitButton:tinymce.ui.SplitButton', { - ColorSplitButton : function(id, s) { - var t = this; - - t.parent(id, s); - - t.settings = s = tinymce.extend({ - colors : '000000,993300,333300,003300,003366,000080,333399,333333,800000,FF6600,808000,008000,008080,0000FF,666699,808080,FF0000,FF9900,99CC00,339966,33CCCC,3366FF,800080,999999,FF00FF,FFCC00,FFFF00,00FF00,00FFFF,00CCFF,993366,C0C0C0,FF99CC,FFCC99,FFFF99,CCFFCC,CCFFFF,99CCFF,CC99FF,FFFFFF', - grid_width : 8, - default_color : '#888888' - }, t.settings); - - t.onShowMenu = new tinymce.util.Dispatcher(t); - - t.onHideMenu = new tinymce.util.Dispatcher(t); - - t.value = s.default_color; - }, - - showMenu : function() { - var t = this, r, p, e, p2; - - if (t.isDisabled()) - return; - - if (!t.isMenuRendered) { - t.renderMenu(); - t.isMenuRendered = true; - } - - if (t.isMenuVisible) - return t.hideMenu(); + var t = this, cp = t.classPrefix; - e = DOM.get(t.id); - DOM.show(t.id + '_menu'); - DOM.addClass(e, 'mceSplitButtonSelected'); - p2 = DOM.getPos(e); - DOM.setStyles(t.id + '_menu', { - left : p2.x, - top : p2.y + e.clientHeight, - zIndex : 200000 + Event.add(t.id, 'click', t.showMenu, t); + Event.add(t.id, 'keydown', function(evt) { + if (evt.keyCode == 32) { // Space + t.showMenu(evt); + Event.cancel(evt); + } }); - e = 0; + Event.add(t.id, 'focus', function() { + if (!t._focused) { + t.keyDownHandler = Event.add(t.id, 'keydown', function(e) { + if (e.keyCode == 40) { + t.showMenu(); + Event.cancel(e); + } + }); + t.keyPressHandler = Event.add(t.id, 'keypress', function(e) { + var v; + if (e.keyCode == 13) { + // Fake select on enter + v = t.selectedValue; + t.selectedValue = null; // Needs to be null to fake change + Event.cancel(e); + t.settings.onselect(v); + } + }); + } - Event.add(DOM.doc, 'mousedown', t.hideMenu, t); - t.onShowMenu.dispatch(t); + t._focused = 1; + }); + Event.add(t.id, 'blur', function() { + Event.remove(t.id, 'keydown', t.keyDownHandler); + Event.remove(t.id, 'keypress', t.keyPressHandler); + t._focused = 0; + }); - if (t._focused) { - t._keyHandler = Event.add(t.id + '_menu', 'keydown', function(e) { - if (e.keyCode == 27) - t.hideMenu(); + // Old IE doesn't have hover on all elements + if (tinymce.isIE6 || !DOM.boxModel) { + Event.add(t.id, 'mouseover', function() { + if (!DOM.hasClass(t.id, cp + 'Disabled')) + DOM.addClass(t.id, cp + 'Hover'); }); - DOM.select('a', t.id + '_menu')[0].focus(); // Select first link + Event.add(t.id, 'mouseout', function() { + if (!DOM.hasClass(t.id, cp + 'Disabled')) + DOM.removeClass(t.id, cp + 'Hover'); + }); } - t.isMenuVisible = 1; + t.onPostRender.dispatch(t, DOM.get(t.id)); }, - hideMenu : function(e) { - var t = this; - - // Prevent double toogles by canceling the mouse click event to the button - if (e && e.type == "mousedown" && DOM.getParent(e.target, function(e) {return e.id === t.id + '_open';})) - return; + destroy : function() { + this.parent(); - if (!e || !DOM.getParent(e.target, '.mceSplitButtonMenu')) { - DOM.removeClass(t.id, 'mceSplitButtonSelected'); - Event.remove(DOM.doc, 'mousedown', t.hideMenu, t); - Event.remove(t.id + '_menu', 'keydown', t._keyHandler); - DOM.hide(t.id + '_menu'); - } + Event.clear(this.id + '_text'); + Event.clear(this.id + '_open'); + } + }); +})(tinymce); - t.onHideMenu.dispatch(t); +(function(tinymce) { + var DOM = tinymce.DOM, Event = tinymce.dom.Event, each = tinymce.each, Dispatcher = tinymce.util.Dispatcher; - t.isMenuVisible = 0; + tinymce.create('tinymce.ui.NativeListBox:tinymce.ui.ListBox', { + NativeListBox : function(id, s) { + this.parent(id, s); + this.classPrefix = 'mceNativeListBox'; }, - renderMenu : function() { - var t = this, m, i = 0, s = t.settings, n, tb, tr, w; - - w = DOM.add(s.menu_container, 'div', {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'}); + setDisabled : function(s) { + DOM.get(this.id).disabled = s; + this.setAriaProperty('disabled', s); + }, - n = DOM.add(m, 'table', {'class' : 'mceColorSplitMenu'}); - tb = DOM.add(n, 'tbody'); + isDisabled : function() { + return DOM.get(this.id).disabled; + }, - // Generate color grid - i = 0; - each(is(s.colors, 'array') ? s.colors : s.colors.split(','), function(c) { - c = c.replace(/^#/, ''); + select : function(va) { + var t = this, fv, f; - if (!i--) { - tr = DOM.add(tb, 'tr'); - i = s.grid_width - 1; - } + if (va == undefined) + return t.selectByIndex(-1); - n = DOM.add(tr, 'td'); + // Is string or number make function selector + if (va && typeof(va)=="function") + f = va; + else { + f = function(v) { + return v == va; + }; + } - n = DOM.add(n, 'a', { - href : 'javascript:;', - style : { - backgroundColor : '#' + c - }, - _mce_color : '#' + c + // Do we need to do something? + if (va != t.selectedValue) { + // Find item + each(t.items, function(o, i) { + if (f(o.value)) { + fv = 1; + t.selectByIndex(i); + return false; + } }); - }); - if (s.more_colors_func) { - n = DOM.add(tb, 'tr'); - n = DOM.add(n, 'td', {colspan : s.grid_width, 'class' : 'mceMoreColors'}); - n = DOM.add(n, 'a', {id : t.id + '_more', href : 'javascript:;', onclick : 'return false;', 'class' : 'mceMoreColors'}, s.more_colors_title); - - Event.add(n, 'click', function(e) { - s.more_colors_func.call(s.more_colors_scope || this); - return Event.cancel(e); // Cancel to fix onbeforeunload problem - }); + if (!fv) + t.selectByIndex(-1); } + }, - DOM.addClass(m, 'mceColorSplitMenu'); + selectByIndex : function(idx) { + DOM.get(this.id).selectedIndex = idx + 1; + this.selectedValue = this.items[idx] ? this.items[idx].value : null; + }, - Event.add(t.id + '_menu', 'click', function(e) { - var c; + add : function(n, v, a) { + var o, t = this; - e = e.target; + a = a || {}; + a.value = v; - if (e.nodeName == 'A' && (c = e.getAttribute('_mce_color'))) - t.setColor(c); + if (t.isRendered()) + DOM.add(DOM.get(this.id), 'option', a, n); - return Event.cancel(e); // Prevent IE auto save warning - }); + o = { + title : n, + value : v, + attribs : a + }; - return w; + t.items.push(o); + t.onAdd.dispatch(t, o); }, - setColor : function(c) { - var t = this; + getLength : function() { + return this.items.length; + }, - DOM.setStyle(t.id + '_preview', 'backgroundColor', c); + renderHTML : function() { + var h, t = this; - t.value = c; - t.hideMenu(); - t.settings.onselect(c); - }, + h = DOM.createHTML('option', {value : ''}, '-- ' + t.settings.title + ' --'); - postRender : function() { - var t = this, id = t.id; + each(t.items, function(it) { + h += DOM.createHTML('option', {value : it.value}, it.title); + }); - t.parent(); - DOM.add(id + '_action', 'div', {id : id + '_preview', 'class' : 'mceColorPreview'}); - DOM.setStyle(t.id + '_preview', 'backgroundColor', t.value); + h = DOM.createHTML('select', {id : t.id, 'class' : 'mceNativeListBox', 'aria-labelledby': t.id + '_aria'}, h); + h += DOM.createHTML('span', {id : t.id + '_aria', 'style': 'display: none'}, t.settings.title); + return h; }, - destroy : function() { - this.parent(); + postRender : function() { + var t = this, ch, changeListenerAdded = true; - Event.clear(this.id + '_menu'); - Event.clear(this.id + '_more'); - DOM.remove(this.id + '_menu'); - } - }); -})(tinymce); + t.rendered = true; -tinymce.create('tinymce.ui.Toolbar:tinymce.ui.Container', { - renderHTML : function() { - var t = this, h = '', c, co, dom = tinymce.DOM, s = t.settings, i, pr, nx, cl; + function onChange(e) { + var v = t.items[e.target.selectedIndex - 1]; - cl = t.controls; - for (i=0; i')); - } + // Accessibility keyhandler + Event.add(t.id, 'keydown', function(e) { + var bf; - // Add toolbar end before list box and after the previous button - // This is to fix the o2k7 editor skins - if (pr && co.ListBox) { - if (pr.Button || pr.SplitButton) - h += dom.createHTML('td', {'class' : 'mceToolbarEnd'}, dom.createHTML('span', null, '')); - } + Event.remove(t.id, 'change', ch); + changeListenerAdded = false; - // Render control HTML + bf = Event.add(t.id, 'blur', function() { + if (changeListenerAdded) return; + changeListenerAdded = true; + Event.add(t.id, 'change', onChange); + Event.remove(t.id, 'blur', bf); + }); - // IE 8 quick fix, needed to propertly generate a hit area for anchors - if (dom.stdMode) - h += '' + co.renderHTML() + ''; - else - h += '' + co.renderHTML() + ''; + //prevent default left and right keys on chrome - so that the keyboard navigation is used. + if (tinymce.isWebKit && (e.keyCode==37 ||e.keyCode==39)) { + return Event.prevent(e); + } + + if (e.keyCode == 13 || e.keyCode == 32) { + onChange(e); + return Event.cancel(e); + } + }); - // Add toolbar start after list box and before the next button - // This is to fix the o2k7 editor skins - if (nx && co.ListBox) { - if (nx.Button || nx.SplitButton) - h += dom.createHTML('td', {'class' : 'mceToolbarStart'}, dom.createHTML('span', null, '')); - } + t.onPostRender.dispatch(t, DOM.get(t.id)); } + }); +})(tinymce); - c = 'mceToolbarEnd'; +(function(tinymce) { + var DOM = tinymce.DOM, Event = tinymce.dom.Event, each = tinymce.each; - if (co.Button) - c += ' mceToolbarEndButton'; - else if (co.SplitButton) - c += ' mceToolbarEndSplitButton'; - else if (co.ListBox) - c += ' mceToolbarEndListBox'; + tinymce.create('tinymce.ui.MenuButton:tinymce.ui.Button', { + MenuButton : function(id, s, ed) { + this.parent(id, s, ed); - h += dom.createHTML('td', {'class' : c}, dom.createHTML('span', null, '')); + this.onRenderMenu = new tinymce.util.Dispatcher(this); - return dom.createHTML('table', {id : t.id, 'class' : 'mceToolbar' + (s['class'] ? ' ' + s['class'] : ''), cellpadding : '0', cellspacing : '0', align : t.settings.align || ''}, '' + h + ''); - } -}); + s.menu_container = s.menu_container || DOM.doc.body; + }, -(function(tinymce) { - var Dispatcher = tinymce.util.Dispatcher, each = tinymce.each; + showMenu : function() { + var t = this, p1, p2, e = DOM.get(t.id), m; - tinymce.create('tinymce.AddOnManager', { - items : [], - urls : {}, - lookup : {}, + if (t.isDisabled()) + return; + + if (!t.isMenuRendered) { + t.renderMenu(); + t.isMenuRendered = true; + } + + if (t.isMenuVisible) + return t.hideMenu(); + + p1 = DOM.getPos(t.settings.menu_container); + p2 = DOM.getPos(e); + + m = t.menu; + m.settings.offset_x = p2.x; + m.settings.offset_y = p2.y; + 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); - onAdd : new Dispatcher(this), + Event.add(DOM.doc, 'mousedown', t.hideMenu, t); + t.setState('Selected', 1); - get : function(n) { - return this.lookup[n]; + t.isMenuVisible = 1; }, - requireLangPack : function(n) { - var s = tinymce.settings; + renderMenu : function() { + var t = this, m; - if (s && s.language) - tinymce.ScriptLoader.add(this.urls[n] + '/langs/' + s.language + '.js'); - }, + m = t.settings.control_manager.createDropMenu(t.id + '_menu', { + menu_line : 1, + 'class' : this.classPrefix + 'Menu', + icons : t.settings.icons + }); - add : function(id, o) { - this.items.push(o); - this.lookup[id] = o; - this.onAdd.dispatch(this, id, o); + m.onHideMenu.add(function() { + t.hideMenu(); + t.focus(); + }); - return o; + t.onRenderMenu.dispatch(t, m); + t.menu = m; }, - load : function(n, u, cb, s) { + hideMenu : function(e) { var t = this; - if (t.urls[n]) + // Prevent double toogles by canceling the mouse click event to the button + if (e && e.type == "mousedown" && DOM.getParent(e.target, function(e) {return e.id === t.id || e.id === t.id + '_open';})) return; - if (u.indexOf('/') != 0 && u.indexOf('://') == -1) - u = tinymce.baseURL + '/' + u; - - t.urls[n] = u.substring(0, u.lastIndexOf('/')); - tinymce.ScriptLoader.add(u, cb, s); - } - }); + if (!e || !DOM.getParent(e.target, '.mceMenu')) { + t.setState('Selected', 0); + Event.remove(DOM.doc, 'mousedown', t.hideMenu, t); + if (t.menu) + t.menu.hideMenu(); + } - // Create plugin and theme managers - tinymce.PluginManager = new tinymce.AddOnManager(); - tinymce.ThemeManager = new tinymce.AddOnManager(); -}(tinymce)); + t.isMenuVisible = 0; + }, -(function(tinymce) { - // Shorten names - var each = tinymce.each, extend = tinymce.extend, - DOM = tinymce.DOM, Event = tinymce.dom.Event, - ThemeManager = tinymce.ThemeManager, PluginManager = tinymce.PluginManager, - explode = tinymce.explode, - Dispatcher = tinymce.util.Dispatcher, undefined, instanceCounter = 0; + postRender : function() { + var t = this, s = t.settings; - // Setup some URLs where the editor API is located and where the document is - tinymce.documentBaseURL = window.location.href.replace(/[\?#].*$/, '').replace(/[\/\\][^\/]+$/, ''); - if (!/[\/\\]$/.test(tinymce.documentBaseURL)) - tinymce.documentBaseURL += '/'; + Event.add(t.id, 'click', function() { + if (!t.isDisabled()) { + if (s.onclick) + s.onclick(t.value); - tinymce.baseURL = new tinymce.util.URI(tinymce.documentBaseURL).toAbsolute(tinymce.baseURL); + t.showMenu(); + } + }); + } + }); +})(tinymce); - tinymce.baseURI = new tinymce.util.URI(tinymce.baseURL); +(function(tinymce) { + var DOM = tinymce.DOM, Event = tinymce.dom.Event, each = tinymce.each; - // Add before unload listener - // This was required since IE was leaking memory if you added and removed beforeunload listeners - // with attachEvent/detatchEvent so this only adds one listener and instances can the attach to the onBeforeUnload event - tinymce.onBeforeUnload = new Dispatcher(tinymce); + tinymce.create('tinymce.ui.SplitButton:tinymce.ui.MenuButton', { + SplitButton : function(id, s, ed) { + this.parent(id, s, ed); + this.classPrefix = 'mceSplitButton'; + }, - // Must be on window or IE will leak if the editor is placed in frame or iframe - Event.add(window, 'beforeunload', function(e) { - tinymce.onBeforeUnload.dispatch(tinymce, e); - }); + renderHTML : function() { + var h, t = this, s = t.settings, h1; - tinymce.onAddEditor = new Dispatcher(tinymce); + h = ''; - tinymce.onRemoveEditor = new Dispatcher(tinymce); + if (s.image) + h1 = DOM.createHTML('img ', {src : s.image, role: 'presentation', 'class' : 'mceAction ' + s['class']}); + else + h1 = DOM.createHTML('span', {'class' : 'mceAction ' + s['class']}, ''); - tinymce.EditorManager = extend(tinymce, { - editors : [], + h1 += DOM.createHTML('span', {'class': 'mceVoiceLabel mceIconOnly', id: t.id + '_voice', style: 'display:none;'}, s.title); + h += '' + DOM.createHTML('a', {role: 'button', id : t.id + '_action', tabindex: '-1', href : 'javascript:;', 'class' : 'mceAction ' + s['class'], onclick : "return false;", onmousedown : 'return false;', title : s.title}, h1) + ''; + + h1 = DOM.createHTML('span', {'class' : 'mceOpen ' + s['class']}, ''); + h += '' + DOM.createHTML('a', {role: 'button', id : t.id + '_open', tabindex: '-1', href : 'javascript:;', 'class' : 'mceOpen ' + s['class'], onclick : "return false;", onmousedown : 'return false;', title : s.title}, h1) + ''; - i18n : {}, + h += ''; + h = DOM.createHTML('table', { role: 'presentation', 'class' : 'mceSplitButton mceSplitButtonEnabled ' + s['class'], cellpadding : '0', cellspacing : '0', title : s.title}, h); + return DOM.createHTML('div', {id : t.id, role: 'button', tabindex: '0', 'aria-labelledby': t.id + '_voice', 'aria-haspopup': 'true'}, h); + }, - activeEditor : null, + postRender : function() { + var t = this, s = t.settings, activate; - init : function(s) { - var t = this, pl, sl = tinymce.ScriptLoader, e, el = [], ed; + if (s.onclick) { + activate = function(evt) { + if (!t.isDisabled()) { + s.onclick(t.value); + Event.cancel(evt); + } + }; + Event.add(t.id + '_action', 'click', activate); + Event.add(t.id, ['click', 'keydown'], function(evt) { + var DOM_VK_SPACE = 32, DOM_VK_ENTER = 14, DOM_VK_RETURN = 13, DOM_VK_UP = 38, DOM_VK_DOWN = 40; + if ((evt.keyCode === 32 || evt.keyCode === 13 || evt.keyCode === 14) && !evt.altKey && !evt.ctrlKey && !evt.metaKey) { + activate(); + Event.cancel(evt); + } else if (evt.type === 'click' || evt.keyCode === DOM_VK_DOWN) { + t.showMenu(); + Event.cancel(evt); + } + }); + } - function execCallback(se, n, s) { - var f = se[n]; + Event.add(t.id + '_open', 'click', function (evt) { + t.showMenu(); + Event.cancel(evt); + }); + Event.add([t.id, t.id + '_open'], 'focus', function() {t._focused = 1;}); + Event.add([t.id, t.id + '_open'], 'blur', function() {t._focused = 0;}); - if (!f) - return; + // Old IE doesn't have hover on all elements + if (tinymce.isIE6 || !DOM.boxModel) { + Event.add(t.id, 'mouseover', function() { + if (!DOM.hasClass(t.id, 'mceSplitButtonDisabled')) + DOM.addClass(t.id, 'mceSplitButtonHover'); + }); - if (tinymce.is(f, 'string')) { - s = f.replace(/\.\w+$/, ''); - s = s ? tinymce.resolve(s) : 0; - f = tinymce.resolve(f); - } + Event.add(t.id, 'mouseout', function() { + if (!DOM.hasClass(t.id, 'mceSplitButtonDisabled')) + DOM.removeClass(t.id, 'mceSplitButtonHover'); + }); + } + }, - return f.apply(s || this, Array.prototype.slice.call(arguments, 2)); - }; + destroy : function() { + this.parent(); - s = extend({ - theme : "simple", - language : "en" - }, s); + Event.clear(this.id + '_action'); + Event.clear(this.id + '_open'); + Event.clear(this.id); + } + }); +})(tinymce); - t.settings = s; +(function(tinymce) { + var DOM = tinymce.DOM, Event = tinymce.dom.Event, is = tinymce.is, each = tinymce.each; - // Legacy call - Event.add(document, 'init', function() { - var l, co; + tinymce.create('tinymce.ui.ColorSplitButton:tinymce.ui.SplitButton', { + ColorSplitButton : function(id, s, ed) { + var t = this; - execCallback(s, 'onpageload'); + t.parent(id, s, ed); - switch (s.mode) { - case "exact": - l = s.elements || ''; + t.settings = s = tinymce.extend({ + colors : '000000,993300,333300,003300,003366,000080,333399,333333,800000,FF6600,808000,008000,008080,0000FF,666699,808080,FF0000,FF9900,99CC00,339966,33CCCC,3366FF,800080,999999,FF00FF,FFCC00,FFFF00,00FF00,00FFFF,00CCFF,993366,C0C0C0,FF99CC,FFCC99,FFFF99,CCFFCC,CCFFFF,99CCFF,CC99FF,FFFFFF', + grid_width : 8, + default_color : '#888888' + }, t.settings); - if(l.length > 0) { - each(explode(l), function(v) { - if (DOM.get(v)) { - ed = new tinymce.Editor(v, s); - el.push(ed); - ed.render(1); - } else { - each(document.forms, function(f) { - each(f.elements, function(e) { - if (e.name === v) { - v = 'mce_editor_' + instanceCounter++; - DOM.setAttrib(e, 'id', v); + t.onShowMenu = new tinymce.util.Dispatcher(t); - ed = new tinymce.Editor(v, s); - el.push(ed); - ed.render(1); - } - }); - }); - } - }); - } - break; + t.onHideMenu = new tinymce.util.Dispatcher(t); - case "textareas": - case "specific_textareas": - function hasClass(n, c) { - return c.constructor === RegExp ? c.test(n.className) : DOM.hasClass(n, c); - }; + t.value = s.default_color; + }, - each(DOM.select('textarea'), function(v) { - if (s.editor_deselector && hasClass(v, s.editor_deselector)) - return; + showMenu : function() { + var t = this, r, p, e, p2; - 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; + if (t.isDisabled()) + return; - // Generate unique name if missing or already exists - if (!v.id || t.get(v.id)) - v.id = DOM.uniqueId(); + if (!t.isMenuRendered) { + t.renderMenu(); + t.isMenuRendered = true; + } - ed = new tinymce.Editor(v.id, s); - el.push(ed); - ed.render(1); - } - }); - break; - } + if (t.isMenuVisible) + return t.hideMenu(); - // Call onInit when all editors are initialized - if (s.oninit) { - l = co = 0; + e = DOM.get(t.id); + DOM.show(t.id + '_menu'); + DOM.addClass(e, 'mceSplitButtonSelected'); + p2 = DOM.getPos(e); + DOM.setStyles(t.id + '_menu', { + left : p2.x, + top : p2.y + e.clientHeight, + zIndex : 200000 + }); + e = 0; - each(el, function(ed) { - co++; + Event.add(DOM.doc, 'mousedown', t.hideMenu, t); + t.onShowMenu.dispatch(t); - if (!ed.initialized) { - // Wait for it - ed.onInit.add(function() { - l++; + if (t._focused) { + t._keyHandler = Event.add(t.id + '_menu', 'keydown', function(e) { + if (e.keyCode == 27) + t.hideMenu(); + }); - // All done - if (l == co) - execCallback(s, 'oninit'); - }); - } else - l++; + DOM.select('a', t.id + '_menu')[0].focus(); // Select first link + } - // All done - if (l == co) - execCallback(s, 'oninit'); - }); - } - }); + t.isMenuVisible = 1; }, - get : function(id) { - if (id === undefined) - return this.editors; + hideMenu : function(e) { + var t = this; + + if (t.isMenuVisible) { + // Prevent double toogles by canceling the mouse click event to the button + if (e && e.type == "mousedown" && DOM.getParent(e.target, function(e) {return e.id === t.id + '_open';})) + return; - return this.editors[id]; - }, + if (!e || !DOM.getParent(e.target, '.mceSplitButtonMenu')) { + DOM.removeClass(t.id, 'mceSplitButtonSelected'); + Event.remove(DOM.doc, 'mousedown', t.hideMenu, t); + Event.remove(t.id + '_menu', 'keydown', t._keyHandler); + DOM.hide(t.id + '_menu'); + } - getInstanceById : function(id) { - return this.get(id); + t.isMenuVisible = 0; + t.onHideMenu.dispatch(); + } }, - add : function(editor) { - var self = this, editors = self.editors; + renderMenu : function() { + var t = this, m, i = 0, s = t.settings, n, tb, tr, w, context; - // Add named and index editor instance - editors[editor.id] = editor; - editors.push(editor); + 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'}); - self._setActive(editor); - self.onAddEditor.dispatch(self, editor); + n = DOM.add(m, 'table', {role: 'presentation', 'class' : 'mceColorSplitMenu'}); + tb = DOM.add(n, 'tbody'); + // Generate color grid + i = 0; + each(is(s.colors, 'array') ? s.colors : s.colors.split(','), function(c) { + c = c.replace(/^#/, ''); - return editor; - }, + if (!i--) { + tr = DOM.add(tb, 'tr'); + i = s.grid_width - 1; + } - remove : function(editor) { - var t = this, i, editors = t.editors; + n = DOM.add(tr, 'td'); + var settings = { + href : 'javascript:;', + style : { + backgroundColor : '#' + c + }, + 'title': t.editor.getLang('colors.' + c, c), + 'data-mce-color' : '#' + c + }; - // Not in the collection - if (!editors[editor.id]) - return null; + // adding a proper ARIA role = button causes JAWS to read things incorrectly on IE. + if (!tinymce.isIE ) { + settings['role']= 'option'; + } - delete editors[editor.id]; + n = DOM.add(n, 'a', settings); - for (i = 0; i < editors.length; i++) { - if (editors[i] == editor) { - editors.splice(i, 1); - break; + if (t.editor.forcedHighContrastMode) { + n = DOM.add(n, 'canvas', { width: 16, height: 16, 'aria-hidden': 'true' }); + if (n.getContext && (context = n.getContext("2d"))) { + context.fillStyle = '#' + c; + context.fillRect(0, 0, 16, 16); + } else { + // No point leaving a canvas element around if it's not supported for drawing on anyway. + DOM.remove(n); + } } + }); + + if (s.more_colors_func) { + n = DOM.add(tb, 'tr'); + n = DOM.add(n, 'td', {colspan : s.grid_width, 'class' : 'mceMoreColors'}); + n = DOM.add(n, 'a', {role: 'option', id : t.id + '_more', href : 'javascript:;', onclick : 'return false;', 'class' : 'mceMoreColors'}, s.more_colors_title); + + Event.add(n, 'click', function(e) { + s.more_colors_func.call(s.more_colors_scope || this); + return Event.cancel(e); // Cancel to fix onbeforeunload problem + }); } - // Select another editor since the active one was removed - if (t.activeEditor == editor) - t._setActive(editors[0]); + 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(); + } + }); - editor.destroy(); - t.onRemoveEditor.dispatch(t, editor); + // Prevent IE from scrolling and hindering click to occur #4019 + Event.add(t.id + '_menu', 'mousedown', function(e) {return Event.cancel(e);}); - return editor; - }, + Event.add(t.id + '_menu', 'click', function(e) { + var c; - execCommand : function(c, u, v) { - var t = this, ed = t.get(v), w; + e = DOM.getParent(e.target, 'a', tb); - // Manager commands - switch (c) { - case "mceFocus": - ed.focus(); - return true; + if (e && e.nodeName.toLowerCase() == 'a' && (c = e.getAttribute('data-mce-color'))) + t.setColor(c); - case "mceAddEditor": - case "mceAddControl": - if (!t.get(v)) - new tinymce.Editor(v, t.settings).render(); + return false; // Prevent IE auto save warning + }); - return true; + return w; + }, - case "mceAddFrameControl": - w = v.window; + setColor : function(c) { + this.displayColor(c); + this.hideMenu(); + this.settings.onselect(c); + }, + + displayColor : function(c) { + var t = this; - // Add tinyMCE global instance and tinymce namespace to specified window - w.tinyMCE = tinyMCE; - w.tinymce = tinymce; + DOM.setStyle(t.id + '_preview', 'backgroundColor', c); - tinymce.DOM.doc = w.document; - tinymce.DOM.win = w; + t.value = c; + }, - ed = new tinymce.Editor(v.element_id, v); - ed.render(); + postRender : function() { + var t = this, id = t.id; - // Fix IE memory leaks - if (tinymce.isIE) { - function clr() { - ed.destroy(); - w.detachEvent('onunload', clr); - w = w.tinyMCE = w.tinymce = null; // IE leak - }; + t.parent(); + DOM.add(id + '_action', 'div', {id : id + '_preview', 'class' : 'mceColorPreview'}); + DOM.setStyle(t.id + '_preview', 'backgroundColor', t.value); + }, - w.attachEvent('onunload', clr); - } + destroy : function() { + this.parent(); - v.page_window = null; + Event.clear(this.id + '_menu'); + Event.clear(this.id + '_more'); + DOM.remove(this.id + '_menu'); + } + }); +})(tinymce); - return true; +(function(tinymce) { +// Shorten class names +var dom = tinymce.DOM, each = tinymce.each, Event = tinymce.dom.Event; +tinymce.create('tinymce.ui.ToolbarGroup:tinymce.ui.Container', { + renderHTML : function() { + var t = this, h = [], controls = t.controls, each = tinymce.each, settings = t.settings; + + h.push('
    '); + //TODO: ACC test this out - adding a role = application for getting the landmarks working well. + h.push(""); + h.push(''); + each(controls, function(toolbar) { + h.push(toolbar.renderHTML()); + }); + h.push(""); + h.push('
    '); - case "mceRemoveEditor": - case "mceRemoveControl": - if (ed) - ed.remove(); + return h.join(''); + }, + + focus : function() { + var t = this; + dom.get(t.id).focus(); + }, + + postRender : function() { + var t = this, items = []; - return true; + each(t.controls, function(toolbar) { + each (toolbar.controls, function(control) { + if (control.id) { + items.push(control); + } + }); + }); - case 'mceToggleEditor': - if (!ed) { - t.execCommand('mceAddControl', 0, v); - return true; - } + t.keyNav = new tinymce.ui.KeyboardNavigation({ + root: t.id, + items: items, + onCancel: function() { + //Move focus if webkit so that navigation back will read the item. + if (tinymce.isWebKit) { + dom.get(t.editor.id+"_ifr").focus(); + } + t.editor.focus(); + }, + excludeFromTabOrder: !t.settings.tab_focus_toolbar + }); + }, + + destroy : function() { + var self = this; - if (ed.isHidden()) - ed.show(); - else - ed.hide(); + self.parent(); + self.keyNav.destroy(); + Event.clear(self.id); + } +}); +})(tinymce); - return true; - } +(function(tinymce) { +// Shorten class names +var dom = tinymce.DOM, each = tinymce.each; +tinymce.create('tinymce.ui.Toolbar:tinymce.ui.Container', { + renderHTML : function() { + var t = this, h = '', c, co, s = t.settings, i, pr, nx, cl; - // Run command on active editor - if (t.activeEditor) - return t.activeEditor.execCommand(c, u, v); + cl = t.controls; + for (i=0; i')); + } - return false; - }, + // Add toolbar end before list box and after the previous button + // This is to fix the o2k7 editor skins + if (pr && co.ListBox) { + if (pr.Button || pr.SplitButton) + h += dom.createHTML('td', {'class' : 'mceToolbarEnd'}, dom.createHTML('span', null, '')); + } - triggerSave : function() { - each(this.editors, function(e) { - e.save(); - }); - }, + // Render control HTML - addI18n : function(p, o) { - var lo, i18n = this.i18n; + // IE 8 quick fix, needed to propertly generate a hit area for anchors + if (dom.stdMode) + h += '' + co.renderHTML() + ''; + else + h += '' + co.renderHTML() + ''; - if (!tinymce.is(p, 'string')) { - each(p, function(o, lc) { - each(o, function(o, g) { - each(o, function(o, k) { - if (g === 'common') - i18n[lc + '.' + k] = o; - else - i18n[lc + '.' + g + '.' + k] = o; - }); - }); - }); - } else { - each(o, function(o, k) { - i18n[p + '.' + k] = o; - }); + // Add toolbar start after list box and before the next button + // This is to fix the o2k7 editor skins + if (nx && co.ListBox) { + if (nx.Button || nx.SplitButton) + h += dom.createHTML('td', {'class' : 'mceToolbarStart'}, dom.createHTML('span', null, '')); } - }, + } - // Private methods + c = 'mceToolbarEnd'; - _setActive : function(editor) { - this.selectedInstance = this.activeEditor = editor; - } - }); -})(tinymce); + if (co.Button) + c += ' mceToolbarEndButton'; + else if (co.SplitButton) + c += ' mceToolbarEndSplitButton'; + else if (co.ListBox) + c += ' mceToolbarEndListBox'; -(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, - isIE = tinymce.isIE, isWebKit = tinymce.isWebKit, is = tinymce.is, - ThemeManager = tinymce.ThemeManager, PluginManager = tinymce.PluginManager, - inArray = tinymce.inArray, grep = tinymce.grep, explode = tinymce.explode; + h += dom.createHTML('td', {'class' : c}, dom.createHTML('span', null, '')); + + return dom.createHTML('table', {id : t.id, 'class' : 'mceToolbar' + (s['class'] ? ' ' + s['class'] : ''), cellpadding : '0', cellspacing : '0', align : t.settings.align || '', role: 'presentation', tabindex: '-1'}, '' + h + ''); + } +}); +})(tinymce); - tinymce.create('tinymce.Editor', { - Editor : function(id, s) { - var t = this; +(function(tinymce) { + var Dispatcher = tinymce.util.Dispatcher, each = tinymce.each; - t.id = t.editorId = id; + tinymce.create('tinymce.AddOnManager', { + AddOnManager : function() { + var self = this; - t.execCommands = {}; - t.queryStateCommands = {}; - t.queryValueCommands = {}; + self.items = []; + self.urls = {}; + self.lookup = {}; + self.onAdd = new Dispatcher(self); + }, - t.isNotDirty = false; + get : function(n) { + if (this.lookup[n]) { + return this.lookup[n].instance; + } else { + return undefined; + } + }, - t.plugins = {}; + dependencies : function(n) { + var result; + if (this.lookup[n]) { + result = this.lookup[n].dependencies; + } + return result || []; + }, - // Add events to the editor - each([ - 'onPreInit', + requireLangPack : function(n) { + var s = tinymce.settings; - 'onBeforeRenderUI', + if (s && s.language && s.language_load !== false) + tinymce.ScriptLoader.add(this.urls[n] + '/langs/' + s.language + '.js'); + }, - 'onPostRender', + add : function(id, o, dependencies) { + this.items.push(o); + this.lookup[id] = {instance:o, dependencies:dependencies}; + this.onAdd.dispatch(this, id, o); - 'onInit', + return o; + }, + createUrl: function(baseUrl, dep) { + if (typeof dep === "object") { + return dep + } else { + return {prefix: baseUrl.prefix, resource: dep, suffix: baseUrl.suffix}; + } + }, - 'onRemove', + addComponents: function(pluginName, scripts) { + var pluginUrl = this.urls[pluginName]; + tinymce.each(scripts, function(script){ + tinymce.ScriptLoader.add(pluginUrl+"/"+script); + }); + }, - 'onActivate', + load : function(n, u, cb, s) { + var t = this, url = u; - 'onDeactivate', + function loadDependencies() { + var dependencies = t.dependencies(n); + tinymce.each(dependencies, function(dep) { + var newUrl = t.createUrl(u, dep); + t.load(newUrl.resource, newUrl, undefined, undefined); + }); + if (cb) { + if (s) { + cb.call(s); + } else { + cb.call(tinymce.ScriptLoader); + } + } + } - 'onClick', + if (t.urls[n]) + return; + if (typeof u === "object") + url = u.prefix + u.resource + u.suffix; - 'onEvent', + if (url.indexOf('/') != 0 && url.indexOf('://') == -1) + url = tinymce.baseURL + '/' + url; - 'onMouseUp', + t.urls[n] = url.substring(0, url.lastIndexOf('/')); - 'onMouseDown', + if (t.lookup[n]) { + loadDependencies(); + } else { + tinymce.ScriptLoader.add(url, loadDependencies, s); + } + } + }); - 'onDblClick', + // Create plugin and theme managers + tinymce.PluginManager = new tinymce.AddOnManager(); + tinymce.ThemeManager = new tinymce.AddOnManager(); +}(tinymce)); - 'onKeyDown', +(function(tinymce) { + // Shorten names + var each = tinymce.each, extend = tinymce.extend, + DOM = tinymce.DOM, Event = tinymce.dom.Event, + ThemeManager = tinymce.ThemeManager, PluginManager = tinymce.PluginManager, + explode = tinymce.explode, + Dispatcher = tinymce.util.Dispatcher, undefined, instanceCounter = 0; - 'onKeyUp', + // Setup some URLs where the editor API is located and where the document is + tinymce.documentBaseURL = window.location.href.replace(/[\?#].*$/, '').replace(/[\/\\][^\/]+$/, ''); + if (!/[\/\\]$/.test(tinymce.documentBaseURL)) + tinymce.documentBaseURL += '/'; - 'onKeyPress', + tinymce.baseURL = new tinymce.util.URI(tinymce.documentBaseURL).toAbsolute(tinymce.baseURL); - 'onContextMenu', + tinymce.baseURI = new tinymce.util.URI(tinymce.baseURL); - 'onSubmit', + // Add before unload listener + // This was required since IE was leaking memory if you added and removed beforeunload listeners + // with attachEvent/detatchEvent so this only adds one listener and instances can the attach to the onBeforeUnload event + tinymce.onBeforeUnload = new Dispatcher(tinymce); - 'onReset', + // Must be on window or IE will leak if the editor is placed in frame or iframe + Event.add(window, 'beforeunload', function(e) { + tinymce.onBeforeUnload.dispatch(tinymce, e); + }); - 'onPaste', + tinymce.onAddEditor = new Dispatcher(tinymce); - 'onPreProcess', + tinymce.onRemoveEditor = new Dispatcher(tinymce); - 'onPostProcess', + tinymce.EditorManager = extend(tinymce, { + editors : [], - 'onBeforeSetContent', + i18n : {}, - 'onBeforeGetContent', + activeEditor : null, - 'onSetContent', + init : function(s) { + var t = this, pl, sl = tinymce.ScriptLoader, e, el = [], ed; - 'onGetContent', + 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(); + } - 'onLoadContent', + elm.setAttribute('id', id); + } - 'onSaveContent', + return id; + }; - 'onNodeChange', + function execCallback(se, n, s) { + var f = se[n]; - 'onChange', + if (!f) + return; - 'onBeforeExecCommand', + if (tinymce.is(f, 'string')) { + s = f.replace(/\.\w+$/, ''); + s = s ? tinymce.resolve(s) : 0; + f = tinymce.resolve(f); + } - 'onExecCommand', + return f.apply(s || this, Array.prototype.slice.call(arguments, 2)); + }; - 'onUndo', + s = extend({ + theme : "simple", + language : "en" + }, s); - 'onRedo', + t.settings = s; - 'onVisualAid', + // Legacy call + Event.bind(window, 'ready', function() { + var l, co; - 'onSetProgressState' - ], function(e) { - t[e] = new Dispatcher(t); - }); + execCallback(s, 'onpageload'); - t.settings = s = extend({ - id : id, - language : 'en', - docs_language : 'en', - theme : 'simple', - 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, - doctype : tinymce.isIE6 ? '' : '', // Use old doctype on IE 6 to avoid horizontal scroll - visual_table_class : 'mceItemTable', - visual : 1, - font_size_style_values : 'xx-small,x-small,small,medium,large,x-large,xx-large', - apply_source_formatting : 1, - directionality : 'ltr', - forced_root_block : 'p', - valid_elements : '@[id|class|style|title|dir 0) { + each(explode(l), function(v) { + if (DOM.get(v)) { + ed = new tinymce.Editor(v, s); + el.push(ed); + ed.render(1); + } else { + each(document.forms, function(f) { + each(f.elements, function(e) { + if (e.name === v) { + v = 'mce_editor_' + instanceCounter++; + DOM.setAttrib(e, 'id', v); - t.baseURI = tinymce.baseURI; + ed = new tinymce.Editor(v, s); + el.push(ed); + ed.render(1); + } + }); + }); + } + }); + } + break; - // Call setup - t.execCallback('setup', t); - }, + case "textareas": + case "specific_textareas": + function hasClass(n, c) { + return c.constructor === RegExp ? c.test(n.className) : DOM.hasClass(n, c); + }; - render : function(nst) { - var t = this, s = t.settings, id = t.id, sl = tinymce.ScriptLoader; + each(DOM.select('textarea'), function(elm) { + if (s.editor_deselector && hasClass(elm, s.editor_deselector)) + return; - // Page is not loaded yet, wait for it - if (!Event.domLoaded) { - Event.add(document, 'init', function() { - t.render(); - }); - return; - } + 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); + }); + } + } - tinyMCE.settings = s; + // Call onInit when all editors are initialized + if (s.oninit) { + l = co = 0; - // Element not found, then skip initialization - if (!t.getElement()) - return; + each(el, function(ed) { + co++; + + if (!ed.initialized) { + // Wait for it + ed.onInit.add(function() { + l++; - // Is a iPad/iPhone, then skip initialization. We need to sniff here since the - // browser says it has contentEditable support but there is no visible caret - // We will remove this check ones Apple implements full contentEditable support - if (tinymce.isIDevice) - return; + // All done + if (l == co) + execCallback(s, 'oninit'); + }); + } else + l++; - // Add hidden input for non input elements inside form elements - if (!/TEXTAREA|INPUT/i.test(t.getElement().nodeName) && s.hidden_input && DOM.getParent(id, 'form')) - DOM.insertAfter(DOM.create('input', {type : 'hidden', name : id}), id); + // All done + if (l == co) + execCallback(s, 'oninit'); + }); + } + }); + }, - if (tinymce.WindowManager) - t.windowManager = new tinymce.WindowManager(t); + get : function(id) { + if (id === undefined) + return this.editors; - if (s.encoding == 'xml') { - t.onGetContent.add(function(ed, o) { - if (o.save) - o.content = DOM.encode(o.content); - }); - } + return this.editors[id]; + }, - if (s.add_form_submit_trigger) { - t.onSubmit.addToTop(function() { - if (t.initialized) { - t.save(); - t.isNotDirty = 1; - } - }); - } + getInstanceById : function(id) { + return this.get(id); + }, - if (s.add_unload_trigger) { - t._beforeUnload = tinyMCE.onBeforeUnload.add(function() { - if (t.initialized && !t.destroyed && !t.isHidden()) - t.save({format : 'raw', no_events : true}); - }); - } + add : function(editor) { + var self = this, editors = self.editors; - tinymce.addUnload(t.destroy, t); + // Add named and index editor instance + editors[editor.id] = editor; + editors.push(editor); - if (s.submit_patch) { - t.onBeforeRenderUI.add(function() { - var n = t.getElement().form; + self._setActive(editor); + self.onAddEditor.dispatch(self, editor); - if (!n) - return; - // Already patched - if (n._mceOldSubmit) - return; + return editor; + }, - // Check page uses id="submit" or name="submit" for it's submit button - if (!n.submit.nodeType && !n.submit.length) { - t.formElement = n; - n._mceOldSubmit = n.submit; - n.submit = function() { - // Save all instances - tinymce.triggerSave(); - t.isNotDirty = 1; + remove : function(editor) { + var t = this, i, editors = t.editors; - return t.formElement._mceOldSubmit(t.formElement); - }; - } + // Not in the collection + if (!editors[editor.id]) + return null; - n = null; - }); + delete editors[editor.id]; + + for (i = 0; i < editors.length; i++) { + if (editors[i] == editor) { + editors.splice(i, 1); + break; + } } - // Load scripts - function loadScripts() { - if (s.language) - sl.add(tinymce.baseURL + '/langs/' + s.language + '.js'); + // Select another editor since the active one was removed + if (t.activeEditor == editor) + t._setActive(editors[0]); - if (s.theme && s.theme.charAt(0) != '-' && !ThemeManager.urls[s.theme]) - ThemeManager.load(s.theme, 'themes/' + s.theme + '/editor_template' + tinymce.suffix + '.js'); + editor.destroy(); + t.onRemoveEditor.dispatch(t, editor); - each(explode(s.plugins), function(p) { - if (p && p.charAt(0) != '-' && !PluginManager.urls[p]) { - // Skip safari plugin, since it is removed as of 3.3b1 - if (p == 'safari') - return; + return editor; + }, - PluginManager.load(p, 'plugins/' + p + '/editor_plugin' + tinymce.suffix + '.js'); - } - }); + execCommand : function(c, u, v) { + var t = this, ed = t.get(v), w; - // Init when que is loaded - sl.loadQueue(function() { - if (!t.removed) - t.init(); - }); - }; + // Manager commands + switch (c) { + case "mceFocus": + ed.focus(); + return true; - loadScripts(); - }, + case "mceAddEditor": + case "mceAddControl": + if (!t.get(v)) + new tinymce.Editor(v, t.settings).render(); - init : function() { - var n, t = this, s = t.settings, w, h, e = t.getElement(), o, ti, u, bi, bc, re; + return true; - tinymce.add(t); + case "mceAddFrameControl": + w = v.window; - if (s.theme) { - s.theme = s.theme.replace(/-/, ''); - o = ThemeManager.get(s.theme); - t.theme = new o(); + // Add tinyMCE global instance and tinymce namespace to specified window + w.tinyMCE = tinyMCE; + w.tinymce = tinymce; - if (t.theme.init && s.init_theme) - t.theme.init(t, ThemeManager.urls[s.theme] || tinymce.documentBaseURL.replace(/\/$/, '')); - } + tinymce.DOM.doc = w.document; + tinymce.DOM.win = w; - // Create all plugins - each(explode(s.plugins.replace(/\-/g, '')), function(p) { - var c = PluginManager.get(p), u = PluginManager.urls[p] || tinymce.documentBaseURL.replace(/\/$/, ''), po; + ed = new tinymce.Editor(v.element_id, v); + ed.render(); - if (c) { - po = new c(t, u); + // Fix IE memory leaks + if (tinymce.isIE) { + function clr() { + ed.destroy(); + w.detachEvent('onunload', clr); + w = w.tinyMCE = w.tinymce = null; // IE leak + }; - t.plugins[p] = po; + w.attachEvent('onunload', clr); + } - if (po.init) - po.init(t, u); - } - }); + v.page_window = null; - // Setup popup CSS path(s) - if (s.popup_css !== false) { - if (s.popup_css) - s.popup_css = t.documentBaseURI.toAbsolute(s.popup_css); - else - s.popup_css = t.baseURI.toAbsolute("themes/" + s.theme + "/skins/" + s.skin + "/dialog.css"); - } + return true; - if (s.popup_css_add) - s.popup_css += ',' + t.documentBaseURI.toAbsolute(s.popup_css_add); + case "mceRemoveEditor": + case "mceRemoveControl": + if (ed) + ed.remove(); - t.controlManager = new tinymce.ControlManager(t); + return true; - if (s.custom_undo_redo) { - // Add initial undo level - t.onBeforeExecCommand.add(function(ed, cmd, ui, val, a) { - if (cmd != 'Undo' && cmd != 'Redo' && cmd != 'mceRepaint' && (!a || !a.skip_undo)) { - if (!t.undoManager.hasUndo()) - t.undoManager.add(); + case 'mceToggleEditor': + if (!ed) { + t.execCommand('mceAddControl', 0, v); + return true; } - }); - t.onExecCommand.add(function(ed, cmd, ui, val, a) { - if (cmd != 'Undo' && cmd != 'Redo' && cmd != 'mceRepaint' && (!a || !a.skip_undo)) - t.undoManager.add(); - }); + if (ed.isHidden()) + ed.show(); + else + ed.hide(); + + return true; } - t.onExecCommand.add(function(ed, c) { - // Don't refresh the select lists until caret move - if (!/^(FontName|FontSize)$/.test(c)) - t.nodeChanged(); - }); + // Run command on active editor + if (t.activeEditor) + return t.activeEditor.execCommand(c, u, v); - // Remove ghost selections on images and tables in Gecko - if (isGecko) { - function repaint(a, o) { - if (!o || !o.initial) - t.execCommand('mceRepaint'); - }; + return false; + }, - t.onUndo.add(repaint); - t.onRedo.add(repaint); - t.onSetContent.add(repaint); - } + execInstanceCommand : function(id, c, u, v) { + var ed = this.get(id); - // Enables users to override the control factory - t.onBeforeRenderUI.dispatch(t, t.controlManager); + if (ed) + return ed.execCommand(c, u, v); - // Measure box - if (s.render_ui) { - w = s.width || e.style.width || e.offsetWidth; - h = s.height || e.style.height || e.offsetHeight; - t.orgDisplay = e.style.display; - re = /^[0-9\.]+(|px)$/i; + return false; + }, - if (re.test('' + w)) - w = Math.max(parseInt(w) + (o.deltaWidth || 0), 100); + triggerSave : function() { + each(this.editors, function(e) { + e.save(); + }); + }, - if (re.test('' + h)) - h = Math.max(parseInt(h) + (o.deltaHeight || 0), 100); + addI18n : function(p, o) { + var lo, i18n = this.i18n; - // Render UI - o = t.theme.renderUI({ - targetNode : e, - width : w, - height : h, - deltaWidth : s.delta_width, - deltaHeight : s.delta_height + if (!tinymce.is(p, 'string')) { + each(p, function(o, lc) { + each(o, function(o, g) { + each(o, function(o, k) { + if (g === 'common') + i18n[lc + '.' + k] = o; + else + i18n[lc + '.' + g + '.' + k] = o; + }); + }); + }); + } else { + each(o, function(o, k) { + i18n[p + '.' + k] = o; }); - - t.editorContainer = o.editorContainer; } + }, + // Private methods - // User specified a document.domain value - if (document.domain && location.hostname != document.domain) - tinymce.relaxedDomain = document.domain; + _setActive : function(editor) { + this.selectedInstance = this.activeEditor = editor; + } + }); +})(tinymce); - // Resize editor - DOM.setStyles(o.sizeContainer || o.editorContainer, { - width : w, - height : h - }); +(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, + isIE = tinymce.isIE, isWebKit = tinymce.isWebKit, is = tinymce.is, + ThemeManager = tinymce.ThemeManager, PluginManager = tinymce.PluginManager, + inArray = tinymce.inArray, grep = tinymce.grep, explode = tinymce.explode, VK = tinymce.VK; - h = (o.iframeHeight || h) + (typeof(h) == 'number' ? (o.deltaHeight || 0) : ''); - if (h < 100) - h = 100; + tinymce.create('tinymce.Editor', { + Editor : function(id, s) { + var t = this; - t.iframeHTML = s.doctype + ''; + t.id = t.editorId = id; - // We only need to override paths if we have to - // IE has a bug where it remove site absolute urls to relative ones if this is specified - if (s.document_base_url != tinymce.documentBaseURL) - t.iframeHTML += ''; + t.execCommands = {}; + t.queryStateCommands = {}; + t.queryValueCommands = {}; - t.iframeHTML += ''; + t.isNotDirty = false; - if (tinymce.relaxedDomain) - t.iframeHTML += ''; + t.plugins = {}; - bi = s.body_id || 'tinymce'; - if (bi.indexOf('=') != -1) { - bi = t.getParam('body_id', '', 'hash'); - bi = bi[t.id] || bi; - } + // Add events to the editor + each([ + 'onPreInit', - bc = s.body_class || ''; - if (bc.indexOf('=') != -1) { - bc = t.getParam('body_class', '', 'hash'); - bc = bc[t.id] || ''; - } + 'onBeforeRenderUI', - t.iframeHTML += ''; + 'onPostRender', - // Domain relaxing enabled, then set document domain - if (tinymce.relaxedDomain) { - // We need to write the contents here in IE since multiple writes messes up refresh button and back button - if (isIE || (tinymce.isOpera && parseFloat(opera.version()) >= 9.5)) - 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();})()'; - else if (tinymce.isOpera) - u = 'javascript:(function(){document.open();document.domain="' + document.domain + '";document.close();ed.setupIframe();})()'; - } + 'onLoad', - // Create iframe - n = DOM.add(o.iframeContainer, 'iframe', { - id : t.id + "_ifr", - src : u || 'javascript:""', // Workaround for HTTPS warning in IE6/7 - frameBorder : '0', - style : { - width : '100%', - height : h - } - }); + 'onInit', - t.contentAreaContainer = o.iframeContainer; - DOM.get(o.editorContainer).style.display = t.orgDisplay; - DOM.get(t.id).style.display = 'none'; + 'onRemove', - if (!isIE || !tinymce.relaxedDomain) - t.setupIframe(); + 'onActivate', - e = n = o = null; // Cleanup - }, + 'onDeactivate', - setupIframe : function() { - var t = this, s = t.settings, e = DOM.get(t.id), d = t.getDoc(), h, b; + 'onClick', - // Setup iframe body - if (!isIE || !tinymce.relaxedDomain) { - d.open(); - d.write(t.iframeHTML); - d.close(); - } + 'onEvent', - // Design mode needs to be added here Ctrl+A will fail otherwise - if (!isIE) { - try { - if (!s.readonly) - d.designMode = 'On'; - } catch (ex) { - // Will fail on Gecko if the editor is placed in an hidden container element - // The design mode will be set ones the editor is focused - } - } + 'onMouseUp', - // IE needs to use contentEditable or it will display non secure items for HTTPS - if (isIE) { - // It will not steal focus if we hide it while setting contentEditable - b = t.getBody(); - DOM.hide(b); + 'onMouseDown', - if (!s.readonly) - b.contentEditable = true; + 'onDblClick', - DOM.show(b); - } + 'onKeyDown', - t.dom = new tinymce.dom.DOMUtils(t.getDoc(), { - 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, - valid_styles : s.valid_styles - }); + 'onKeyUp', - t.schema = new tinymce.dom.Schema(); + 'onKeyPress', - t.serializer = new tinymce.dom.Serializer(extend(s, { - valid_elements : s.verify_html === false ? '*[*]' : s.valid_elements, - dom : t.dom, - schema : t.schema - })); + 'onContextMenu', - t.selection = new tinymce.dom.Selection(t.dom, t.getWin(), t.serializer); + 'onSubmit', - t.formatter = new tinymce.Formatter(this); + 'onReset', - // 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', styles : {'float' : 'left'}} - ], + 'onPaste', - aligncenter : [ - {selector : 'p,h1,h2,h3,h4,h5,h6,td,th,div,ul,ol,li', styles : {textAlign : 'center'}}, - {selector : 'img', styles : {display : 'block', marginLeft : 'auto', marginRight : 'auto'}}, - {selector : 'table', styles : {marginLeft : 'auto', marginRight : 'auto'}} - ], + 'onPreProcess', - alignright : [ - {selector : 'p,h1,h2,h3,h4,h5,h6,td,th,div,ul,ol,li', styles : {textAlign : 'right'}}, - {selector : 'img,table', styles : {'float' : 'right'}} - ], + 'onPostProcess', - alignfull : [ - {selector : 'p,h1,h2,h3,h4,h5,h6,td,th,div,ul,ol,li', styles : {textAlign : 'justify'}} - ], + 'onBeforeSetContent', - bold : [ - {inline : 'strong'}, - {inline : 'span', styles : {fontWeight : 'bold'}}, - {inline : 'b'} - ], + 'onBeforeGetContent', - italic : [ - {inline : 'em'}, - {inline : 'span', styles : {fontStyle : 'italic'}}, - {inline : 'i'} - ], + 'onSetContent', - underline : [ - {inline : 'span', styles : {textDecoration : 'underline'}, exact : true}, - {inline : 'u'} - ], + 'onGetContent', - strikethrough : [ - {inline : 'span', styles : {textDecoration : 'line-through'}, exact : true}, - {inline : 'u'} - ], + 'onLoadContent', - forecolor : {inline : 'span', styles : {color : '%value'}}, - hilitecolor : {inline : 'span', styles : {backgroundColor : '%value'}}, - 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'}, + 'onSaveContent', - 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} - ] - }); + 'onNodeChange', - // 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'}); - }); + 'onChange', - // Register user defined formats - t.formatter.register(t.settings.formats); + 'onBeforeExecCommand', - t.undoManager = new tinymce.UndoManager(t); + 'onExecCommand', - // Pass through - t.undoManager.onAdd.add(function(um, l) { - if (!l.initial) - return t.onChange.dispatch(t, l, um); - }); + 'onUndo', - t.undoManager.onUndo.add(function(um, l) { - return t.onUndo.dispatch(t, l, um); - }); + 'onRedo', - t.undoManager.onRedo.add(function(um, l) { - return t.onRedo.dispatch(t, l, um); - }); + 'onVisualAid', + + 'onSetProgressState', - t.forceBlocks = new tinymce.ForceBlocks(t, { - forced_root_block : s.forced_root_block + 'onSetAttrib' + ], function(e) { + t[e] = new Dispatcher(t); }); - t.editorCommands = new tinymce.EditorCommands(t); + t.settings = s = extend({ + id : id, + language : 'en', + docs_language : 'en', + theme : 'simple', + 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, + doctype : tinymce.isIE6 ? '' : '', // Use old doctype on IE 6 to avoid horizontal scroll + visual_table_class : 'mceItemTable', + visual : 1, + font_size_style_values : 'xx-small,x-small,small,medium,large,x-large,xx-large', + font_size_legacy_values : 'xx-small,small,medium,large,x-large,xx-large,300%', // See: http://www.w3.org/TR/CSS2/fonts.html#propdef-font-size + apply_source_formatting : 1, + directionality : 'ltr', + forced_root_block : 'p', + hidden_input : 1, + padd_empty_editor : 1, + render_ui : 1, + init_theme : 1, + force_p_newlines : 1, + indentation : '30px', + keep_styles : 1, + fix_table_elements : 1, + inline_styles : 1, + 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,section,article,hgroup,aside', + 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', + validate : true, + entity_encoding : 'named', + url_converter : t.convertURL, + url_converter_scope : t, + ie7_compat : true + }, s); - // Pass through - t.serializer.onPreProcess.add(function(se, o) { - return t.onPreProcess.dispatch(t, o, se); + t.documentBaseURI = new tinymce.util.URI(s.document_base_url || tinymce.documentBaseURL, { + base_uri : tinyMCE.baseURI }); - t.serializer.onPostProcess.add(function(se, o) { - return t.onPostProcess.dispatch(t, o, se); - }); + t.baseURI = tinymce.baseURI; - t.onPreInit.dispatch(t); + t.contentCSS = []; - if (!s.gecko_spellcheck) - t.getBody().spellcheck = 0; + // Call setup + t.execCallback('setup', t); + }, - if (!s.readonly) - t._addEvents(); + render : function(nst) { + var t = this, s = t.settings, id = t.id, sl = tinymce.ScriptLoader; - t.controlManager.onPostRender.dispatch(t, t.controlManager); - t.onPostRender.dispatch(t); + // Page is not loaded yet, wait for it + if (!Event.domLoaded) { + Event.add(window, 'ready', function() { + t.render(); + }); + return; + } - if (s.directionality) - t.getBody().dir = s.directionality; + tinyMCE.settings = s; - if (s.nowrap) - t.getBody().style.whiteSpace = "nowrap"; + // Element not found, then skip initialization + if (!t.getElement()) + return; - if (s.custom_elements) { - function handleCustom(ed, o) { - each(explode(s.custom_elements), function(v) { - var n; + // 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 + if (tinymce.isIDevice && !tinymce.isIOS5) + return; - if (v.indexOf('~') === 0) { - v = v.substring(1); - n = 'span'; - } else - n = 'div'; + // Add hidden input for non input elements inside form elements + if (!/TEXTAREA|INPUT/i.test(t.getElement().nodeName) && s.hidden_input && DOM.getParent(id, 'form')) + DOM.insertAfter(DOM.create('input', {type : 'hidden', name : id}), id); - o.content = o.content.replace(new RegExp('<(' + v + ')([^>]*)>', 'g'), '<' + n + ' _mce_name="$1"$2>'); - o.content = o.content.replace(new RegExp('', 'g'), ''); - }); - }; + if (tinymce.WindowManager) + t.windowManager = new tinymce.WindowManager(t); - t.onBeforeSetContent.add(handleCustom); - t.onPostProcess.add(function(ed, o) { - if (o.set) - handleCustom(ed, o); + if (s.encoding == 'xml') { + t.onGetContent.add(function(ed, o) { + if (o.save) + o.content = DOM.encode(o.content); }); } - 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.add_form_submit_trigger) { + t.onSubmit.addToTop(function() { + if (t.initialized) { + t.save(); + t.isNotDirty = 1; + } }); } - 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.add_unload_trigger) { + t._beforeUnload = tinyMCE.onBeforeUnload.add(function() { + if (t.initialized && !t.destroyed && !t.isHidden()) + t.save({format : 'raw', no_events : true}); }); } - if (s.onchange_callback) { - t.onChange.add(function(ed, l) { - t.execCallback('onchange_callback', t, l); - }); - } + tinymce.addUnload(t.destroy, t); - if (s.convert_newlines_to_brs) { - t.onBeforeSetContent.add(function(ed, o) { - if (o.initial) - o.content = o.content.replace(/\r?\n/g, '
    '); - }); - } + if (s.submit_patch) { + t.onBeforeRenderUI.add(function() { + var n = t.getElement().form; - if (s.fix_nesting && isIE) { - t.onBeforeSetContent.add(function(ed, o) { - o.content = t._fixNesting(o.content); + if (!n) + return; + + // Already patched + if (n._mceOldSubmit) + return; + + // Check page uses id="submit" or name="submit" for it's submit button + if (!n.submit.nodeType && !n.submit.length) { + t.formElement = n; + n._mceOldSubmit = n.submit; + n.submit = function() { + // Save all instances + tinymce.triggerSave(); + t.isNotDirty = 1; + + return t.formElement._mceOldSubmit(t.formElement); + }; + } + + n = null; }); } - if (s.preformatted) { - t.onPostProcess.add(function(ed, o) { - o.content = o.content.replace(/^\s*/, ''); - o.content = o.content.replace(/<\/pre>\s*$/, ''); + // Load scripts + function loadScripts() { + if (s.language && s.language_load !== false) + sl.add(tinymce.baseURL + '/langs/' + s.language + '.js'); - if (o.set) - o.content = '
    ' + o.content + '
    '; + if (s.theme && 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) { + if (p &&!PluginManager.urls[p]) { + if (p.charAt(0) == '-') { + p = p.substr(1, p.length); + 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); + PluginManager.load(dep.resource, dep); + + }); + } else { + // Skip safari plugin, since it is removed as of 3.3b1 + if (p == 'safari') { + return; + } + PluginManager.load(p, {prefix:'plugins/', resource: p, suffix:'/editor_plugin' + tinymce.suffix + '.js'}); + } + } }); - } - if (s.verify_css_classes) { - t.serializer.attribValueFilter = function(n, v) { - var s, cl; + // Init when que is loaded + sl.loadQueue(function() { + if (!t.removed) + t.init(); + }); + }; - if (n == 'class') { - // Build regexp for classes - if (!t.classesRE) { - cl = t.dom.getClasses(); + loadScripts(); + }, - if (cl.length > 0) { - s = ''; + init : function() { + var n, t = this, s = t.settings, w, h, e = t.getElement(), o, ti, u, bi, bc, re, i, initializedPlugins = []; - each (cl, function(o) { - s += (s ? '|' : '') + o['class']; - }); + tinymce.add(t); - t.classesRE = new RegExp('(' + s + ')', 'gi'); - } - } + s.aria_label = s.aria_label || DOM.getAttrib(e, 'aria-label', t.getLang('aria.rich_text_area')); - return !t.classesRE || /(\bmceItem\w+\b|\bmceTemp\w+\b)/g.test(v) || t.classesRE.test(v) ? v : ''; - } + if (s.theme) { + s.theme = s.theme.replace(/-/, ''); + o = ThemeManager.get(s.theme); + t.theme = new o(); - return v; - }; + if (t.theme.init && s.init_theme) + t.theme.init(t, ThemeManager.urls[s.theme] || tinymce.documentBaseURL.replace(/\/$/, '')); } + function initPlugin(p) { + var c = PluginManager.get(p), u = PluginManager.urls[p] || tinymce.documentBaseURL.replace(/\/$/, ''), po; + if (c && tinymce.inArray(initializedPlugins,p) === -1) { + each(PluginManager.dependencies(p), function(dep){ + initPlugin(dep); + }); + po = new c(t, u); - if (s.cleanup_callback) { - t.onBeforeSetContent.add(function(ed, o) { - o.content = t.execCallback('cleanup_callback', 'insert_to_editor', o.content, o); - }); + t.plugins[p] = po; - t.onPreProcess.add(function(ed, o) { - if (o.set) - t.execCallback('cleanup_callback', 'insert_to_editor_dom', o.node, o); + if (po.init) { + po.init(t, u); + initializedPlugins.push(p); + } + } + } + + // Create all plugins + each(explode(s.plugins.replace(/\-/g, '')), initPlugin); - if (o.get) - t.execCallback('cleanup_callback', 'get_from_editor_dom', o.node, o); - }); + // Setup popup CSS path(s) + if (s.popup_css !== false) { + if (s.popup_css) + s.popup_css = t.documentBaseURI.toAbsolute(s.popup_css); + else + s.popup_css = t.baseURI.toAbsolute("themes/" + s.theme + "/skins/" + s.skin + "/dialog.css"); + } - t.onPostProcess.add(function(ed, o) { - if (o.set) - o.content = t.execCallback('cleanup_callback', 'insert_to_editor', o.content, o); + if (s.popup_css_add) + s.popup_css += ',' + t.documentBaseURI.toAbsolute(s.popup_css_add); - if (o.get) - o.content = t.execCallback('cleanup_callback', 'get_from_editor', o.content, o); - }); - } + t.controlManager = new tinymce.ControlManager(t); - 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 (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(); }); - } - 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); + t.onExecCommand.add(function(ed, cmd, ui, val, a) { + if (cmd != 'Undo' && cmd != 'Redo' && cmd != 'mceRepaint' && (!a || !a.skip_undo)) + t.undoManager.add(); }); } - // Add visual aids when new contents is added - t.onSetContent.add(function() { - t.addVisual(t.getBody()); + t.onExecCommand.add(function(ed, c) { + // Don't refresh the select lists until caret move + if (!/^(FontName|FontSize)$/.test(c)) + t.nodeChanged(); }); - // Remove empty contents - if (s.padd_empty_editor) { - t.onPostProcess.add(function(ed, o) { - o.content = o.content.replace(/^(]*>( | |\s|\u00a0|)<\/p>[\r\n]*|
    [\r\n]*)$/, ''); - }); + // 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); } - 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; + // Enables users to override the control factory + t.onBeforeRenderUI.dispatch(t, t.controlManager); - if (ed.dom.isBlock(pn) && pn.lastChild === n) - ed.dom.add(pn, 'br', {'_mce_bogus' : 1}); - }); - }; + // Measure box + if (s.render_ui) { + w = s.width || e.style.width || e.offsetWidth; + h = s.height || e.style.height || e.offsetHeight; + t.orgDisplay = e.style.display; + re = /^[0-9\.]+(|px)$/i; - t.onExecCommand.add(function(ed, cmd) { - if (cmd === 'CreateLink') - fixLinks(ed); + if (re.test('' + w)) + w = Math.max(parseInt(w) + (o.deltaWidth || 0), 100); + + if (re.test('' + h)) + h = Math.max(parseInt(h) + (o.deltaHeight || 0), 100); + + // Render UI + o = t.theme.renderUI({ + targetNode : e, + width : w, + height : h, + deltaWidth : s.delta_width, + deltaHeight : s.delta_height }); - t.onSetContent.add(t.selection.onSetContent.add(fixLinks)); + t.editorContainer = o.editorContainer; + } + + + // User specified a document.domain value + if (document.domain && location.hostname != document.domain) + tinymce.relaxedDomain = document.domain; + + // Resize editor + DOM.setStyles(o.sizeContainer || o.editorContainer, { + width : w, + height : h + }); - if (!s.readonly) { - try { - // Design mode must be set here once again to fix a bug where - // Ctrl+A/Delete/Backspace didn't work if the editor was added using mceAddControl then removed then added again - d.designMode = 'Off'; - d.designMode = 'On'; - } catch (ex) { - // Will fail on Gecko if the editor is placed in an hidden container element - // The design mode will be set ones the editor is focused - } - } + // Load specified content CSS last + if (s.content_css) { + tinymce.each(explode(s.content_css), function(u) { + t.contentCSS.push(t.documentBaseURI.toAbsolute(u)); + }); } - // A small timeout was needed since firefox will remove. Bug: #1838304 - setTimeout(function () { - if (t.removed) - return; - - t.load({initial : true, format : (s.cleanup_on_startup ? 'html' : 'raw')}); - t.startContent = t.getContent({format : 'raw'}); - t.initialized = true; + h = (o.iframeHeight || h) + (typeof(h) == 'number' ? (o.deltaHeight || 0) : ''); + if (h < 100) + h = 100; - 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}); + t.iframeHTML = s.doctype + ''; - // Load specified content CSS last - if (s.content_css) { - tinymce.each(explode(s.content_css), function(u) { - t.dom.loadCSS(t.documentBaseURI.toAbsolute(u)); - }); - } + // We only need to override paths if we have to + // IE has a bug where it remove site absolute urls to relative ones if this is specified + if (s.document_base_url != tinymce.documentBaseURL) + t.iframeHTML += ''; - // Handle auto focus - if (s.auto_focus) { - setTimeout(function () { - var ed = tinymce.get(s.auto_focus); + // 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 += ''; - ed.selection.select(ed.getBody(), 1); - ed.selection.collapse(1); - ed.getWin().focus(); - }, 100); - } - }, 1); - - e = null; - }, + t.iframeHTML += ''; + // Load the CSS by injecting them into the HTML this will reduce "flicker" + for (i = 0; i < t.contentCSS.length; i++) { + t.iframeHTML += ''; + } - focus : function(sf) { - var oed, t = this, ce = t.settings.content_editable, ieRng, controlElm, doc = t.getDoc(); + t.contentCSS = []; - if (!sf) { - // Get selected control element - ieRng = t.selection.getRng(); - if (ieRng.item) { - controlElm = ieRng.item(0); - } + bi = s.body_id || 'tinymce'; + if (bi.indexOf('=') != -1) { + bi = t.getParam('body_id', '', 'hash'); + bi = bi[t.id] || bi; + } - // Is not content editable - if (!ce) - t.getWin().focus(); + bc = s.body_class || ''; + if (bc.indexOf('=') != -1) { + bc = t.getParam('body_class', '', 'hash'); + bc = bc[t.id] || ''; + } - // Restore selected control element - // This is needed when for example an image is selected within a - // layer a call to focus will then remove the control selection - if (controlElm && controlElm.ownerDocument == doc) { - ieRng = doc.body.createControlRange(); - ieRng.addElement(controlElm); - ieRng.select(); - } + 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();})()'; } - if (tinymce.activeEditor != t) { - if ((oed = tinymce.activeEditor) != null) - oed.onDeactivate.dispatch(oed, t); + // Create iframe + // TODO: ACC add the appropriate description on this. + n = DOM.add(o.iframeContainer, 'iframe', { + id : t.id + "_ifr", + src : u || 'javascript:""', // Workaround for HTTPS warning in IE6/7 + frameBorder : '0', + allowTransparency : "true", + title : s.aria_label, + style : { + width : '100%', + height : h, + display : 'block' // Important for Gecko to render the iframe correctly + } + }); - t.onActivate.dispatch(t, oed); - } + t.contentAreaContainer = o.iframeContainer; + DOM.get(o.editorContainer).style.display = t.orgDisplay; + DOM.get(t.id).style.display = 'none'; + DOM.setAttrib(t.id, 'aria-hidden', true); - tinymce._setActive(t); + if (!tinymce.relaxedDomain || !u) + t.setupIframe(); + + e = n = o = null; // Cleanup }, - execCallback : function(n) { - var t = this, f = t.settings[n], s; + setupIframe : function() { + var t = this, s = t.settings, e = DOM.get(t.id), d = t.getDoc(), h, b; - if (!f) - return; + // Setup iframe body + if (!isIE || !tinymce.relaxedDomain) { + d.open(); + d.write(t.iframeHTML); + d.close(); - // Look through lookup - if (t.callbackLookup && (s = t.callbackLookup[n])) { - f = s.func; - s = s.scope; + if (tinymce.relaxedDomain) + d.domain = tinymce.relaxedDomain; } - if (is(f, 'string')) { - s = f.replace(/\.\w+$/, ''); - s = s ? tinymce.resolve(s) : 0; - f = tinymce.resolve(f); - t.callbackLookup = t.callbackLookup || {}; - t.callbackLookup[n] = {func : f, scope : s}; - } + // It will not steal focus while setting contentEditable + b = t.getBody(); + b.disabled = true; - return f.apply(s || t, Array.prototype.slice.call(arguments, 1)); - }, + if (!s.readonly) + b.contentEditable = true; - translate : function(s) { - var c = this.settings.language || 'en', i18n = tinymce.i18n; + b.disabled = false; - if (!s) - return ''; + t.schema = new tinymce.html.Schema(s); - return i18n[c + '.' + s] || s.replace(/{\#([^}]+)\}/g, function(a, b) { - return i18n[c + '.' + b] || '{#' + b + '}'; + t.dom = new tinymce.dom.DOMUtils(t.getDoc(), { + 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 }); - }, - getLang : function(n, dv) { - return tinymce.i18n[(this.settings.language || 'en') + '.' + n] || (is(dv) ? dv : '{#' + n + '}'); - }, + t.parser = new tinymce.html.DomParser(s, t.schema); - getParam : function(n, dv, ty) { - var tr = tinymce.trim, v = is(this.settings[n]) ? this.settings[n] : dv, o; + // 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); + } + } + }); + } - if (ty === 'hash') { - o = {}; + // 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; - if (is(v, 'string')) { - each(v.indexOf('=') > 0 ? v.split(/[;,](?![^=;,]*(?:[;,]|$))/) : v.split(','), function(v) { - v = v.split('='); + while (i--) { + node = nodes[i]; + value = node.attr(name); + internalName = 'data-mce-' + name; - if (v.length > 1) - o[tr(v[0])] = tr(v[1]); + // Add internal attribute if we need to we don't on a refresh of the document + if (!node.attributes.map[internalName]) { + if (name === "style") + node.attr(internalName, dom.serializeStyle(dom.parseStyle(value), node.name)); else - o[tr(v[0])] = tr(v); - }); - } else - o = v; - - return o; - } + node.attr(internalName, t.convertURL(value, name, node.name)); + } + } + }); - return v; - }, + // Keep scripts from executing + t.parser.addNodeFilter('script', function(nodes, name) { + var i = nodes.length, node; - nodeChanged : function(o) { - var t = this, s = t.selection, n = (isIE ? s.getNode() : s.getStart()) || t.getBody(); + while (i--) { + node = nodes[i]; + node.attr('type', 'mce-' + (node.attr('type') || 'text/javascript')); + } + }); - // Fix for bug #1896577 it seems that this can not be fired while the editor is loading - if (t.initialized) { - o = o || {}; - n = isIE && n.ownerDocument != t.getDoc() ? t.getBody() : n; // Fix for IE initial state + t.parser.addNodeFilter('#cdata', function(nodes, name) { + var i = nodes.length, node; - // Get parents and add them to object - o.parents = []; - t.dom.getParent(n, function(node) { - if (node.nodeName == 'BODY') - return true; + while (i--) { + node = nodes[i]; + node.type = 8; + node.name = '#comment'; + node.value = '[CDATA[' + node.value + ']]'; + } + }); - o.parents.push(node); - }); + t.parser.addNodeFilter('p,h1,h2,h3,h4,h5,h6,div', function(nodes, name) { + var i = nodes.length, node, nonEmptyElements = t.schema.getNonEmptyElements(); - t.onNodeChange.dispatch( - t, - o ? o.controlManager || t.controlManager : t.controlManager, - n, - s.isCollapsed(), - o - ); - } - }, + while (i--) { + node = nodes[i]; - addButton : function(n, s) { - var t = this; + if (node.isEmpty(nonEmptyElements)) + node.empty().append(new tinymce.html.Node('br', 1)).shortEnded = true; + } + }); - t.buttons = t.buttons || {}; - t.buttons[n] = s; - }, + t.serializer = new tinymce.dom.Serializer(s, t.dom, t.schema); - addCommand : function(n, f, s) { - this.execCommands[n] = {func : f, scope : s || this}; - }, + t.selection = new tinymce.dom.Selection(t.dom, t.getWin(), t.serializer); - addQueryStateHandler : function(n, f, s) { - this.queryStateCommands[n] = {func : f, scope : s || this}; - }, + t.formatter = new tinymce.Formatter(this); - addQueryValueHandler : function(n, f, s) { - this.queryValueCommands[n] = {func : f, scope : s || 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'}, defaultBlock: 'div'}, + {selector : 'img,table', collapsed : false, styles : {'float' : 'left'}} + ], - addShortcut : function(pa, desc, cmd_func, sc) { - var t = this, c; + aligncenter : [ + {selector : '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'}} + ], - if (!t.settings.custom_shortcuts) - return false; + alignright : [ + {selector : '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'}} + ], - t.shortcuts = t.shortcuts || {}; + alignfull : [ + {selector : 'p,h1,h2,h3,h4,h5,h6,td,th,div,ul,ol,li', styles : {textAlign : 'justify'}, defaultBlock: 'div'} + ], - if (is(cmd_func, 'string')) { - c = cmd_func; + bold : [ + {inline : 'strong', remove : 'all'}, + {inline : 'span', styles : {fontWeight : 'bold'}}, + {inline : 'b', remove : 'all'} + ], - cmd_func = function() { - t.execCommand(c, false, null); - }; - } + italic : [ + {inline : 'em', remove : 'all'}, + {inline : 'span', styles : {fontStyle : 'italic'}}, + {inline : 'i', remove : 'all'} + ], - if (is(cmd_func, 'object')) { - c = cmd_func; + underline : [ + {inline : 'span', styles : {textDecoration : 'underline'}, exact : true}, + {inline : 'u', remove : 'all'} + ], - cmd_func = function() { - t.execCommand(c[0], c[1], c[2]); - }; - } + strikethrough : [ + {inline : 'span', styles : {textDecoration : 'line-through'}, exact : true}, + {inline : 'strike', remove : 'all'} + ], - each(explode(pa), function(pa) { - var o = { - func : cmd_func, - scope : sc || this, - desc : desc, - alt : false, - ctrl : false, - shift : false - }; + 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'}, - each(explode(pa, '+'), function(v) { - switch (v) { - case 'alt': - case 'ctrl': - case 'shift': - o[v] = true; - break; + link : {inline : 'a', selector : 'a', remove : 'all', split : true, deep : true, + onmatch : function(node) { + return true; + }, - default: - o.charCode = v.charCodeAt(0); - o.keyCode = v.toUpperCase().charCodeAt(0); + 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} + ] + }); - t.shortcuts[(o.ctrl ? 'ctrl' : '') + ',' + (o.alt ? 'alt' : '') + ',' + (o.shift ? 'shift' : '') + ',' + o.keyCode] = o; + // 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'}); }); - return true; - }, + // Register user defined formats + t.formatter.register(t.settings.formats); - execCommand : function(cmd, ui, val, a) { - var t = this, s = 0, o, st; + t.undoManager = new tinymce.UndoManager(t); - if (!/^(mceAddUndoLevel|mceEndUndoLevel|mceBeginUndoLevel|mceRepaint|SelectAll)$/.test(cmd) && (!a || !a.skip_focus)) - t.focus(); + // Pass through + t.undoManager.onAdd.add(function(um, l) { + if (um.hasUndo()) + return t.onChange.dispatch(t, l, um); + }); - o = {}; - t.onBeforeExecCommand.dispatch(t, cmd, ui, val, o); - if (o.terminate) - return false; + t.undoManager.onUndo.add(function(um, l) { + return t.onUndo.dispatch(t, l, um); + }); - // Command callback - if (t.execCallback('execcommand_callback', t.id, t.selection.getNode(), cmd, ui, val)) { - t.onExecCommand.dispatch(t, cmd, ui, val, a); - return true; - } + t.undoManager.onRedo.add(function(um, l) { + return t.onRedo.dispatch(t, l, um); + }); - // Registred commands - if (o = t.execCommands[cmd]) { - st = o.func.call(o.scope, ui, val); + t.forceBlocks = new tinymce.ForceBlocks(t); + t.enterKey = new tinymce.EnterKey(t); - // Fall through on true - if (st !== true) { - t.onExecCommand.dispatch(t, cmd, ui, val, a); - return st; - } - } + t.editorCommands = new tinymce.EditorCommands(t); - // Plugin commands - each(t.plugins, function(p) { - if (p.execCommand && p.execCommand(cmd, ui, val)) { - t.onExecCommand.dispatch(t, cmd, ui, val, a); - s = 1; - return false; - } + // Pass through + t.serializer.onPreProcess.add(function(se, o) { + return t.onPreProcess.dispatch(t, o, se); }); - if (s) - return true; + t.serializer.onPostProcess.add(function(se, o) { + return t.onPostProcess.dispatch(t, o, se); + }); - // Theme commands - if (t.theme && t.theme.execCommand && t.theme.execCommand(cmd, ui, val)) { - t.onExecCommand.dispatch(t, cmd, ui, val, a); - return true; - } + t.onPreInit.dispatch(t); - // Execute global commands - if (tinymce.GlobalCommands.execCommand(t, cmd, ui, val)) { - t.onExecCommand.dispatch(t, cmd, ui, val, a); - return true; - } + if (!s.gecko_spellcheck) + t.getBody().spellcheck = 0; - // Editor commands - if (t.editorCommands.execCommand(cmd, ui, val)) { - t.onExecCommand.dispatch(t, cmd, ui, val, a); - return true; - } + if (!s.readonly) + t._addEvents(); - // Browser commands - t.getDoc().execCommand(cmd, ui, val); - t.onExecCommand.dispatch(t, cmd, ui, val, a); - }, + t.controlManager.onPostRender.dispatch(t, t.controlManager); + t.onPostRender.dispatch(t); - queryCommandState : function(cmd) { - var t = this, o, s; + t.quirks = new tinymce.util.Quirks(this); - // Is hidden then return undefined - if (t._isHidden()) - return; + if (s.directionality) + t.getBody().dir = s.directionality; - // Registred commands - if (o = t.queryStateCommands[cmd]) { - s = o.func.call(o.scope); + if (s.nowrap) + t.getBody().style.whiteSpace = "nowrap"; - // Fall though on true - if (s !== true) - return s; + 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()); + }); } - // Registred commands - o = t.editorCommands.queryCommandState(cmd); - if (o !== -1) - return o; + if (s.save_callback) { + t.onSaveContent.add(function(ed, o) { + var h = t.execCallback('save_callback', t.id, o.content, t.getBody()); - // Browser commands - try { - return this.getDoc().queryCommandState(cmd); - } catch (ex) { - // Fails sometimes see bug: 1896577 + if (h) + o.content = h; + }); } - }, - queryCommandValue : function(c) { - var t = this, o, s; - - // Is hidden then return undefined - if (t._isHidden()) - return; + if (s.onchange_callback) { + t.onChange.add(function(ed, l) { + t.execCallback('onchange_callback', t, l); + }); + } - // Registred commands - if (o = t.queryValueCommands[c]) { - s = o.func.call(o.scope); + 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 ''; + }); + }); + } + }); + } - // Fall though on true - if (s !== true) - return s; + if (s.convert_newlines_to_brs) { + t.onBeforeSetContent.add(function(ed, o) { + if (o.initial) + o.content = o.content.replace(/\r?\n/g, '
    '); + }); } - // Registred commands - o = t.editorCommands.queryCommandValue(c); - if (is(o)) - return o; + if (s.preformatted) { + t.onPostProcess.add(function(ed, o) { + o.content = o.content.replace(/^\s*/, ''); + o.content = o.content.replace(/<\/pre>\s*$/, ''); - // Browser commands - try { - return this.getDoc().queryCommandValue(c); - } catch (ex) { - // Fails sometimes see bug: 1896577 + if (o.set) + o.content = '
    ' + o.content + '
    '; + }); } - }, - show : function() { - var t = this; + if (s.verify_css_classes) { + t.serializer.attribValueFilter = function(n, v) { + var s, cl; - DOM.show(t.getContainer()); - DOM.hide(t.id); - t.load(); - }, + if (n == 'class') { + // Build regexp for classes + if (!t.classesRE) { + cl = t.dom.getClasses(); - hide : function() { - var t = this, d = t.getDoc(); + if (cl.length > 0) { + s = ''; - // Fixed bug where IE has a blinking cursor left from the editor - if (isIE && d) - d.execCommand('SelectAll'); + each (cl, function(o) { + s += (s ? '|' : '') + o['class']; + }); - // We must save before we hide so Safari doesn't crash - t.save(); - DOM.hide(t.getContainer()); - DOM.setStyle(t.id, 'display', t.orgDisplay); - }, + t.classesRE = new RegExp('(' + s + ')', 'gi'); + } + } - isHidden : function() { - return !DOM.isHidden(this.id); - }, + return !t.classesRE || /(\bmceItem\w+\b|\bmceTemp\w+\b)/g.test(v) || t.classesRE.test(v) ? v : ''; + } - setProgressState : function(b, ti, o) { - this.onSetProgressState.dispatch(this, b, ti, o); + return v; + }; + } - return b; - }, + if (s.cleanup_callback) { + t.onBeforeSetContent.add(function(ed, o) { + o.content = t.execCallback('cleanup_callback', 'insert_to_editor', o.content, o); + }); - load : function(o) { - var t = this, e = t.getElement(), h; + t.onPreProcess.add(function(ed, o) { + if (o.set) + t.execCallback('cleanup_callback', 'insert_to_editor_dom', o.node, o); - if (e) { - o = o || {}; - o.load = true; + if (o.get) + t.execCallback('cleanup_callback', 'get_from_editor_dom', o.node, o); + }); - // Double encode existing entities in the value - h = t.setContent(is(e.value) ? e.value : e.innerHTML, o); - o.element = e; + t.onPostProcess.add(function(ed, o) { + if (o.set) + o.content = t.execCallback('cleanup_callback', 'insert_to_editor', o.content, o); + + if (o.get) + o.content = t.execCallback('cleanup_callback', 'get_from_editor', o.content, o); + }); + } + + 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 (s.handle_event_callback) { + t.onEvent.add(function(ed, e, o) { + if (t.execCallback('handle_event_callback', e, ed, o) === false) + Event.cancel(e); + }); + } + + // Add visual aids when new contents is added + t.onSetContent.add(function() { + t.addVisual(t.getBody()); + }); + + // Remove empty contents + if (s.padd_empty_editor) { + t.onPostProcess.add(function(ed, o) { + o.content = o.content.replace(/^(]*>( | |\s|\u00a0|)<\/p>[\r\n]*|
    [\r\n]*)$/, ''); + }); + } - if (!o.no_events) - t.onLoadContent.dispatch(t, o); + 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; - o.element = e = null; + if (ed.dom.isBlock(pn) && pn.lastChild === n) + ed.dom.add(pn, 'br', {'data-mce-bogus' : 1}); + }); + }; - return h; + t.onExecCommand.add(function(ed, cmd) { + if (cmd === 'CreateLink') + fixLinks(ed); + }); + + t.onSetContent.add(t.selection.onSetContent.add(fixLinks)); } - }, - save : function(o) { - var t = this, e = t.getElement(), h, f; + t.load({initial : true, format : 'html'}); + t.startContent = t.getContent({format : 'raw'}); + t.undoManager.add(); + t.initialized = true; - if (!e || !t.initialized) - return; + 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}); - o = o || {}; - o.save = true; + // Load specified content CSS last + each(t.contentCSS, function(u) { + t.dom.loadCSS(u); + }); - // Add undo level will trigger onchange event - if (!o.no_events) { - t.undoManager.typing = 0; - t.undoManager.add(); - } + // Handle auto focus + if (s.auto_focus) { + setTimeout(function () { + var ed = tinymce.get(s.auto_focus); - o.element = e; - h = o.content = t.getContent(o); + ed.selection.select(ed.getBody(), 1); + ed.selection.collapse(1); + ed.getBody().focus(); + ed.getWin().focus(); + }, 100); + } - if (!o.no_events) - t.onSaveContent.dispatch(t, o); + e = null; + }, - h = o.content; - if (!/TEXTAREA|INPUT/i.test(e.nodeName)) { - e.innerHTML = h; + focus : function(sf) { + var oed, t = this, selection = t.selection, ce = t.settings.content_editable, ieRng, controlElm, doc = t.getDoc(); - // Update hidden form element - if (f = DOM.getParent(t.id, 'form')) { - each(f.elements, function(e) { - if (e.name == t.id) { - e.value = h; - return false; - } - }); + if (!sf) { + // Get selected control element + ieRng = selection.getRng(); + if (ieRng.item) { + controlElm = ieRng.item(0); } - } else - e.value = h; - o.element = e = null; - - return h; - }, + t._refreshContentEditable(); - setContent : function(h, o) { - var t = this; + // Is not content editable + if (!ce) + t.getWin().focus(); - o = o || {}; - o.format = o.format || 'html'; - o.set = true; - o.content = h; + // Focus the body as well since it's contentEditable + if (tinymce.isGecko) { + t.getBody().focus(); + } - if (!o.no_events) - t.onBeforeSetContent.dispatch(t, o); + // Restore selected control element + // This is needed when for example an image is selected within a + // layer a call to focus will then remove the control selection + if (controlElm && controlElm.ownerDocument == doc) { + ieRng = doc.body.createControlRange(); + ieRng.addElement(controlElm); + ieRng.select(); + } - // Padd empty content in Gecko and Safari. Commands will otherwise fail on the content - // It will also be impossible to place the caret in the editor unless there is a BR element present - if (!tinymce.isIE && (h.length === 0 || /^\s+$/.test(h))) { - o.content = t.dom.setHTML(t.getBody(), '
    '); - o.format = 'raw'; } - o.content = t.dom.setHTML(t.getBody(), tinymce.trim(o.content)); + if (tinymce.activeEditor != t) { + if ((oed = tinymce.activeEditor) != null) + oed.onDeactivate.dispatch(oed, t); - if (o.format != 'raw' && t.settings.cleanup) { - o.getInner = true; - o.content = t.dom.setHTML(t.getBody(), t.serializer.serialize(t.getBody(), o)); + t.onActivate.dispatch(t, oed); } - if (!o.no_events) - t.onSetContent.dispatch(t, o); - - return o.content; + tinymce._setActive(t); }, - getContent : function(o) { - var t = this, h; + execCallback : function(n) { + var t = this, f = t.settings[n], s; - o = o || {}; - o.format = o.format || 'html'; - o.get = true; + if (!f) + return; - if (!o.no_events) - t.onBeforeGetContent.dispatch(t, o); + // Look through lookup + if (t.callbackLookup && (s = t.callbackLookup[n])) { + f = s.func; + s = s.scope; + } - if (o.format != 'raw' && t.settings.cleanup) { - o.getInner = true; - h = t.serializer.serialize(t.getBody(), o); - } else - h = t.getBody().innerHTML; + if (is(f, 'string')) { + s = f.replace(/\.\w+$/, ''); + s = s ? tinymce.resolve(s) : 0; + f = tinymce.resolve(f); + t.callbackLookup = t.callbackLookup || {}; + t.callbackLookup[n] = {func : f, scope : s}; + } - h = h.replace(/^\s*|\s*$/g, ''); - o.content = h; + return f.apply(s || t, Array.prototype.slice.call(arguments, 1)); + }, - if (!o.no_events) - t.onGetContent.dispatch(t, o); + translate : function(s) { + var c = this.settings.language || 'en', i18n = tinymce.i18n; - return o.content; - }, + if (!s) + return ''; - isDirty : function() { - var t = this; + return i18n[c + '.' + s] || s.replace(/{\#([^}]+)\}/g, function(a, b) { + return i18n[c + '.' + b] || '{#' + b + '}'; + }); + }, - return tinymce.trim(t.startContent) != tinymce.trim(t.getContent({format : 'raw', no_events : 1})) && !t.isNotDirty; + getLang : function(n, dv) { + return tinymce.i18n[(this.settings.language || 'en') + '.' + n] || (is(dv) ? dv : '{#' + n + '}'); }, - getContainer : function() { - var t = this; + getParam : function(n, dv, ty) { + var tr = tinymce.trim, v = is(this.settings[n]) ? this.settings[n] : dv, o; - if (!t.container) - t.container = DOM.get(t.editorContainer || t.id + '_parent'); + if (ty === 'hash') { + o = {}; - return t.container; - }, + if (is(v, 'string')) { + each(v.indexOf('=') > 0 ? v.split(/[;,](?![^=;,]*(?:[;,]|$))/) : v.split(','), function(v) { + v = v.split('='); - getContentAreaContainer : function() { - return this.contentAreaContainer; - }, + if (v.length > 1) + o[tr(v[0])] = tr(v[1]); + else + o[tr(v[0])] = tr(v); + }); + } else + o = v; - getElement : function() { - return DOM.get(this.settings.content_element || this.id); + return o; + } + + return v; }, - getWin : function() { - var t = this, e; + nodeChanged : function(o) { + var t = this, s = t.selection, n = s.getStart() || t.getBody(); - if (!t.contentWindow) { - e = DOM.get(t.id + "_ifr"); + // Fix for bug #1896577 it seems that this can not be fired while the editor is loading + if (t.initialized) { + o = o || {}; + n = isIE && n.ownerDocument != t.getDoc() ? t.getBody() : n; // Fix for IE initial state - if (e) - t.contentWindow = e.contentWindow; - } + // Get parents and add them to object + o.parents = []; + t.dom.getParent(n, function(node) { + if (node.nodeName == 'BODY') + return true; - return t.contentWindow; + o.parents.push(node); + }); + + t.onNodeChange.dispatch( + t, + o ? o.controlManager || t.controlManager : t.controlManager, + n, + s.isCollapsed(), + o + ); + } }, - getDoc : function() { - var t = this, w; + addButton : function(n, s) { + var t = this; - if (!t.contentDocument) { - w = t.getWin(); + t.buttons = t.buttons || {}; + t.buttons[n] = s; + }, - if (w) - t.contentDocument = w.document; - } + addCommand : function(name, callback, scope) { + this.execCommands[name] = {func : callback, scope : scope || this}; + }, - return t.contentDocument; + addQueryStateHandler : function(name, callback, scope) { + this.queryStateCommands[name] = {func : callback, scope : scope || this}; }, - getBody : function() { - return this.bodyElement || this.getDoc().body; + addQueryValueHandler : function(name, callback, scope) { + this.queryValueCommands[name] = {func : callback, scope : scope || this}; }, - convertURL : function(u, n, e) { - var t = this, s = t.settings; + addShortcut : function(pa, desc, cmd_func, sc) { + var t = this, c; - // Use callback instead - if (s.urlconverter_callback) - return t.execCallback('urlconverter_callback', u, e, true, n); + if (!t.settings.custom_shortcuts) + return false; - // 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; + t.shortcuts = t.shortcuts || {}; - // Convert to relative - if (s.relative_urls) - return t.documentBaseURI.toRelative(u); + if (is(cmd_func, 'string')) { + c = cmd_func; - // Convert to absolute - u = t.documentBaseURI.toAbsolute(u, s.remove_script_host); + cmd_func = function() { + t.execCommand(c, false, null); + }; + } - return u; - }, + if (is(cmd_func, 'object')) { + c = cmd_func; - addVisual : function(e) { - var t = this, s = t.settings; + cmd_func = function() { + t.execCommand(c[0], c[1], c[2]); + }; + } - e = e || t.getBody(); + each(explode(pa), function(pa) { + var o = { + func : cmd_func, + scope : sc || this, + desc : desc, + alt : false, + ctrl : false, + shift : false + }; - if (!is(t.hasVisual)) - t.hasVisual = s.visual; + each(explode(pa, '+'), function(v) { + switch (v) { + case 'alt': + case 'ctrl': + case 'shift': + o[v] = true; + break; + + default: + o.charCode = v.charCodeAt(0); + o.keyCode = v.toUpperCase().charCodeAt(0); + } + }); + + t.shortcuts[(o.ctrl ? 'ctrl' : '') + ',' + (o.alt ? 'alt' : '') + ',' + (o.shift ? 'shift' : '') + ',' + o.keyCode] = o; + }); - each(t.dom.select('table,a', e), function(e) { - var v; + return true; + }, - switch (e.nodeName) { - case 'TABLE': - v = t.dom.getAttrib(e, 'border'); + execCommand : function(cmd, ui, val, a) { + var t = this, s = 0, o, st; - if (!v || v == '0') { - if (t.hasVisual) - t.dom.addClass(e, s.visual_table_class); - else - t.dom.removeClass(e, s.visual_table_class); - } + if (!/^(mceAddUndoLevel|mceEndUndoLevel|mceBeginUndoLevel|mceRepaint|SelectAll)$/.test(cmd) && (!a || !a.skip_focus)) + t.focus(); - return; + a = extend({}, a); + t.onBeforeExecCommand.dispatch(t, cmd, ui, val, a); + if (a.terminate) + return false; - case 'A': - v = t.dom.getAttrib(e, 'name'); + // Command callback + if (t.execCallback('execcommand_callback', t.id, t.selection.getNode(), cmd, ui, val)) { + t.onExecCommand.dispatch(t, cmd, ui, val, a); + return true; + } - if (v) { - if (t.hasVisual) - t.dom.addClass(e, 'mceItemAnchor'); - else - t.dom.removeClass(e, 'mceItemAnchor'); - } + // Registred commands + if (o = t.execCommands[cmd]) { + st = o.func.call(o.scope, ui, val); - return; + // Fall through on true + if (st !== true) { + t.onExecCommand.dispatch(t, cmd, ui, val, a); + return st; } - }); - - t.onVisualAid.dispatch(t, e, t.hasVisual); - }, + } - remove : function() { - var t = this, e = t.getContainer(); + // Plugin commands + each(t.plugins, function(p) { + if (p.execCommand && p.execCommand(cmd, ui, val)) { + t.onExecCommand.dispatch(t, cmd, ui, val, a); + s = 1; + return false; + } + }); - t.removed = 1; // Cancels post remove event execution - t.hide(); + if (s) + return true; - t.execCallback('remove_instance_callback', t); - t.onRemove.dispatch(t); + // Theme commands + if (t.theme && t.theme.execCommand && t.theme.execCommand(cmd, ui, val)) { + t.onExecCommand.dispatch(t, cmd, ui, val, a); + return true; + } - // Clear all execCommand listeners this is required to avoid errors if the editor was removed inside another command - t.onExecCommand.listeners = []; + // Editor commands + if (t.editorCommands.execCommand(cmd, ui, val)) { + t.onExecCommand.dispatch(t, cmd, ui, val, a); + return true; + } - tinymce.remove(t); - DOM.remove(e); + // Browser commands + t.getDoc().execCommand(cmd, ui, val); + t.onExecCommand.dispatch(t, cmd, ui, val, a); }, - destroy : function(s) { - var t = this; + queryCommandState : function(cmd) { + var t = this, o, s; - // One time is enough - if (t.destroyed) + // Is hidden then return undefined + if (t._isHidden()) return; - if (!s) { - tinymce.removeUnload(t.destroy); - tinyMCE.onBeforeUnload.remove(t._beforeUnload); + // Registred commands + if (o = t.queryStateCommands[cmd]) { + s = o.func.call(o.scope); - // Manual destroy - if (t.theme && t.theme.destroy) - t.theme.destroy(); + // Fall though on true + if (s !== true) + return s; + } - // Destroy controls, selection and dom - t.controlManager.destroy(); - t.selection.destroy(); - t.dom.destroy(); + // Registred commands + o = t.editorCommands.queryCommandState(cmd); + if (o !== -1) + return o; - // Remove all events + // Browser commands + try { + return this.getDoc().queryCommandState(cmd); + } catch (ex) { + // Fails sometimes see bug: 1896577 + } + }, - // 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()); - } + queryCommandValue : function(c) { + var t = this, o, s; - Event.clear(t.getBody()); - Event.clear(t.formElement); - } + // Is hidden then return undefined + if (t._isHidden()) + return; - if (t.formElement) { - t.formElement.submit = t.formElement._mceOldSubmit; - t.formElement._mceOldSubmit = null; + // Registred commands + if (o = t.queryValueCommands[c]) { + s = o.func.call(o.scope); + + // Fall though on true + if (s !== true) + return s; } - t.contentAreaContainer = t.formElement = t.container = t.settings.content_element = t.bodyElement = t.contentDocument = t.contentWindow = null; + // Registred commands + o = t.editorCommands.queryCommandValue(c); + if (is(o)) + return o; - if (t.selection) - t.selection = t.selection.win = t.selection.dom = t.selection.dom.doc = null; + // Browser commands + try { + return this.getDoc().queryCommandValue(c); + } catch (ex) { + // Fails sometimes see bug: 1896577 + } + }, - t.destroyed = 1; + show : function() { + var t = this; + + DOM.show(t.getContainer()); + DOM.hide(t.id); + t.load(); }, - // Internal functions + hide : function() { + var t = this, d = t.getDoc(); - _addEvents : function() { - // 'focus', 'blur', 'dblclick', 'beforedeactivate', submit, reset - var t = this, i, s = t.settings, 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 - }; + // Fixed bug where IE has a blinking cursor left from the editor + if (isIE && d) + d.execCommand('SelectAll'); - function eventHandler(e, o) { - var ty = e.type; + // We must save before we hide so Safari doesn't crash + t.save(); + DOM.hide(t.getContainer()); + DOM.setStyle(t.id, 'display', t.orgDisplay); + }, - // Don't fire events when it's removed - if (t.removed) - return; + isHidden : function() { + return !DOM.isHidden(this.id); + }, - // Generic event handler - if (t.onEvent.dispatch(t, e, o) !== false) { - // Specific event handler - t[lo[e.fakeType || e.type]].dispatch(t, e, o); - } - }; + setProgressState : function(b, ti, o) { + this.onSetProgressState.dispatch(this, b, ti, o); - // Add DOM events - each(lo, function(v, k) { - switch (k) { - case 'contextmenu': - if (tinymce.isOpera) { - // Fake contextmenu on Opera - t.dom.bind(t.getBody(), 'mousedown', function(e) { - if (e.ctrlKey) { - e.fakeType = 'contextmenu'; - eventHandler(e); - } - }); - } else - t.dom.bind(t.getBody(), k, eventHandler); - break; + return b; + }, - case 'paste': - t.dom.bind(t.getBody(), k, function(e) { - eventHandler(e); - }); - break; + load : function(o) { + var t = this, e = t.getElement(), h; - case 'submit': - case 'reset': - t.dom.bind(t.getElement().form || DOM.getParent(t.id, 'form'), k, eventHandler); - break; + if (e) { + o = o || {}; + o.load = true; - default: - t.dom.bind(s.content_editable ? t.getBody() : t.getDoc(), k, eventHandler); - } - }); + // Double encode existing entities in the value + h = t.setContent(is(e.value) ? e.value : e.innerHTML, o); + o.element = e; - t.dom.bind(s.content_editable ? t.getBody() : (isGecko ? t.getDoc() : t.getWin()), 'focus', function(e) { - t.focus(true); - }); + if (!o.no_events) + t.onLoadContent.dispatch(t, o); + o.element = e = null; - // 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) { - // Convert all images to absolute URLs -/* t.onSetContent.add(function(ed, o) { - each(ed.dom.select('img'), function(e) { - var v; + return h; + } + }, - if (v = e.getAttribute('_mce_src')) - e.src = t.documentBaseURI.toAbsolute(v); - }) - });*/ + save : function(o) { + var t = this, e = t.getElement(), h, f; - t.dom.bind(t.getDoc(), 'DOMNodeInserted', function(e) { - var v; + if (!e || !t.initialized) + return; - e = e.target; + o = o || {}; + o.save = true; - if (e.nodeType === 1 && e.nodeName === 'IMG' && (v = e.getAttribute('_mce_src'))) - e.src = t.documentBaseURI.toAbsolute(v); - }); + // Add undo level will trigger onchange event + if (!o.no_events) { + t.undoManager.typing = false; + t.undoManager.add(); } - // Set various midas options in Gecko - if (isGecko) { - function setOpts() { - var t = this, d = t.getDoc(), s = t.settings; + o.element = e; + h = o.content = t.getContent(o); + + if (!o.no_events) + t.onSaveContent.dispatch(t, o); + + h = o.content; - if (isGecko && !s.readonly) { - if (t._isHidden()) { - try { - if (!s.content_editable) - d.designMode = 'On'; - } catch (ex) { - // Fails if it's hidden - } - } + if (!/TEXTAREA|INPUT/i.test(e.nodeName)) { + e.innerHTML = h; - 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) {} + // Update hidden form element + if (f = DOM.getParent(t.id, 'form')) { + each(f.elements, function(e) { + if (e.name == t.id) { + e.value = h; + return false; } + }); + } + } else + e.value = h; - if (!s.table_inline_editing) - try {d.execCommand('enableInlineTableEditing', false, false);} catch (ex) {} + o.element = e = null; - if (!s.object_resizing) - try {d.execCommand('enableObjectResizing', false, false);} catch (ex) {} - } - }; + return h; + }, - t.onBeforeExecCommand.add(setOpts); - t.onMouseDown.add(setOpts); - } + setContent : function(content, args) { + var self = this, rootNode, body = self.getBody(), forcedRootBlockName; - // Workaround for bug, http://bugs.webkit.org/show_bug.cgi?id=12250 - // WebKit can't even do simple things like selecting an image - // This also fixes so it's possible to select mceItemAnchors - if (tinymce.isWebKit) { - t.onClick.add(function(ed, e) { - e = e.target; + // Setup args object + args = args || {}; + args.format = args.format || 'html'; + args.set = true; + args.content = content; - // Needs tobe the setBaseAndExtend or it will fail to select floated images - if (e.nodeName == 'IMG' || (e.nodeName == 'A' && t.dom.hasClass(e, 'mceItemAnchor'))) - t.selection.getSel().setBaseAndExtent(e, 0, e, 1); - }); - } + // Do preprocessing + if (!args.no_events) + self.onBeforeSetContent.dispatch(self, args); - // Add node change handlers - t.onMouseUp.add(t.nodeChanged); - //t.onClick.add(t.nodeChanged); - t.onKeyUp.add(function(ed, e) { - var c = e.keyCode; + content = args.content; - 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(); - }); + // Padd empty content in Gecko and Safari. Commands will otherwise fail on the content + // It will also be impossible to place the caret in the editor unless there is a BR element present + if (!tinymce.isIE && (content.length === 0 || /^\s+$/.test(content))) { + forcedRootBlockName = self.settings.forced_root_block; + if (forcedRootBlockName) + content = '<' + forcedRootBlockName + '>
    '; + else + content = '
    '; - // Add reset handler - t.onReset.add(function() { - t.setContent(t.startContent, {format : 'raw'}); - }); + body.innerHTML = content; + self.selection.select(body, true); + self.selection.collapse(true); + return; + } - // 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'); - } + // Parse and serialize the html + if (args.format !== 'raw') { + content = new tinymce.html.Serializer({}, self.schema).serialize( + self.parser.parse(content) + ); + } - // 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'); + // Set the new cleaned contents to the editor + args.content = tinymce.trim(content); + self.dom.setHTML(body, args.content); - // BlockFormat shortcuts keys - for (i=1; i<=6; i++) - t.addShortcut('ctrl+' + i, '', ['FormatBlock', false, 'h' + i]); + // Do post processing + if (!args.no_events) + self.onSetContent.dispatch(self, args); - t.addShortcut('ctrl+7', '', ['FormatBlock', false, '

    ']); - t.addShortcut('ctrl+8', '', ['FormatBlock', false, '

    ']); - t.addShortcut('ctrl+9', '', ['FormatBlock', false, '
    ']); + self.selection.normalize(); - function find(e) { - var v = null; + return args.content; + }, - if (!e.altKey && !e.ctrlKey && !e.metaKey) - return v; + getContent : function(args) { + var self = this, content; - each(t.shortcuts, function(o) { - if (tinymce.isMac && o.ctrl != e.metaKey) - return; - else if (!tinymce.isMac && o.ctrl != e.ctrlKey) - return; + // Setup args object + args = args || {}; + args.format = args.format || 'html'; + args.get = true; - if (o.alt != e.altKey) - return; + // Do preprocessing + if (!args.no_events) + self.onBeforeGetContent.dispatch(self, args); - if (o.shift != e.shiftKey) - return; + // Get raw contents or by default the cleaned contents + if (args.format == 'raw') + content = self.getBody().innerHTML; + else + content = self.serializer.serialize(self.getBody(), args); - if (e.keyCode == o.keyCode || (e.charCode && e.charCode == o.charCode)) { - v = o; - return false; - } - }); + args.content = tinymce.trim(content); - return v; - }; + // Do post processing + if (!args.no_events) + self.onGetContent.dispatch(self, args); - t.onKeyUp.add(function(ed, e) { - var o = find(e); + return args.content; + }, - if (o) - return Event.cancel(e); - }); + isDirty : function() { + var self = this; - t.onKeyPress.add(function(ed, e) { - var o = find(e); + return tinymce.trim(self.startContent) != tinymce.trim(self.getContent({format : 'raw', no_events : 1})) && !self.isNotDirty; + }, - if (o) - return Event.cancel(e); - }); + getContainer : function() { + var t = this; - t.onKeyDown.add(function(ed, e) { - var o = find(e); + if (!t.container) + t.container = DOM.get(t.editorContainer || t.id + '_parent'); - if (o) { - o.func.call(o.scope); - return Event.cancel(e); - } - }); - } + return t.container; + }, - 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 - t.dom.bind(t.getDoc(), 'controlselect', function(e) { - var re = t.resizeInfo, cb; + getContentAreaContainer : function() { + return this.contentAreaContainer; + }, - e = e.target; + getElement : function() { + return DOM.get(this.settings.content_element || this.id); + }, - // Don't do this action for non image elements - if (e.nodeName !== 'IMG') - return; + getWin : function() { + var t = this, e; - if (re) - t.dom.unbind(re.node, re.ev, re.cb); + if (!t.contentWindow) { + e = DOM.get(t.id + "_ifr"); - if (!t.dom.hasClass(e, 'mceItemNoResize')) { - ev = 'resizeend'; - cb = t.dom.bind(e, ev, function(e) { - var v; + if (e) + t.contentWindow = e.contentWindow; + } - e = e.target; + return t.contentWindow; + }, - if (v = t.dom.getStyle(e, 'width')) { - t.dom.setAttrib(e, 'width', v.replace(/[^0-9%]+/g, '')); - t.dom.setStyle(e, 'width', ''); - } + getDoc : function() { + var t = this, w; - if (v = t.dom.getStyle(e, 'height')) { - t.dom.setAttrib(e, 'height', v.replace(/[^0-9%]+/g, '')); - t.dom.setStyle(e, 'height', ''); - } - }); - } else { - ev = 'resizestart'; - cb = t.dom.bind(e, 'resizestart', Event.cancel, Event); - } + if (!t.contentDocument) { + w = t.getWin(); - re = t.resizeInfo = { - node : e, - ev : ev, - cb : cb - }; - }); + if (w) + t.contentDocument = w.document; + } - t.onKeyDown.add(function(ed, e) { - switch (e.keyCode) { - case 8: - // Fix IE control + backspace browser bug - if (t.selection.getRng().item) { - ed.dom.remove(t.selection.getRng().item(0)); - return Event.cancel(e); - } - } - }); + return t.contentDocument; + }, - /*if (t.dom.boxModel) { - t.getBody().style.height = '100%'; + getBody : function() { + return this.bodyElement || this.getDoc().body; + }, - Event.add(t.getWin(), 'resize', function(e) { - var docElm = t.getDoc().documentElement; + convertURL : function(u, n, e) { + var t = this, s = t.settings; - docElm.style.height = (docElm.offsetHeight - 10) + 'px'; - }); - }*/ - } + // Use callback instead + if (s.urlconverter_callback) + return t.execCallback('urlconverter_callback', u, e, true, n); - if (tinymce.isOpera) { - t.onClick.add(function(ed, e) { - Event.prevent(e); - }); - } + // 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; - // Add custom undo/redo handlers - if (s.custom_undo_redo) { - function addUndo() { - t.undoManager.typing = 0; - t.undoManager.add(); - }; + // Convert to relative + if (s.relative_urls) + return t.documentBaseURI.toRelative(u); - t.dom.bind(t.getDoc(), 'focusout', function(e) { - if (!t.removed && t.undoManager.typing) - addUndo(); - }); + // Convert to absolute + u = t.documentBaseURI.toAbsolute(u, s.remove_script_host); - t.onKeyUp.add(function(ed, e) { - if ((e.keyCode >= 33 && e.keyCode <= 36) || (e.keyCode >= 37 && e.keyCode <= 40) || e.keyCode == 13 || e.keyCode == 45 || e.ctrlKey) - addUndo(); - }); + return u; + }, - t.onKeyDown.add(function(ed, e) { - var rng, tmpRng, parent, offset; - - // IE has a really odd bug where the DOM might include an node that doesn't have - // a proper structure. If you try to access nodeValue it would throw an illegal value exception. - // This seems to only happen when you delete contents and it seems to be avoidable if you refresh the element - // after you delete contents from it. See: #3008923 - if (isIE && e.keyCode == 46) { - rng = t.selection.getRng(); - - if (rng.parentElement) { - parent = rng.parentElement(); - - // Get the current caret position within the element - tmpRng = rng.duplicate(); - tmpRng.moveToElementText(parent); - tmpRng.setEndPoint('EndToEnd', rng); - offset = tmpRng.text.length; - - // Select next word when ctrl key is used in combo with delete - if (e.ctrlKey) { - rng.moveEnd('word', 1); - rng.select(); - } + addVisual : function(e) { + var t = this, s = t.settings; - // Delete contents - t.selection.getSel().clear(); + e = e || t.getBody(); - // Check if we are within the same parent - if (rng.parentElement() == parent) { - try { - // Update the HTML and hopefully it will remove the artifacts - parent.innerHTML = parent.innerHTML; - } catch (ex) { - // And since it's IE it can sometimes produce an unknown runtime error - } + if (!is(t.hasVisual)) + t.hasVisual = s.visual; - // Restore the caret position - tmpRng.moveToElementText(parent); - tmpRng.collapse(); - tmpRng.move('character', offset); - tmpRng.select(); - } + each(t.dom.select('table,a', e), function(e) { + var v; - // Block the default delete behavior since it might be broken - e.preventDefault(); - return; - } - } + switch (e.nodeName) { + case 'TABLE': + v = t.dom.getAttrib(e, 'border'); - // Is caracter positon keys - if ((e.keyCode >= 33 && e.keyCode <= 36) || (e.keyCode >= 37 && e.keyCode <= 40) || e.keyCode == 13 || e.keyCode == 45) { - if (t.undoManager.typing) - addUndo(); + if (!v || v == '0') { + if (t.hasVisual) + t.dom.addClass(e, s.visual_table_class); + else + t.dom.removeClass(e, s.visual_table_class); + } return; - } - - if (!t.undoManager.typing) { - t.undoManager.add(); - t.undoManager.typing = 1; - } - }); - t.onMouseDown.add(function() { - if (t.undoManager.typing) - addUndo(); - }); - } - }, + case 'A': + v = t.dom.getAttrib(e, 'name'); - _isHidden : function() { - var s; + if (v) { + if (t.hasVisual) + t.dom.addClass(e, 'mceItemAnchor'); + else + t.dom.removeClass(e, 'mceItemAnchor'); + } - if (!isGecko) - return 0; + return; + } + }); - // Weird, wheres that cursor selection? - s = this.selection.getSel(); - return (!s || !s.rangeCount || s.rangeCount == 0); + t.onVisualAid.dispatch(t, e, t.hasVisual); }, - // Fix for bug #1867292 - _fixNesting : function(s) { - var d = [], i; - - s = s.replace(/<(\/)?([^\s>]+)[^>]*?>/g, function(a, b, c) { - var e; + remove : function() { + var t = this, e = t.getContainer(); - // Handle end element - if (b === '/') { - if (!d.length) - return ''; + if (!t.removed) { + t.removed = 1; // Cancels post remove event execution + t.hide(); - if (c !== d[d.length - 1].tag) { - for (i=d.length - 1; i>=0; i--) { - if (d[i].tag === c) { - d[i].close = 1; - break; - } - } + // Remove all events - return ''; - } else { - d.pop(); + // 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()); + } - if (d.length && d[d.length - 1].close) { - a = a + ''; - d.pop(); - } - } - } else { - // Ignore these - if (/^(br|hr|input|meta|img|link|param)$/i.test(c)) - return a; + Event.clear(t.getBody()); + Event.clear(t.formElement); + Event.unbind(e); - // Ignore closed ones - if (/\/>$/.test(a)) - return a; + t.execCallback('remove_instance_callback', t); + t.onRemove.dispatch(t); - d.push({tag : c}); // Push start element - } + // Clear all execCommand listeners this is required to avoid errors if the editor was removed inside another command + t.onExecCommand.listeners = []; - return a; - }); + tinymce.remove(t); + DOM.remove(e); + } + }, - // End all open tags - for (i=d.length - 1; i>=0; i--) - s += ''; + destroy : function(s) { + var t = this; - return s; - } - }); -})(tinymce); + // One time is enough + if (t.destroyed) + return; -(function(tinymce) { - // Added for compression purposes - var each = tinymce.each, undefined, TRUE = true, FALSE = false; + // 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()); + } - tinymce.EditorCommands = function(editor) { - var dom = editor.dom, - selection = editor.selection, - commands = {state: {}, exec : {}, value : {}}, - settings = editor.settings, - bookmark; + if (!s) { + tinymce.removeUnload(t.destroy); + tinyMCE.onBeforeUnload.remove(t._beforeUnload); - function execCommand(command, ui, value) { - var func; + // Manual destroy + if (t.theme && t.theme.destroy) + t.theme.destroy(); - command = command.toLowerCase(); - if (func = commands.exec[command]) { - func(command, ui, value); - return TRUE; + // Destroy controls, selection and dom + t.controlManager.destroy(); + t.selection.destroy(); + t.dom.destroy(); } - return FALSE; - }; + if (t.formElement) { + t.formElement.submit = t.formElement._mceOldSubmit; + t.formElement._mceOldSubmit = null; + } - function queryCommandState(command) { - var func; + t.contentAreaContainer = t.formElement = t.container = t.settings.content_element = t.bodyElement = t.contentDocument = t.contentWindow = null; - command = command.toLowerCase(); - if (func = commands.state[command]) - return func(command); + if (t.selection) + t.selection = t.selection.win = t.selection.dom = t.selection.dom.doc = null; - return -1; - }; + t.destroyed = 1; + }, - function queryCommandValue(command) { - var func; + // Internal functions - command = command.toLowerCase(); - if (func = commands.value[command]) - return func(command); + _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 + }; - return FALSE; - }; + function eventHandler(e, o) { + var ty = e.type; - function addCommands(command_list, type) { - type = type || 'exec'; + // Don't fire events when it's removed + if (t.removed) + return; - each(command_list, function(callback, command) { - each(command.toLowerCase().split(','), function(command) { - commands[type][command] = callback; - }); - }); - }; + // Generic event handler + if (t.onEvent.dispatch(t, e, o) !== false) { + // Specific event handler + t[lo[e.fakeType || e.type]].dispatch(t, e, o); + } + }; - // Expose public methods - tinymce.extend(this, { - execCommand : execCommand, - queryCommandState : queryCommandState, - queryCommandValue : queryCommandValue, - addCommands : addCommands - }); + // Add DOM events + each(lo, function(v, k) { + switch (k) { + case 'contextmenu': + dom.bind(t.getDoc(), k, eventHandler); + break; - // Private methods + case 'paste': + dom.bind(t.getBody(), k, function(e) { + eventHandler(e); + }); + break; - function execNativeCommand(command, ui, value) { - if (ui === undefined) - ui = FALSE; + case 'submit': + case 'reset': + dom.bind(t.getElement().form || DOM.getParent(t.id, 'form'), k, eventHandler); + break; - if (value === undefined) - value = null; + default: + dom.bind(s.content_editable ? t.getBody() : t.getDoc(), k, eventHandler); + } + }); - return editor.getDoc().execCommand(command, ui, value); - }; + dom.bind(s.content_editable ? t.getBody() : (isGecko ? t.getDoc() : t.getWin()), 'focus', function(e) { + t.focus(true); + }); - function isFormatMatch(name) { - return editor.formatter.match(name); - }; - function toggleFormat(name, value) { - editor.formatter.toggle(name, value ? {value : value} : undefined); - }; + // 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; - function storeSelection(type) { - bookmark = selection.getBookmark(type); - }; + e = e.target; - function restoreSelection() { - selection.moveToBookmark(bookmark); - }; + if (e.nodeType === 1 && e.nodeName === 'IMG' && (v = e.getAttribute('data-mce-src'))) + e.src = t.documentBaseURI.toAbsolute(v); + }); + } - // Add execCommand overrides - addCommands({ - // Ignore these, added for compatibility - 'mceResetDesignMode,mceBeginUndoLevel' : function() {}, + // Set various midas options in Gecko + if (isGecko) { + function setOpts() { + var t = this, d = t.getDoc(), s = t.settings; - // Add undo manager logic - 'mceEndUndoLevel,mceAddUndoLevel' : function() { - editor.undoManager.add(); - }, + if (isGecko && !s.readonly) { + t._refreshContentEditable(); - 'Cut,Copy,Paste' : function(command) { - var doc = editor.getDoc(), failed; + 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) {} + } - // Try executing the native command - try { - execNativeCommand(command); - } catch (ex) { - // Command failed - failed = TRUE; - } + if (!s.table_inline_editing) + try {d.execCommand('enableInlineTableEditing', false, false);} catch (ex) {} - // Present alert message about clipboard access not being available - if (failed || !doc.queryCommandSupported(command)) { - if (tinymce.isGecko) { - editor.windowManager.confirm(editor.getLang('clipboard_msg'), function(state) { - if (state) - open('http://www.mozilla.org/editor/midasdemo/securityprefs.html', '_blank'); - }); - } else - editor.windowManager.alert(editor.getLang('clipboard_no_support')); - } - }, + if (!s.object_resizing) + try {d.execCommand('enableObjectResizing', false, false);} catch (ex) {} + } + }; - // Override unlink command - unlink : function(command) { - if (selection.isCollapsed()) - selection.select(selection.getNode()); + t.onBeforeExecCommand.add(setOpts); + t.onMouseDown.add(setOpts); + } - execNativeCommand(command); - selection.collapse(FALSE); - }, + // Add node change handlers + t.onMouseUp.add(t.nodeChanged); + //t.onClick.add(t.nodeChanged); + t.onKeyUp.add(function(ed, e) { + var c = e.keyCode; - // Override justify commands to use the text formatter engine - 'JustifyLeft,JustifyCenter,JustifyRight,JustifyFull' : function(command) { - var align = command.substring(7); + 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(); + }); - // Remove all other alignments first - each('left,center,right,full'.split(','), function(name) { - if (align != name) - editor.formatter.remove('align' + name); - }); - toggleFormat('align' + align); - }, + // Add block quote deletion handler + t.onKeyDown.add(function(ed, e) { + if (e.keyCode != VK.BACKSPACE) + return; - // Override list commands to fix WebKit bug - 'InsertUnorderedList,InsertOrderedList' : function(command) { - var listElm, listParent; + var rng = ed.selection.getRng(); + if (!rng.collapsed) + return; - execNativeCommand(command); + var n = rng.startContainer; + var offset = rng.startOffset; - // WebKit produces lists within block elements so we need to split them - // we will replace the native list creation logic to custom logic later on - // TODO: Remove this when the list creation logic is removed - listElm = dom.getParent(selection.getNode(), 'ol,ul'); - if (listElm) { - listParent = listElm.parentNode; + while (n && n.nodeType && n.nodeType != 1 && n.parentNode) + n = n.parentNode; - // If list is within a text block then split that block - if (/^(H[1-6]|P|ADDRESS|PRE)$/.test(listParent.nodeName)) { - storeSelection(); - dom.split(listParent, listElm); - restoreSelection(); - } - } - }, + // 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); - // Override commands to use the text formatter engine - 'Bold,Italic,Underline,Strikethrough' : function(command) { - toggleFormat(command); - }, + // Move the caret to the beginning of n + rng.setStart(n, 0); + rng.setEnd(n, 0); + ed.selection.setRng(rng); + ed.selection.collapse(false); + } + }); - // Override commands to use the text formatter engine - 'ForeColor,HiliteColor,FontName' : function(command, ui, value) { - toggleFormat(command, value); - }, - FontSize : function(command, ui, value) { - var fontClasses, fontSizes; - // Convert font size 1-7 to styles - if (value >= 1 && value <= 7) { - fontSizes = tinymce.explode(settings.font_size_style_values); - fontClasses = tinymce.explode(settings.font_size_classes); + // Add reset handler + t.onReset.add(function() { + t.setContent(t.startContent, {format : 'raw'}); + }); - if (fontClasses) - value = fontClasses[value - 1] || value; - else - value = fontSizes[value - 1] || value; + // 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'); } - toggleFormat(command, value); - }, + // 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'); - RemoveFormat : function(command) { - editor.formatter.remove(command); - }, + // BlockFormat shortcuts keys + for (i=1; i<=6; i++) + t.addShortcut('ctrl+' + i, '', ['FormatBlock', false, 'h' + i]); - mceBlockQuote : function(command) { - toggleFormat('blockquote'); - }, + t.addShortcut('ctrl+7', '', ['FormatBlock', false, 'p']); + t.addShortcut('ctrl+8', '', ['FormatBlock', false, 'div']); + t.addShortcut('ctrl+9', '', ['FormatBlock', false, 'address']); - FormatBlock : function(command, ui, value) { - return toggleFormat(value); - }, + function find(e) { + var v = null; - mceCleanup : function() { - var bookmark = selection.getBookmark(); + if (!e.altKey && !e.ctrlKey && !e.metaKey) + return v; - editor.setContent(editor.getContent({cleanup : TRUE}), {cleanup : TRUE}); + each(t.shortcuts, function(o) { + if (tinymce.isMac && o.ctrl != e.metaKey) + return; + else if (!tinymce.isMac && o.ctrl != e.ctrlKey) + return; - selection.moveToBookmark(bookmark); - }, + if (o.alt != e.altKey) + return; - mceRemoveNode : function(command, ui, value) { - var node = value || selection.getNode(); + if (o.shift != e.shiftKey) + return; - // Make sure that the body node isn't removed - if (node != editor.getBody()) { - storeSelection(); - editor.dom.remove(node, TRUE); - restoreSelection(); - } - }, + if (e.keyCode == o.keyCode || (e.charCode && e.charCode == o.charCode)) { + v = o; + return false; + } + }); - mceSelectNodeDepth : function(command, ui, value) { - var counter = 0; + return v; + }; - dom.getParent(selection.getNode(), function(node) { - if (node.nodeType == 1 && counter++ == value) { - selection.select(node); - return FALSE; - } - }, editor.getBody()); - }, + t.onKeyUp.add(function(ed, e) { + var o = find(e); - mceSelectNode : function(command, ui, value) { - selection.select(value); - }, + if (o) + return Event.cancel(e); + }); - mceInsertContent : function(command, ui, value) { - selection.setContent(value); - }, + t.onKeyPress.add(function(ed, e) { + var o = find(e); - mceInsertRawHTML : function(command, ui, value) { - selection.setContent('tiny_mce_marker'); - editor.setContent(editor.getContent().replace(/tiny_mce_marker/g, value)); - }, + if (o) + return Event.cancel(e); + }); - mceSetContent : function(command, ui, value) { - editor.setContent(value); - }, + t.onKeyDown.add(function(ed, e) { + var o = find(e); - 'Indent,Outdent' : function(command) { - var intentValue, indentUnit, value; + if (o) { + o.func.call(o.scope); + return Event.cancel(e); + } + }); + } - // Setup indent level - intentValue = settings.indentation; - indentUnit = /[a-z%]+$/i.exec(intentValue); - intentValue = parseInt(intentValue); + 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; - if (!queryCommandState('InsertUnorderedList') && !queryCommandState('InsertOrderedList')) { - each(selection.getSelectedBlocks(), function(element) { - if (command == 'outdent') { - value = Math.max(0, parseInt(element.style.paddingLeft || 0) - intentValue); - dom.setStyle(element, 'paddingLeft', value ? value + indentUnit : ''); - } else - dom.setStyle(element, 'paddingLeft', (parseInt(element.style.paddingLeft || 0) + intentValue) + indentUnit); - }); - } else - execNativeCommand(command); - }, + e = e.target; - mceRepaint : function() { - var bookmark; + // Don't do this action for non image elements + if (e.nodeName !== 'IMG') + return; - if (tinymce.isGecko) { - try { - storeSelection(TRUE); + if (re) + dom.unbind(re.node, re.ev, re.cb); - if (selection.getSel()) - selection.getSel().selectAllChildren(editor.getBody()); + if (!dom.hasClass(e, 'mceItemNoResize')) { + ev = 'resizeend'; + cb = dom.bind(e, ev, function(e) { + var v; - selection.collapse(TRUE); - restoreSelection(); - } catch (ex) { - // Ignore + e = e.target; + + if (v = dom.getStyle(e, 'width')) { + dom.setAttrib(e, 'width', v.replace(/[^0-9%]+/g, '')); + dom.setStyle(e, 'width', ''); + } + + 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); } - } - }, - mceToggleFormat : function(command, ui, value) { - editor.formatter.toggle(value); - }, + re = t.resizeInfo = { + node : e, + ev : ev, + cb : cb + }; + }); + } - InsertHorizontalRule : function() { - selection.setContent('
    '); - }, + if (tinymce.isOpera) { + t.onClick.add(function(ed, e) { + Event.prevent(e); + }); + } - mceToggleVisualAid : function() { - editor.hasVisual = !editor.hasVisual; - editor.addVisual(); - }, + // Add custom undo/redo handlers + if (s.custom_undo_redo) { + function addUndo() { + t.undoManager.typing = false; + t.undoManager.add(); + }; - mceReplaceContent : function(command, ui, value) { - selection.setContent(value.replace(/\{\$selection\}/g, selection.getContent({format : 'text'}))); - }, + var focusLostFunc = tinymce.isGecko ? 'blur' : 'focusout'; + dom.bind(t.getDoc(), focusLostFunc, function(e){ + if (!t.removed && t.undoManager.typing) + addUndo(); + }); - mceInsertLink : function(command, ui, value) { - var link = dom.getParent(selection.getNode(), 'a'); + // Add undo level when contents is drag/dropped within the editor + t.dom.bind(t.dom.getRoot(), 'dragend', function(e) { + addUndo(); + }); - if (tinymce.is(value, 'string')) - value = {href : value}; + t.onKeyUp.add(function(ed, e) { + var keyCode = e.keyCode; - if (!link) { - execNativeCommand('CreateLink', FALSE, 'javascript:mctmp(0);'); - each(dom.select('a[href=javascript:mctmp(0);]'), function(link) { - dom.setAttribs(link, value); - }); - } else { - if (value.href) - dom.setAttribs(link, value); - else - editor.dom.remove(link, TRUE); - } - }, - - selectAll : function() { - var root = dom.getRoot(); - var rng = dom.createRng(); - rng.setStart(root, 0); - rng.setEnd(root, root.childNodes.length); - editor.selection.setRng(rng); - } - }); + if ((keyCode >= 33 && keyCode <= 36) || (keyCode >= 37 && keyCode <= 40) || keyCode == 13 || keyCode == 45 || e.ctrlKey) + addUndo(); + }); - // Add queryCommandState overrides - addCommands({ - // Override justify commands - 'JustifyLeft,JustifyCenter,JustifyRight,JustifyFull' : function(command) { - return isFormatMatch('align' + command.substring(7)); - }, + t.onKeyDown.add(function(ed, e) { + var keyCode = e.keyCode, sel; - 'Bold,Italic,Underline,Strikethrough' : function(command) { - return isFormatMatch(command); - }, + if (keyCode == 8) { + sel = t.getDoc().selection; - mceBlockQuote : function() { - return isFormatMatch('blockquote'); - }, + // Fix IE control + backspace browser bug + if (sel && sel.createRange && sel.createRange().item) { + t.undoManager.beforeChange(); + ed.dom.remove(sel.createRange().item(0)); + addUndo(); - Outdent : function() { - var node; + return Event.cancel(e); + } + } - if (settings.inline_styles) { - if ((node = dom.getParent(selection.getStart(), dom.isBlock)) && parseInt(node.style.paddingLeft) > 0) - return TRUE; + // 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(); - if ((node = dom.getParent(selection.getEnd(), dom.isBlock)) && parseInt(node.style.paddingLeft) > 0) - return TRUE; - } + if (t.undoManager.typing) + addUndo(); + + return; + } - return queryCommandState('InsertUnorderedList') || queryCommandState('InsertOrderedList') || (!settings.inline_styles && !!dom.getParent(selection.getNode(), 'BLOCKQUOTE')); - }, + // 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(); + } + }); - 'InsertUnorderedList,InsertOrderedList' : function(command) { - return dom.getParent(selection.getNode(), command == 'insertunorderedlist' ? 'UL' : 'OL'); + t.onMouseDown.add(function() { + if (t.undoManager.typing) + addUndo(); + }); } - }, 'state'); + }, - // Add queryCommandValue overrides - addCommands({ - 'FontSize,FontName' : function(command) { - var value = 0, parent; + _refreshContentEditable : function() { + var self = this, body, parent; - if (parent = dom.getParent(selection.getNode(), 'span')) { - if (command == 'fontsize') - value = parent.style.fontSize; - else - value = parent.style.fontFamily.replace(/, /g, ',').replace(/[\'\"]/g, '').toLowerCase(); - } + // 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; - return value; + parent.removeChild(body); + parent.appendChild(body); + + body.focus(); } - }, 'value'); + }, - // Add undo manager logic - if (settings.custom_undo_redo) { - addCommands({ - Undo : function() { - editor.undoManager.undo(); - }, + _isHidden : function() { + var s; - Redo : function() { - editor.undoManager.redo(); - } - }); + if (!isGecko) + return 0; + + // Weird, wheres that cursor selection? + s = this.selection.getSel(); + return (!s || !s.rangeCount || s.rangeCount == 0); } - }; + }); })(tinymce); + (function(tinymce) { - var Dispatcher = tinymce.util.Dispatcher; + // Added for compression purposes + var each = tinymce.each, undefined, TRUE = true, FALSE = false; - tinymce.UndoManager = function(editor) { - var self, index = 0, data = []; + tinymce.EditorCommands = function(editor) { + var dom = editor.dom, + selection = editor.selection, + commands = {state: {}, exec : {}, value : {}}, + settings = editor.settings, + formatter = editor.formatter, + bookmark; - function getContent() { - return tinymce.trim(editor.getContent({format : 'raw', no_events : 1})); - }; + function execCommand(command, ui, value) { + var func; - return self = { - typing : 0, + command = command.toLowerCase(); + if (func = commands.exec[command]) { + func(command, ui, value); + return TRUE; + } - onAdd : new Dispatcher(self), - onUndo : new Dispatcher(self), - onRedo : new Dispatcher(self), + return FALSE; + }; - add : function(level) { - var i, settings = editor.settings, lastLevel; + function queryCommandState(command) { + var func; - level = level || {}; - level.content = getContent(); + command = command.toLowerCase(); + if (func = commands.state[command]) + return func(command); - // Add undo level if needed - lastLevel = data[index]; - if (lastLevel && lastLevel.content == level.content) { - if (index > 0 || data.length == 1) - return null; - } + return -1; + }; - // 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]; + function queryCommandValue(command) { + var func; - data.length--; - index = data.length; - } - } + command = command.toLowerCase(); + if (func = commands.value[command]) + return func(command); - // Get a non intrusive normalized bookmark - level.bookmark = editor.selection.getBookmark(2, true); + return FALSE; + }; - // Crop array if needed - if (index < data.length - 1) { - // Treat first level as initial - if (index == 0) - data = []; - else - data.length = index + 1; - } + function addCommands(command_list, type) { + type = type || 'exec'; - data.push(level); - index = data.length - 1; + each(command_list, function(callback, command) { + each(command.toLowerCase().split(','), function(command) { + commands[type][command] = callback; + }); + }); + }; - self.onAdd.dispatch(self, level); - editor.isNotDirty = 0; + // Expose public methods + tinymce.extend(this, { + execCommand : execCommand, + queryCommandState : queryCommandState, + queryCommandValue : queryCommandValue, + addCommands : addCommands + }); - return level; - }, + // Private methods - undo : function() { - var level, i; + function execNativeCommand(command, ui, value) { + if (ui === undefined) + ui = FALSE; - if (self.typing) { - self.add(); - self.typing = 0; - } + if (value === undefined) + value = null; - if (index > 0) { - level = data[--index]; + return editor.getDoc().execCommand(command, ui, value); + }; - editor.setContent(level.content, {format : 'raw'}); - editor.selection.moveToBookmark(level.bookmark); + function isFormatMatch(name) { + return formatter.match(name); + }; - self.onUndo.dispatch(self, level); - } + function toggleFormat(name, value) { + formatter.toggle(name, value ? {value : value} : undefined); + }; - return level; - }, + function storeSelection(type) { + bookmark = selection.getBookmark(type); + }; - redo : function() { - var level; + function restoreSelection() { + selection.moveToBookmark(bookmark); + }; - if (index < data.length - 1) { - level = data[++index]; + // Add execCommand overrides + addCommands({ + // Ignore these, added for compatibility + 'mceResetDesignMode,mceBeginUndoLevel' : function() {}, - editor.setContent(level.content, {format : 'raw'}); - editor.selection.moveToBookmark(level.bookmark); + // Add undo manager logic + 'mceEndUndoLevel,mceAddUndoLevel' : function() { + editor.undoManager.add(); + }, - self.onRedo.dispatch(self, level); + 'Cut,Copy,Paste' : function(command) { + var doc = editor.getDoc(), failed; + + // Try executing the native command + try { + execNativeCommand(command); + } catch (ex) { + // Command failed + failed = TRUE; } - return level; + // Present alert message about clipboard access not being available + if (failed || !doc.queryCommandSupported(command)) { + if (tinymce.isGecko) { + editor.windowManager.confirm(editor.getLang('clipboard_msg'), function(state) { + if (state) + open('http://www.mozilla.org/editor/midasdemo/securityprefs.html', '_blank'); + }); + } else + editor.windowManager.alert(editor.getLang('clipboard_no_support')); + } }, - clear : function() { - data = []; - index = self.typing = 0; - }, + // Override unlink command + unlink : function(command) { + if (selection.isCollapsed()) + selection.select(selection.getNode()); - hasUndo : function() { - return index > 0 || self.typing; + execNativeCommand(command); + selection.collapse(FALSE); }, - hasRedo : function() { - return index < data.length - 1; - } - }; - }; -})(tinymce); + // Override justify commands to use the text formatter engine + 'JustifyLeft,JustifyCenter,JustifyRight,JustifyFull' : function(command) { + var align = command.substring(7); -(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); - } + // Remove all other alignments first + each('left,center,right,full'.split(','), function(name) { + if (align != name) + formatter.remove('align' + name); + }); - clone.removeAttribute('id'); - } - } while (node = node.parentNode); + toggleFormat('align' + align); + execCommand('mceRepaint'); + }, - if (clone) - return {wrapper : clone, inner : inner}; - }; + // Override list commands to fix WebKit bug + 'InsertUnorderedList,InsertOrderedList' : function(command) { + var listElm, listParent; - // Checks if the selection/caret is at the end of the specified block element - function isAtEnd(rng, par) { - var rng2 = par.ownerDocument.createRange(); + execNativeCommand(command); - rng2.setStart(rng.endContainer, rng.endOffset); - rng2.setEndAfter(par); + // WebKit produces lists within block elements so we need to split them + // we will replace the native list creation logic to custom logic later on + // TODO: Remove this when the list creation logic is removed + listElm = dom.getParent(selection.getNode(), 'ol,ul'); + if (listElm) { + listParent = listElm.parentNode; - // 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; - }; + // If list is within a text block then split that block + if (/^(H[1-6]|P|ADDRESS|PRE)$/.test(listParent.nodeName)) { + storeSelection(); + dom.split(listParent, listElm); + restoreSelection(); + } + } + }, + + // Override commands to use the text formatter engine + 'Bold,Italic,Underline,Strikethrough,Superscript,Subscript' : function(command) { + toggleFormat(command); + }, + + // Override commands to use the text formatter engine + 'ForeColor,HiliteColor,FontName' : function(command, ui, value) { + toggleFormat(command, value); + }, - function isEmpty(n) { - n = n.innerHTML; + FontSize : function(command, ui, value) { + var fontClasses, fontSizes; - n = n.replace(/<(img|hr|table|input|select|textarea)[ \>]/gi, '-'); // Keep these convert them to - chars - n = n.replace(/<[^>]+>/g, ''); // Remove all tags + // Convert font size 1-7 to styles + if (value >= 1 && value <= 7) { + fontSizes = tinymce.explode(settings.font_size_style_values); + fontClasses = tinymce.explode(settings.font_size_classes); - return n.replace(/[ \u00a0\t\r\n]+/g, '') == ''; - }; + if (fontClasses) + value = fontClasses[value - 1] || value; + else + value = fontSizes[value - 1] || value; + } - function splitList(selection, dom, li) { - var listBlock, block; + toggleFormat(command, value); + }, - if (isEmpty(li)) { - listBlock = dom.getParent(li, 'ul,ol'); + RemoveFormat : function(command) { + formatter.remove(command); + }, - if (!dom.getParent(listBlock.parentNode, 'ul,ol')) { - dom.split(listBlock, li); - block = dom.create('p', 0, '
    '); - dom.replace(block, li); - selection.select(block, 1); - } + mceBlockQuote : function(command) { + toggleFormat('blockquote'); + }, - return FALSE; - } + FormatBlock : function(command, ui, value) { + return toggleFormat(value || 'p'); + }, - return TRUE; - }; + mceCleanup : function() { + var bookmark = selection.getBookmark(); - tinymce.create('tinymce.ForceBlocks', { - ForceBlocks : function(ed) { - var t = this, s = ed.settings, elm; + editor.setContent(editor.getContent({cleanup : TRUE}), {cleanup : TRUE}); - t.editor = ed; - t.dom = ed.dom; - elm = (s.forced_root_block || 'p').toLowerCase(); - s.element = elm.toUpperCase(); + selection.moveToBookmark(bookmark); + }, - ed.onPreInit.add(t.setup, t); + mceRemoveNode : function(command, ui, value) { + var node = value || selection.getNode(); - t.reOpera = new RegExp('(\\u00a0| | )<\/' + elm + '>', 'gi'); - t.rePadd = new RegExp(']+)><\\\/p>|]+)\\\/>|]+)>\\s+<\\\/p>|

    <\\\/p>||

    \\s+<\\\/p>'.replace(/p/g, elm), 'gi'); - t.reNbsp2BR1 = new RegExp(']+)>[\\s\\u00a0]+<\\\/p>|

    [\\s\\u00a0]+<\\\/p>'.replace(/p/g, elm), 'gi'); - t.reNbsp2BR2 = new RegExp('<%p()([^>]+)>( | )<\\\/%p>|<%p>( | )<\\\/%p>'.replace(/%p/g, elm), 'gi'); - t.reBR2Nbsp = new RegExp(']+)>\\s*
    \\s*<\\\/p>|

    \\s*
    \\s*<\\\/p>'.replace(/p/g, elm), 'gi'); + // Make sure that the body node isn't removed + if (node != editor.getBody()) { + storeSelection(); + editor.dom.remove(node, TRUE); + restoreSelection(); + } + }, - function padd(ed, o) { - if (isOpera) - o.content = o.content.replace(t.reOpera, ''); + mceSelectNodeDepth : function(command, ui, value) { + var counter = 0; - o.content = o.content.replace(t.rePadd, '<' + elm + '$1$2$3$4$5$6>\u00a0'); + dom.getParent(selection.getNode(), function(node) { + if (node.nodeType == 1 && counter++ == value) { + selection.select(node); + return FALSE; + } + }, editor.getBody()); + }, - if (!isIE && !isOpera && o.set) { - // Use   instead of BR in padded paragraphs - o.content = o.content.replace(t.reNbsp2BR1, '<' + elm + '$1$2>
    '); - o.content = o.content.replace(t.reNbsp2BR2, '<' + elm + '$1$2>
    '); - } else - o.content = o.content.replace(t.reBR2Nbsp, '<' + elm + '$1$2>\u00a0'); - }; + mceSelectNode : function(command, ui, value) { + selection.select(value); + }, - ed.onBeforeSetContent.add(padd); - ed.onPostProcess.add(padd); + mceInsertContent : function(command, ui, value) { + var parser, serializer, parentNode, rootNode, fragment, args, + marker, nodeRect, viewPortRect, rng, node, node2, bookmarkHtml, viewportBodyElement; - if (s.forced_root_block) { - ed.onInit.add(t.forceRoots, t); - ed.onSetContent.add(t.forceRoots, t); - ed.onBeforeGetContent.add(t.forceRoots, t); - } - }, + //selection.normalize(); - setup : function() { - var t = this, ed = t.editor, s = ed.settings, dom = ed.dom, selection = ed.selection; + // Setup parser and serializer + parser = editor.parser; + serializer = new tinymce.html.Serializer({}, editor.schema); + bookmarkHtml = '\uFEFF'; - // Force root blocks when typing and when getting output - if (s.forced_root_block) { - ed.onBeforeExecCommand.add(t.forceRoots, t); - ed.onKeyUp.add(t.forceRoots, t); - ed.onPreProcess.add(t.forceRoots, t); - } + // Run beforeSetContent handlers on the HTML to be inserted + args = {content: value, format: 'html'}; + selection.onBeforeSetContent.dispatch(selection, args); + value = args.content; - 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); - } - }); - } - } + // Add caret at end of contents if it's missing + if (value.indexOf('{$caret}') == -1) + value += '{$caret}'; - 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 - }); + // Replace the caret marker with a span bookmark element + value = value.replace(/\{\$caret\}/, bookmarkHtml); - ed.onKeyPress.add(function(ed, e) { - t._previousFormats = 0; + // Insert node maker where we will insert the new HTML and get it's parent + if (!selection.isCollapsed()) + editor.getDoc().execCommand('Delete', false, null); - // 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()); - }); + parentNode = selection.getNode(); - 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; + // Parse the fragment within the context of the parent node + args = {context : parentNode.nodeName.toLowerCase()}; + fragment = parser.parse(value, args); - // Parent is an empty block - if (!parent.hasChildNodes()) { - parent = dom.getParent(parent, dom.isBlock); + // Move the caret to a more suitable location + node = fragment.lastChild; + if (node.attr('id') == 'mce_marker') { + marker = node; - if (parent) { - parent.innerHTML = ''; - - if (t._previousFormats) { - parent.appendChild(fmt.wrapper); - fmt.inner.innerHTML = '\uFEFF'; - } else - parent.innerHTML = '\uFEFF'; - - selection.select(parent, 1); - ed.getDoc().execCommand('Delete', false, null); - } - } + for (node = node.prev; node; node = node.walk(true)) { + if (node.type == 3 || !dom.isBlock(node.name)) { + node.parent.insert(marker, node, node.name === 'br'); + break; } - }); + } } - if (isGecko) { - ed.onKeyDown.add(function(ed, e) { - if ((e.keyCode == 8 || e.keyCode == 46) && !e.shiftKey) - t.backspaceDelete(e, e.keyCode == 8); - }); - } - } + // If parser says valid we can insert the contents into that parent + if (!args.invalid) { + value = serializer.serialize(fragment); - // 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; + // Check if parent is empty or only has one BR element then set the innerHTML of that parent + node = parentNode.firstChild; + node2 = parentNode.lastChild; + if (!node || (node === node2 && node.nodeName === 'BR')) + dom.setHTML(parentNode, value); + else + selection.setContent(value); + } else { + // If the fragment was invalid within that context then we need + // to parse and process the parent it's inserted into - // Insert BR element - rng.insertNode(br = dom.create('br')); + // Insert bookmark node and get the parent + selection.setContent(bookmarkHtml); + parentNode = editor.selection.getNode(); + rootNode = editor.getBody(); - // Place caret after BR - rng.setStartAfter(br); - rng.setEndAfter(br); - selection.setRng(rng); + // Opera will return the document node when selection is in root + if (parentNode.nodeType == 9) + parentNode = node = rootNode; + else + node = parentNode; - // 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); + // Find the ancestor just before the root element + while (node !== rootNode) { + parentNode = node; + node = node.parentNode; } - // 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); + // Get the outer/inner HTML depending on if we are in the root and parser and serialize that + value = parentNode == rootNode ? rootNode.innerHTML : dom.getOuterHTML(parentNode); + value = serializer.serialize( + parser.parse( + // Need to replace by using a function since $ in the contents would otherwise be a problem + value.replace(//i, function() { + return serializer.serialize(fragment); + }) + ) + ); + + // Set the inner/outer HTML depending on if we are in the root or not + if (parentNode == rootNode) + dom.setHTML(rootNode, value); + else + dom.setOuterHTML(parentNode, value); + } - // 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); - }; + marker = dom.get('mce_marker'); - 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); - } - }); - } + // Scroll range into view scrollIntoView on element can't be used since it will scroll the main view port as well + nodeRect = dom.getRect(marker); + viewPortRect = dom.getViewPort(editor.getWin()); - // Padd empty inline elements within block elements - // For example:

    becomes

     

    - ed.onPreProcess.add(function(ed, o) { - each(dom.select('p,h1,h2,h3,h4,h5,h6,div', o.node), function(p) { - if (isEmpty(p)) { - each(dom.select('span,em,strong,b,i', o.node), function(n) { - if (!n.hasChildNodes()) { - n.appendChild(ed.getDoc().createTextNode('\u00a0')); - return FALSE; // Break the loop one padding is enough - } - }); - } - }); - }); + // Check if node is out side the viewport if it is then scroll to it + if ((nodeRect.y + nodeRect.h > viewPortRect.y + viewPortRect.h || nodeRect.y < viewPortRect.y) || + (nodeRect.x > viewPortRect.x + viewPortRect.w || nodeRect.x < viewPortRect.x)) { + viewportBodyElement = tinymce.isIE ? editor.getDoc().documentElement : editor.getBody(); + viewportBodyElement.scrollLeft = nodeRect.x; + viewportBodyElement.scrollTop = nodeRect.y - viewPortRect.h + 25; + } - // 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; - }); + // Move selection before marker and remove it + rng = dom.createRng(); - ed.onKeyUp.add(function(ed, e) { - var bl, n = selection.getNode(), b = ed.getBody(); + // If previous sibling is a text node set the selection to the end of that node + node = marker.previousSibling; + if (node && node.nodeType == 3) { + rng.setStart(node, node.nodeValue.length); + } else { + // If the previous sibling isn't a text node or doesn't exist set the selection before the marker node + rng.setStartBefore(marker); + rng.setEndBefore(marker); + } - 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'); + // Remove the marker node and set the new range + dom.remove(marker); + selection.setRng(rng); - if (bl) { - dom.rename(bl, s.element); - ed.nodeChanged(); - } - } - }); - } - } - }, + // Dispatch after event and add any visual elements needed + selection.onSetContent.dispatch(selection, args); + editor.addVisual(); + }, - find : function(n, t, s) { - var ed = this.editor, w = ed.getDoc().createTreeWalker(n, 4, null, FALSE), c = -1; + mceInsertRawHTML : function(command, ui, value) { + selection.setContent('tiny_mce_marker'); + editor.setContent(editor.getContent().replace(/tiny_mce_marker/g, function() { return value })); + }, - while (n = w.nextNode()) { - c++; + mceSetContent : function(command, ui, value) { + editor.setContent(value); + }, - // Index by node - if (t == 0 && n == s) - return c; + 'Indent,Outdent' : function(command) { + var intentValue, indentUnit, value; - // Node by index - if (t == 1 && c == s) - return n; - } + // Setup indent level + intentValue = settings.indentation; + indentUnit = /[a-z%]+$/i.exec(intentValue); + intentValue = parseInt(intentValue); - return -1; - }, + 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); + dom.setStyle(element, 'paddingLeft', value ? value + indentUnit : ''); + } else + dom.setStyle(element, 'paddingLeft', (parseInt(element.style.paddingLeft || 0) + intentValue) + indentUnit); + }); + } else + execNativeCommand(command); + }, - forceRoots : function(ed, e) { - var t = this, ed = t.editor, b = ed.getBody(), d = ed.getDoc(), se = ed.selection, s = se.getSel(), r = se.getRng(), si = -2, ei, so, eo, tr, c = -0xFFFFFF; - var nx, bl, bp, sp, le, nl = b.childNodes, i, n, eid; + mceRepaint : function() { + var bookmark; - // Fix for bug #1863847 - //if (e && e.keyCode == 13) - // return TRUE; + if (tinymce.isGecko) { + try { + storeSelection(TRUE); - // Wrap non blocks into blocks - for (i = nl.length - 1; i >= 0; i--) { - nx = nl[i]; + if (selection.getSel()) + selection.getSel().selectAllChildren(editor.getBody()); - // Ignore internal elements - if (nx.nodeType === 1 && nx.getAttribute('_mce_type')) { - bl = null; - continue; + selection.collapse(TRUE); + restoreSelection(); + } catch (ex) { + // Ignore + } } + }, - // Is text or non block element - if (nx.nodeType === 3 || (!t.dom.isBlock(nx) && nx.nodeType !== 8 && !/^(script|mce:script|style|mce:style)$/i.test(nx.nodeName))) { - if (!bl) { - // Create new block but ignore whitespace - if (nx.nodeType != 3 || /[^\s]/g.test(nx.nodeValue)) { - // Store selection - if (si == -2 && r) { - if (!isIE) { - // If selection is element then mark it - if (r.startContainer.nodeType == 1 && (n = r.startContainer.childNodes[r.startOffset]) && n.nodeType == 1) { - // Save the id of the selected element - eid = n.getAttribute("id"); - n.setAttribute("id", "__mce"); - } else { - // If element is inside body, might not be the case in contentEdiable mode - if (ed.dom.getParent(r.startContainer, function(e) {return e === b;})) { - so = r.startOffset; - eo = r.endOffset; - si = t.find(b, 0, r.startContainer); - ei = t.find(b, 0, r.endContainer); - } - } - } else { - // Force control range into text range - if (r.item) { - tr = d.body.createTextRange(); - tr.moveToElementText(r.item(0)); - r = tr; - } + mceToggleFormat : function(command, ui, value) { + formatter.toggle(value); + }, - tr = d.body.createTextRange(); - tr.moveToElementText(b); - tr.collapse(1); - bp = tr.move('character', c) * -1; + InsertHorizontalRule : function() { + editor.execCommand('mceInsertContent', false, '
    '); + }, - tr = r.duplicate(); - tr.collapse(1); - sp = tr.move('character', c) * -1; + mceToggleVisualAid : function() { + editor.hasVisual = !editor.hasVisual; + editor.addVisual(); + }, - tr = r.duplicate(); - tr.collapse(0); - le = (tr.move('character', c) * -1) - sp; + mceReplaceContent : function(command, ui, value) { + editor.execCommand('mceInsertContent', false, value.replace(/\{\$selection\}/g, selection.getContent({format : 'text'}))); + }, - si = sp - bp; - ei = le; - } - } + mceInsertLink : function(command, ui, value) { + var anchor; - // Uses replaceChild instead of cloneNode since it removes selected attribute from option elements on IE - // See: http://support.microsoft.com/kb/829907 - bl = ed.dom.create(ed.settings.forced_root_block); - nx.parentNode.replaceChild(bl, nx); - bl.appendChild(nx); - } - } else { - if (bl.hasChildNodes()) - bl.insertBefore(nx, bl.firstChild); - else - bl.appendChild(nx); - } - } else - bl = null; // Time to create new block - } + if (typeof(value) == 'string') + value = {href : value}; - // Restore selection - if (si != -2) { - if (!isIE) { - bl = b.getElementsByTagName(ed.settings.element)[0]; - r = d.createRange(); + anchor = dom.getParent(selection.getNode(), 'a'); - // Select last location or generated block - if (si != -1) - r.setStart(t.find(b, 1, si), so); - else - r.setStart(bl, 0); + // Spaces are never valid in URLs and it's a very common mistake for people to make so we fix it here. + value.href = value.href.replace(' ', '%20'); - // Select last location or generated block - if (ei != -1) - r.setEnd(t.find(b, 1, ei), eo); - else - r.setEnd(bl, 0); + // Remove existing links if there could be child links or that the href isn't specified + if (!anchor || !value.href) { + formatter.remove('link'); + } - if (s) { - s.removeAllRanges(); - s.addRange(r); - } - } else { - try { - r = s.createRange(); - r.moveToElementText(b); - r.collapse(1); - r.moveStart('character', si); - r.moveEnd('character', ei); - r.select(); - } catch (ex) { - // Ignore - } + // Apply new link to selection + if (value.href) { + formatter.apply('link', value, anchor); } - } else if (!isIE && (n = ed.dom.get('__mce'))) { - // Restore the id of the selected element - if (eid) - n.setAttribute('id', eid); - else - n.removeAttribute('id'); + }, - // Move caret before selected element - r = d.createRange(); - r.setStartBefore(n); - r.setEndBefore(n); - se.setRng(r); - } - }, + selectAll : function() { + var root = dom.getRoot(), rng = dom.createRng(); - getParentBlock : function(n) { - var d = this.dom; + rng.setStart(root, 0); + rng.setEnd(root, root.childNodes.length); - return d.getParent(n, d.isBlock); - }, + editor.selection.setRng(rng); + } + }); - 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; + // Add queryCommandState overrides + addCommands({ + // Override justify commands + 'JustifyLeft,JustifyCenter,JustifyRight,JustifyFull' : function(command) { + var name = 'align' + command.substring(7); + // Use Formatter.matchNode instead of Formatter.match so that we don't match on parent node. This fixes bug where for both left + // and right align buttons can be active. This could occur when selected nodes have align right and the parent has align left. + var nodes = selection.isCollapsed() ? [selection.getNode()] : 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) { + return isFormatMatch(command); + }, - // 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; + mceBlockQuote : function() { + return isFormatMatch('blockquote'); + }, - // Setup before range - rb = d.createRange(); + Outdent : function() { + var node; - // 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); + if (settings.inline_styles) { + if ((node = dom.getParent(selection.getStart(), dom.isBlock)) && parseInt(node.style.paddingLeft) > 0) + return TRUE; - // Setup after range - ra = d.createRange(); + if ((node = dom.getParent(selection.getEnd(), dom.isBlock)) && parseInt(node.style.paddingLeft) > 0) + return TRUE; + } - // 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); + return queryCommandState('InsertUnorderedList') || queryCommandState('InsertOrderedList') || (!settings.inline_styles && !!dom.getParent(selection.getNode(), 'BLOCKQUOTE')); + }, - // 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; + 'InsertUnorderedList,InsertOrderedList' : function(command) { + return dom.getParent(selection.getNode(), command == 'insertunorderedlist' ? 'UL' : 'OL'); + } + }, 'state'); - // 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 + // Add queryCommandValue overrides + addCommands({ + 'FontSize,FontName' : function(command) { + var value = 0, parent; - // 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 (parent = dom.getParent(selection.getNode(), 'span')) { + if (command == 'fontsize') + value = parent.style.fontSize; + else + value = parent.style.fontFamily.replace(/, /g, ',').replace(/[\'\"]/g, '').toLowerCase(); } - // Move caret into the last one - r = d.createRange(); - r.selectNodeContents(aft); - r.collapse(1); - ed.selection.setRng(r); - - return FALSE; + return value; } + }, 'value'); - // 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); - } + // Add undo manager logic + if (settings.custom_undo_redo) { + addCommands({ + Undo : function() { + editor.undoManager.undo(); + }, + + Redo : function() { + editor.undoManager.redo(); + } + }); + } + }; +})(tinymce); + +(function(tinymce) { + var Dispatcher = tinymce.util.Dispatcher; - // 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; + tinymce.UndoManager = function(editor) { + var self, index = 0, data = [], beforeBookmark; - // Get start and end blocks - sb = t.getParentBlock(sn); - eb = t.getParentBlock(en); - bn = sb ? sb.nodeName : se.element; // Get block name to create + 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, '')); + }; - // 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); + return self = { + typing : false, - return TRUE; - } + onAdd : new Dispatcher(self), - // 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; - } + onUndo : new Dispatcher(self), - // 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; - } + onRedo : new Dispatcher(self), - // 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; - } + beforeChange : function() { + beforeBookmark = editor.selection.getBookmark(2, true); + }, - // 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); + add : function(level) { + var i, settings = editor.settings, lastLevel; - // Remove id from after clone - aft.removeAttribute('id'); + level = level || {}; + level.content = getContent(); - // 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); + // Add undo level if needed + lastLevel = data[index]; + if (lastLevel && lastLevel.content == level.content) + return null; - // 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; + // Set before bookmark on previous level + if (data[index]) + data[index].beforeBookmark = beforeBookmark; - sc = n; - } while ((n = n.previousSibling ? n.previousSibling : n.parentNode)); + // 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]; - // 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; + data.length--; + index = data.length; + } + } - ec = n; - } while ((n = n.nextSibling ? n.nextSibling : n.parentNode)); + // Get a non intrusive normalized bookmark + level.bookmark = editor.selection.getBookmark(2, true); - // Place first chop part into before block element - if (sc.nodeName == bn) - rb.setStart(sc, 0); - else - rb.setStartBefore(sc); + // Crop array if needed + if (index < data.length - 1) + data.length = index + 1; - rb.setEnd(sn, so); - bef.appendChild(rb.cloneContents() || d.createTextNode('')); // Empty text node needed for Safari + data.push(level); + index = data.length - 1; - // Place secnd chop part within new block element - try { - ra.setEndAfter(ec); - } catch(ex) { - //console.debug(s.focusNode, s.focusOffset); - } + self.onAdd.dispatch(self, level); + editor.isNotDirty = 0; - ra.setStart(en, eo); - aft.appendChild(ra.cloneContents() || d.createTextNode('')); // Empty text node needed for Safari + return level; + }, - // 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); - } + undo : function() { + var level, i; - if (!ec.nextSibling && ec.parentNode.nodeName == bn) - r.setEndAfter(ec.parentNode); - else - r.setEnd(ra.endContainer, ra.endOffset); + if (self.typing) { + self.add(); + self.typing = false; + } - // Delete and replace it with new block elements - r.deleteContents(); + if (index > 0) { + level = data[--index]; - if (isOpera) - ed.getWin().scrollTo(0, vp.y); + editor.setContent(level.content, {format : 'raw'}); + editor.selection.moveToBookmark(level.beforeBookmark); - // Never wrap blocks in blocks - if (bef.firstChild && bef.firstChild.nodeName == bn) - bef.innerHTML = bef.firstChild.innerHTML; + self.onUndo.dispatch(self, level); + } - if (aft.firstChild && aft.firstChild.nodeName == bn) - aft.innerHTML = aft.firstChild.innerHTML; + return level; + }, - // Padd empty blocks - if (isEmpty(bef)) - bef.innerHTML = '
    '; + redo : function() { + var level; - function appendStyles(e, en) { - var nl = [], nn, n, i; + if (index < data.length - 1) { + level = data[++index]; - e.innerHTML = ''; + editor.setContent(level.content, {format : 'raw'}); + editor.selection.moveToBookmark(level.bookmark); - // 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); + self.onRedo.dispatch(self, level); } - // Append style elements to aft - if (nl.length > 0) { - for (i = nl.length - 1, nn = e; i >= 0; i--) - nn = nn.appendChild(nl[i]); + return level; + }, - // Padd most inner style element - nl[0].innerHTML = isOpera ? ' ' : '
    '; // Extra space for Opera so that the caret can move there - return nl[0]; // Move caret to most inner element - } else - e.innerHTML = isOpera ? ' ' : '
    '; // Extra space for Opera so that the caret can move there - }; + clear : function() { + data = []; + index = 0; + self.typing = false; + }, - // Fill empty afterblook with current style - if (isEmpty(aft)) - car = appendStyles(aft, en); + hasUndo : function() { + return index > 0 || this.typing; + }, - // 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); + hasRedo : function() { + return index < data.length - 1 && !this.typing; } + }; + }; +})(tinymce); - // Normalize - aft.normalize(); - bef.normalize(); +tinymce.ForceBlocks = function(editor) { + var settings = editor.settings, dom = editor.dom, selection = editor.selection, blockElements = editor.schema.getBlockElements(); - function first(n) { - return d.createTreeWalker(n, NodeFilter.SHOW_TEXT, null, FALSE).nextNode() || n; - }; + // Force root blocks + if (settings.forced_root_block) { + function addRootBlocks() { + var node = selection.getStart(), rootNode = editor.getBody(), rng, startContainer, startOffset, endContainer, endOffset, rootBlockNode, tempNode, offset = -0xFFFFFF; - // Move cursor and scroll into view - r = d.createRange(); - r.selectNodeContents(isGecko ? first(car || aft) : car || aft); - r.collapse(1); - s.removeAllRanges(); - s.addRange(r); + if (!node || node.nodeType !== 1 || !settings.forced_root_block) + return; - // 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; + // Check if node is wrapped in block + while (node != rootNode) { + if (blockElements[node.nodeName]) + return; - // Is element within viewport - if (y < vp.y || y + ch > 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 - //console.debug('SCROLL!', 'vp.y: ' + vp.y, 'y' + y, 'vp.h' + vp.h, 'clientHeight' + aft.clientHeight, 'yyy: ' + (y < vp.y ? y : y - vp.h + aft.clientHeight)); + node = node.parentNode; } - return FALSE; - }, - - 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; + // 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); + } - // 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); + tmpRng = rng.duplicate(); + tmpRng.collapse(true); + startOffset = tmpRng.move('character', offset) * -1; - // 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; - } + if (!tmpRng.collapsed) { + tmpRng = rng.duplicate(); + tmpRng.collapse(false); + endOffset = (tmpRng.move('character', offset) * -1) - startOffset; } } - // 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)) ; + // 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(settings.forced_root_block); + node.parentNode.insertBefore(rootBlockNode, node); + } - 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; + tempNode = node; + node = node.nextSibling; + rootBlockNode.appendChild(tempNode); + } else { + rootBlockNode = null; + node = node.nextSibling; + } + } - // 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); + if (rng.setStart) { + rng.setStart(startContainer, startOffset); + rng.setEnd(endContainer, endOffset); + selection.setRng(rng); + } else { + try { + rng = editor.getDoc().body.createTextRange(); + rng.moveToElementText(rootNode); + rng.collapse(true); + rng.moveStart('character', startOffset); - // Remove the target container - ed.dom.remove(sc); - } + if (endOffset > 0) + rng.moveEnd('character', endOffset); - return Event.cancel(e); - } + rng.select(); + } catch (ex) { + // Ignore } } - } - }); -})(tinymce); + + editor.nodeChanged(); + }; + + editor.onKeyUp.add(addRootBlocks); + editor.onClick.add(addRootBlocks); + } +}; (function(tinymce) { // Shorten names @@ -12484,11 +14767,16 @@ tinymce.create('tinymce.ui.Toolbar:tinymce.ui.Container', { id = t.prefix + id; - if (ed.settings.use_native_selects) + + function useNativeListForAccessibility(ed) { + return ed.settings.use_accessible_selects && !tinymce.isGecko + } + + 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); + c = new cls(id, s, ed); } t.controls[id] = c; @@ -12543,11 +14831,11 @@ tinymce.create('tinymce.ui.Toolbar:tinymce.ui.Container', { if (s.menu_button) { cls = cc || t._cls.menubutton || tinymce.ui.MenuButton; - c = new cls(id, s); + 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); + c = new cls(id, s, ed); } return t.add(c); @@ -12590,7 +14878,7 @@ tinymce.create('tinymce.ui.Toolbar:tinymce.ui.Container', { id = t.prefix + id; cls = cc || t._cls.splitbutton || tinymce.ui.SplitButton; - c = t.add(new cls(id, s)); + c = t.add(new cls(id, s, ed)); ed.onMouseDown.add(c.hideMenu, c); return c; @@ -12630,7 +14918,7 @@ tinymce.create('tinymce.ui.Toolbar:tinymce.ui.Container', { id = t.prefix + id; cls = cc || t._cls.colorsplitbutton || tinymce.ui.ColorSplitButton; - c = new cls(id, s); + c = new cls(id, s, ed); ed.onMouseDown.add(c.hideMenu, c); // Remove the menu element when the editor is removed @@ -12662,13 +14950,25 @@ tinymce.create('tinymce.ui.Toolbar:tinymce.ui.Container', { id = t.prefix + id; cls = cc || t._cls.toolbar || tinymce.ui.Toolbar; - c = new cls(id, s); + c = new cls(id, s, t.editor); if (t.get(id)) return null; 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); + }, createSeparator : function(cc) { var cls = cc || this._cls.separator || tinymce.ui.Separator; @@ -12805,53 +15105,6 @@ tinymce.create('tinymce.ui.Toolbar:tinymce.ui.Container', { } }); }(tinymce)); -(function(tinymce) { - function CommandManager() { - var execCommands = {}, queryStateCommands = {}, queryValueCommands = {}; - - function add(collection, cmd, func, scope) { - if (typeof(cmd) == 'string') - cmd = [cmd]; - - tinymce.each(cmd, function(cmd) { - collection[cmd.toLowerCase()] = {func : func, scope : scope}; - }); - }; - - tinymce.extend(this, { - add : function(cmd, func, scope) { - add(execCommands, cmd, func, scope); - }, - - addQueryStateHandler : function(cmd, func, scope) { - add(queryStateCommands, cmd, func, scope); - }, - - addQueryValueHandler : function(cmd, func, scope) { - add(queryValueCommands, cmd, func, scope); - }, - - execCommand : function(scope, cmd, ui, value, args) { - if (cmd = execCommands[cmd.toLowerCase()]) { - if (cmd.func.call(scope || cmd.scope, ui, value, args) !== false) - return true; - } - }, - - queryCommandValue : function() { - if (cmd = queryValueCommands[cmd.toLowerCase()]) - return cmd.func.call(scope || cmd.scope, ui, value, args); - }, - - queryCommandState : function() { - if (cmd = queryStateCommands[cmd.toLowerCase()]) - return cmd.func.call(scope || cmd.scope, ui, value, args); - } - }); - }; - - tinymce.GlobalCommands = new CommandManager(); -})(tinymce); (function(tinymce) { tinymce.Formatter = function(ed) { var formats = {}, @@ -12860,16 +15113,28 @@ tinymce.create('tinymce.ui.Toolbar:tinymce.ui.Container', { selection = ed.selection, TreeWalker = tinymce.dom.TreeWalker, rangeUtils = new tinymce.dom.RangeUtils(dom), - isValid = ed.schema.isValid, + isValid = ed.schema.isValidChild, isBlock = dom.isBlock, forcedRootBlock = ed.settings.forced_root_block, nodeIndex = dom.nodeIndex, - INVISIBLE_CHAR = '\uFEFF', + INVISIBLE_CHAR = tinymce.isGecko ? '\u200B' : '\uFEFF', MCE_ATTR_RE = /^(src|href|style)$/, FALSE = false, TRUE = true, - undefined, - pendingFormats = {apply : [], remove : []}; + undefined; + + // Returns the content editable state of a node + function getContentEditable(node) { + var contentEditable = node.getAttribute("data-mce-contenteditable"); + + // Check for fake content editable + if (contentEditable && contentEditable !== "inherit") { + return contentEditable; + } + + // Check for real content editable + return node.contentEditable !== "inherit" ? node.contentEditable : null; + }; function isArray(obj) { return obj instanceof Array; @@ -12880,7 +15145,7 @@ tinymce.create('tinymce.ui.Toolbar:tinymce.ui.Container', { }; function isCaretNode(node) { - return node.nodeType === 1 && (node.face === 'mceinline' || node.style.fontFamily === 'mceinline'); + return node.nodeType === 1 && node.id === '_mce_caret'; }; // Public functions @@ -12929,37 +15194,40 @@ tinymce.create('tinymce.ui.Toolbar:tinymce.ui.Container', { } }; - function apply(name, vars, node) { - var formatList = get(name), format = formatList[0], bookmark, rng, i; + var getTextDecoration = function(node) { + var decoration; - 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; - } - } - } + ed.dom.getParent(node, function(n) { + decoration = ed.dom.getStyle(n, 'text-decoration'); + return decoration && decoration !== 'none'; + }); + + return decoration; + }; + + 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); } + } + }; - return rng; - }; + function apply(name, vars, node) { + var formatList = get(name), format = formatList[0], bookmark, rng, i, isCollapsed = selection.isCollapsed(); function setElementFormat(elm, fmt) { fmt = fmt || format; if (elm) { + if (fmt.onformat) { + fmt.onformat(elm, fmt, vars, node); + } + each(fmt.styles, function(value, name) { dom.setStyle(elm, name, replaceVars(value, vars)); }); @@ -12976,9 +15244,90 @@ tinymce.create('tinymce.ui.Toolbar:tinymce.ui.Container', { }); } }; + 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; + + rng.setEnd(newEnd, endOffset); + } + + 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; + } + }); + + // 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 = dom.clone(wrapElm, 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 currentWrapElm; + } + }; - function applyRngStyle(rng) { - var newWrappers = [], wrapName, wrapElm; + function applyRngStyle(rng, bookmark, node_specific) { + var newWrappers = [], wrapName, wrapElm, contentEditable = true; // Setup wrapper element wrapName = format.inline || format.block; @@ -12989,7 +15338,18 @@ tinymce.create('tinymce.ui.Toolbar:tinymce.ui.Container', { var currentWrapElm; function process(node) { - var nodeName = node.nodeName.toLowerCase(), parentName = node.parentNode.nodeName.toLowerCase(), found; + var nodeName, parentName, found, hasContentEditableState, lastContentEditable; + + lastContentEditable = contentEditable; + nodeName = node.nodeName.toLowerCase(); + parentName = node.parentNode.nodeName.toLowerCase(); + + // 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 + } // Stop wrapping on br elements if (isEq(nodeName, 'br')) { @@ -13009,7 +15369,7 @@ tinymce.create('tinymce.ui.Toolbar:tinymce.ui.Container', { } // Can we rename the block - if (format.block && !format.wrapper && isTextBlock(nodeName)) { + if (contentEditable && !hasContentEditableState && format.block && !format.wrapper && isTextBlock(nodeName)) { node = dom.rename(node, wrapName); setElementFormat(node); newWrappers.push(node); @@ -13021,6 +15381,11 @@ tinymce.create('tinymce.ui.Toolbar:tinymce.ui.Container', { 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; + } + if (dom.is(node, format.selector) && !isCaretNode(node)) { setElementFormat(node, format); found = true; @@ -13035,22 +15400,30 @@ tinymce.create('tinymce.ui.Toolbar:tinymce.ui.Container', { } // Is it valid to wrap this item - if (isValid(wrapName, nodeName) && isValid(parentName, wrapName)) { + 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 = wrapElm.cloneNode(FALSE); + currentWrapElm = dom.clone(wrapElm, FALSE); node.parentNode.insertBefore(currentWrapElm, node); newWrappers.push(currentWrapElm); } 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); + if (hasContentEditableState) { + contentEditable = lastContentEditable; // Restore last contentEditable state from stack + } + // End the last wrapper currentWrapElm = 0; } @@ -13060,7 +15433,32 @@ tinymce.create('tinymce.ui.Toolbar:tinymce.ui.Container', { each(nodes, process); }); + // 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 (node.nodeName === 'A') { + currentWrapElm = dom.clone(wrapElm, FALSE); + newWrappers.push(currentWrapElm); + + children = tinymce.grep(node.childNodes); + for (i = 0; i < children.length; i++) + currentWrapElm.appendChild(children[i]); + + node.appendChild(currentWrapElm); + } + + each(tinymce.grep(node.childNodes), process); + }; + + process(node); + }); + } + // Cleanup + each(newWrappers, function(node) { var childCount; @@ -13087,7 +15485,7 @@ tinymce.create('tinymce.ui.Toolbar:tinymce.ui.Container', { // If child was found and of the same type as the current node if (child && matchName(child, format)) { - clone = child.cloneNode(FALSE); + clone = dom.clone(child, FALSE); setElementFormat(clone); dom.replace(clone, node, TRUE); @@ -13099,8 +15497,9 @@ tinymce.create('tinymce.ui.Toolbar:tinymce.ui.Container', { childCount = getChildCount(node); - // Remove empty nodes - if (childCount === 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; } @@ -13116,6 +15515,19 @@ tinymce.create('tinymce.ui.Toolbar:tinymce.ui.Container', { // this: text // will become: text each(dom.select(format.inline, node), function(child) { + var parent; + + // 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; + + do { + if (parent.nodeName === 'A') + return; + } while (parent = parent.parentNode); + } + removeFormat(format, vars, child, format.exact ? child : null); }); }); @@ -13139,7 +15551,7 @@ tinymce.create('tinymce.ui.Toolbar:tinymce.ui.Container', { } // Merge next and previous siblings if they are similar texttext becomes texttext - if (node) { + if (node && format.merge_siblings !== false) { node = mergeSiblings(getNonWhiteSpaceSibling(node), node); node = mergeSiblings(node, getNonWhiteSpaceSibling(node, TRUE)); } @@ -13149,20 +15561,38 @@ tinymce.create('tinymce.ui.Toolbar:tinymce.ui.Container', { if (format) { if (node) { - rng = dom.createRng(); - - rng.setStartBefore(node); - rng.setEndAfter(node); - - applyRngStyle(expandRng(rng, formatList)); + if (node.nodeType) { + rng = dom.createRng(); + rng.setStartBefore(node); + rng.setEndAfter(node); + applyRngStyle(expandRng(rng, formatList), null, true); + } else { + applyRngStyle(node, null, true); + } } else { - if (!selection.isCollapsed() || !format.inline) { + 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(); + + // 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); + } + // Apply formatting to selection + ed.selection.setRng(adjustSelectionToVisibleSelection()); bookmark = selection.getBookmark(); - applyRngStyle(expandRng(selection.getRng(TRUE), formatList)); + applyRngStyle(expandRng(selection.getRng(TRUE), formatList), bookmark); + + // 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); + } selection.moveToBookmark(bookmark); - selection.setRng(moveStart(selection.getRng(TRUE))); + moveStart(selection.getRng(TRUE)); ed.nodeChanged(); } else performCaretAction('apply', name, vars); @@ -13171,25 +15601,40 @@ tinymce.create('tinymce.ui.Toolbar:tinymce.ui.Container', { }; function remove(name, vars, node) { - var formatList = get(name), format = formatList[0], bookmark, i, rng; + var formatList = get(name), format = formatList[0], bookmark, i, rng, contentEditable = true; // Merges the styles for each node function process(node) { - var children, i, l; + var children, i, l, localContentEditable, lastContentEditable, hasContentEditableState; + + // 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 + } // Grab the children first since the nodelist might be changed children = tinymce.grep(node.childNodes); // Process current node - for (i = 0, l = formatList.length; i < l; i++) { - if (removeFormat(formatList[i], vars, node, node)) - break; + if (contentEditable && !hasContentEditableState) { + for (i = 0, l = formatList.length; i < l; i++) { + if (removeFormat(formatList[i], vars, node, node)) + break; + } } // Process the children if (format.deep) { - for (i = 0, l = children.length; i < l; i++) - process(children[i]); + if (children.length) { + for (i = 0, l = children.length; i < l; i++) + process(children[i]); + + if (hasContentEditableState) { + contentEditable = lastContentEditable; // Restore last contentEditable state from stack + } + } } }; @@ -13220,7 +15665,7 @@ tinymce.create('tinymce.ui.Toolbar:tinymce.ui.Container', { formatRootParent = format_root.parentNode; for (parent = container.parentNode; parent && parent != formatRootParent; parent = parent.parentNode) { - clone = parent.cloneNode(FALSE); + clone = dom.clone(parent, FALSE); for (i = 0; i < formatList.length; i++) { if (removeFormat(formatList[i], vars, clone, clone)) { @@ -13285,8 +15730,8 @@ tinymce.create('tinymce.ui.Toolbar:tinymce.ui.Container', { if (startContainer != endContainer) { // Wrap start/end nodes in span element since these might be cloned/moved - startContainer = wrap(startContainer, 'span', {id : '_start', _mce_type : 'bookmark'}); - endContainer = wrap(endContainer, 'span', {id : '_end', _mce_type : 'bookmark'}); + 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); @@ -13309,30 +15754,56 @@ tinymce.create('tinymce.ui.Toolbar:tinymce.ui.Container', { 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); + } }); }); }; // Handle node if (node) { - rng = dom.createRng(); - rng.setStartBefore(node); - rng.setEndAfter(node); - removeRngStyle(rng); + if (node.nodeType) { + rng = dom.createRng(); + rng.setStartBefore(node); + rng.setEndAfter(node); + removeRngStyle(rng); + } else { + removeRngStyle(node); + } + return; } - if (!selection.isCollapsed() || !format.inline) { + if (!selection.isCollapsed() || !format.inline || dom.select('td.mceSelected,th.mceSelected').length) { bookmark = selection.getBookmark(); removeRngStyle(selection.getRng(TRUE)); selection.moveToBookmark(bookmark); + + // Check if start element still has formatting then we are at: "text|text" and need to move the start into the next text node + if (format.inline && match(name, vars, selection.getStart())) { + moveStart(selection.getRng(true)); + } + ed.nodeChanged(); } else performCaretAction('remove', name, vars); + + // Removed this logic since it breaks unit tests and produces empty caret elements since they will be destroyed in the cleanup process + // Also there must be a better way to rerender a table and I couldn't reproduce the case causing this might be some old WebKit + /* + // When you remove formatting from a table cell in WebKit (cell, not the contents of a cell) there is a rendering issue with column width + if (tinymce.isWebKit) { + ed.execCommand('mceCleanup'); + }*/ }; function toggle(name, vars, node) { - if (match(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); @@ -13344,6 +15815,11 @@ tinymce.create('tinymce.ui.Toolbar:tinymce.ui.Container', { 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); + } + // Check all items if (items) { // Non indexed object @@ -13396,7 +15872,7 @@ tinymce.create('tinymce.ui.Toolbar:tinymce.ui.Container', { }; function match(name, vars, node) { - var startNode, i; + var startNode; function matchParents(node) { // Find first node with similar format settings @@ -13412,21 +15888,6 @@ tinymce.create('tinymce.ui.Toolbar:tinymce.ui.Container', { if (node) return matchParents(node); - // Check pending formats - if (selection.isCollapsed()) { - for (i = pendingFormats.apply.length - 1; i >= 0; i--) { - if (pendingFormats.apply[i].name == name) - return true; - } - - for (i = pendingFormats.remove.length - 1; i >= 0; i--) { - if (pendingFormats.remove[i].name == name) - return false; - } - - return matchParents(selection.getNode()); - } - // Check selected node node = selection.getNode(); if (matchParents(node)) @@ -13445,33 +15906,6 @@ tinymce.create('tinymce.ui.Toolbar:tinymce.ui.Container', { function matchAll(names, vars) { var startElement, matchedFormatNames = [], checkedMap = {}, i, ni, name; - // If the selection is collapsed then check pending formats - if (selection.isCollapsed()) { - for (ni = 0; ni < names.length; ni++) { - // If the name is to be removed, then stop it from being added - for (i = pendingFormats.remove.length - 1; i >= 0; i--) { - name = names[ni]; - - if (pendingFormats.remove[i].name == name) { - checkedMap[name] = true; - break; - } - } - } - - // If the format is to be applied - for (i = pendingFormats.apply.length - 1; i >= 0; i--) { - for (ni = 0; ni < names.length; ni++) { - name = names[ni]; - - if (!checkedMap[name] && pendingFormats.apply[i].name == name) { - checkedMap[name] = true; - matchedFormatNames.push(name); - } - } - } - } - // Check start of selection for formats startElement = selection.getStart(); dom.getParent(startElement, function(node) { @@ -13580,7 +16014,7 @@ tinymce.create('tinymce.ui.Toolbar:tinymce.ui.Container', { }; function isWhiteSpaceNode(node) { - return node && node.nodeType === 3 && /^([\s\r\n]+|)$/.test(node.nodeValue); + return node && node.nodeType === 3 && /^([\t \r\n]+|)$/.test(node.nodeValue); }; function wrap(node, name, attrs) { @@ -13593,39 +16027,64 @@ tinymce.create('tinymce.ui.Toolbar:tinymce.ui.Container', { }; function expandRng(rng, format, remove) { - var startContainer = rng.startContainer, + var sibling, lastIdx, leaf, + startContainer = rng.startContainer, startOffset = rng.startOffset, endContainer = rng.endContainer, - endOffset = rng.endOffset, sibling, lastIdx; + endOffset = rng.endOffset, sibling, lastIdx, leaf, endPoint; // This function walks up the tree if there is no siblings before/after the node - function findParentContainer(container, child_name, sibling_name, root) { - var parent, child; - - root = root || dom.getRoot(); + function findParentContainer(start) { + var container, parent, child, sibling, siblingName; - for (;;) { - // Check if we can move up are we at root level or body level - parent = container.parentNode; + container = parent = start ? startContainer : endContainer; + siblingName = start ? 'previousSibling' : 'nextSibling'; + root = dom.getRoot(); - // Stop expanding on block elements or root depending on format - if (parent == root || (!format[0].block_expand && isBlock(parent))) + // If it's a text node and the offset is inside the text + if (container.nodeType == 3 && !isWhiteSpaceNode(container)) { + if (start ? startOffset > 0 : endOffset < container.nodeValue.length) { return container; + } + } - for (sibling = parent[child_name]; sibling && sibling != container; sibling = sibling[sibling_name]) { - if (sibling.nodeType == 1 && !isBookmarkNode(sibling)) - return container; + for (;;) { + // Stop expanding on block elements + if (!format[0].block_expand && isBlock(parent)) + return parent; + + // Walk left/right + for (sibling = parent[siblingName]; sibling; sibling = sibling[siblingName]) { + if (!isBookmarkNode(sibling) && !isWhiteSpaceNode(sibling)) { + return parent; + } + } - if (sibling.nodeType == 3 && !isWhiteSpaceNode(sibling)) - return container; + // Check if we can move up are we at root level or body level + if (parent.parentNode == root) { + container = parent; + break; } - container = container.parentNode; + parent = parent.parentNode; } return container; }; + // 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 { node: node, offset: offset }; + } + // If index based start position then resolve it if (startContainer.nodeType == 1 && startContainer.hasChildNodes()) { lastIdx = startContainer.childNodes.length - 1; @@ -13644,32 +16103,161 @@ tinymce.create('tinymce.ui.Toolbar:tinymce.ui.Container', { endOffset = endContainer.nodeValue.length; } - // Exclude bookmark nodes if possible - if (isBookmarkNode(startContainer.parentNode)) - startContainer = startContainer.parentNode; + // Expands the node to the closes contentEditable false element if it exists + function findParentContentEditable(node) { + var parent = node; + + while (parent) { + if (parent.nodeType === 1 && getContentEditable(parent)) { + return getContentEditable(parent) === "false" ? parent : node; + } + + parent = parent.parentNode; + } + + return node; + }; - if (isBookmarkNode(startContainer)) + // Expand to closest contentEditable element + startContainer = findParentContentEditable(startContainer); + endContainer = findParentContentEditable(endContainer); + + // Exclude bookmark nodes if possible + if (isBookmarkNode(startContainer.parentNode) || isBookmarkNode(startContainer)) { + startContainer = isBookmarkNode(startContainer) ? startContainer : startContainer.parentNode; startContainer = startContainer.nextSibling || startContainer; - if (isBookmarkNode(endContainer.parentNode)) - endContainer = endContainer.parentNode; + if (startContainer.nodeType == 3) + startOffset = 0; + } - if (isBookmarkNode(endContainer)) + if (isBookmarkNode(endContainer.parentNode) || isBookmarkNode(endContainer)) { + endContainer = isBookmarkNode(endContainer) ? endContainer : endContainer.parentNode; endContainer = endContainer.previousSibling || endContainer; + if (endContainer.nodeType == 3) + endOffset = endContainer.length; + } + + if (format[0].inline) { + if (rng.collapsed) { + function findWordEndPoint(container, offset, start) { + var walker, node, pos, lastTextNode; + + function findSpace(node, offset) { + var pos, pos2, str = node.nodeValue; + + if (typeof(offset) == "undefined") { + offset = start ? str.length : 0; + } + + if (start) { + pos = str.lastIndexOf(' ', offset); + pos2 = str.lastIndexOf('\u00a0', offset); + pos = pos > pos2 ? pos : pos2; + + // Include the space on remove to avoid tag soup + if (pos !== -1 && !remove) { + pos++; + } + } else { + pos = str.indexOf(' ', offset); + pos2 = str.indexOf('\u00a0', offset); + pos = pos !== -1 && (pos2 === -1 || pos < pos2) ? pos : pos2; + } + + return pos; + }; + + if (container.nodeType === 3) { + pos = findSpace(container, offset); + + if (pos !== -1) { + return {container : container, offset : pos}; + } + + lastTextNode = container; + } + + // Walk the nodes inside the block + walker = new TreeWalker(container, dom.getParent(container, isBlock) || ed.getBody()); + while (node = walker[start ? 'prev' : 'next']()) { + if (node.nodeType === 3) { + lastTextNode = node; + pos = findSpace(node); + + if (pos !== -1) { + return {container : node, offset : pos}; + } + } else if (isBlock(node)) { + break; + } + } + + if (lastTextNode) { + if (start) { + offset = 0; + } else { + offset = lastTextNode.length; + } + + return {container: lastTextNode, offset: offset}; + } + } + + // Expand left to closest word boundery + endPoint = findWordEndPoint(startContainer, startOffset, true); + if (endPoint) { + startContainer = endPoint.container; + startOffset = endPoint.offset; + } + + // Expand right to closest word boundery + endPoint = findWordEndPoint(endContainer, endOffset); + if (endPoint) { + endContainer = endPoint.container; + endOffset = endPoint.offset; + } + } + + // Avoid applying formatting to a trailing space. + leaf = findLeaf(endContainer, endOffset); + if (leaf.node) { + while (leaf.node && leaf.offset === 0 && leaf.node.previousSibling) + leaf = findLeaf(leaf.node.previousSibling); + + if (leaf.node && leaf.offset > 0 && leaf.node.nodeType === 3 && + leaf.node.nodeValue.charAt(leaf.offset - 1) === ' ') { + + 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; + } + } + } + } + // 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) { - startContainer = findParentContainer(startContainer, 'firstChild', 'nextSibling'); - endContainer = findParentContainer(endContainer, 'lastChild', 'previousSibling'); + if (!format[0].inline || (startContainer.nodeType != 3 || startOffset === 0)) { + startContainer = findParentContainer(true); + } + + if (!format[0].inline || (endContainer.nodeType != 3 || endOffset === endContainer.nodeValue.length)) { + endContainer = findParentContainer(); + } } // Expand start/end container to matching selector if (format[0].selector && format[0].expand !== FALSE && !format[0].inline) { function findSelectorEndPoint(container, sibling_name) { - var parents, i, y; + var parents, i, y, curFormat; if (container.nodeType == 3 && container.nodeValue.length == 0 && container[sibling_name]) container = container[sibling_name]; @@ -13677,7 +16265,13 @@ tinymce.create('tinymce.ui.Toolbar:tinymce.ui.Container', { parents = getParents(container); for (i = 0; i < parents.length; i++) { for (y = 0; y < format.length; y++) { - if (dom.is(parents[i], format[y].selector)) + curFormat = format[y]; + + // If collapsed state is set then skip formats that doesn't match that + if ("collapsed" in curFormat && curFormat.collapsed !== rng.collapsed) + continue; + + if (dom.is(parents[i], curFormat.selector)) return parents[i]; } } @@ -13731,10 +16325,10 @@ tinymce.create('tinymce.ui.Toolbar:tinymce.ui.Container', { // Non block element then try to expand up the leaf if (format[0].block) { if (!isBlock(startContainer)) - startContainer = findParentContainer(startContainer, 'firstChild', 'nextSibling'); + startContainer = findParentContainer(true); if (!isBlock(endContainer)) - endContainer = findParentContainer(endContainer, 'lastChild', 'previousSibling'); + endContainer = findParentContainer(); } } @@ -13787,7 +16381,7 @@ tinymce.create('tinymce.ui.Toolbar:tinymce.ui.Container', { // Remove style attribute if it's empty if (stylesModified && dom.getAttrib(node, 'style') == '') { node.removeAttribute('style'); - node.removeAttribute('_mce_style'); + node.removeAttribute('data-mce-style'); } // Remove attributes @@ -13828,7 +16422,7 @@ tinymce.create('tinymce.ui.Toolbar:tinymce.ui.Container', { // Remove mce prefixed attributes if (MCE_ATTR_RE.test(name)) - node.removeAttribute('_mce_' + name); + node.removeAttribute('data-mce-' + name); node.removeAttribute(name); } @@ -13913,7 +16507,7 @@ tinymce.create('tinymce.ui.Toolbar:tinymce.ui.Container', { }; function isBookmarkNode(node) { - return node && node.nodeType == 1 && node.getAttribute('_mce_type') == 'bookmark'; + return node && node.nodeType == 1 && node.getAttribute('data-mce-type') == 'bookmark'; }; function mergeSiblings(prev, next) { @@ -13959,184 +16553,394 @@ tinymce.create('tinymce.ui.Toolbar:tinymce.ui.Container', { } } - // 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; - } + // 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 TRUE; + }; + + // Attribs are not the same + if (!compareObjects(getAttribs(node1), getAttribs(node2))) + return FALSE; + + // Styles are not the same + if (!compareObjects(dom.parseStyle(dom.getAttrib(node1, 'style')), dom.parseStyle(dom.getAttrib(node2, 'style')))) + return FALSE; + + return TRUE; + }; + + // 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 (sibling.nodeType == 1 && !isBookmarkNode(sibling)) + return sibling; + } + + 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); + } + + // Remove next node + dom.remove(next); + + // Move children into prev node + each(tinymce.grep(next.childNodes), function(node) { + prev.appendChild(node); + }); + + return prev; + } + } + + return next; + }; + + function isTextBlock(name) { + return /^(h[1-6]|p|div|pre|address|dl|dt|dd)$/.test(name); + }; + + function getContainer(rng, start) { + var container, offset, lastIdx, walker; + + container = rng[start ? 'startContainer' : 'endContainer']; + offset = rng[start ? 'startOffset' : 'endOffset']; + + if (container.nodeType == 1) { + lastIdx = container.childNodes.length - 1; + + if (!start && offset) + offset--; + + container = container.childNodes[offset > lastIdx ? lastIdx : offset]; + } + + // If start text node is excluded then walk to the next node + if (container.nodeType === 3 && start && offset >= container.nodeValue.length) { + container = new TreeWalker(container, ed.getBody()).next() || container; + } + + // If end text node is excluded then walk to the previous node + if (container.nodeType === 3 && !start && offset == 0) { + container = new TreeWalker(container, ed.getBody()).prev() || container; + } + + return container; + }; + + function performCaretAction(type, name, vars) { + var caretContainerId = '_mce_caret', debug = ed.settings.caret_debug; + + // Creates a caret container bogus element + function createCaretContainer(fill) { + var caretContainer = dom.create('span', {id: caretContainerId, 'data-mce-bogus': true, style: debug ? 'color:red' : ''}); + + if (fill) { + caretContainer.appendChild(ed.getDoc().createTextNode(INVISIBLE_CHAR)); + } + + return caretContainer; + }; + + function isCaretContainerEmpty(node, nodes) { + while (node) { + if ((node.nodeType === 3 && node.nodeValue !== INVISIBLE_CHAR) || node.childNodes.length > 1) { + return false; + } + + // Collect nodes + if (nodes && node.nodeType === 1) { + nodes.push(node); + } + + node = node.firstChild; + } + + return true; + }; + + // Returns any parent caret container element + function getParentCaretContainer(node) { + while (node) { + if (node.id === caretContainerId) { + return node; + } + + node = node.parentNode; + } + }; + + // Finds the first text node in the specified node + function findFirstTextNode(node) { + var walker; + + if (node) { + walker = new TreeWalker(node, node); + + 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()); + + if (!node) { + while (node = dom.get(caretContainerId)) { + removeCaretContainer(node, false); + } + } + } else { + rng = selection.getRng(true); + + if (isCaretContainerEmpty(node)) { + if (move_caret !== false) { + rng.setStartBefore(node); + rng.setEndBefore(node); + } + + dom.remove(node); + } else { + child = findFirstTextNode(node); + + if (child.nodeValue.charAt(0) === INVISIBLE_CHAR) { + child = child.deleteData(0, 1); + } + + dom.remove(node, 1); + } + + selection.setRng(rng); + } + }; + + // Applies formatting to the caret postion + function applyCaretFormat() { + var rng, caretContainer, textNode, offset, bookmark, container, text; + + rng = selection.getRng(true); + offset = rng.startOffset; + container = rng.startContainer; + text = container.nodeValue; + + caretContainer = getParentCaretContainer(selection.getStart()); + if (caretContainer) { + textNode = findFirstTextNode(caretContainer); + } + + // Expand to word is caret is in the middle of a text node and the char before/after is a alpha numeric character + if (text && offset > 0 && offset < text.length && /\w/.test(text.charAt(offset)) && /\w/.test(text.charAt(offset - 1))) { + // Get bookmark of caret position + bookmark = selection.getBookmark(); - return TRUE; - }; + // Collapse bookmark range (WebKit) + rng.collapse(true); - // Attribs are not the same - if (!compareObjects(getAttribs(node1), getAttribs(node2))) - return FALSE; + // Expand the range to the closest word and split it at those points + rng = expandRng(rng, get(name)); + rng = rangeUtils.split(rng); - // Styles are not the same - if (!compareObjects(dom.parseStyle(dom.getAttrib(node1, 'style')), dom.parseStyle(dom.getAttrib(node2, 'style')))) - return FALSE; + // Apply the format to the range + apply(name, vars, rng); - return TRUE; - }; + // Move selection back to caret position + selection.moveToBookmark(bookmark); + } else { + if (!caretContainer || textNode.nodeValue !== INVISIBLE_CHAR) { + caretContainer = createCaretContainer(true); + textNode = caretContainer.firstChild; - // 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 && !isWhiteSpaceNode(sibling)) - return node; + rng.insertNode(caretContainer); + offset = 1; - if (sibling.nodeType == 1 && !isBookmarkNode(sibling)) - return sibling; + apply(name, vars, caretContainer); + } else { + apply(name, vars, caretContainer); } - return node; - }; + // Move selection to text node + selection.setCursorLocation(textNode, offset); + } + }; - // If previous sibling is empty then jump over it - prev = findElementSibling(prev, 'previousSibling'); - next = findElementSibling(next, 'nextSibling'); + function removeCaretFormat() { + var rng = selection.getRng(true), container, offset, bookmark, + hasContentAfter, node, formatNode, parents = [], i, caretContainer; - // 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); + container = rng.startContainer; + offset = rng.startOffset; + node = container; + + if (container.nodeType == 3) { + if (offset != container.nodeValue.length || container.nodeValue === INVISIBLE_CHAR) { + hasContentAfter = true; } - // Remove next node - dom.remove(next); + node = node.parentNode; + } - // Move children into prev node - each(tinymce.grep(next.childNodes), function(node) { - prev.appendChild(node); - }); + while (node) { + if (matchNode(node, name, vars)) { + formatNode = node; + break; + } - return prev; + if (node.nextSibling) { + hasContentAfter = true; + } + + parents.push(node); + node = node.parentNode; } - } - return next; - }; + // Node doesn't have the specified format + if (!formatNode) { + return; + } - function isTextBlock(name) { - return /^(h[1-6]|p|div|pre|address|dl|dt|dd)$/.test(name); - }; + // Is there contents after the caret then remove the format on the element + if (hasContentAfter) { + // Get bookmark of caret position + bookmark = selection.getBookmark(); - function getContainer(rng, start) { - var container, offset, lastIdx; + // Collapse bookmark range (WebKit) + rng.collapse(true); - container = rng[start ? 'startContainer' : 'endContainer']; - offset = rng[start ? 'startOffset' : 'endOffset']; + // Expand the range to the closest word and split it at those points + rng = expandRng(rng, get(name), true); + rng = rangeUtils.split(rng); - if (container.nodeType == 1) { - lastIdx = container.childNodes.length - 1; + // Remove the format from the range + remove(name, vars, rng); - if (!start && offset) - offset--; + // Move selection back to caret position + selection.moveToBookmark(bookmark); + } else { + caretContainer = createCaretContainer(); - container = container.childNodes[offset > lastIdx ? lastIdx : offset]; - } + node = caretContainer; + for (i = parents.length - 1; i >= 0; i--) { + node.appendChild(dom.clone(parents[i], false)); + node = node.firstChild; + } - return container; - }; + // Insert invisible character into inner most format element + node.appendChild(dom.doc.createTextNode(INVISIBLE_CHAR)); + node = node.firstChild; - function performCaretAction(type, name, vars) { - var i, currentPendingFormats = pendingFormats[type], - otherPendingFormats = pendingFormats[type == 'apply' ? 'remove' : 'apply']; + // Insert caret container after the formated node + dom.insertAfter(caretContainer, formatNode); - function hasPending() { - return pendingFormats.apply.length || pendingFormats.remove.length; + // Move selection to text node + selection.setCursorLocation(node, 1); + } }; - function resetPending() { - pendingFormats.apply = []; - pendingFormats.remove = []; - }; + // 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; + + if (isCaretContainerEmpty(getParentCaretContainer(selection.getStart()), nodes)) { + // Mark children + i = nodes.length; + while (i--) { + dom.setAttrib(nodes[i], 'data-mce-bogus', '1'); + } + } + }); - function perform(caret_node) { - // Apply pending formats - each(pendingFormats.apply.reverse(), function(item) { - apply(item.name, item.vars, caret_node); + // Remove caret container on mouse up and on key up + tinymce.each('onMouseUp onKeyUp'.split(' '), function(name) { + ed[name].addToTop(function() { + removeCaretContainer(); + }); }); - // Remove pending formats - each(pendingFormats.remove.reverse(), function(item) { - remove(item.name, item.vars, caret_node); + // 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())); + } }); - dom.remove(caret_node, 1); - resetPending(); - }; + self._hasCaretEvents = true; + } - // Check if it already exists then ignore it - for (i = currentPendingFormats.length - 1; i >= 0; i--) { - if (currentPendingFormats[i].name == name) - return; + // Do apply or remove caret format + if (type == "apply") { + applyCaretFormat(); + } else { + removeCaretFormat(); } + }; - currentPendingFormats.push({name : name, vars : vars}); + function moveStart(rng) { + var container = rng.startContainer, + offset = rng.startOffset, + walker, node, nodes, tmpNode; - // Check if it's in the other type, then remove it - for (i = otherPendingFormats.length - 1; i >= 0; i--) { - if (otherPendingFormats[i].name == name) - otherPendingFormats.splice(i, 1); + // Convert text node into index if possible + if (container.nodeType == 3 && offset >= container.nodeValue.length) { + // Get the parent container location and walk from there + container = container.parentNode; + offset = nodeIndex(container) + 1; } - // Pending apply or remove formats - if (hasPending()) { - ed.getDoc().execCommand('FontName', false, 'mceinline'); - pendingFormats.lastRng = selection.getRng(); - - // IE will convert the current word - each(dom.select('font,span'), function(node) { - var bookmark; + // 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) + 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 (isCaretNode(node)) { - bookmark = selection.getBookmark(); - perform(node); - selection.moveToBookmark(bookmark); - ed.nodeChanged(); + return; } - }); - - // Only register listeners once if we need to - if (!pendingFormats.isListening && hasPending()) { - pendingFormats.isListening = true; - - each('onKeyDown,onKeyUp,onKeyPress,onMouseUp'.split(','), function(event) { - ed[event].addToTop(function(ed, e) { - // Do we have pending formats and is the selection moved has moved - if (hasPending() && !tinymce.dom.RangeUtils.compareRanges(pendingFormats.lastRng, selection.getRng())) { - each(dom.select('font,span'), function(node) { - var textNode, rng; - - // Look for marker - if (isCaretNode(node)) { - textNode = node.firstChild; - - if (textNode) { - perform(node); - - rng = dom.createRng(); - rng.setStart(textNode, textNode.nodeValue.length); - rng.setEnd(textNode, textNode.nodeValue.length); - selection.setRng(rng); - ed.nodeChanged(); - } else - dom.remove(node); - } - }); - - // Always unbind and clear pending styles on keyup - if (e.type == 'keyup' || e.type == 'mouseup') - resetPending(); - } - }); - }); } } }; @@ -14147,12 +16951,15 @@ tinymce.onAddEditor.add(function(tinymce, ed) { var filters, fontSizes, dom, settings = ed.settings; if (settings.inline_styles) { - fontSizes = tinymce.explode(settings.font_size_style_values); + fontSizes = tinymce.explode(settings.font_size_legacy_values); function replaceWithSpan(node, styles) { - dom.replace(dom.create('span', { - style : styles - }), node, 1); + tinymce.each(styles, function(value, name) { + if (value) + dom.setStyle(node, name, value); + }); + + dom.rename(node, 'span'); }; filters = { @@ -14189,6 +16996,7 @@ tinymce.onAddEditor.add(function(tinymce, ed) { }; ed.onPreProcess.add(convert); + ed.onSetContent.add(convert); ed.onInit.add(function() { ed.selection.onSetContent.add(convert); @@ -14196,3 +17004,393 @@ tinymce.onAddEditor.add(function(tinymce, ed) { } }); +(function(tinymce) { + var TreeWalker = tinymce.dom.TreeWalker; + + tinymce.EnterKey = function(editor) { + var dom = editor.dom, selection = editor.selection, settings = editor.settings, undoManager = editor.undoManager; + + function handleEnterKey(evt) { + var rng = selection.getRng(true), tmpRng, container, offset, parentBlock, newBlock, fragment, containerBlock, parentBlockName, containerBlockName, newBlockName; + + // Returns true if the block can be split into two blocks or not + function canSplitBlock(node) { + return node && dom.isBlock(node) && !/^(TD|TH|CAPTION)$/.test(node.nodeName) && !/^(fixed|absolute)/i.test(node.style.position); + }; + + // 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; + + rng = dom.createRng(); + + if (root.hasChildNodes()) { + walker = new TreeWalker(root, root); + + while (node = walker.current()) { + if (node.nodeType == 3) { + rng.setStart(node, 0); + rng.setEnd(node, 0); + break; + } + + if (/^(BR|IMG)$/.test(node.nodeName)) { + rng.setStartBefore(node); + rng.setEndBefore(node); + break; + } + + lastNode = node; + node = walker.next(); + } + + if (!node) { + rng.setStart(lastNode, 0); + rng.setEnd(lastNode, 0); + } + } else { + if (root.nodeName == 'BR') { + rng.setStartAfter(root); + rng.setEndAfter(root); + } else { + rng.setStart(root, 0); + rng.setEnd(root, 0); + } + } + + selection.setRng(rng); + + viewPort = dom.getViewPort(editor.getWin()); + + // 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 + } + }; + + // 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; + + block = name ? dom.create(name) : parentBlock.cloneNode(false); + caretNode = block; + + // Clone any parent styles + do { + if (/^(SPAN|STRONG|B|EM|I|FONT|STRIKE|U)$/.test(node.nodeName)) { + clonedNode = node.cloneNode(false); + dom.setAttrib(clonedNode, 'id', ''); // Remove ID since it needs to be document unique + + if (block.hasChildNodes()) { + clonedNode.appendChild(block.firstChild); + block.appendChild(clonedNode); + } else { + caretNode = clonedNode; + block.appendChild(clonedNode); + } + } + } while (node = node.parentNode); + + // BR is needed in empty blocks on non IE browsers + if (!tinymce.isIE) { + caretNode.innerHTML = '
    '; + } + + return block; + }; + + // Returns true/false if the caret is at the start/end of the parent block element + function isCaretAtStartOrEndOfBlock(start) { + var walker, node; + + // 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; + } + + // Walk the DOM and look for text nodes or non empty elements + walker = new TreeWalker(container, parentBlock); + while (node = (start ? walker.prev() : walker.next())) { + if (node.nodeType === 1) { + // Ignore bogus elements + if (node.getAttribute('data-mce-bogus')) { + continue; + } + + // Keep empty elements like + name = node.nodeName.toLowerCase(); + if (name === 'IMG') { + return false; + } + } else if (node.nodeType === 3 && !/^[ \t\r\n]*$/.test(node.nodeValue)) { + return false; + } + } + + return true; + }; + + // Wraps any text nodes or inline elements in the specified forced root block name + function wrapSelfAndSiblingsInDefaultBlock(container, offset) { + var newBlock, parentBlock, startNode, node, next; + + // Not in a block element or in a table cell or caption + parentBlock = dom.getParent(container, dom.isBlock); + if (newBlockName && !evt.shiftKey && (!parentBlock || !canSplitBlock(parentBlock))) { + parentBlock = parentBlock || dom.getRoot(); + + if (!parentBlock.hasChildNodes()) { + newBlock = dom.create(newBlockName); + parentBlock.appendChild(newBlock); + rng.setStart(newBlock, 0); + rng.setEnd(newBlock, 0); + return newBlock; + } + + // Find parent that is the first child of parentBlock + node = container; + while (node.parentNode != parentBlock) { + node = node.parentNode; + } + + // Loop left to find start node start wrapping at + while (node && !dom.isBlock(node)) { + startNode = node; + node = node.previousSibling; + } + + if (startNode) { + newBlock = dom.create(newBlockName); + startNode.parentNode.insertBefore(newBlock, startNode); + + // 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); + } + } + + return container; + }; + + // 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']; + + // 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); + } else { + // 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(); + }; + + // Walks the parent block to the right and look for BR elements + function hasRightSideBr() { + var walker = new TreeWalker(container, parentBlock), node; + + while (node = walker.current()) { + if (node.nodeName == 'BR') { + return true; + } + + node = walker.next(); + } + } + + // Inserts a BR element if the forced_root_block option is set to false or empty string + function insertBr() { + var brElm, extraBr, documentMode; + + 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; + } + } + + brElm = dom.create('br'); + rng.insertNode(brElm); + + // Rendering modes below IE8 doesn't display BR elements in PRE unless we have a \n before it + documentMode = dom.doc.documentMode; + if (tinymce.isIE && parentBlockName == 'PRE' && (!documentMode || documentMode < 8)) { + brElm.parentNode.insertBefore(dom.doc.createTextNode('\r'), brElm); + } + + if (!extraBr) { + rng.setStartAfter(brElm); + rng.setEndAfter(brElm); + } else { + rng.setStartBefore(brElm); + rng.setEndBefore(brElm); + } + + selection.setRng(rng); + undoManager.add(); + }; + + // 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]+/, ''); + } + + node = node.firstChild; + } while (node); + }; + + // Delete any selected contents + if (!rng.collapsed) { + editor.execCommand('Delete'); + return; + } + + // Event is blocked by some other handler for example the lists plugin + if (evt.isDefaultPrevented()) { + return; + } + + // Setup range items and newBlockName + container = rng.startContainer; + offset = rng.startOffset; + newBlockName = settings.forced_root_block; + newBlockName = newBlockName ? newBlockName.toUpperCase() : ''; + + // Resolve node index + if (container.nodeType == 1 && container.hasChildNodes()) { + container = container.childNodes[Math.min(offset, container.childNodes.length - 1)] || container; + offset = 0; + } + + undoManager.beforeChange(); + + // 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

    + container = wrapSelfAndSiblingsInDefaultBlock(container, offset); + + // Find parent block and setup empty block paddings + parentBlock = dom.getParent(container, dom.isBlock); + containerBlock = parentBlock ? dom.getParent(parentBlock.parentNode, dom.isBlock) : null; + + // Setup block names + parentBlockName = parentBlock ? parentBlock.nodeName.toUpperCase() : ''; // IE < 9 & HTML5 + containerBlockName = containerBlock ? containerBlock.nodeName.toUpperCase() : ''; // IE < 9 & HTML5 + + // Handle enter inside an empty list item + if (parentBlockName == 'LI' && dom.isEmpty(parentBlock)) { + // Let the list plugin or browser handle nested lists for now + if (/^(UL|OL|LI)$/.test(containerBlock.parentNode.nodeName)) { + return false; + } + + handleEmptyListItem(); + return; + } + + // 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 (!evt.shiftKey) { + insertBr(); + return; + } + } else { + // If no root block is configured then insert a BR by default or if the shiftKey is pressed + if ((!newBlockName && !evt.shiftKey && parentBlockName != 'LI') || (newBlockName && evt.shiftKey)) { + insertBr(); + return; + } + } + + // Default block name if it's not configured + newBlockName = newBlockName || 'P'; + + // 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(); + } + + // 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); + } + } else if (isCaretAtStartOrEndOfBlock(true)) { + // Insert new block before + newBlock = parentBlock.parentNode.insertBefore(createNewBlock(), parentBlock); + } 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); + } + + dom.setAttrib(newBlock, 'id', ''); // Remove ID since it needs to be document unique + moveToCaretPosition(newBlock); + undoManager.add(); + } + + editor.onKeyDown.add(function(ed, evt) { + if (evt.keyCode == 13) { + if (handleEnterKey(evt) !== false) { + evt.preventDefault(); + } + } + }); + }; +})(tinymce); -- cgit v1.2.3