From 6d1df5faea99fd7ecdcd35b3af0ed75714a236a3 Mon Sep 17 00:00:00 2001
From: alec
' + h;
@@ -1800,7 +1882,7 @@ tinymce.create('static tinymce.util.XHR', {
// DOM tree if contents like this
<\/p>|
]+)><\/p>|
/gi, ''); + h = h.replace(/
<\/p>|
]+)><\/p>|
/gi, ''); set(); @@ -1811,12 +1893,12 @@ tinymce.create('static tinymce.util.XHR', { n = nl[i]; if (!n.hasChildNodes()) { - if (!n.mce_keep) { + if (!n._mce_keep) { x = 1; // Is broken break; } - n.removeAttribute('mce_keep'); + n.removeAttribute('_mce_keep'); } } } @@ -1825,13 +1907,13 @@ tinymce.create('static tinymce.util.XHR', { 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, '
]+)>|
/ig, '
']); t.addShortcut('ctrl+8', '', ['FormatBlock', false, '
]+)><\\\/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');
- if (ed.settings.convert_fonts_to_spans) {
- this._applyInlineStyle('span', {style : {backgroundColor : val}});
- return;
- }
+ function padd(ed, o) {
+ if (isOpera)
+ o.content = o.content.replace(t.reOpera, '' + elm + '>');
- function set(s) {
- if (!isGecko)
- return;
+ o.content = o.content.replace(t.rePadd, '<' + elm + '$1$2$3$4$5$6>\u00a0' + elm + '>');
- try {
- // Try new Gecko method
- d.execCommand("styleWithCSS", 0, s);
- } catch (ex) {
- // Use old
- d.execCommand("useCSS", 0, !s);
- }
+ if (!isIE && !isOpera && o.set) {
+ // Use instead of BR in padded paragraphs
+ o.content = o.content.replace(t.reNbsp2BR1, '<' + elm + '$1$2>
' + elm + '>');
+ o.content = o.content.replace(t.reNbsp2BR2, '<' + elm + '$1$2>
' + elm + '>');
+ } else
+ o.content = o.content.replace(t.reBR2Nbsp, '<' + elm + '$1$2>\u00a0' + elm + '>');
};
- if (isGecko || isOpera) {
- set(true);
- d.execCommand('hilitecolor', false, val);
- set(false);
- } else
- d.execCommand('BackColor', false, val);
- },
+ ed.onBeforeSetContent.add(padd);
+ ed.onPostProcess.add(padd);
- FormatBlock : function(ui, val) {
- var t = this, ed = t.editor, s = ed.selection, dom = ed.dom, bl, nb, b;
+ if (s.forced_root_block) {
+ ed.onInit.add(t.forceRoots, t);
+ ed.onSetContent.add(t.forceRoots, t);
+ ed.onBeforeGetContent.add(t.forceRoots, t);
+ }
+ },
- function isBlock(n) {
- return /^(P|DIV|H[1-6]|ADDRESS|BLOCKQUOTE|PRE)$/.test(n.nodeName);
- };
+ setup : function() {
+ var t = this, ed = t.editor, s = ed.settings, dom = ed.dom, selection = ed.selection;
- bl = dom.getParent(s.getNode(), function(n) {
- return isBlock(n);
- });
+ // 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);
+ }
- // IE has an issue where it removes the parent div if you change format on the paragrah in
Content
') : val; - - if (val.indexOf('<') == -1) - val = '<' + val + '>'; - - if (tinymce.isGecko) - val = val.replace(/<(div|blockquote|code|dt|dd|dl|samp)>/gi, '$1'); - - ed.getDoc().execCommand('FormatBlock', false, val); - }, - - mceCleanup : function() { - var ed = this.editor, s = ed.selection, b = s.getBookmark(); - ed.setContent(ed.getContent()); - s.moveToBookmark(b); - }, - - mceRemoveNode : function(ui, val) { - var ed = this.editor, s = ed.selection, b, n = val || s.getNode(); - - // Make sure that the body node isn't removed - if (n == ed.getBody()) - return; - - b = s.getBookmark(); - ed.dom.remove(n, 1); - s.moveToBookmark(b); - ed.nodeChanged(); - }, - - mceSelectNodeDepth : function(ui, val) { - var ed = this.editor, s = ed.selection, c = 0; + if (!isIE && s.force_p_newlines) { + ed.onKeyPress.add(function(ed, e) { + if (e.keyCode == 13 && !e.shiftKey && !t.insertPara(e)) + Event.cancel(e); + }); - ed.dom.getParent(s.getNode(), function(n) { - if (n.nodeType == 1 && c++ == val) { - s.select(n); - ed.nodeChanged(); - return false; + if (isGecko) { + ed.onKeyDown.add(function(ed, e) { + if ((e.keyCode == 8 || e.keyCode == 46) && !e.shiftKey) + t.backspaceDelete(e, e.keyCode == 8); + }); } - }, ed.getBody()); - }, - - mceSelectNode : function(u, v) { - this.editor.selection.select(v); - }, + } - mceInsertContent : function(ui, val) { - this.editor.selection.setContent(val); - }, + // 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; - mceInsertRawHTML : function(ui, val) { - var ed = this.editor; + // Insert BR element + rng.insertNode(br = dom.create('br')); - ed.selection.setContent('tiny_mce_marker'); - ed.setContent(ed.getContent().replace(/tiny_mce_marker/g, val)); - }, + // Place caret after BR + rng.setStartAfter(br); + rng.setEndAfter(br); + selection.setRng(rng); - mceRepaint : function() { - var s, b, e = this.editor; + // 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); + } - if (tinymce.isGecko) { - try { - s = e.selection; - b = s.getBookmark(true); + // Create a temporary DIV after the BR and get the position as it + // seems like getPos() returns 0 for text nodes and BR elements. + dom.insertAfter(div, br); + divYPos = dom.getPos(div).y; + dom.remove(div); - if (s.getSel()) - s.getSel().selectAllChildren(e.getBody()); + // 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); + }; - s.collapse(true); - s.moveToBookmark(b); - } catch (ex) { - // Ignore - } + ed.onKeyPress.add(function(ed, e) { + if (e.keyCode == 13 && (e.shiftKey || s.force_br_newlines)) { + insertBr(ed); + Event.cancel(e); + } + }); } - }, - queryStateUnderline : function() { - var ed = this.editor, n = ed.selection.getNode(); - - if (n && n.nodeName == 'A') - return false; + // 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 + } + }); + } + }); + }); - return this._queryState('Underline'); - }, + // 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; + }); - queryStateOutdent : function() { - var ed = this.editor, n; + ed.onKeyUp.add(function(ed, e) { + var bl, n = selection.getNode(), b = ed.getBody(); - if (ed.settings.inline_styles) { - if ((n = ed.dom.getParent(ed.selection.getStart(), ed.dom.isBlock)) && parseInt(n.style.paddingLeft) > 0) - return true; + 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'); - if ((n = ed.dom.getParent(ed.selection.getEnd(), ed.dom.isBlock)) && parseInt(n.style.paddingLeft) > 0) - return true; + if (bl) { + dom.rename(bl, s.element); + ed.nodeChanged(); + } + } + }); + } } - - return this.queryStateInsertUnorderedList() || this.queryStateInsertOrderedList() || (!ed.settings.inline_styles && !!ed.dom.getParent(ed.selection.getNode(), 'BLOCKQUOTE')); - }, - - queryStateInsertUnorderedList : function() { - return this.editor.dom.getParent(this.editor.selection.getNode(), 'UL'); - }, - - queryStateInsertOrderedList : function() { - return this.editor.dom.getParent(this.editor.selection.getNode(), 'OL'); - }, - - queryStatemceBlockQuote : function() { - return !!this.editor.dom.getParent(this.editor.selection.getStart(), function(n) {return n.nodeName === 'BLOCKQUOTE';}); }, - _applyInlineStyle : function(na, at, op) { - var t = this, ed = t.editor, dom = ed.dom, bm, lo = {}, kh, found; - - na = na.toUpperCase(); - - if (op && op.check_classes && at['class']) - op.check_classes.push(at['class']); - - function removeEmpty() { - each(dom.select(na).reverse(), function(n) { - var c = 0; + find : function(n, t, s) { + var ed = this.editor, w = ed.getDoc().createTreeWalker(n, 4, null, FALSE), c = -1; - // Check if there is any attributes - each(dom.getAttribs(n), function(an) { - if (an.nodeName.substring(0, 1) != '_' && dom.getAttrib(n, an.nodeName) != '') { - //console.log(dom.getOuterHTML(n), dom.getAttrib(n, an.nodeName)); - c++; - } - }); + while (n = w.nextNode()) { + c++; - // No attributes then remove the element and keep the children - if (c == 0) - dom.remove(n, 1); - }); - }; + // Index by node + if (t == 0 && n == s) + return c; - function replaceFonts() { - var bm; + // Node by index + if (t == 1 && c == s) + return n; + } - each(dom.select('span,font'), function(n) { - if (n.style.fontFamily == 'mceinline' || n.face == 'mceinline') { - if (!bm) - bm = ed.selection.getBookmark(); + return -1; + }, - at._mce_new = '1'; - dom.replace(dom.create(na, at), n, 1); - } - }); + 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; - // Remove redundant elements - each(dom.select(na + '[_mce_new]'), function(n) { - function removeStyle(n) { - if (n.nodeType == 1) { - each(at.style, function(v, k) { - dom.setStyle(n, k, ''); - }); + // Fix for bug #1863847 + //if (e && e.keyCode == 13) + // return TRUE; - // Remove spans with the same class or marked classes - if (at['class'] && n.className && op) { - each(op.check_classes, function(c) { - if (dom.hasClass(n, c)) - dom.removeClass(n, c); - }); - } - } - }; + // Wrap non blocks into blocks + for (i = nl.length - 1; i >= 0; i--) { + nx = nl[i]; - // Remove specified style information from child elements - each(dom.select(na, n), removeStyle); + // Ignore internal elements + if (nx.nodeType === 1 && nx.getAttribute('_mce_type')) { + bl = null; + continue; + } - // Remove the specified style information on parent if current node is only child (IE) - if (n.parentNode && n.parentNode.nodeType == 1 && n.parentNode.childNodes.length == 1) - removeStyle(n.parentNode); + // 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 { + tr = d.body.createTextRange(); + tr.moveToElementText(b); + tr.collapse(1); + bp = tr.move('character', c) * -1; - // Remove the child elements style info if a parent already has it - dom.getParent(n.parentNode, function(pn) { - if (pn.nodeType == 1) { - if (at.style) { - each(at.style, function(v, k) { - var sv; + tr = r.duplicate(); + tr.collapse(1); + sp = tr.move('character', c) * -1; - if (!lo[k] && (sv = dom.getStyle(pn, k))) { - if (sv === v) - dom.setStyle(n, k, ''); + tr = r.duplicate(); + tr.collapse(0); + le = (tr.move('character', c) * -1) - sp; - lo[k] = 1; - } - }); + si = sp - bp; + ei = le; + } } - // Remove spans with the same class or marked classes - if (at['class'] && pn.className && op) { - each(op.check_classes, function(c) { - if (dom.hasClass(pn, c)) - dom.removeClass(n, c); - }); - } + // 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); } - - return false; - }); - - n.removeAttribute('_mce_new'); - }); - - removeEmpty(); - ed.selection.moveToBookmark(bm); - - return !!bm; - }; - - // Create inline elements - ed.focus(); - ed.getDoc().execCommand('FontName', false, 'mceinline'); - replaceFonts(); - - if (kh = t._applyInlineStyle.keyhandler) { - ed.onKeyUp.remove(kh); - ed.onKeyPress.remove(kh); - ed.onKeyDown.remove(kh); - ed.onSetContent.remove(t._applyInlineStyle.chandler); + } else { + if (bl.hasChildNodes()) + bl.insertBefore(nx, bl.firstChild); + else + bl.appendChild(nx); + } + } else + bl = null; // Time to create new block } - if (ed.selection.isCollapsed()) { - // IE will format the current word so this code can't be executed on that browser + // Restore selection + if (si != -2) { if (!isIE) { - each(dom.getParents(ed.selection.getNode(), 'span'), function(n) { - each(at.style, function(v, k) { - var kv; - - if (kv = dom.getStyle(n, k)) { - if (kv == v) { - dom.setStyle(n, k, ''); - found = 2; - return false; - } - - found = 1; - return false; - } - }); - - if (found) - return false; - }); - - if (found == 2) { - bm = ed.selection.getBookmark(); - - removeEmpty(); + bl = b.getElementsByTagName(ed.settings.element)[0]; + r = d.createRange(); - ed.selection.moveToBookmark(bm); + // Select last location or generated block + if (si != -1) + r.setStart(t.find(b, 1, si), so); + else + r.setStart(bl, 0); - // Node change needs to be detached since the onselect event - // for the select box will run the onclick handler after onselect call. Todo: Add a nicer fix! - window.setTimeout(function() { - ed.nodeChanged(); - }, 1); + // Select last location or generated block + if (ei != -1) + r.setEnd(t.find(b, 1, ei), eo); + else + r.setEnd(bl, 0); - return; + 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 } } + } 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'); - // Start collecting styles - t._pendingStyles = tinymce.extend(t._pendingStyles || {}, at.style); - - t._applyInlineStyle.chandler = ed.onSetContent.add(function() { - delete t._pendingStyles; - }); + // Move caret before selected element + r = d.createRange(); + r.setStartBefore(n); + r.setEndBefore(n); + se.setRng(r); + } + }, - t._applyInlineStyle.keyhandler = kh = function(e) { - // Use pending styles - if (t._pendingStyles) { - at.style = t._pendingStyles; - delete t._pendingStyles; - } + getParentBlock : function(n) { + var d = this.dom; - if (replaceFonts()) { - ed.onKeyDown.remove(t._applyInlineStyle.keyhandler); - ed.onKeyPress.remove(t._applyInlineStyle.keyhandler); - } + return d.getParent(n, d.isBlock); + }, - if (e.type == 'keyup') - ed.onKeyUp.remove(t._applyInlineStyle.keyhandler); - }; + 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; - ed.onKeyDown.add(kh); - ed.onKeyPress.add(kh); - ed.onKeyUp.add(kh); - } else - t._pendingStyles = 0; - } - }); -})(tinymce);(function(tinymce) { - tinymce.create('tinymce.UndoManager', { - index : 0, - data : null, - typing : 0, + // 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; - UndoManager : function(ed) { - var t = this, Dispatcher = tinymce.util.Dispatcher; + // Setup before range + rb = d.createRange(); - t.editor = ed; - t.data = []; - t.onAdd = new Dispatcher(this); - t.onUndo = new Dispatcher(this); - t.onRedo = new Dispatcher(this); - }, + // 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); - add : function(l) { - var t = this, i, ed = t.editor, b, s = ed.settings, la; + // Setup after range + ra = d.createRange(); - l = l || {}; - l.content = l.content || ed.getContent({format : 'raw', no_events : 1}); + // 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); - // Add undo level if needed - l.content = l.content.replace(/^\s*|\s*$/g, ''); - la = t.data[t.index > 0 && (t.index == 0 || t.index == t.data.length) ? t.index - 1 : t.index]; - if (!l.initial && la && l.content == la.content) - return null; + // 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; - // Time to compress - if (s.custom_undo_redo_levels) { - if (t.data.length > s.custom_undo_redo_levels) { - for (i = 0; i < t.data.length - 1; i++) - t.data[i] = t.data[i + 1]; + // 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 - t.data.length--; - t.index = t.data.length; + // Create two new block elements + if (sn.childNodes.length == 0) { + ed.dom.add(sn, se.element, null, '
]+)><\\\/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');
+ e.innerHTML = '';
- function padd(ed, o) {
- if (isOpera)
- o.content = o.content.replace(t.reOpera, '' + elm + '>');
+ // 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);
+ }
- o.content = o.content.replace(t.rePadd, '<' + elm + '$1$2$3$4$5$6>\u00a0' + elm + '>');
+ // Append style elements to aft
+ if (nl.length > 0) {
+ for (i = nl.length - 1, nn = e; i >= 0; i--)
+ nn = nn.appendChild(nl[i]);
- if (!isIE && !isOpera && o.set) {
- // Use instead of BR in padded paragraphs
- o.content = o.content.replace(t.reNbsp2BR1, '<' + elm + '$1$2>
' + elm + '>');
- o.content = o.content.replace(t.reNbsp2BR2, '<' + elm + '$1$2>
' + elm + '>');
+ // 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
- o.content = o.content.replace(t.reBR2Nbsp, '<' + elm + '$1$2>\u00a0' + elm + '>');
+ e.innerHTML = isOpera ? ' ' : '
'; // Extra space for Opera so that the caret can move there
};
- ed.onBeforeSetContent.add(padd);
- ed.onPostProcess.add(padd);
+ // Fill empty afterblook with current style
+ if (isEmpty(aft))
+ car = appendStyles(aft, en);
- if (s.forced_root_block) {
- ed.onInit.add(t.forceRoots, t);
- ed.onSetContent.add(t.forceRoots, t);
- ed.onBeforeGetContent.add(t.forceRoots, t);
+ // 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);
}
- },
- setup : function() {
- var t = this, ed = t.editor, s = ed.settings;
+ // Normalize
+ aft.normalize();
+ bef.normalize();
- // Force root blocks when typing and when getting output
- if (s.forced_root_block) {
- ed.onKeyUp.add(t.forceRoots, t);
- ed.onPreProcess.add(t.forceRoots, t);
- }
+ function first(n) {
+ return d.createTreeWalker(n, NodeFilter.SHOW_TEXT, null, FALSE).nextNode() || n;
+ };
- if (s.force_br_newlines) {
- // Force IE to produce BRs on enter
- if (isIE) {
- ed.onKeyPress.add(function(ed, e) {
- var n, s = ed.selection;
+ // 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 (e.keyCode == 13 && s.getNode().nodeName != 'LI') {
- s.setContent('
', {format : 'raw'});
- n = ed.dom.get('__');
- n.removeAttribute('id');
- s.select(n);
- s.collapse();
- return Event.cancel(e);
- }
- });
- }
+ // 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;
- 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));
}
- if (!isIE && s.force_p_newlines) {
-/* ed.onPreProcess.add(function(ed, o) {
- each(ed.dom.select('br', o.node), function(n) {
- var p = n.parentNode;
-
- // Replace
- if (p && p.nodeName == 'p' && (p.childNodes.length == 1 || p.lastChild == n)) { - p.replaceChild(ed.getDoc().createTextNode('\u00a0'), n); - } - }); - });*/ + return FALSE; + }, - ed.onKeyPress.add(function(ed, e) { - if (e.keyCode == 13 && !e.shiftKey) { - if (!t.insertPara(e)) - Event.cancel(e); - } - }); + 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; - if (isGecko) { - ed.onKeyDown.add(function(ed, e) { - if ((e.keyCode == 8 || e.keyCode == 46) && !e.shiftKey) - t.backspaceDelete(e, e.keyCode == 8); - }); - } - } + // 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)) ; - function ren(rn, na) { - var ne = ed.dom.create(na); + 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; - each(rn.attributes, function(a) { - if (a.specified && a.nodeValue) - ne.setAttribute(a.nodeName.toLowerCase(), a.nodeValue); - }); + // 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); - each(rn.childNodes, function(n) { - ne.appendChild(n.cloneNode(true)); - }); + // Remove the target container + ed.dom.remove(sc); + } - rn.parentNode.replaceChild(ne, rn); + return Event.cancel(e); + } + } + } - return ne; - }; + // Gecko generates BR elements here and there, we don't like those so lets remove them + function handler(e) { + var pr; - // Padd empty inline elements within block elements - // For example:
becomes
- ed.onPreProcess.add(function(ed, o) { - each(ed.dom.select('p,h1,h2,h3,h4,h5,h6,div', o.node), function(p) { - if (isEmpty(p)) { - each(ed.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 - } - }); - } - }); - }); + e = e.target; - // 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 = ed.selection.getNode().nodeName; - }); + // A new BR was created in a block element, remove it + if (e && e.parentNode && e.nodeName == 'BR' && (n = t.getParentBlock(e))) { + pr = e.previousSibling; - ed.onKeyUp.add(function(ed, e) { - var bl, sel = ed.selection, n = sel.getNode(), b = ed.getBody(); + Event.remove(b, 'DOMNodeInserted', handler); - if (b.childNodes.length === 1 && n.nodeName == 'P') { - n = ren(n, s.element); - sel.select(n); - sel.collapse(); - ed.nodeChanged(); - } else if (e.keyCode == 13 && !e.shiftKey && t.lastElm != 'P') { - bl = ed.dom.getParent(n, 'p'); + // Is there whitespace at the end of the node before then we might need the pesky BR + // to place the caret at a correct location see bug: #2013943 + if (pr && pr.nodeType == 3 && /\s+$/.test(pr.nodeValue)) + return; - if (bl) { - ren(bl, s.element); - ed.nodeChanged(); - } - } - }); + // Only remove BR elements that got inserted in the middle of the text + if (e.previousSibling || e.nextSibling) + ed.dom.remove(e); } - } - }, + }; - find : function(n, t, s) { - var ed = this.editor, w = ed.getDoc().createTreeWalker(n, 4, null, false), c = -1; + // Listen for new nodes + Event._add(b, 'DOMNodeInserted', handler); - while (n = w.nextNode()) { - c++; + // Remove listener + window.setTimeout(function() { + Event._remove(b, 'DOMNodeInserted', handler); + }, 1); + } + }); +})(tinymce); - // Index by node - if (t == 0 && n == s) - return c; +(function(tinymce) { + // Shorten names + var DOM = tinymce.DOM, Event = tinymce.dom.Event, each = tinymce.each, extend = tinymce.extend; - // Node by index - if (t == 1 && c == s) - return n; - } + tinymce.create('tinymce.ControlManager', { + ControlManager : function(ed, s) { + var t = this, i; - return -1; + s = s || {}; + t.editor = ed; + t.controls = {}; + t.onAdd = new tinymce.util.Dispatcher(t); + t.onPostRender = new tinymce.util.Dispatcher(t); + t.prefix = s.prefix || ed.id + '_'; + t._cls = {}; + + t.onPostRender.add(function() { + each(t.controls, function(c) { + c.postRender(); + }); + }); }, - 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; + get : function(id) { + return this.controls[this.prefix + id] || this.controls[id]; + }, - // Fix for bug #1863847 - //if (e && e.keyCode == 13) - // return true; + setActive : function(id, s) { + var c = null; - // Wrap non blocks into blocks - for (i = nl.length - 1; i >= 0; i--) { - nx = nl[i]; + if (c = this.get(id)) + c.setActive(s); - // 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 { - tr = d.body.createTextRange(); - tr.moveToElementText(b); - tr.collapse(1); - bp = tr.move('character', c) * -1; + return c; + }, - tr = r.duplicate(); - tr.collapse(1); - sp = tr.move('character', c) * -1; + setDisabled : function(id, s) { + var c = null; - tr = r.duplicate(); - tr.collapse(0); - le = (tr.move('character', c) * -1) - sp; + if (c = this.get(id)) + c.setDisabled(s); - si = sp - bp; - ei = le; - } - } + return c; + }, - // 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 + add : function(c) { + var t = this; + + if (c) { + t.controls[c.id] = c; + t.onAdd.dispatch(c, t); } - // Restore selection - if (si != -2) { - if (!isIE) { - bl = b.getElementsByTagName(ed.settings.element)[0]; - r = d.createRange(); + return c; + }, - // Select last location or generated block - if (si != -1) - r.setStart(t.find(b, 1, si), so); - else - r.setStart(bl, 0); + createControl : function(n) { + var c, t = this, ed = t.editor; - // Select last location or generated block - if (ei != -1) - r.setEnd(t.find(b, 1, ei), eo); - else - r.setEnd(bl, 0); + each(ed.plugins, function(p) { + if (p.createControl) { + c = p.createControl(n, t); - 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 - } + if (c) + return false; } - } 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); + switch (n) { + case "|": + case "separator": + return t.createSeparator(); } - }, - getParentBlock : function(n) { - var d = this.dom; + if (!c && ed.buttons && (c = ed.buttons[n])) + return t.createButton(n, c); - return d.getParent(n, d.isBlock); + return t.add(c); }, - insertPara : function(e) { - var t = this, ed = t.editor, dom = ed.dom, d = ed.getDoc(), se = ed.settings, s = ed.selection.getSel(), r = s.getRangeAt(0), b = d.body; - var rb, ra, dir, sn, so, en, eo, sb, eb, bn, bef, aft, sc, ec, n, vp = dom.getViewPort(ed.getWin()), y, ch, car; - - // 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; + createDropMenu : function(id, s, cc) { + var t = this, ed = t.editor, c, bm, v, cls; - // Setup before range - rb = d.createRange(); + s = extend({ + 'class' : 'mceDropDown', + constrain : ed.settings.constrain_menus + }, s); - // 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); + s['class'] = s['class'] + ' ' + ed.getParam('skin') + 'Skin'; + if (v = ed.getParam('skin_variant')) + s['class'] += ' ' + ed.getParam('skin') + 'Skin' + v.substring(0, 1).toUpperCase() + v.substring(1); - // Setup after range - ra = d.createRange(); + id = t.prefix + id; + cls = cc || t._cls.dropmenu || tinymce.ui.DropMenu; + c = t.controls[id] = new cls(id, s); + c.onAddItem.add(function(c, o) { + var s = o.settings; - // 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); + s.title = ed.getLang(s.title, s.title); - // 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; + if (!s.onclick) { + s.onclick = function(v) { + if (s.cmd) + ed.execCommand(s.cmd, s.ui || false, s.value); + }; + } + }); - // 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 + ed.onRemove.add(function() { + c.destroy(); + }); - // Create two new block elements - if (sn.childNodes.length == 0) { - ed.dom.add(sn, se.element, null, '
A|
B
- // This logic will merge them into this:A|B
- if (e.keyCode == 46) { - if (r.collapsed) { - par = dom.getParent(sc, 'p,h1,h2,h3,h4,h5,h6,div'); + function applyRngStyle(rng) { + var newWrappers = [], wrapName, wrapElm; - if (par) { - rng = dom.createRng(); + // Setup wrapper element + wrapName = format.inline || format.block; + wrapElm = dom.create(wrapName); + setElementFormat(wrapElm); - rng.setStart(sc, r.startOffset); - rng.setEndAfter(par); + rangeUtils.walk(rng, function(nodes) { + var currentWrapElm; - // 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 - if (dom.getOuterHTML(rng.cloneContents()).replace(/<[^>]+>/g, '').length == 0) { - nextBlock = dom.getNext(par, 'p,h1,h2,h3,h4,h5,h6,div'); + function process(node) { + var nodeName = node.nodeName.toLowerCase(), parentName = node.parentNode.nodeName.toLowerCase(), found; - // Copy all children from next sibling block and remove it - if (nextBlock) { - each(nextBlock.childNodes, function(node) { - par.appendChild(node.cloneNode(true)); - }); + // Stop wrapping on br elements + if (isEq(nodeName, 'br')) { + currentWrapElm = 0; - dom.remove(nextBlock); - } + // Remove any br elements when we wrap things + if (format.block) + dom.remove(node); - // Block the default even since the Gecko team might eventually fix this - // We will remove this logic once they do we can't feature detect this one - e.preventDefault(); return; } - } - } - } - */ - // The caret sometimes gets stuck in Gecko if you delete empty paragraphs - // This workaround removes the element by hand and moves the caret to the previous element - 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)) ; + // If node is wrapper type + if (format.wrapper && matchNode(node, name, vars)) { + currentWrapElm = 0; + return; + } - 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; + // Can we rename the block + if (format.block && !format.wrapper && isTextBlock(nodeName)) { + node = dom.rename(node, wrapName); + setElementFormat(node); + newWrappers.push(node); + currentWrapElm = 0; + return; + } - // 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); + // Handle selector patterns + if (format.selector) { + // Look for matching formats + each(formatList, function(format) { + if (dom.is(node, format.selector) && !isCaretNode(node)) { + setElementFormat(node, format); + found = true; + } + }); - // Remove the target container - ed.dom.remove(sc); + // Contine processing if a selector match wasn't found and a inline element is defined + if (!format.inline || found) { + currentWrapElm = 0; + return; + } } - return Event.cancel(e); - } - } - } + // Is it valid to wrap this item + if (isValid(wrapName, nodeName) && isValid(parentName, wrapName)) { + // Start wrapping + if (!currentWrapElm) { + // Wrap the node + currentWrapElm = wrapElm.cloneNode(FALSE); + node.parentNode.insertBefore(currentWrapElm, node); + newWrappers.push(currentWrapElm); + } - // Gecko generates BR elements here and there, we don't like those so lets remove them - function handler(e) { - var pr; + currentWrapElm.appendChild(node); + } else { + // Start a new wrapper for possible children + currentWrapElm = 0; - e = e.target; + each(tinymce.grep(node.childNodes), process); - // A new BR was created in a block element, remove it - if (e && e.parentNode && e.nodeName == 'BR' && (n = t.getParentBlock(e))) { - pr = e.previousSibling; + // End the last wrapper + currentWrapElm = 0; + } + }; - Event.remove(b, 'DOMNodeInserted', handler); + // Process siblings from range + each(nodes, process); + }); - // Is there whitespace at the end of the node before then we might need the pesky BR - // to place the caret at a correct location see bug: #2013943 - if (pr && pr.nodeType == 3 && /\s+$/.test(pr.nodeValue)) - return; + // Cleanup + each(newWrappers, function(node) { + var childCount; - // Only remove BR elements that got inserted in the middle of the text - if (e.previousSibling || e.nextSibling) - ed.dom.remove(e); - } - }; + function getChildCount(node) { + var count = 0; - // Listen for new nodes - Event._add(b, 'DOMNodeInserted', handler); + each(node.childNodes, function(node) { + if (!isWhiteSpaceNode(node) && !isBookmarkNode(node)) + count++; + }); - // Remove listener - window.setTimeout(function() { - Event._remove(b, 'DOMNodeInserted', handler); - }, 1); - } - }); -})(tinymce); -(function(tinymce) { - // Shorten names - var DOM = tinymce.DOM, Event = tinymce.dom.Event, each = tinymce.each, extend = tinymce.extend; + return count; + }; - tinymce.create('tinymce.ControlManager', { - ControlManager : function(ed, s) { - var t = this, i; + function mergeStyles(node) { + var child, clone; - s = s || {}; - t.editor = ed; - t.controls = {}; - t.onAdd = new tinymce.util.Dispatcher(t); - t.onPostRender = new tinymce.util.Dispatcher(t); - t.prefix = s.prefix || ed.id + '_'; - t._cls = {}; + each(node.childNodes, function(node) { + if (node.nodeType == 1 && !isBookmarkNode(node) && !isCaretNode(node)) { + child = node; + return FALSE; // break loop + } + }); - t.onPostRender.add(function() { - each(t.controls, function(c) { - c.postRender(); - }); - }); - }, + // If child was found and of the same type as the current node + if (child && matchName(child, format)) { + clone = child.cloneNode(FALSE); + setElementFormat(clone); - get : function(id) { - return this.controls[this.prefix + id] || this.controls[id]; - }, + dom.replace(clone, node, TRUE); + dom.remove(child, 1); + } - setActive : function(id, s) { - var c = null; + return clone || node; + }; - if (c = this.get(id)) - c.setActive(s); + childCount = getChildCount(node); - return c; - }, + // Remove empty nodes + if (childCount === 0) { + dom.remove(node, 1); + return; + } - setDisabled : function(id, s) { - var c = null; + if (format.inline || format.wrapper) { + // Merges the current node with it's children of similar type to reduce the number of elements + if (!format.exact && childCount === 1) + node = mergeStyles(node); + + // Remove/merge children + each(formatList, function(format) { + // Merge all children of similar type will move styles from child to parent + // this: text + // will become: text + each(dom.select(format.inline, node), function(child) { + removeFormat(format, vars, child, format.exact ? child : null); + }); + }); - if (c = this.get(id)) - c.setDisabled(s); + // Look for parent with similar style format + dom.getParent(node.parentNode, function(parent) { + if (matchNode(parent, name, vars)) { + dom.remove(node, 1); + node = 0; + return TRUE; + } + }); - return c; - }, + // Merge next and previous siblings if they are similar texttext becomes texttext + if (node) { + node = mergeSiblings(getNonWhiteSpaceSibling(node), node); + node = mergeSiblings(node, getNonWhiteSpaceSibling(node, TRUE)); + } + } + }); + }; - add : function(c) { - var t = this; + if (format) { + if (node) { + rng = dom.createRng(); - if (c) { - t.controls[c.id] = c; - t.onAdd.dispatch(c, t); + rng.setStartBefore(node); + rng.setEndAfter(node); + + applyRngStyle(rng); + } else { + if (!selection.isCollapsed() || !format.inline) { + // Apply formatting to selection + bookmark = selection.getBookmark(); + applyRngStyle(expandRng(selection.getRng(TRUE), formatList)); + + selection.moveToBookmark(bookmark); + selection.setRng(moveStart(selection.getRng(TRUE))); + ed.nodeChanged(); + } else + performCaretAction('apply', name, vars); + } } + }; - return c; - }, + function remove(name, vars, node) { + var formatList = get(name), format = formatList[0], bookmark, i, rng; - createControl : function(n) { - var c, t = this, ed = t.editor; + // Merges the styles for each node + function process(node) { + var children, i, l; - each(ed.plugins, function(p) { - if (p.createControl) { - c = p.createControl(n, t); + // Grab the children first since the nodelist might be changed + children = tinymce.grep(node.childNodes); - if (c) - return false; + // Process current node + for (i = 0, l = formatList.length; i < l; i++) { + if (removeFormat(formatList[i], vars, node, node)) + break; } - }); - switch (n) { - case "|": - case "separator": - return t.createSeparator(); - } + // Process the children + if (format.deep) { + for (i = 0, l = children.length; i < l; i++) + process(children[i]); + } + }; - if (!c && ed.buttons && (c = ed.buttons[n])) - return t.createButton(n, c); + function findFormatRoot(container) { + var formatRoot; - return t.add(c); - }, + // Find format root + each(getParents(container.parentNode).reverse(), function(parent) { + var format; - createDropMenu : function(id, s, cc) { - var t = this, ed = t.editor, c, bm, v, cls; + // Find format root element + if (!formatRoot && parent.id != '_start' && parent.id != '_end') { + // Is the node matching the format we are looking for + format = matchNode(parent, name, vars); + if (format && format.split !== false) + formatRoot = parent; + } + }); - s = extend({ - 'class' : 'mceDropDown', - constrain : ed.settings.constrain_menus - }, s); + return formatRoot; + }; - s['class'] = s['class'] + ' ' + ed.getParam('skin') + 'Skin'; - if (v = ed.getParam('skin_variant')) - s['class'] += ' ' + ed.getParam('skin') + 'Skin' + v.substring(0, 1).toUpperCase() + v.substring(1); + function wrapAndSplit(format_root, container, target, split) { + var parent, clone, lastClone, firstClone, i, formatRootParent; - id = t.prefix + id; - cls = cc || t._cls.dropmenu || tinymce.ui.DropMenu; - c = t.controls[id] = new cls(id, s); - c.onAddItem.add(function(c, o) { - var s = o.settings; + // Format root found then clone formats and split it + if (format_root) { + formatRootParent = format_root.parentNode; - s.title = ed.getLang(s.title, s.title); + for (parent = container.parentNode; parent && parent != formatRootParent; parent = parent.parentNode) { + clone = parent.cloneNode(FALSE); - if (!s.onclick) { - s.onclick = function(v) { - ed.execCommand(s.cmd, s.ui || false, s.value); - }; - } - }); + for (i = 0; i < formatList.length; i++) { + if (removeFormat(formatList[i], vars, clone, clone)) { + clone = 0; + break; + } + } - ed.onRemove.add(function() { - c.destroy(); - }); + // Build wrapper node + if (clone) { + if (lastClone) + clone.appendChild(lastClone); - // Fix for bug #1897785, #1898007 - if (tinymce.isIE) { - c.onShowMenu.add(function() { - // IE 8 needs focus in order to store away a range with the current collapsed caret location - ed.focus(); + if (!firstClone) + firstClone = clone; - bm = ed.selection.getBookmark(1); - }); + lastClone = clone; + } + } - c.onHideMenu.add(function() { - if (bm) { - ed.selection.moveToBookmark(bm); - bm = 0; + // Never split block elements if the format is mixed + if (split && (!format.mixed || !isBlock(format_root))) + container = dom.split(format_root, container); + + // Wrap container in cloned formats + if (lastClone) { + target.parentNode.insertBefore(lastClone, target); + firstClone.appendChild(target); } - }); - } + } - return t.add(c); - }, + return container; + }; - createListBox : function(id, s, cc) { - var t = this, ed = t.editor, cmd, c, cls; + function splitToFormatRoot(container) { + return wrapAndSplit(findFormatRoot(container), container, container, true); + }; - if (t.get(id)) - return null; + function unwrap(start) { + var node = dom.get(start ? '_start' : '_end'), + out = node[start ? 'firstChild' : 'lastChild']; - s.title = ed.translate(s.title); - s.scope = s.scope || ed; + dom.remove(node, 1); - if (!s.onselect) { - s.onselect = function(v) { - ed.execCommand(s.cmd, s.ui || false, v || s.value); - }; - } + return out; + }; - s = extend({ - title : s.title, - 'class' : 'mce_' + id, - scope : s.scope, - control_manager : t - }, s); + function removeRngStyle(rng) { + var startContainer, endContainer; + + rng = expandRng(rng, formatList, TRUE); - id = t.prefix + id; + if (format.split) { + startContainer = getContainer(rng, TRUE); + endContainer = getContainer(rng); - if (ed.settings.use_native_selects) - c = new tinymce.ui.NativeListBox(id, s); - else { - cls = cc || t._cls.listbox || tinymce.ui.ListBox; - c = new cls(id, s); - } + 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'}); - t.controls[id] = c; + // Split start/end + splitToFormatRoot(startContainer); + splitToFormatRoot(endContainer); - // Fix focus problem in Safari - if (tinymce.isWebKit) { - c.onPostRender.add(function(c, n) { - // Store bookmark on mousedown - Event.add(n, 'mousedown', function() { - ed.bookmark = ed.selection.getBookmark(1); - }); + // Unwrap start/end to get real elements again + startContainer = unwrap(TRUE); + endContainer = unwrap(); + } else + startContainer = endContainer = splitToFormatRoot(startContainer); - // Restore on focus, since it might be lost - Event.add(n, 'focus', function() { - ed.selection.moveToBookmark(ed.bookmark); - ed.bookmark = null; + // Update range positions since they might have changed after the split operations + rng.startContainer = startContainer.parentNode; + rng.startOffset = nodeIndex(startContainer); + rng.endContainer = endContainer.parentNode; + rng.endOffset = nodeIndex(endContainer) + 1; + } + + // Remove items between start/end + rangeUtils.walk(rng, function(nodes) { + each(nodes, function(node) { + process(node); }); }); + }; + + // Handle node + if (node) { + rng = dom.createRng(); + rng.setStartBefore(node); + rng.setEndAfter(node); + removeRngStyle(rng); + return; } - if (c.hideMenu) - ed.onMouseDown.add(c.hideMenu, c); + if (!selection.isCollapsed() || !format.inline) { + bookmark = selection.getBookmark(); + removeRngStyle(selection.getRng(TRUE)); + selection.moveToBookmark(bookmark); + ed.nodeChanged(); + } else + performCaretAction('remove', name, vars); + }; - return t.add(c); - }, + function toggle(name, vars, node) { + if (match(name, vars, node)) + remove(name, vars, node); + else + apply(name, vars, node); + }; - createButton : function(id, s, cc) { - var t = this, ed = t.editor, o, c, cls; + function matchNode(node, name, vars) { + var formatList = get(name), format, i, classes; - if (t.get(id)) - return null; + function matchItems(node, format, item_name) { + var key, value, items = format[item_name], i; - s.title = ed.translate(s.title); - s.label = ed.translate(s.label); - s.scope = s.scope || ed; + // Check all items + if (items) { + // Non indexed object + if (items.length === undefined) { + for (key in items) { + if (items.hasOwnProperty(key)) { + if (item_name === 'attributes') + value = dom.getAttrib(node, key); + else + value = getStyle(node, key); - if (!s.onclick && !s.menu_button) { - s.onclick = function() { - ed.execCommand(s.cmd, s.ui || false, s.value); - }; - } + if (!isEq(value, replaceVars(items[key], vars))) + return; + } + } + } else { + // Only one match needed for indexed arrays + for (i = 0; i < items.length; i++) { + if (item_name === 'attributes' ? dom.getAttrib(node, items[i]) : getStyle(node, items[i])) + return format; + } + } + } - s = extend({ - title : s.title, - 'class' : 'mce_' + id, - unavailable_prefix : ed.getLang('unavailable', ''), - scope : s.scope, - control_manager : t - }, s); + return format; + }; - id = t.prefix + id; + if (formatList && node) { + // Check each format in list + for (i = 0; i < formatList.length; i++) { + format = formatList[i]; + + // Name name, attributes, styles and classes + if (matchName(node, format) && matchItems(node, format, 'attributes') && matchItems(node, format, 'styles')) { + // Match classes + if (classes = format.classes) { + for (i = 0; i < classes.length; i++) { + if (!dom.hasClass(node, classes[i])) + return; + } + } - if (s.menu_button) { - cls = cc || t._cls.menubutton || tinymce.ui.MenuButton; - c = new cls(id, s); - ed.onMouseDown.add(c.hideMenu, c); - } else { - cls = t._cls.button || tinymce.ui.Button; - c = new cls(id, s); + return format; + } + } } + }; - return t.add(c); - }, + function match(name, vars, node) { + var startNode, i; - createMenuButton : function(id, s, cc) { - s = s || {}; - s.menu_button = 1; + function matchParents(node) { + // Find first node with similar format settings + node = dom.getParent(node, function(node) { + return !!matchNode(node, name, vars); + }); - return this.createButton(id, s, cc); - }, + // Do an exact check on the similar format element + return matchNode(node, name, vars); + }; - createSplitButton : function(id, s, cc) { - var t = this, ed = t.editor, cmd, c, cls; + // Check specified node + if (node) + return matchParents(node); - if (t.get(id)) - return null; + // Check pending formats + if (selection.isCollapsed()) { + for (i = pendingFormats.apply.length - 1; i >= 0; i--) { + if (pendingFormats.apply[i].name == name) + return true; + } - s.title = ed.translate(s.title); - s.scope = s.scope || ed; + for (i = pendingFormats.remove.length - 1; i >= 0; i--) { + if (pendingFormats.remove[i].name == name) + return false; + } - if (!s.onclick) { - s.onclick = function(v) { - ed.execCommand(s.cmd, s.ui || false, v || s.value); - }; + return matchParents(selection.getNode()); } - if (!s.onselect) { - s.onselect = function(v) { - ed.execCommand(s.cmd, s.ui || false, v || s.value); - }; + // Check selected node + node = selection.getNode(); + if (matchParents(node)) + return TRUE; + + // Check start node if it's different + startNode = selection.getStart(); + if (startNode != node) { + if (matchParents(startNode)) + return TRUE; } - s = extend({ - title : s.title, - 'class' : 'mce_' + id, - scope : s.scope, - control_manager : t - }, s); + return FALSE; + }; - id = t.prefix + id; - cls = cc || t._cls.splitbutton || tinymce.ui.SplitButton; - c = t.add(new cls(id, s)); - ed.onMouseDown.add(c.hideMenu, c); + function canApply(name) { + var formatList = get(name), startNode, parents, i, x, selector; - return c; - }, + if (formatList) { + startNode = selection.getStart(); + parents = getParents(startNode); - createColorSplitButton : function(id, s, cc) { - var t = this, ed = t.editor, cmd, c, cls, bm; + for (x = formatList.length - 1; x >= 0; x--) { + selector = formatList[x].selector; - if (t.get(id)) - return null; + // Format is not selector based, then always return TRUE + if (!selector) + return TRUE; - s.title = ed.translate(s.title); - s.scope = s.scope || ed; + for (i = parents.length - 1; i >= 0; i--) { + if (dom.is(parents[i], selector)) + return TRUE; + } + } + } - if (!s.onclick) { - s.onclick = function(v) { - if (tinymce.isIE) - bm = ed.selection.getBookmark(1); + return FALSE; + }; - ed.execCommand(s.cmd, s.ui || false, v || s.value); - }; - } + // Expose to public + tinymce.extend(this, { + get : get, + register : register, + apply : apply, + remove : remove, + toggle : toggle, + match : match, + matchNode : matchNode, + canApply : canApply + }); - if (!s.onselect) { - s.onselect = function(v) { - ed.execCommand(s.cmd, s.ui || false, v || s.value); - }; - } + // Private functions - s = extend({ - title : s.title, - 'class' : 'mce_' + id, - 'menu_class' : ed.getParam('skin') + 'Skin', - scope : s.scope, - more_colors_title : ed.getLang('more_colors') - }, s); + function matchName(node, format) { + // Check for inline match + if (isEq(node, format.inline)) + return TRUE; - id = t.prefix + id; - cls = cc || t._cls.colorsplitbutton || tinymce.ui.ColorSplitButton; - c = new cls(id, s); - ed.onMouseDown.add(c.hideMenu, c); + // Check for block match + if (isEq(node, format.block)) + return TRUE; - // Remove the menu element when the editor is removed - ed.onRemove.add(function() { - c.destroy(); - }); + // Check for selector match + if (format.selector) + return dom.is(node, format.selector); + }; - // Fix for bug #1897785, #1898007 - if (tinymce.isIE) { - c.onShowMenu.add(function() { - // IE 8 needs focus in order to store away a range with the current collapsed caret location - ed.focus(); - bm = ed.selection.getBookmark(1); - }); + function isEq(str1, str2) { + str1 = str1 || ''; + str2 = str2 || ''; - c.onHideMenu.add(function() { - if (bm) { - ed.selection.moveToBookmark(bm); - bm = 0; - } - }); - } + str1 = str1.nodeName || str1; + str2 = str2.nodeName || str2; - return t.add(c); - }, + return str1.toLowerCase() == str2.toLowerCase(); + }; - createToolbar : function(id, s, cc) { - var c, t = this, cls; + function getStyle(node, name) { + var styleVal = dom.getStyle(node, name); - id = t.prefix + id; - cls = cc || t._cls.toolbar || tinymce.ui.Toolbar; - c = new cls(id, s); + // Force the format to hex + if (name == 'color' || name == 'backgroundColor') + styleVal = dom.toHex(styleVal); - if (t.get(id)) - return null; + // Opera will return bold as 700 + if (name == 'fontWeight' && styleVal == 700) + styleVal = 'bold'; - return t.add(c); - }, + return '' + styleVal; + }; - createSeparator : function(cc) { - var cls = cc || this._cls.separator || tinymce.ui.Separator; + function replaceVars(value, vars) { + if (typeof(value) != "string") + value = value(vars); + else if (vars) { + value = value.replace(/%(\w+)/g, function(str, name) { + return vars[name] || str; + }); + } - return new cls(); - }, + return value; + }; - setControlType : function(n, c) { - return this._cls[n.toLowerCase()] = c; - }, - - destroy : function() { - each(this.controls, function(c) { - c.destroy(); - }); + function isWhiteSpaceNode(node) { + return node && node.nodeType === 3 && /^\s*$/.test(node.nodeValue); + }; - this.controls = null; - } - }); -})(tinymce); -(function(tinymce) { - var Dispatcher = tinymce.util.Dispatcher, each = tinymce.each, isIE = tinymce.isIE, isOpera = tinymce.isOpera; + function wrap(node, name, attrs) { + var wrapper = dom.create(name, attrs); - tinymce.create('tinymce.WindowManager', { - WindowManager : function(ed) { - var t = this; + node.parentNode.insertBefore(wrapper, node); + wrapper.appendChild(node); - t.editor = ed; - t.onOpen = new Dispatcher(t); - t.onClose = new Dispatcher(t); - t.params = {}; - t.features = {}; - }, + return wrapper; + }; - open : function(s, p) { - var t = this, f = '', x, y, mo = t.editor.settings.dialog_type == 'modal', w, sw, sh, vp = tinymce.DOM.getViewPort(), u; + function expandRng(rng, format, remove) { + var startContainer = rng.startContainer, + startOffset = rng.startOffset, + endContainer = rng.endContainer, + endOffset = rng.endOffset, sibling, lastIdx; - // Default some options - s = s || {}; - p = p || {}; - sw = isOpera ? vp.w : screen.width; // Opera uses windows inside the Opera window - sh = isOpera ? vp.h : screen.height; - s.name = s.name || 'mc_' + new Date().getTime(); - s.width = parseInt(s.width || 320); - s.height = parseInt(s.height || 240); - s.resizable = true; - s.left = s.left || parseInt(sw / 2.0) - (s.width / 2.0); - s.top = s.top || parseInt(sh / 2.0) - (s.height / 2.0); - p.inline = false; - p.mce_width = s.width; - p.mce_height = s.height; - p.mce_auto_focus = s.auto_focus; + // 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; - if (mo) { - if (isIE) { - s.center = true; - s.help = false; - s.dialogWidth = s.width + 'px'; - s.dialogHeight = s.height + 'px'; - s.scroll = s.scrollbars || false; - } - } + root = root || dom.getRoot(); - // Build features string - each(s, function(v, k) { - if (tinymce.is(v, 'boolean')) - v = v ? 'yes' : 'no'; + for (;;) { + // Check if we can move up are we at root level or body level + parent = container.parentNode; - if (!/^(name|url)$/.test(k)) { - if (isIE && mo) - f += (f ? ';' : '') + k + ':' + v; - else - f += (f ? ',' : '') + k + '=' + v; + // Stop expanding on block elements or root depending on format + if (parent == root || (!format[0].block_expand && isBlock(parent))) + return container; + + for (sibling = parent[child_name]; sibling && sibling != container; sibling = sibling[sibling_name]) { + if (sibling.nodeType == 1 && !isBookmarkNode(sibling)) + return container; + + if (sibling.nodeType == 3 && !isWhiteSpaceNode(sibling)) + return container; + } + + container = container.parentNode; } - }); - t.features = s; - t.params = p; - t.onOpen.dispatch(t, s, p); + return container; + }; + + // If index based start position then resolve it + if (startContainer.nodeType == 1 && startContainer.hasChildNodes()) { + lastIdx = startContainer.childNodes.length - 1; + startContainer = startContainer.childNodes[startOffset > lastIdx ? lastIdx : startOffset]; - u = s.url || s.file; - u = tinymce._addVer(u); + if (startContainer.nodeType == 3) + startOffset = 0; + } - try { - if (isIE && mo) { - w = 1; - window.showModalDialog(u, window, f); - } else - w = window.open(u, s.name, f); - } catch (ex) { - // Ignore + // If index based end position then resolve it + if (endContainer.nodeType == 1 && endContainer.hasChildNodes()) { + lastIdx = endContainer.childNodes.length - 1; + endContainer = endContainer.childNodes[endOffset > lastIdx ? lastIdx : endOffset - 1]; + + if (endContainer.nodeType == 3) + endOffset = endContainer.nodeValue.length; } - if (!w) - alert(t.editor.getLang('popup_blocked')); - }, + // Exclude bookmark nodes if possible + if (isBookmarkNode(startContainer.parentNode)) + startContainer = startContainer.parentNode; - close : function(w) { - w.close(); - this.onClose.dispatch(this); - }, + if (isBookmarkNode(startContainer)) + startContainer = startContainer.nextSibling || startContainer; - createInstance : function(cl, a, b, c, d, e) { - var f = tinymce.resolve(cl); + if (isBookmarkNode(endContainer.parentNode)) + endContainer = endContainer.parentNode; - return new f(a, b, c, d, e); - }, + if (isBookmarkNode(endContainer)) + endContainer = endContainer.previousSibling || endContainer; - confirm : function(t, cb, s, w) { - w = w || window; + // 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'); + } - cb.call(s || this, w.confirm(this._decode(this.editor.getLang(t, t)))); - }, + // 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; - alert : function(tx, cb, s, w) { - var t = this; + if (container.nodeType == 3 && container.nodeValue.length == 0 && container[sibling_name]) + container = container[sibling_name]; - w = w || window; - w.alert(t._decode(t.editor.getLang(tx, tx))); + 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)) + return parents[i]; + } + } - if (cb) - cb.call(s || t); - }, + return container; + }; - // Internal functions + // Find new startContainer/endContainer if there is better one + startContainer = findSelectorEndPoint(startContainer, 'previousSibling'); + endContainer = findSelectorEndPoint(endContainer, 'nextSibling'); + } - _decode : function(s) { - return tinymce.DOM.decode(s).replace(/\\n/g, '\n'); - } - }); -}(tinymce));(function(tinymce) { - tinymce.CommandManager = function() { - var execCommands = {}, queryStateCommands = {}, queryValueCommands = {}; + // Expand start/end container to matching block element or text node + if (format[0].block || format[0].selector) { + function findBlockEndPoint(container, sibling_name, sibling_name2) { + var node; - function add(collection, cmd, func, scope) { - if (typeof(cmd) == 'string') - cmd = [cmd]; + // Expand to block of similar type + if (!format[0].wrapper) + node = dom.getParent(container, format[0].block); - tinymce.each(cmd, function(cmd) { - collection[cmd.toLowerCase()] = {func : func, scope : scope}; - }); - }; + // Expand to first wrappable block element or any block element + if (!node) + node = dom.getParent(container.nodeType == 3 ? container.parentNode : container, isBlock); - tinymce.extend(this, { - add : function(cmd, func, scope) { - add(execCommands, cmd, func, scope); - }, + // Exclude inner lists from wrapping + if (node && format[0].wrapper) + node = getParents(node, 'ul,ol').reverse()[0] || node; - addQueryStateHandler : function(cmd, func, scope) { - add(queryStateCommands, cmd, func, scope); - }, + // Didn't find a block element look for first/last wrappable element + if (!node) { + node = container; - addQueryValueHandler : function(cmd, func, scope) { - add(queryValueCommands, cmd, func, scope); - }, + while (node[sibling_name] && !isBlock(node[sibling_name])) { + node = node[sibling_name]; - 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; + // Break on BR but include it will be removed later on + // we can't remove it now since we need to check if it can be wrapped + if (isEq(node, 'br')) + break; + } + } + + return node || container; + }; + + // Find new startContainer/endContainer if there is better one + startContainer = findBlockEndPoint(startContainer, 'previousSibling'); + endContainer = findBlockEndPoint(endContainer, 'nextSibling'); + + // Non block element then try to expand up the leaf + if (format[0].block) { + if (!isBlock(startContainer)) + startContainer = findParentContainer(startContainer, 'firstChild', 'nextSibling'); + + if (!isBlock(endContainer)) + endContainer = findParentContainer(endContainer, 'lastChild', 'previousSibling'); } - }, + } - queryCommandValue : function() { - if (cmd = queryValueCommands[cmd.toLowerCase()]) - return cmd.func.call(scope || cmd.scope, ui, value, args); - }, + // Setup index for startContainer + if (startContainer.nodeType == 1) { + startOffset = nodeIndex(startContainer); + startContainer = startContainer.parentNode; + } - queryCommandState : function() { - if (cmd = queryStateCommands[cmd.toLowerCase()]) - return cmd.func.call(scope || cmd.scope, ui, value, args); + // Setup index for endContainer + if (endContainer.nodeType == 1) { + endOffset = nodeIndex(endContainer) + 1; + endContainer = endContainer.parentNode; } - }); - }; - tinymce.GlobalCommands = new tinymce.CommandManager(); -})(tinymce);(function(tinymce) { - function processRange(dom, start, end, callback) { - var ancestor, n, startPoint, endPoint, sib; + // Return new range like object + return { + startContainer : startContainer, + startOffset : startOffset, + endContainer : endContainer, + endOffset : endOffset + }; + } - function findEndPoint(n, c) { - do { - if (n.parentNode == c) - return n; + function removeFormat(format, vars, node, compare_node) { + var i, attrs, stylesModified; - n = n.parentNode; - } while(n); - }; + // Check if node matches format + if (!matchName(node, format)) + return FALSE; - function process(n) { - callback(n); - tinymce.walk(n, callback, 'childNodes'); - }; + // Should we compare with format attribs and styles + if (format.remove != 'all') { + // Remove styles + each(format.styles, function(value, name) { + value = replaceVars(value, vars); - // Find common ancestor and end points - ancestor = dom.findCommonAncestor(start, end); - startPoint = findEndPoint(start, ancestor) || start; - endPoint = findEndPoint(end, ancestor) || end; + // Indexed array + if (typeof(name) === 'number') { + name = value; + compare_node = 0; + } - // Process left leaf - for (n = start; n && n != startPoint; n = n.parentNode) { - for (sib = n.nextSibling; sib; sib = sib.nextSibling) - process(sib); - } + if (!compare_node || isEq(getStyle(compare_node, name), value)) + dom.setStyle(node, name, ''); - // Process middle from start to end point - if (startPoint != endPoint) { - for (n = startPoint.nextSibling; n && n != endPoint; n = n.nextSibling) - process(n); - } else - process(startPoint); - - // Process right leaf - for (n = end; n && n != endPoint; n = n.parentNode) { - for (sib = n.previousSibling; sib; sib = sib.previousSibling) - process(sib); - } - }; + stylesModified = 1; + }); - tinymce.GlobalCommands.add('RemoveFormat', function() { - var ed = this, dom = ed.dom, s = ed.selection, r = s.getRng(1), nodes = [], bm, start, end, sc, so, ec, eo, n; + // Remove style attribute if it's empty + if (stylesModified && dom.getAttrib(node, 'style') == '') { + node.removeAttribute('style'); + node.removeAttribute('_mce_style'); + } - function findFormatRoot(n) { - var sp; + // Remove attributes + each(format.attributes, function(value, name) { + var valueOut; - dom.getParent(n, function(n) { - if (dom.is(n, ed.getParam('removeformat_selector'))) - sp = n; + value = replaceVars(value, vars); - return dom.isBlock(n); - }, ed.getBody()); + // Indexed array + if (typeof(name) === 'number') { + name = value; + compare_node = 0; + } - return sp; - }; + if (!compare_node || isEq(dom.getAttrib(compare_node, name), value)) { + // Keep internal classes + if (name == 'class') { + value = dom.getAttrib(node, name); + if (value) { + // Build new class value where everything is removed except the internal prefixed classes + valueOut = ''; + each(value.split(/\s+/), function(cls) { + if (/mce\w+/.test(cls)) + valueOut += (valueOut ? ' ' : '') + cls; + }); - function collect(n) { - if (dom.is(n, ed.getParam('removeformat_selector'))) - nodes.push(n); - }; + // We got some internal classes left + if (valueOut) { + dom.setAttrib(node, name, valueOut); + return; + } + } + } - function walk(n) { - collect(n); - tinymce.walk(n, collect, 'childNodes'); - }; + // IE6 has a bug where the attribute doesn't get removed correctly + if (name == "class") + node.removeAttribute('className'); - bm = s.getBookmark(); - sc = r.startContainer; - ec = r.endContainer; - so = r.startOffset; - eo = r.endOffset; - sc = sc.nodeType == 1 ? sc.childNodes[Math.min(so, sc.childNodes.length - 1)] : sc; - ec = ec.nodeType == 1 ? ec.childNodes[Math.min(so == eo ? eo : eo - 1, ec.childNodes.length - 1)] : ec; + // Remove mce prefixed attributes + if (MCE_ATTR_RE.test(name)) + node.removeAttribute('_mce_' + name); - // Same container - if (sc == ec) { // TEXT_NODE - start = findFormatRoot(sc); + node.removeAttribute(name); + } + }); - // Handle single text node - if (sc.nodeType == 3) { - if (start && start.nodeType == 1) { // ELEMENT - n = sc.splitText(so); - n.splitText(eo - so); - dom.split(start, n); + // Remove classes + each(format.classes, function(value) { + value = replaceVars(value, vars); + + if (!compare_node || dom.hasClass(compare_node, value)) + dom.removeClass(node, value); + }); - s.moveToBookmark(bm); + // Check for non internal attributes + attrs = dom.getAttribs(node); + for (i = 0; i < attrs.length; i++) { + if (attrs[i].nodeName.indexOf('_') !== 0) + return FALSE; } + } - return; + // Remove the inline child if it's empty for example or + if (format.remove != 'none') { + removeNode(node, format); + return TRUE; } + }; - // Handle single element - walk(dom.split(start, sc) || sc); - } else { - // Find start/end format root - start = findFormatRoot(sc); - end = findFormatRoot(ec); + function removeNode(node, format) { + var parentNode = node.parentNode, rootBlockElm; + + if (format.block) { + if (!forcedRootBlock) { + function find(node, next, inc) { + node = getNonWhiteSpaceSibling(node, next, inc); - // Split start text node - if (start) { - if (sc.nodeType == 3) { // TEXT - // Since IE doesn't support white space nodes in the DOM we need to - // add this invisible character so that the splitText function can split the contents - if (so == sc.nodeValue.length) - sc.nodeValue += '\uFEFF'; // Yet another pesky IE fix + return !node || (node.nodeName == 'BR' || isBlock(node)); + }; + + // Append BR elements if needed before we remove the block + if (isBlock(node) && !isBlock(parentNode)) { + if (!find(node, FALSE) && !find(node.firstChild, TRUE, 1)) + node.insertBefore(dom.create('br'), node.firstChild); - sc = sc.splitText(so); + if (!find(node, TRUE) && !find(node.lastChild, FALSE, 1)) + node.appendChild(dom.create('br')); + } + } else { + // Wrap the block in a forcedRootBlock if we are at the root of document + if (parentNode == dom.getRoot()) { + if (!format.list_block || !isEq(node, format.list_block)) { + each(tinymce.grep(node.childNodes), function(node) { + if (isValid(forcedRootBlock, node.nodeName.toLowerCase())) { + if (!rootBlockElm) + rootBlockElm = wrap(node, forcedRootBlock); + else + rootBlockElm.appendChild(node); + } else + rootBlockElm = 0; + }); + } + } } } - // Split end text node - if (end) { - if (ec.nodeType == 3) // TEXT - ec.splitText(eo); - } + // Never remove nodes that isn't the specified inline element if a selector is specified too + if (format.selector && format.inline && !isEq(format.inline, node)) + return; - // If the start and end format root is the same then we need to wrap - // the end node in a span since the split calls might change the reference - // Example:x[yz---12]3
- if (start && start == end) - dom.replace(dom.create('span', {id : '__end'}, ec.cloneNode(true)), ec); + dom.remove(node, 1); + }; - // Split all start containers down to the format root - if (start) - start = dom.split(start, sc); - else - start = sc; + function getNonWhiteSpaceSibling(node, next, inc) { + if (node) { + next = next ? 'nextSibling' : 'previousSibling'; - // If there is a span wrapper use that one instead - if (n = dom.get('__end')) { - ec = n; - end = findFormatRoot(ec); + for (node = inc ? node : node[next]; node; node = node[next]) { + if (node.nodeType == 1 || !isWhiteSpaceNode(node)) + return node; + } } + }; - // Split all end containers down to the format root - if (end) - end = dom.split(end, ec); - else - end = ec; + function isBookmarkNode(node) { + return node && node.nodeType == 1 && node.getAttribute('_mce_type') == 'bookmark'; + }; - // Collect nodes in between - processRange(dom, start, end, collect); + function mergeSiblings(prev, next) { + var marker, sibling, tmpSibling; - // Remove invisible character for IE workaround if we find it - if (sc.nodeValue == '\uFEFF') - sc.nodeValue = ''; + function compareElements(node1, node2) { + // Not the same name + if (node1.nodeName != node2.nodeName) + return FALSE; - // Process start/end container elements - walk(ec); - walk(sc); - } + function getAttribs(node) { + var attribs = {}; - // Remove all collected nodes - tinymce.each(nodes, function(n) { - dom.remove(n, 1); - }); + each(dom.getAttribs(node), function(attr) { + var name = attr.nodeName.toLowerCase(); - // Remove leftover wrapper - dom.remove('__end', 1); + // Don't compare internal attributes or style + if (name.indexOf('_') !== 0 && name !== 'style') + attribs[name] = dom.getAttrib(node, name); + }); - s.moveToBookmark(bm); - }); -})(tinymce); -(function(tinymce) { - tinymce.GlobalCommands.add('mceBlockQuote', function() { - var ed = this, s = ed.selection, dom = ed.dom, sb, eb, n, bm, bq, r, bq2, i, nl; + return attribs; + }; - function getBQ(e) { - return dom.getParent(e, function(n) {return n.nodeName === 'BLOCKQUOTE';}); - }; + function compareObjects(obj1, obj2) { + var value, name; - // Get start/end block - sb = dom.getParent(s.getStart(), dom.isBlock); - eb = dom.getParent(s.getEnd(), dom.isBlock); + for (name in obj1) { + // Obj1 has item obj2 doesn't have + if (obj1.hasOwnProperty(name)) { + value = obj2[name]; - // Remove blockquote(s) - if (bq = getBQ(sb)) { - if (sb != eb || sb.childNodes.length > 1 || (sb.childNodes.length == 1 && sb.firstChild.nodeName != 'BR')) - bm = s.getBookmark(); + // Obj2 doesn't have obj1 item + if (value === undefined) + return FALSE; - // Move all elements after the end block into new bq - if (getBQ(eb)) { - bq2 = bq.cloneNode(false); + // Obj2 item has a different value + if (obj1[name] != value) + return FALSE; - while (n = eb.nextSibling) - bq2.appendChild(n.parentNode.removeChild(n)); - } + // Delete similar value + delete obj2[name]; + } + } - // Add new bq after - if (bq2) - dom.insertAfter(bq2, bq); + // 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; + } - // Move all selected blocks after the current bq - nl = s.getSelectedBlocks(sb, eb); - for (i = nl.length - 1; i >= 0; i--) { - dom.insertAfter(nl[i], bq); - } + return TRUE; + }; - // Empty bq, then remove it - if (/^\s*$/.test(bq.innerHTML)) - dom.remove(bq, 1); // Keep children so boomark restoration works correctly + // Attribs are not the same + if (!compareObjects(getAttribs(node1), getAttribs(node2))) + return FALSE; - // Empty bq, then remote it - if (bq2 && /^\s*$/.test(bq2.innerHTML)) - dom.remove(bq2, 1); // Keep children so boomark restoration works correctly + // Styles are not the same + if (!compareObjects(dom.parseStyle(dom.getAttrib(node1, 'style')), dom.parseStyle(dom.getAttrib(node2, 'style')))) + return FALSE; - if (!bm) { - // Move caret inside empty block element - if (!tinymce.isIE) { - r = ed.getDoc().createRange(); - r.setStart(sb, 0); - r.setEnd(sb, 0); - s.setRng(r); - } else { - s.select(sb); - s.collapse(0); + return TRUE; + }; - // IE misses the empty block some times element so we must move back the caret - if (dom.getParent(s.getStart(), dom.isBlock) != sb) { - r = s.getRng(); - r.move('character', -1); - r.select(); + // 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; + + if (sibling.nodeType == 1 && !isBookmarkNode(sibling)) + return sibling; } - } - } else - ed.selection.moveToBookmark(bm); - return; - } + return node; + }; - // Since IE can start with a totally empty document we need to add the first bq and paragraph - if (tinymce.isIE && !sb && !eb) { - ed.getDoc().execCommand('Indent'); - n = getBQ(s.getNode()); - n.style.margin = n.dir = ''; // IE adds margin and dir to bq - return; - } + // If previous sibling is empty then jump over it + prev = findElementSibling(prev, 'previousSibling'); + next = findElementSibling(next, 'nextSibling'); + + // Compare next and previous nodes + if (compareElements(prev, next)) { + // Append nodes between + for (sibling = prev.nextSibling; sibling && sibling != next;) { + tmpSibling = sibling; + sibling = sibling.nextSibling; + prev.appendChild(tmpSibling); + } - if (!sb || !eb) - return; + // Remove next node + dom.remove(next); - // If empty paragraph node then do not use bookmark - if (sb != eb || sb.childNodes.length > 1 || (sb.childNodes.length == 1 && sb.firstChild.nodeName != 'BR')) - bm = s.getBookmark(); + // Move children into prev node + each(tinymce.grep(next.childNodes), function(node) { + prev.appendChild(node); + }); - // Move selected block elements into a bq - tinymce.each(s.getSelectedBlocks(getBQ(s.getStart()), getBQ(s.getEnd())), function(e) { - // Found existing BQ add to this one - if (e.nodeName == 'BLOCKQUOTE' && !bq) { - bq = e; - return; + return prev; + } } - // No BQ found, create one - if (!bq) { - bq = dom.create('blockquote'); - e.parentNode.insertBefore(bq, e); + return next; + }; + + function isTextBlock(name) { + return /^(h[1-6]|p|div|pre|address)$/.test(name); + }; + + function getContainer(rng, start) { + var container, offset, lastIdx; + + 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]; } - // Add children from existing BQ - if (e.nodeName == 'BLOCKQUOTE' && bq) { - n = e.firstChild; + return container; + }; - while (n) { - bq.appendChild(n.cloneNode(true)); - n = n.nextSibling; - } + function performCaretAction(type, name, vars) { + var i, currentPendingFormats = pendingFormats[type], + otherPendingFormats = pendingFormats[type == 'apply' ? 'remove' : 'apply']; - dom.remove(e); - return; + function hasPending() { + return pendingFormats.apply.length || pendingFormats.remove.length; + }; + + function resetPending() { + pendingFormats.apply = []; + pendingFormats.remove = []; + }; + + function perform(caret_node) { + // Apply pending formats + each(pendingFormats.apply.reverse(), function(item) { + apply(item.name, item.vars, caret_node); + }); + + // Remove pending formats + each(pendingFormats.remove.reverse(), function(item) { + remove(item.name, item.vars, caret_node); + }); + + dom.remove(caret_node, 1); + resetPending(); + }; + + // Check if it already exists then ignore it + for (i = currentPendingFormats.length - 1; i >= 0; i--) { + if (currentPendingFormats[i].name == name) + return; } - // Add non BQ element to BQ - bq.appendChild(dom.remove(e)); - }); + currentPendingFormats.push({name : name, vars : vars}); - if (!bm) { - // Move caret inside empty block element - if (!tinymce.isIE) { - r = ed.getDoc().createRange(); - r.setStart(sb, 0); - r.setEnd(sb, 0); - s.setRng(r); - } else { - s.select(sb); - s.collapse(1); + // 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); } - } else - s.moveToBookmark(bm); - }); -})(tinymce); -(function(tinymce) { - tinymce.each(['Cut', 'Copy', 'Paste'], function(cmd) { - tinymce.GlobalCommands.add(cmd, function() { - var ed = this, doc = ed.getDoc(); - try { - doc.execCommand(cmd, false, null); + // Pending apply or remove formats + if (hasPending()) { + ed.getDoc().execCommand('FontName', false, 'mceinline'); - // On WebKit the command will just be ignored if it's not enabled - if (!doc.queryCommandEnabled(cmd)) - throw 'Error'; - } catch (ex) { - if (tinymce.isGecko) { - ed.windowManager.confirm(ed.getLang('clipboard_msg'), function(s) { - if (s) - open('http://www.mozilla.org/editor/midasdemo/securityprefs.html', '_blank'); + // IE will convert the current word + each(dom.select('font,span'), function(node) { + var bookmark; + + if (isCaretNode(node)) { + bookmark = selection.getBookmark(); + perform(node); + selection.moveToBookmark(bookmark); + ed.nodeChanged(); + } + }); + + // Only register listeners once if we need to + if (!pendingFormats.isListening && hasPending()) { + pendingFormats.isListening = true; + + each('onKeyDown,onKeyUp,onKeyPress,onMouseUp'.split(','), function(event) { + ed[event].addToTop(function(ed, e) { + if (hasPending()) { + each(dom.select('font,span'), function(node) { + var bookmark, textNode, rng; + + // Look for marker + if (isCaretNode(node)) { + textNode = node.firstChild; + + perform(node); + + rng = dom.createRng(); + rng.setStart(textNode, textNode.nodeValue.length); + rng.setEnd(textNode, textNode.nodeValue.length); + selection.setRng(rng); + ed.nodeChanged(); + } + }); + + // Always unbind and clear pending styles on keyup + if (e.type == 'keyup' || e.type == 'mouseup') + resetPending(); + } + }); }); - } else - ed.windowManager.alert(ed.getLang('clipboard_no_support')); + } } - }); - }); + }; + }; })(tinymce); -(function(tinymce) { - tinymce.GlobalCommands.add('InsertHorizontalRule', function() { - if (tinymce.isOpera) - return this.getDoc().execCommand('InsertHorizontalRule', false, ''); - this.selection.setContent('