From 2be122cb5032567e88bb926b8aac250fd488cf68 Mon Sep 17 00:00:00 2001 From: Brett Profitt Date: Tue, 4 Oct 2011 20:59:33 -0700 Subject: Refs #3853. Upgraded TinyMCE to 3.4.6. Embed still inserts the content in the wrong place for IE 8. --- .../tinymce/jscripts/tiny_mce/tiny_mce_src.js | 2253 +++++++++++++------- 1 file changed, 1460 insertions(+), 793 deletions(-) (limited to 'mod/tinymce/vendor/tinymce/jscripts/tiny_mce/tiny_mce_src.js') diff --git a/mod/tinymce/vendor/tinymce/jscripts/tiny_mce/tiny_mce_src.js b/mod/tinymce/vendor/tinymce/jscripts/tiny_mce/tiny_mce_src.js index c1d47fcb7..358de47f2 100644 --- a/mod/tinymce/vendor/tinymce/jscripts/tiny_mce/tiny_mce_src.js +++ b/mod/tinymce/vendor/tinymce/jscripts/tiny_mce/tiny_mce_src.js @@ -5,9 +5,9 @@ var tinymce = { majorVersion : '3', - minorVersion : '4.2', + minorVersion : '4.6', - releaseDate : '2011-04-07', + releaseDate : '2011-09-29', _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) { @@ -484,6 +492,7 @@ })(window); + tinymce.create('tinymce.util.Dispatcher', { scope : null, listeners : null, @@ -542,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); @@ -550,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; } @@ -561,8 +571,10 @@ 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 @@ -1024,12 +1036,155 @@ tinymce.create('static tinymce.util.XHR', { } }); }()); +(function(tinymce){ + tinymce.VK = { + DELETE:46, + BACKSPACE:8 + + } + +})(tinymce); + +(function(tinymce) { + var VK = tinymce.VK, BACKSPACE = VK.BACKSPACE, DELETE = VK.DELETE; + + function cleanupStylesWhenDeleting(ed) { + var dom = ed.dom, selection = ed.selection; + + ed.onKeyDown.add(function(ed, e) { + var rng, blockElm, node, clonedSpan, isDelete; + + isDelete = e.keyCode == DELETE; + if (isDelete || e.keyCode == BACKSPACE) { + e.preventDefault(); + rng = selection.getRng(); + + // Find root block + blockElm = dom.getParent(rng.startContainer, dom.isBlock); + + // On delete clone the root span of the next block element + if (isDelete) + blockElm = dom.getNext(blockElm, dom.isBlock); + + // Locate root span element and clone it since it would otherwise get merged by the "apple-style-span" on delete/backspace + if (blockElm) { + node = blockElm.firstChild; + + // Ignore empty text nodes + while (node.nodeType == 3 && node.nodeValue.length == 0) + node = node.nextSibling; + + if (node && node.nodeName === 'SPAN') { + clonedSpan = node.cloneNode(false); + } + } + + // Do the backspace/delete actiopn + 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(); + + if (clonedSpan) { + dom.replace(clonedSpan.cloneNode(false), span, true); + } else { + dom.remove(span, true); + } + + // Restore the selection + selection.moveToBookmark(bm); + }); + } + }); + }; + + function emptyEditorWhenDeleting(ed) { + ed.onKeyUp.add(function(ed, e) { + var keyCode = e.keyCode; + + if (keyCode == DELETE || keyCode == BACKSPACE) { + if (ed.dom.isEmpty(ed.getBody())) { + ed.setContent('', {format : 'raw'}); + ed.nodeChanged(); + return; + } + } + }); + }; + + function inputMethodFocus(ed) { + ed.dom.bind(ed.getDoc(), 'focusin', function() { + ed.selection.setRng(ed.selection.getRng()); + }); + }; + + 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); + } + }); + } + }; + + function selectControlElements(ed) { + ed.onClick.add(function(ed, e) { + e = e.target; + + // Workaround for bug, http://bugs.webkit.org/show_bug.cgi?id=12250 + // WebKit can't even do simple things like selecting an image + // Needs tobe the setBaseAndExtend or it will fail to select floated images + if (/^(IMG|HR)$/.test(e.nodeName)) + ed.selection.getSel().setBaseAndExtent(e, 0, e, 1); + + if (e.nodeName == 'A' && ed.dom.hasClass(e, 'mceItemAnchor')) + ed.selection.select(e); + + ed.nodeChanged(); + }); + }; + + tinymce.create('tinymce.util.Quirks', { + Quirks: function(ed) { + // WebKit + if (tinymce.isWebKit) { + cleanupStylesWhenDeleting(ed); + emptyEditorWhenDeleting(ed); + inputMethodFocus(ed); + selectControlElements(ed); + } + + // IE + if (tinymce.isIE) { + emptyEditorWhenDeleting(ed); + } + + // Gecko + if (tinymce.isGecko) { + focusBody(ed); + } + } + }); +})(tinymce); (function(tinymce) { var namedEntities, baseEntities, reverseEntities, - attrsCharsRegExp = /[&\"\u007E-\uD7FF]|[\uD800-\uDBFF][\uDC00-\uDFFF]/g, + attrsCharsRegExp = /[&<>\"\u007E-\uD7FF]|[\uD800-\uDBFF][\uDC00-\uDFFF]/g, textCharsRegExp = /[<>&\u007E-\uD7FF]|[\uD800-\uDBFF][\uDC00-\uDFFF]/g, rawCharsRegExp = /[<>&\"\']/g, - entityRegExp = /&(#)?([\w]+);/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", @@ -1040,7 +1195,7 @@ tinymce.create('static tinymce.util.XHR', { // Raw entities baseEntities = { - '"' : '"', + '\"' : '"', // Needs to be escaped since the YUI compressor would otherwise break the code "'" : ''', '<' : '<', '>' : '>', @@ -1194,7 +1349,7 @@ tinymce.create('static tinymce.util.XHR', { decode : function(text) { return text.replace(entityRegExp, function(all, numeric, value) { if (numeric) { - value = parseInt(value); + value = parseInt(value, numeric.length === 2 ? 16 : 10); // Support upper UTF if (value > 0xFFFF) { @@ -1221,10 +1376,10 @@ tinymce.html.Styles = function(settings, schema) { settings = settings || {}; - encodingItems = '\\" \\\' \\; \\: ; : _'.split(' '); + encodingItems = '\\" \\\' \\; \\: ; : \uFEFF'.split(' '); for (i = 0; i < encodingItems.length; i++) { - encodingLookup[encodingItems[i]] = '_' + i; - encodingLookup['_' + i] = encodingItems[i]; + encodingLookup[encodingItems[i]] = '\uFEFF' + i; + encodingLookup['\uFEFF' + i] = encodingItems[i]; } function toHex(match, r, g, b) { @@ -1319,7 +1474,7 @@ tinymce.html.Styles = function(settings, schema) { // It will also decode the \" \' if keep_slashes is set to fale or omitted function decode(str, keep_slashes) { if (isEncoded) { - str = str.replace(/_[0-9]/g, function(str) { + str = str.replace(/\uFEFF[0-9]/g, function(str) { return encodingLookup[str]; }); } @@ -1332,7 +1487,7 @@ tinymce.html.Styles = function(settings, schema) { if (css) { // Encode \" \' % and ; and : inside strings so they don't interfere with the style parsing - css = css.replace(/\\[\"\';:_]/g, encode).replace(/\"[^\"]+\"|\'[^\']+\'/g, function(str) { + css = css.replace(/\\[\"\';:\uFEFF]/g, encode).replace(/\"[^\"]+\"|\'[^\']+\'/g, function(str) { return str.replace(/[;:]/g, encode); }); @@ -1399,7 +1554,7 @@ tinymce.html.Styles = function(settings, schema) { var css = '', name, value; function serializeStyles(name) { - var styleList, i, l, name, value; + var styleList, i, l, value; styleList = schema.styles[name]; if (styleList) { @@ -1417,7 +1572,7 @@ tinymce.html.Styles = function(settings, schema) { if (element_name && schema && schema.styles) { // Serialize global styles and element specific styles serializeStyles('*'); - serializeStyles(name); + serializeStyles(element_name); } else { // Output the styles in the order they are inside the object for (name in styles) { @@ -1434,8 +1589,8 @@ tinymce.html.Styles = function(settings, schema) { }; (function(tinymce) { - var transitional = {}, boolAttrMap, blockElementsMap, shortEndedElementsMap, nonEmptyElementsMap, - whiteSpaceElementsMap, selfClosingElementsMap, makeMap = tinymce.makeMap, each = tinymce.each; + var transitional = {}, boolAttrMap, blockElementsMap, shortEndedElementsMap, nonEmptyElementsMap, customElementsMap = {}, + defaultWhiteSpaceElementsMap, selfClosingElementsMap, makeMap = tinymce.makeMap, each = tinymce.each; function split(str, delim) { return str.split(delim || ','); @@ -1596,14 +1751,14 @@ tinymce.html.Styles = function(settings, schema) { 'body[E|onload|onunload|background|bgcolor|text|link|vlink|alink][#|Y]' ); - boolAttrMap = makeMap('checked,compact,declare,defer,disabled,ismap,multiple,nohref,noresize,noshade,nowrap,readonly,selected,preload,autoplay,loop,controls'); + boolAttrMap = makeMap('checked,compact,declare,defer,disabled,ismap,multiple,nohref,noresize,noshade,nowrap,readonly,selected,autoplay,loop,controls'); shortEndedElementsMap = makeMap('area,base,basefont,br,col,frame,hr,img,input,isindex,link,meta,param,embed,source'); - nonEmptyElementsMap = tinymce.extend(makeMap('td,th,iframe,video,object'), shortEndedElementsMap); - whiteSpaceElementsMap = makeMap('pre,script,style'); + nonEmptyElementsMap = tinymce.extend(makeMap('td,th,iframe,video,audio,object'), shortEndedElementsMap); + defaultWhiteSpaceElementsMap = makeMap('pre,script,style,textarea'); selfClosingElementsMap = makeMap('colgroup,dd,dt,li,options,p,td,tfoot,th,thead,tr'); tinymce.html.Schema = function(settings) { - var self = this, elements = {}, children = {}, patternElements = [], validStyles; + var self = this, elements = {}, children = {}, patternElements = [], validStyles, whiteSpaceElementsMap; settings = settings || {}; @@ -1621,6 +1776,8 @@ tinymce.html.Styles = function(settings, schema) { }); } + whiteSpaceElementsMap = settings.whitespace_elements ? makeMap(settings.whitespace_elements) : defaultWhiteSpaceElementsMap; + // Converts a wildcard expression string to a regexp for example *a will become /.*a/. function patternToRegExp(str) { return new RegExp('^' + str.replace(/([?+*])/g, '.$1') + '$'); @@ -1785,10 +1942,16 @@ tinymce.html.Styles = function(settings, schema) { if (custom_elements) { each(split(custom_elements), function(rule) { var matches = customElementRegExp.exec(rule), - cloneName = matches[1] === '~' ? 'span' : 'div', + inline = matches[1] === '~', + cloneName = inline ? 'span' : 'div', name = matches[2]; children[name] = children[cloneName]; + customElementsMap[name] = cloneName; + + // If it's not marked as inline then add it to valid block elements + if (!inline) + blockElementsMap[name] = {}; // Add custom elements at span/div positions each(children, function(element, child) { @@ -1827,7 +1990,24 @@ tinymce.html.Styles = function(settings, schema) { } }); } - } + }; + + function getElementRule(name) { + var element = elements[name], i; + + // Exact match found + if (element) + return element; + + // No exact match then try the patterns + i = patternElements.length; + while (i--) { + element = patternElements[i]; + + if (element.pattern.test(name)) + return element; + } + }; if (!settings.valid_elements) { // No valid elements defined then clone the elements from the transitional spec @@ -1850,7 +2030,7 @@ tinymce.html.Styles = function(settings, schema) { elements.img.attributesDefault = [{name: 'alt', value: ''}]; // Remove these if they are empty by default - each(split('ol,ul,li,sub,sup,blockquote,tr,div,span,font,a,table,tbody'), function(name) { + each(split('ol,ul,sub,sup,blockquote,span,font,a,table,tbody,tr'), function(name) { elements[name].removeEmpty = true; }); @@ -1868,6 +2048,10 @@ tinymce.html.Styles = function(settings, schema) { // Todo: Remove this when we fix list handling to be valid addValidChildren('+ol[ul|ol],+ul[ul|ol]'); + // If the user didn't allow span only allow internal spans + if (!getElementRule('span')) + addValidElements('span[!data-mce-type|*]'); + // Delete invalid elements if (settings.invalid_elements) { tinymce.each(tinymce.explode(settings.invalid_elements), function(item) { @@ -1910,21 +2094,10 @@ tinymce.html.Styles = function(settings, schema) { return !!(parent && parent[child]); }; - self.getElementRule = function(name) { - var element = elements[name], i; - - // Exact match found - if (element) - return element; - - // No exact match then try the patterns - i = patternElements.length; - while (i--) { - element = patternElements[i]; + self.getElementRule = getElementRule; - if (element.pattern.test(name)) - return element; - } + self.getCustomElements = function() { + return customElementsMap; }; self.addValidElements = addValidElements; @@ -1958,10 +2131,10 @@ tinymce.html.Styles = function(settings, schema) { }); self.parse = function(html) { - var self = this, matches, index = 0, value, endRegExp, stack = [], attrList, i, text, name, - shortEndedElements, fillAttrsMap, isShortEnded, validate, elementRule, isValidElement, attr, attribsValue, + 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; + tokenRegExp, attrRegExp, specialElements, attrValue, idCount = 0, decode = tinymce.html.Entities.decode, fixSelfClosing, isIE; function processEndTag(name) { var pos, i; @@ -2010,7 +2183,10 @@ tinymce.html.Styles = function(settings, schema) { 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 @@ -2018,9 +2194,20 @@ tinymce.html.Styles = function(settings, schema) { self.text(decode(html.substr(index, matches.index - index))); if (value = matches[6]) { // End element - processEndTag(value.toLowerCase()); + 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
  • @@ -2039,6 +2226,12 @@ tinymce.html.Styles = function(settings, schema) { // Parse attributes if (attribsValue = matches[8]) { + isInternalElement = attribsValue.indexOf('data-mce-type') !== -1; // Check if the element is an internal element + + // If the element has internal attributes then remove it if we are told to do so + if (isInternalElement && removeInternalElements) + isValidElement = false; + attrList = []; attrList.map = {}; @@ -2049,7 +2242,7 @@ tinymce.html.Styles = function(settings, schema) { value = name in fillAttrsMap ? name : decode(value || val2 || val3 || ''); // Handle boolean attribute than value attribute // Validate name and value - if (validate && name.indexOf('data-') !== 0) { + if (validate && !isInternalElement && name.indexOf('data-') !== 0) { attrRule = validAttributesMap[name]; // Find rule by pattern matching @@ -2088,7 +2281,7 @@ tinymce.html.Styles = function(settings, schema) { } // Process attributes if validation is enabled - if (validate) { + if (validate && !isInternalElement) { attributesRequired = elementRule.attributesRequired; attributesDefault = elementRule.attributesDefault; attributesForced = elementRule.attributesForced; @@ -2512,6 +2705,10 @@ tinymce.html.Styles = function(settings, schema) { } return true; + }, + + walk : function(prev) { + return walk(this, null, prev); } }); @@ -2695,7 +2892,7 @@ tinymce.html.Styles = function(settings, schema) { self.parse = function(html, args) { var parser, rootNode, node, nodes, i, l, fi, fl, list, name, validate, blockElements, startWhiteSpaceRegExp, invalidChildren = [], - endWhiteSpaceRegExp, allWhiteSpaceRegExp, whiteSpaceElements, children, nonEmptyElements; + endWhiteSpaceRegExp, allWhiteSpaceRegExp, whiteSpaceElements, children, nonEmptyElements, rootBlockName; args = args || {}; matchedNodes = {}; @@ -2704,12 +2901,35 @@ tinymce.html.Styles = function(settings, schema) { 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; + } + + node = next; + }; + }; + function createNode(name, type) { var node = new Node(name, type), list; @@ -2897,35 +3117,28 @@ tinymce.html.Styles = function(settings, schema) { } }, schema); - rootNode = node = new Node(settings.root_name, 11); + rootNode = node = new Node(args.context || settings.root_name, 11); parser.parse(html); - if (validate) - fixInvalidChildren(invalidChildren); - - // Run node filters - for (name in matchedNodes) { - list = nodeFilters[name]; - nodes = matchedNodes[name]; - - // Remove already removed children - fi = nodes.length; - while (fi--) { - if (!nodes[fi].parent) - nodes.splice(fi, 1); - } - - for (i = 0, l = list.length; i < l; i++) - list[i](nodes, name, args); + // Fix invalid children or report invalid children in a contextual parsing + if (validate && invalidChildren.length) { + if (!args.context) + fixInvalidChildren(invalidChildren); + else + args.invalid = true; } - // Run attribute filters - for (i = 0, l = attributeFilters.length; i < l; i++) { - list = attributeFilters[i]; + // Wrap nodes in the root into block elements if the root is body + if (rootBlockName && rootNode.name == 'body') + addRootBlocks(); - if (list.name in matchedAttributes) { - nodes = matchedAttributes[list.name]; + // Run filters only when the contents is valid + if (!args.invalid) { + // Run node filters + for (name in matchedNodes) { + list = nodeFilters[name]; + nodes = matchedNodes[name]; // Remove already removed children fi = nodes.length; @@ -2934,8 +3147,27 @@ tinymce.html.Styles = function(settings, schema) { nodes.splice(fi, 1); } - for (fi = 0, fl = list.callbacks.length; fi < fl; fi++) - list.callbacks[fi](nodes, list.name, args); + for (i = 0, l = list.length; i < l; i++) + list[i](nodes, name, args); + } + + // Run attribute filters + for (i = 0, l = attributeFilters.length; i < l; i++) { + list = attributeFilters[i]; + + if (list.name in matchedAttributes) { + nodes = matchedAttributes[list.name]; + + // Remove already removed children + fi = nodes.length; + while (fi--) { + if (!nodes[fi].parent) + nodes.splice(fi, 1); + } + + for (fi = 0, fl = list.callbacks.length; fi < fl; fi++) + list.callbacks[fi](nodes, list.name, args); + } } } @@ -2950,6 +3182,9 @@ tinymce.html.Styles = function(settings, schema) { 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]; @@ -2986,11 +3221,13 @@ tinymce.html.Styles = function(settings, schema) { elementRule = schema.getElementRule(parent.name); // Remove or padd the element depending on schema rule - if (elementRule.removeEmpty) - parent.remove(); - else if (elementRule.paddEmpty) - parent.empty().append(new tinymce.html.Node('#text', 3)).value = '\u00a0'; - } + if (elementRule) { + if (elementRule.removeEmpty) + parent.remove(); + else if (elementRule.paddEmpty) + parent.empty().append(new tinymce.html.Node('#text', 3)).value = '\u00a0'; + } + } } } } @@ -3247,7 +3484,7 @@ tinymce.html.Writer = function(settings) { }, DOMUtils : function(d, s) { - var t = this, globalStyle; + var t = this, globalStyle, name; t.doc = d; t.win = window; @@ -3278,7 +3515,7 @@ tinymce.html.Writer = function(settings) { } } - if (isIE) { + if (isIE && s.schema) { // Add missing HTML 4/5 elements to IE ('abbr article aside audio canvas ' + 'details figcaption figure footer ' + @@ -3287,6 +3524,11 @@ tinymce.html.Writer = function(settings) { 'time video').replace(/\w+/g, function(name) { d.createElement(name); }); + + // Create all custom elements + for (name in s.schema.getCustomElements()) { + d.createElement(name); + } } tinymce.addUnload(t.destroy, t); @@ -3691,12 +3933,12 @@ tinymce.html.Writer = function(settings) { }, getAttrib : function(e, n, dv) { - var v, t = this; + var v, t = this, undef; e = t.get(e); if (!e || e.nodeType !== 1) - return false; + return dv === undef ? false : dv; if (!is(dv)) dv = ''; @@ -3808,7 +4050,7 @@ tinymce.html.Writer = function(settings) { } } - return (v !== undefined && v !== null && v !== '') ? '' + v : dv; + return (v !== undef && v !== null && v !== '') ? '' + v : dv; }, getPos : function(n, ro) { @@ -3818,14 +4060,17 @@ tinymce.html.Writer = function(settings) { ro = ro || d.body; if (n) { - // Use getBoundingClientRect on IE, Opera has it but it's not perfect - if (isIE && !t.stdMode) { + // 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; - x = t.getStyle(t.select('html')[0], 'borderWidth'); // Remove border - x = (x == 'medium' || t.boxModel && !t.isIE6) && 2 || x; - return {x : n.left + e.scrollLeft - x, y : n.top + e.scrollTop - x}; + // 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; @@ -4280,7 +4525,7 @@ tinymce.html.Writer = function(settings) { }, isEmpty : function(node, elements) { - var self = this, i, attributes, type, walker, name; + var self = this, i, attributes, type, walker, name, parentNode; node = node.firstChild; if (node) { @@ -4296,15 +4541,23 @@ tinymce.html.Writer = function(settings) { continue; // Keep empty elements like - if (elements && elements[node.nodeName.toLowerCase()]) + name = node.nodeName.toLowerCase(); + if (elements && elements[name]) { + // Ignore single BR elements in blocks like


    + parentNode = node.parentNode; + if (name === 'br' && self.isBlock(parentNode) && parentNode.firstChild === node && parentNode.lastChild === node) { + continue; + } + return false; + } - // Keep elements with data attributes or name attribute like + // 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.indexOf('data-') === 0) + if (name === "name" || name === 'data-mce-bookmark') return false; } } @@ -4338,7 +4591,7 @@ tinymce.html.Writer = function(settings) { }, nodeIndex : function(node, normalized) { - var idx = 0, lastNodeType, lastNode, nodeType, nodeValueExists; + var idx = 0, lastNodeType, lastNode, nodeType; if (node) { for (lastNodeType = node.nodeType, node = node.previousSibling, lastNode = node; node; node = node.previousSibling) { @@ -4346,11 +4599,7 @@ tinymce.html.Writer = function(settings) { // Normalize text nodes if (normalized && nodeType == 3) { - // ensure that text nodes that have been removed are handled correctly in Internet Explorer. - // (the nodeValue attribute will not exist, and will error here). - nodeValueExists = false; - try {nodeValueExists = node.nodeValue.length} catch (c) {} - if (nodeType == lastNodeType || !nodeValueExists) + if (nodeType == lastNodeType || !node.nodeValue.length) continue; } idx++; @@ -5193,11 +5442,97 @@ tinymce.html.Writer = function(settings) { (function() { function Selection(selection) { - var t = this, invisibleChar = '\uFEFF', range, lastIERng, dom = selection.dom, TRUE = true, FALSE = false; + var self = this, dom = selection.dom, TRUE = true, FALSE = false; + + function getPosition(rng, start) { + var checkRng, startIndex = 0, endIndex, inside, + children, child, offset, index, position = -1, parent; + + // Setup test range, collapse it and get the parent + checkRng = rng.duplicate(); + checkRng.collapse(start); + parent = checkRng.parentElement(); + + // Check if the selection is within the right document + if (parent.ownerDocument !== selection.dom.doc) + return; + + // IE will report non editable elements as it's parent so look for an editable one + while (parent.contentEditable === "false") { + parent = parent.parentNode; + } + + // If parent doesn't have any children then return that we are inside the element + if (!parent.hasChildNodes()) { + return {node : parent, inside : 1}; + } + + // Setup node list and endIndex + children = parent.children; + endIndex = children.length - 1; + + // Perform a binary search for the position + while (startIndex <= endIndex) { + index = Math.floor((startIndex + endIndex) / 2); + + // Move selection to node and compare the ranges + child = children[index]; + checkRng.moveToElementText(child); + position = checkRng.compareEndPoints(start ? 'StartToStart' : 'EndToEnd', rng); + + // Before/after or an exact match + if (position > 0) { + endIndex = index - 1; + } else if (position < 0) { + startIndex = index + 1; + } else { + return {node : child}; + } + } + + // 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); + + checkRng.setEndPoint(start ? 'EndToStart' : 'EndToEnd', rng); + + // Fix for edge case:
    ..
    ab|c
    + if (checkRng.compareEndPoints(start ? 'StartToStart' : 'StartToEnd', rng) > 0) { + checkRng = rng.duplicate(); + checkRng.collapse(start); + + offset = -1; + while (parent == checkRng.parentElement()) { + if (checkRng.move('character', -1) == 0) + break; + + offset++; + } + } + + offset = offset || checkRng.text.replace('\r\n', ' ').length; + } else { + // Child position is after the selection endpoint + checkRng.collapse(true); + checkRng.setEndPoint(start ? 'StartToStart' : 'StartToEnd', rng); + + // Get the length of the text to find where the endpoint is relative to it's container + offset = checkRng.text.replace('\r\n', ' ').length; + } + + return {node : child, position : position, offset : offset, inside : inside}; + }; // Returns a W3C DOM compatible range object by using the IE Range API function getRange() { - var ieRange = selection.getRng(), domRange = dom.createRng(), element, collapsed; + var ieRange = selection.getRng(), domRange = dom.createRng(), element, collapsed, tmpRange, element2, bookmark, fail; // If selection is outside the current document just return an empty range element = ieRange.item ? ieRange.item(0) : ieRange.parentElement(); @@ -5206,147 +5541,294 @@ tinymce.html.Writer = function(settings) { collapsed = selection.isCollapsed(); - // Handle control selection or text selection of a image - if (ieRange.item || !element.hasChildNodes()) { - if (collapsed) { - domRange.setStart(element, 0); - domRange.setEnd(element, 0); - } else { - domRange.setStart(element.parentNode, dom.nodeIndex(element)); - domRange.setEnd(domRange.startContainer, domRange.startOffset + 1); - } + // Handle control selection + if (ieRange.item) { + domRange.setStart(element.parentNode, dom.nodeIndex(element)); + domRange.setEnd(domRange.startContainer, domRange.startOffset + 1); return domRange; } function findEndPoint(start) { - var marker, container, offset, nodes, startIndex = 0, endIndex, index, parent, checkRng, position; - - // Setup temp range and collapse it - checkRng = ieRange.duplicate(); - checkRng.collapse(start); + var endPoint = getPosition(ieRange, start), container, offset, textNodeOffset = 0, sibling, undef, nodeValue; - // Create marker and insert it at the end of the endpoints parent - marker = dom.create('a'); - parent = checkRng.parentElement(); + container = endPoint.node; + offset = endPoint.offset; - // 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); + if (endPoint.inside && !container.hasChildNodes()) { + domRange[start ? 'setStart' : 'setEnd'](container, 0); return; } - 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); + if (offset === undef) { + domRange[start ? 'setStartBefore' : 'setEndAfter'](container); return; } - // 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; + if (endPoint.position < 0) { + sibling = endPoint.inside ? container.firstChild : container.nextSibling; + + if (!sibling) { + domRange[start ? 'setStartAfter' : 'setEndAfter'](container); + return; } - } - // Setup container - container = position > 0 || index == 0 ? marker.nextSibling : marker.previousSibling; + if (!offset) { + if (sibling.nodeType == 3) + domRange[start ? 'setStart' : 'setEnd'](sibling, 0); + else + domRange[start ? 'setStartBefore' : 'setEndBefore'](sibling); - // Handle element selection - if (container.nodeType == 1) { - dom.remove(marker); + return; + } - // Find offset and container - offset = dom.nodeIndex(container); - container = container.parentNode; + // Find the text node and offset + while (sibling) { + nodeValue = sibling.nodeValue; + textNodeOffset += nodeValue.length; - // Move the offset if we are setting the end or the position is after an element - if (!start || index > 0) - offset++; + // We are at or passed the position we where looking for + if (textNodeOffset >= offset) { + container = sibling; + textNodeOffset -= offset; + textNodeOffset = nodeValue.length - textNodeOffset; + break; + } + + sibling = sibling.nextSibling; + } } else { - // 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; + // Find the text node and offset + sibling = container.previousSibling; + + if (!sibling) + return domRange[start ? 'setStartBefore' : 'setEndBefore'](container); + + // If there isn't any text to loop then use the first position + if (!offset) { + if (container.nodeType == 3) + domRange[start ? 'setStart' : 'setEnd'](sibling, container.nodeValue.length); + else + domRange[start ? 'setStartAfter' : 'setEndAfter'](sibling); + + return; } - dom.remove(marker); + while (sibling) { + textNodeOffset += sibling.nodeValue.length; + + // We are at or passed the position we where looking for + if (textNodeOffset >= offset) { + container = sibling; + textNodeOffset -= offset; + break; + } + + sibling = sibling.previousSibling; + } } - domRange[start ? 'setStart' : 'setEnd'](container, offset); + domRange[start ? 'setStart' : 'setEnd'](container, textNodeOffset); }; - // Find start point - findEndPoint(true); + 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); - // Find end point if needed - if (!collapsed) - findEndPoint(); + // Since the range has moved we need to re-get it + ieRange = selection.getRng(); + + // Find start point + findEndPoint(true); + + // Find end point if needed + if (!collapsed) + findEndPoint(); + } else + throw ex; // Throw other errors + } return domRange; }; - this.addRange = function(rng) { - var ieRng, ctrlRng, startContainer, startOffset, endContainer, endOffset, doc = selection.dom.doc, body = doc.body; - - function setEndPoint(start) { - var container, offset, marker, tmpRng, nodes; + this.getBookmark = function(type) { + var rng = selection.getRng(), start, end, bookmark = {}; - marker = dom.create('a'); - container = start ? startContainer : endContainer; - offset = start ? startOffset : endOffset; - tmpRng = ieRng.duplicate(); + function getIndexes(node) { + var node, parent, root, children, i, indexes = []; - if (container == doc || container == doc.documentElement) { - container = body; - offset = 0; - } + parent = node.parentNode; + root = dom.getRoot().parentNode; - 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; + while (parent != root) { + children = parent.children; - if (nodes.length) { - if (offset >= nodes.length) { - dom.insertAfter(marker, nodes[nodes.length - 1]); - } else { - container.insertBefore(marker, nodes[offset]); + i = children.length; + while (i--) { + if (node === children[i]) { + indexes.push(i); + break; + } + } + + node = parent; + parent = parent.parentNode; + } + + return indexes; + }; + + function getBookmarkEndPoint(start) { + var position; + + position = getPosition(rng, start); + if (position) { + return { + position : position.position, + offset : position.offset, + indexes : getIndexes(position.node), + inside : position.inside + }; + } + }; + + // Non ubstructive bookmark + if (type === 2) { + // Handle text selection + if (!rng.item) { + bookmark.start = getBookmarkEndPoint(true); + + if (!selection.isCollapsed()) + bookmark.end = getBookmarkEndPoint(); + } else + bookmark.start = {ctrl : true, indexes : getIndexes(rng.item(0))}; + } + + return bookmark; + }; + + this.moveToBookmark = function(bookmark) { + var rng, body = dom.doc.body; + + function resolveIndexes(indexes) { + var node, i, idx, children; + + node = dom.getRoot(); + for (i = indexes.length - 1; i >= 0; i--) { + children = node.children; + idx = indexes[i]; + + if (idx <= children.length - 1) { + node = children[idx]; + } + } + + return node; + }; + + function setBookmarkEndPoint(start) { + var endPoint = bookmark[start ? 'start' : 'end'], moveLeft, moveRng, undef; + + if (endPoint) { + moveLeft = endPoint.position > 0; + + moveRng = body.createTextRange(); + moveRng.moveToElementText(resolveIndexes(endPoint.indexes)); + + offset = endPoint.offset; + if (offset !== undef) { + moveRng.collapse(endPoint.inside || moveLeft); + moveRng.moveStart('character', moveLeft ? -offset : offset); + } else + moveRng.collapse(start); + + rng.setEndPoint(start ? 'StartToStart' : 'EndToStart', moveRng); + + if (start) + rng.collapse(true); + } + }; + + if (bookmark.start) { + if (bookmark.start.ctrl) { + rng = body.createControlRange(); + rng.addElement(resolveIndexes(bookmark.start.indexes)); + rng.select(); + } else { + rng = body.createTextRange(); + setBookmarkEndPoint(true); + setBookmarkEndPoint(); + rng.select(); + } + } + }; + + this.addRange = function(rng) { + var ieRng, ctrlRng, startContainer, startOffset, endContainer, endOffset, doc = selection.dom.doc, body = doc.body; + + function setEndPoint(start) { + var container, offset, marker, tmpRng, nodes; + + marker = dom.create('a'); + container = start ? startContainer : endContainer; + offset = start ? startOffset : endOffset; + tmpRng = ieRng.duplicate(); + + if (container == doc || container == doc.documentElement) { + container = body; + offset = 0; + } + + if (container.nodeType == 3) { + container.parentNode.insertBefore(marker, container); + tmpRng.moveToElementText(marker); + tmpRng.moveStart('character', offset); + dom.remove(marker); + ieRng.setEndPoint(start ? 'StartToStart' : 'EndToEnd', tmpRng); + } else { + nodes = container.childNodes; + + if (nodes.length) { + if (offset >= nodes.length) { + dom.insertAfter(marker, nodes[nodes.length - 1]); + } else { + container.insertBefore(marker, nodes[offset]); } tmpRng.moveToElementText(marker); } else { // Empty node selection for example
    |
    - marker = doc.createTextNode(invisibleChar); + marker = doc.createTextNode('\uFEFF'); container.appendChild(marker); tmpRng.moveToElementText(marker.parentNode); tmpRng.collapse(TRUE); @@ -5357,9 +5839,6 @@ tinymce.html.Writer = function(settings) { } } - // Destroy cached range - this.destroy(); - // Setup some shorter versions startContainer = rng.startContainer; startOffset = rng.startOffset; @@ -5389,33 +5868,8 @@ tinymce.html.Writer = function(settings) { ieRng.select(); }; - this.getRangeAt = function() { - // Setup new range if the cache is empty - if (!range || !tinymce.dom.RangeUtils.compareRanges(lastIERng, selection.getRng())) { - range = getRange(); - - // Store away text range for next call - lastIERng = selection.getRng(); - } - - // IE will say that the range is equal then produce an invalid argument exception - // if you perform specific operations in a keyup event. For example Ctrl+Del. - // This hack will invalidate the range cache if the exception occurs - try { - range.startContainer.nextSibling; - } catch (ex) { - range = getRange(); - lastIERng = null; - } - - // Return cached range - return range; - }; - - this.destroy = function() { - // Destroy cached range and last IE range to avoid memory leaks - lastIERng = range = null; - }; + // Expose range method + this.getRangeAt = getRange; }; // Expose the selection object @@ -6734,7 +7188,7 @@ window.tinymce.dom.Sizzle = Sizzle; return; try { - // If IE is used, use the trick by Diego Perini + // 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) { @@ -6926,6 +7380,13 @@ window.tinymce.dom.Sizzle = Sizzle; tinymce.addUnload(t.destroy, t); }, + 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; @@ -6933,6 +7394,7 @@ window.tinymce.dom.Sizzle = Sizzle; wb = wa = ''; s.get = true; s.format = s.format || 'html'; + s.forced_root_block = ''; t.onBeforeGetContent.dispatch(t, s); if (s.format == 'text') @@ -6943,9 +7405,13 @@ window.tinymce.dom.Sizzle = Sizzle; if (n) e.appendChild(n); - } else if (is(r.item) || is(r.htmlText)) - e.innerHTML = r.item ? r.item(0).outerHTML : r.htmlText; - else + } 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(); // Keep whitespace before and after @@ -7017,7 +7483,12 @@ window.tinymce.dom.Sizzle = Sizzle; // Remove the caret position self.dom.remove('__caret'); - self.setRng(rng); + + 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 @@ -7025,7 +7496,12 @@ window.tinymce.dom.Sizzle = Sizzle; rng = self.getRng(); } - rng.pasteHTML(content); + // 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); } // Dispatch set content event @@ -7153,6 +7629,9 @@ window.tinymce.dom.Sizzle = Sizzle; return bookmark; }; + if (t.tridentSel) + return t.tridentSel.getBookmark(type); + return getLocation(); } @@ -7225,10 +7704,6 @@ window.tinymce.dom.Sizzle = Sizzle; moveToBookmark : function(bookmark) { var t = this, dom = t.dom, marker1, marker2, rng, root, startContainer, endContainer, startOffset, endOffset; - // Clear selection cache - if (t.tridentSel) - t.tridentSel.destroy(); - if (bookmark) { if (bookmark.start) { rng = dom.createRng(); @@ -7268,6 +7743,9 @@ window.tinymce.dom.Sizzle = Sizzle; return true; }; + if (t.tridentSel) + return t.tridentSel.moveToBookmark(bookmark); + if (setEndPoint(true) && setEndPoint()) { t.setRng(rng); } @@ -7603,14 +8081,84 @@ window.tinymce.dom.Sizzle = Sizzle; return bl; }, + normalize : function() { + var self = this, rng, normalized; + + // Normalize only on non IE browsers for now + if (tinymce.isIE) + return; + + function normalizeEndPoint(start) { + var container, offset, walker, dom = self.dom, body = dom.getRoot(), node; + + container = rng[(start ? 'start' : 'end') + 'Container']; + offset = rng[(start ? 'start' : 'end') + 'Offset']; + + // If the container is a document move it to the body element + if (container.nodeType === 9) { + container = container.body; + offset = 0; + } + + // If the container is body try move it into the closest text node or position + // TODO: Add more logic here to handle element selection cases + if (container === body) { + // 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; + break; + } + + // Found a BR element that we can place the caret before + if (node.nodeName === 'BR') { + offset = dom.nodeIndex(node); + container = node.parentNode; + break; + } + } while (node = (start ? walker.next() : walker.prev())); + + normalized = true; + } + } + } + + // Set endpoint if it was normalized + if (normalized) + rng['set' + (start ? 'Start' : 'End')](container, offset); + }; + + rng = self.getRng(); + + // Normalize the end points + normalizeEndPoint(true); + + if (rng.collapsed) + normalizeEndPoint(); + + // Set the selection if it was normalized + if (normalized) { + //console.log(self.dom.dumpRng(rng)); + self.setRng(rng); + } + }, + destroy : function(s) { var t = this; t.win = null; - if (t.tridentSel) - t.tridentSel.destroy(); - // Manual destroy then remove unload handler if (!s) tinymce.removeUnload(t.destroy); @@ -7876,7 +8424,7 @@ window.tinymce.dom.Sizzle = Sizzle; // 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').length > 0) { + if (isIE && dom.select('script,style,select,map').length > 0) { content = node.innerHTML; node = node.cloneNode(false); dom.setHTML(node, content); @@ -8527,6 +9075,7 @@ tinymce.dom.TreeWalker = function(start_node, root_node) { } }); })(tinymce); + (function(tinymce) { // Shorten class names var DOM = tinymce.DOM, is = tinymce.is; @@ -9158,8 +9707,7 @@ tinymce.create('tinymce.ui.Separator:tinymce.ui.Control', { l = DOM.encode(s.label || ''); h = ''; - - if (s.image) + if (s.image && !(this.editor &&this.editor.forcedHighContrastMode) ) h += '' + DOM.encode(s.title) + '' + l; else h += '' + (l ? '' + l + '' : ''); @@ -9234,20 +9782,23 @@ tinymce.create('tinymce.ui.Separator:tinymce.ui.Control', { }, selectByIndex : function(idx) { - var t = this, e, o; + var t = this, e, o, label; if (idx != t.selectedIndex) { e = DOM.get(t.id + '_text'); + label = DOM.get(t.id + '_voiceDesc'); o = t.items[idx]; 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); @@ -9276,7 +9827,7 @@ tinymce.create('tinymce.ui.Separator:tinymce.ui.Control', { renderHTML : function() { var h = '', t = this, s = t.settings, cp = t.classPrefix; - h = ''; + h = ''; h += ''; h += ''; @@ -9286,7 +9837,7 @@ tinymce.create('tinymce.ui.Separator:tinymce.ui.Control', { }, showMenu : function() { - var t = this, p1, p2, e = DOM.get(this.id), m; + var t = this, p2, e = DOM.get(this.id), m; if (t.isDisabled() || t.items.length == 0) return; @@ -9299,7 +9850,6 @@ tinymce.create('tinymce.ui.Separator:tinymce.ui.Control', { t.isMenuRendered = true; } - p1 = DOM.getPos(this.settings.menu_container); p2 = DOM.getPos(e); m = t.menu; @@ -9373,6 +9923,7 @@ tinymce.create('tinymce.ui.Separator:tinymce.ui.Control', { if (o.value === undefined) { m.add({ title : o.title, + role : "option", 'class' : 'mceMenuItemTitle', onclick : function() { if (t.settings.onselect('') !== false) @@ -9381,6 +9932,7 @@ tinymce.create('tinymce.ui.Separator:tinymce.ui.Control', { }); } 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 @@ -9456,6 +10008,7 @@ tinymce.create('tinymce.ui.Separator:tinymce.ui.Control', { } }); })(tinymce); + (function(tinymce) { var DOM = tinymce.DOM, Event = tinymce.dom.Event, each = tinymce.each, Dispatcher = tinymce.util.Dispatcher; @@ -9579,6 +10132,11 @@ tinymce.create('tinymce.ui.Separator:tinymce.ui.Control', { Event.remove(t.id, 'blur', bf); }); + //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); @@ -9589,6 +10147,7 @@ tinymce.create('tinymce.ui.Separator:tinymce.ui.Control', { } }); })(tinymce); + (function(tinymce) { var DOM = tinymce.DOM, Event = tinymce.dom.Event, each = tinymce.each; @@ -9708,8 +10267,8 @@ tinymce.create('tinymce.ui.Separator:tinymce.ui.Control', { h += ''; h += ''; - h = DOM.createHTML('table', {id : t.id, role: 'presentation', tabindex: '0', 'class' : 'mceSplitButton mceSplitButtonEnabled ' + s['class'], cellpadding : '0', cellspacing : '0', title : s.title}, h); - return DOM.createHTML('span', {role: 'button', 'aria-labelledby': t.id + '_voice', 'aria-haspopup': 'true'}, 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); }, postRender : function() { @@ -9844,6 +10403,7 @@ tinymce.create('tinymce.ui.Separator:tinymce.ui.Control', { } t.isMenuVisible = 0; + t.onHideMenu.dispatch(); } }, @@ -9982,7 +10542,8 @@ tinymce.create('tinymce.ui.ToolbarGroup:tinymce.ui.Container', { }, focus : function() { - this.keyNav.focus(); + var t = this; + dom.get(t.id).focus(); }, postRender : function() { @@ -10000,6 +10561,10 @@ tinymce.create('tinymce.ui.ToolbarGroup:tinymce.ui.Container', { 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 @@ -10097,7 +10662,19 @@ tinymce.create('tinymce.ui.Toolbar:tinymce.ui.Container', { }, get : function(n) { - return this.lookup[n]; + if (this.lookup[n]) { + return this.lookup[n].instance; + } else { + return undefined; + } + }, + + dependencies : function(n) { + var result; + if (this.lookup[n]) { + result = this.lookup[n].dependencies; + } + return result || []; }, requireLangPack : function(n) { @@ -10107,27 +10684,61 @@ tinymce.create('tinymce.ui.Toolbar:tinymce.ui.Container', { tinymce.ScriptLoader.add(this.urls[n] + '/langs/' + s.language + '.js'); }, - add : function(id, o) { + add : function(id, o, dependencies) { this.items.push(o); - this.lookup[id] = o; + this.lookup[id] = {instance:o, dependencies:dependencies}; this.onAdd.dispatch(this, id, o); return o; }, + createUrl: function(baseUrl, dep) { + if (typeof dep === "object") { + return dep + } else { + return {prefix: baseUrl.prefix, resource: dep, suffix: baseUrl.suffix}; + } + }, + + addComponents: function(pluginName, scripts) { + var pluginUrl = this.urls[pluginName]; + tinymce.each(scripts, function(script){ + tinymce.ScriptLoader.add(pluginUrl+"/"+script); + }); + }, load : function(n, u, cb, s) { - var t = this; + var t = this, url = u; + + 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); + } + } + } if (t.urls[n]) return; + if (typeof u === "object") + url = u.prefix + u.resource + u.suffix; - if (u.indexOf('/') != 0 && u.indexOf('://') == -1) - u = tinymce.baseURL + '/' + u; + if (url.indexOf('/') != 0 && url.indexOf('://') == -1) + url = tinymce.baseURL + '/' + url; - t.urls[n] = u.substring(0, u.lastIndexOf('/')); + t.urls[n] = url.substring(0, url.lastIndexOf('/')); - if (!t.lookup[n]) - tinymce.ScriptLoader.add(u, cb, s); + if (t.lookup[n]) { + loadDependencies(); + } else { + tinymce.ScriptLoader.add(url, loadDependencies, s); + } } }); @@ -10633,10 +11244,10 @@ tinymce.create('tinymce.ui.Toolbar:tinymce.ui.Container', { if (!t.getElement()) return; - // Is a iPad/iPhone, then skip initialization. We need to sniff here since the - // browser says it has contentEditable support but there is no visible caret - // We will remove this check ones Apple implements full contentEditable support - if (tinymce.isIDevice) + // 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; // Add hidden input for non input elements inside form elements @@ -10708,12 +11319,23 @@ tinymce.create('tinymce.ui.Toolbar:tinymce.ui.Container', { ThemeManager.load(s.theme, 'themes/' + s.theme + '/editor_template' + tinymce.suffix + '.js'); 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; - - PluginManager.load(p, 'plugins/' + p + '/editor_plugin' + tinymce.suffix + '.js'); + 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'}); + } } }); @@ -10728,7 +11350,7 @@ tinymce.create('tinymce.ui.Toolbar:tinymce.ui.Container', { }, init : function() { - var n, t = this, s = t.settings, w, h, e = t.getElement(), o, ti, u, bi, bc, re, i; + var n, t = this, s = t.settings, w, h, e = t.getElement(), o, ti, u, bi, bc, re, i, initializedPlugins = []; tinymce.add(t); @@ -10742,20 +11364,25 @@ tinymce.create('tinymce.ui.Toolbar:tinymce.ui.Container', { if (t.theme.init && s.init_theme) t.theme.init(t, ThemeManager.urls[s.theme] || tinymce.documentBaseURL.replace(/\/$/, '')); } - - // Create all plugins - each(explode(s.plugins.replace(/\-/g, '')), function(p) { + function initPlugin(p) { var c = PluginManager.get(p), u = PluginManager.urls[p] || tinymce.documentBaseURL.replace(/\/$/, ''), po; - - if (c) { + if (c && tinymce.inArray(initializedPlugins,p) === -1) { + each(PluginManager.dependencies(p), function(dep){ + initPlugin(dep); + }); po = new c(t, u); t.plugins[p] = po; - if (po.init) + if (po.init) { po.init(t, u); + initializedPlugins.push(p); + } } - }); + } + + // Create all plugins + each(explode(s.plugins.replace(/\-/g, '')), initPlugin); // Setup popup CSS path(s) if (s.popup_css !== false) { @@ -10865,12 +11492,9 @@ tinymce.create('tinymce.ui.Toolbar:tinymce.ui.Container', { t.iframeHTML += ''; - // Firefox 2 doesn't load stylesheets correctly this way - if (!isGecko || !/Firefox\/2/.test(navigator.userAgent)) { - for (i = 0; i < t.contentCSS.length; i++) - t.iframeHTML += ''; - - t.contentCSS = []; + // Load the CSS by injecting them into the HTML this will reduce "flicker" + for (i = 0; i < t.contentCSS.length; i++) { + t.iframeHTML += ''; } bi = s.body_id || 'tinymce'; @@ -10885,12 +11509,12 @@ tinymce.create('tinymce.ui.Toolbar:tinymce.ui.Container', { bc = bc[t.id] || ''; } - t.iframeHTML += ''; + t.iframeHTML += '
    '; // Domain relaxing enabled, then set document domain if (tinymce.relaxedDomain && (isIE || (tinymce.isOpera && parseFloat(opera.version()) < 11))) { // We need to write the contents here in IE since multiple writes messes up refresh button and back button - u = 'javascript:(function(){document.open();document.domain="' + document.domain + '";var ed = window.parent.tinyMCE.get("' + t.id + '");document.write(ed.iframeHTML);document.close();ed.setupIframe();})()'; + u = 'javascript:(function(){document.open();document.domain="' + document.domain + '";var ed = window.parent.tinyMCE.get("' + t.id + '");document.write(ed.iframeHTML);document.close();ed.setupIframe();})()'; } // Create iframe @@ -10898,11 +11522,13 @@ tinymce.create('tinymce.ui.Toolbar:tinymce.ui.Container', { n = DOM.add(o.iframeContainer, 'iframe', { id : t.id + "_ifr", src : u || 'javascript:""', // Workaround for HTTPS warning in IE6/7 - frameBorder : '0', + frameBorder : '0', + allowTransparency : "true", title : s.aria_label, style : { width : '100%', - height : h + height : h, + display : 'block' // Important for Gecko to render the iframe correctly } }); @@ -10930,28 +11556,14 @@ tinymce.create('tinymce.ui.Toolbar:tinymce.ui.Container', { d.domain = tinymce.relaxedDomain; } - // 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 - } - } - - // 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); + // It will not steal focus while setting contentEditable + b = t.getBody(); + b.disabled = true; - if (!s.readonly) - b.contentEditable = true; + if (!s.readonly) + b.contentEditable = true; - DOM.show(b); - } + b.disabled = false; t.schema = new tinymce.html.Schema(s); @@ -10968,38 +11580,44 @@ tinymce.create('tinymce.ui.Toolbar:tinymce.ui.Container', { t.parser = new tinymce.html.DomParser(s, t.schema); - // Force anchor names closed - 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); + // 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); + } } - } - }); + }); + } // 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; + var i = nodes.length, node, dom = t.dom, value, internalName; while (i--) { node = nodes[i]; value = node.attr(name); + internalName = 'data-mce-' + name; - if (name === "style") - node.attr('data-mce-style', dom.serializeStyle(dom.parseStyle(value), node.name)); - else - node.attr('data-mce-' + name, t.convertURL(value, name, node.name)); + // 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 + node.attr(internalName, t.convertURL(value, name, node.name)); + } } }); @@ -11092,6 +11710,18 @@ tinymce.create('tinymce.ui.Toolbar:tinymce.ui.Container', { subscript : {inline : 'sub'}, superscript : {inline : 'sup'}, + link : {inline : 'a', selector : 'a', remove : 'all', split : true, deep : true, + onmatch : function(node) { + return true; + }, + + onformat : function(elm, fmt, vars) { + each(vars, function(value, key) { + t.dom.setAttrib(elm, key, value); + }); + } + }, + removeformat : [ {selector : 'b,strong,em,i,font,u,strike', remove : 'all', split : true, expand : false, block_expand : true, deep : true}, {selector : 'span', attributes : ['style', 'class'], remove : 'empty', split : true, expand : false, deep : true}, @@ -11149,6 +11779,8 @@ tinymce.create('tinymce.ui.Toolbar:tinymce.ui.Container', { t.controlManager.onPostRender.dispatch(t, t.controlManager); t.onPostRender.dispatch(t); + t.quirks = new tinymce.util.Quirks(this); + if (s.directionality) t.getBody().dir = s.directionality; @@ -11298,71 +11930,62 @@ tinymce.create('tinymce.ui.Toolbar:tinymce.ui.Container', { }); t.onSetContent.add(t.selection.onSetContent.add(fixLinks)); - - 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 - } - } } - // A small timeout was needed since firefox will remove. Bug: #1838304 - setTimeout(function () { - if (t.removed) - return; + t.load({initial : true, format : 'html'}); + t.startContent = t.getContent({format : 'raw'}); + t.undoManager.add(); + t.initialized = true; - t.load({initial : true, format : 'html'}); - t.startContent = t.getContent({format : 'raw'}); - t.undoManager.add(); - t.initialized = true; + 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.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}); + // Load specified content CSS last + each(t.contentCSS, function(u) { + t.dom.loadCSS(u); + }); - // Load specified content CSS last - each(t.contentCSS, function(u) { - t.dom.loadCSS(u); - }); + // Handle auto focus + if (s.auto_focus) { + setTimeout(function () { + var ed = tinymce.get(s.auto_focus); - // Handle auto focus - if (s.auto_focus) { - setTimeout(function () { - var ed = tinymce.get(s.auto_focus); + ed.selection.select(ed.getBody(), 1); + ed.selection.collapse(1); + ed.getBody().focus(); + ed.getWin().focus(); + }, 100); + } - ed.selection.select(ed.getBody(), 1); - ed.selection.collapse(1); - ed.getWin().focus(); - }, 100); - } - }, 1); - e = null; }, focus : function(sf) { - var oed, t = this, ce = t.settings.content_editable, ieRng, controlElm, doc = t.getDoc(); + var oed, t = this, selection = t.selection, ce = t.settings.content_editable, ieRng, controlElm, doc = t.getDoc(); if (!sf) { // Get selected control element - ieRng = t.selection.getRng(); + ieRng = selection.getRng(); if (ieRng.item) { controlElm = ieRng.item(0); } + t._refreshContentEditable(); + selection.normalize(); + // Is not content editable if (!ce) t.getWin().focus(); + // Focus the body as well since it's contentEditable + if (tinymce.isGecko) { + t.getBody().focus(); + } + // Restore selected control element // This is needed when for example an image is selected within a // layer a call to focus will then remove the control selection @@ -11756,7 +12379,7 @@ tinymce.create('tinymce.ui.Toolbar:tinymce.ui.Container', { }, setContent : function(content, args) { - var self = this, rootNode, body = self.getBody(); + var self = this, rootNode, body = self.getBody(), forcedRootBlockName; // Setup args object args = args || {}; @@ -11773,7 +12396,15 @@ tinymce.create('tinymce.ui.Toolbar:tinymce.ui.Container', { // 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))) { - body.innerHTML = '
    '; + forcedRootBlockName = self.settings.forced_root_block; + if (forcedRootBlockName) + content = '<' + forcedRootBlockName + '>
    '; + else + content = '
    '; + + body.innerHTML = content; + self.selection.select(body, true); + self.selection.collapse(true); return; } @@ -11792,6 +12423,8 @@ tinymce.create('tinymce.ui.Toolbar:tinymce.ui.Container', { if (!args.no_events) self.onSetContent.dispatch(self, args); + self.selection.normalize(); + return args.content; }, @@ -12078,14 +12711,7 @@ tinymce.create('tinymce.ui.Toolbar:tinymce.ui.Container', { var t = this, d = t.getDoc(), s = t.settings; if (isGecko && !s.readonly) { - if (t._isHidden()) { - try { - if (!s.content_editable) - d.designMode = 'On'; - } catch (ex) { - // Fails if it's hidden - } - } + t._refreshContentEditable(); try { // Try new Gecko method @@ -12108,21 +12734,6 @@ tinymce.create('tinymce.ui.Toolbar:tinymce.ui.Container', { t.onMouseDown.add(setOpts); } - // 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; - - // Needs tobe the setBaseAndExtend or it will fail to select floated images - if (e.nodeName == 'IMG' || (e.nodeName == 'A' && dom.hasClass(e, 'mceItemAnchor'))) { - t.selection.getSel().setBaseAndExtent(e, 0, e, 1); - t.nodeChanged(); - } - }); - } - // Add node change handlers t.onMouseUp.add(t.nodeChanged); //t.onClick.add(t.nodeChanged); @@ -12133,6 +12744,35 @@ tinymce.create('tinymce.ui.Toolbar:tinymce.ui.Container', { t.nodeChanged(); }); + + // Add block quote deletion handler + t.onKeyDown.add(function(ed, e) { + // Was the BACKSPACE key pressed? + if (e.keyCode != 8) + return; + + var n = ed.selection.getRng().startContainer; + var offset = ed.selection.getRng().startOffset; + + while (n && n.nodeType && n.nodeType != 1 && n.parentNode) + n = n.parentNode; + + // Is the cursor at the beginning of a blockquote? + if (n && n.parentNode && n.parentNode.tagName === 'BLOCKQUOTE' && n.parentNode.firstChild == n && offset == 0) { + // Remove the blockquote + ed.formatter.toggle('blockquote', null, n.parentNode); + + // Move the caret to the beginning of n + var rng = ed.selection.getRng(); + rng.setStart(n, 0); + rng.setEnd(n, 0); + ed.selection.setRng(rng); + ed.selection.collapse(false); + } + }); + + + // Add reset handler t.onReset.add(function() { t.setContent(t.startContent, {format : 'raw'}); @@ -12154,9 +12794,9 @@ tinymce.create('tinymce.ui.Toolbar:tinymce.ui.Container', { for (i=1; i<=6; i++) t.addShortcut('ctrl+' + i, '', ['FormatBlock', false, 'h' + i]); - t.addShortcut('ctrl+7', '', ['FormatBlock', false, '

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

    ']); - t.addShortcut('ctrl+9', '', ['FormatBlock', false, '
    ']); + t.addShortcut('ctrl+7', '', ['FormatBlock', false, 'p']); + t.addShortcut('ctrl+8', '', ['FormatBlock', false, 'div']); + t.addShortcut('ctrl+9', '', ['FormatBlock', false, 'address']); function find(e) { var v = null; @@ -12252,21 +12892,6 @@ tinymce.create('tinymce.ui.Toolbar:tinymce.ui.Container', { cb : cb }; }); - - t.onKeyDown.add(function(ed, e) { - var sel; - - switch (e.keyCode) { - case 8: - sel = t.getDoc().selection; - - // Fix IE control + backspace browser bug - if (sel.createRange && sel.createRange().item) { - ed.dom.remove(sel.createRange().item(0)); - return Event.cancel(e); - } - } - }); } if (tinymce.isOpera) { @@ -12293,69 +12918,25 @@ tinymce.create('tinymce.ui.Toolbar:tinymce.ui.Container', { }); t.onKeyUp.add(function(ed, e) { - var rng, parent, bookmark; - - // Fix for bug #3168, to remove odd ".." nodes from the DOM we need to get/set the HTML of the parent node. - if (isIE && e.keyCode == 8) { - rng = t.selection.getRng(); - if (rng.parentElement) { - parent = rng.parentElement(); - bookmark = t.selection.getBookmark(); - parent.innerHTML = parent.innerHTML; - t.selection.moveToBookmark(bookmark); - } - } + var keyCode = e.keyCode; - if ((e.keyCode >= 33 && e.keyCode <= 36) || (e.keyCode >= 37 && e.keyCode <= 40) || e.keyCode == 13 || e.keyCode == 45 || e.ctrlKey) + if ((keyCode >= 33 && keyCode <= 36) || (keyCode >= 37 && keyCode <= 40) || keyCode == 13 || keyCode == 45 || e.ctrlKey) addUndo(); }); t.onKeyDown.add(function(ed, e) { - var rng, parent, bookmark, keyCode = e.keyCode; - - // 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 && keyCode == 46) { - rng = t.selection.getRng(); - - if (rng.parentElement) { - parent = rng.parentElement(); - - if (!t.undoManager.typing) { - t.undoManager.beforeChange(); - t.undoManager.typing = true; - t.undoManager.add(); - } + var keyCode = e.keyCode, sel; - // Select next word when ctrl key is used in combo with delete - if (e.ctrlKey) { - rng.moveEnd('word', 1); - rng.select(); - } - - // Delete contents - t.selection.getSel().clear(); - - // Check if we are within the same parent - if (rng.parentElement() == parent) { - bookmark = t.selection.getBookmark(); - - try { - // Update the HTML and hopefully it will remove the artifacts - parent.innerHTML = parent.innerHTML; - } catch (ex) { - // And since it's IE it can sometimes produce an unknown runtime error - } + if (keyCode == 8) { + sel = t.getDoc().selection; - // Restore the caret position - t.selection.moveToBookmark(bookmark); - } + // Fix IE control + backspace browser bug + if (sel && sel.createRange && sel.createRange().item) { + t.undoManager.beforeChange(); + ed.dom.remove(sel.createRange().item(0)); + addUndo(); - // Block the default delete behavior since it might be broken - e.preventDefault(); - return; + return Event.cancel(e); } } @@ -12375,8 +12956,8 @@ tinymce.create('tinymce.ui.Toolbar:tinymce.ui.Container', { // 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.add(); t.undoManager.typing = true; + t.undoManager.add(); } }); @@ -12385,7 +12966,22 @@ tinymce.create('tinymce.ui.Toolbar:tinymce.ui.Container', { addUndo(); }); } - + + // Fire a nodeChanged when the selection is changed on WebKit this fixes selection issues on iOS5 + // It only fires the nodeChange event every 50ms since it would other wise update the UI when you type and it hogs the CPU + if (tinymce.isWebKit) { + dom.bind(t.getDoc(), 'selectionchange', function() { + if (t.selectionTimer) { + clearTimeout(t.selectionTimer); + t.selectionTimer = 0; + } + + t.selectionTimer = window.setTimeout(function() { + t.nodeChanged(); + }, 50); + }); + } + // Bug fix for FireFox keeping styles from end of selection instead of start. if (tinymce.isGecko) { function getAttributeApplyFunction() { @@ -12393,10 +12989,14 @@ tinymce.create('tinymce.ui.Toolbar:tinymce.ui.Container', { return function() { var target = t.selection.getStart(); - t.dom.removeAllAttribs(target); - each(template, function(attr) { - target.setAttributeNode(attr.cloneNode(true)); - }); + + if (target !== t.getBody()) { + t.dom.removeAllAttribs(target); + + each(template, function(attr) { + target.setAttributeNode(attr.cloneNode(true)); + }); + } }; } @@ -12434,6 +13034,21 @@ tinymce.create('tinymce.ui.Toolbar:tinymce.ui.Container', { } }, + _refreshContentEditable : function() { + var self = this, body, parent; + + // Check if the editor was hidden and the re-initalize contentEditable mode by removing and adding the body again + if (self._isHidden()) { + body = self.getBody(); + parent = body.parentNode; + + parent.removeChild(body); + parent.appendChild(body); + + body.focus(); + } + }, + _isHidden : function() { var s; @@ -12456,6 +13071,7 @@ tinymce.create('tinymce.ui.Toolbar:tinymce.ui.Container', { selection = editor.selection, commands = {state: {}, exec : {}, value : {}}, settings = editor.settings, + formatter = editor.formatter, bookmark; function execCommand(command, ui, value) { @@ -12521,11 +13137,11 @@ tinymce.create('tinymce.ui.Toolbar:tinymce.ui.Container', { }; function isFormatMatch(name) { - return editor.formatter.match(name); + return formatter.match(name); }; function toggleFormat(name, value) { - editor.formatter.toggle(name, value ? {value : value} : undefined); + formatter.toggle(name, value ? {value : value} : undefined); }; function storeSelection(type) { @@ -12585,7 +13201,7 @@ tinymce.create('tinymce.ui.Toolbar:tinymce.ui.Container', { // Remove all other alignments first each('left,center,right,full'.split(','), function(name) { if (align != name) - editor.formatter.remove('align' + name); + formatter.remove('align' + name); }); toggleFormat('align' + align); @@ -12642,7 +13258,7 @@ tinymce.create('tinymce.ui.Toolbar:tinymce.ui.Container', { }, RemoveFormat : function(command) { - editor.formatter.remove(command); + formatter.remove(command); }, mceBlockQuote : function(command) { @@ -12688,22 +13304,15 @@ tinymce.create('tinymce.ui.Toolbar:tinymce.ui.Container', { }, mceInsertContent : function(command, ui, value) { - var caretNode, rng, rootNode, parent, node, rng, nodeRect, viewPortRect, args; + var parser, serializer, parentNode, rootNode, fragment, args, + marker, nodeRect, viewPortRect, rng, node, node2, bookmarkHtml, viewportBodyElement; - function findSuitableCaretNode(node, root_node, next) { - var walker = new tinymce.dom.TreeWalker(next ? node.nextSibling : node.previousSibling, root_node); - - while ((node = walker.current())) { - if ((node.nodeType == 3 && tinymce.trim(node.nodeValue).length) || node.nodeName == 'BR' || node.nodeName == 'IMG') - return node; - - if (next) - walker.next(); - else - walker.prev(); - } - }; + // Setup parser and serializer + parser = editor.parser; + serializer = new tinymce.html.Serializer({}, editor.schema); + bookmarkHtml = '\uFEFF'; + // Run beforeSetContent handlers on the HTML to be inserted args = {content: value, format: 'html'}; selection.onBeforeSetContent.dispatch(selection, args); value = args.content; @@ -12712,89 +13321,114 @@ tinymce.create('tinymce.ui.Toolbar:tinymce.ui.Container', { if (value.indexOf('{$caret}') == -1) value += '{$caret}'; - // Set the content at selection to a span and replace it's contents with the value - selection.setContent('\uFEFF', {no_events : false}); - dom.setOuterHTML('__mce', value.replace(/\{\$caret\}/, '\uFEFF')); + // Replace the caret marker with a span bookmark element + value = value.replace(/\{\$caret\}/, bookmarkHtml); - caretNode = dom.select('#__mce')[0]; - rootNode = dom.getRoot(); + // Insert node maker where we will insert the new HTML and get it's parent + if (!selection.isCollapsed()) + editor.getDoc().execCommand('Delete', false, null); - // Move the caret into the last suitable location within the previous sibling if it's a block since the block might be split - if (caretNode.previousSibling && dom.isBlock(caretNode.previousSibling) || caretNode.parentNode == rootNode) { - node = findSuitableCaretNode(caretNode, rootNode); - if (node) { - if (node.nodeName == 'BR') - node.parentNode.insertBefore(caretNode, node); - else - dom.insertAfter(caretNode, node); + parentNode = selection.getNode(); + + // Parse the fragment within the context of the parent node + args = {context : parentNode.nodeName.toLowerCase()}; + fragment = parser.parse(value, args); + + // Move the caret to a more suitable location + node = fragment.lastChild; + if (node.attr('id') == 'mce_marker') { + marker = node; + + 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; + } } } - // Find caret root parent and clean it up using the serializer to avoid nesting - while (caretNode) { - if (caretNode === rootNode) { - // Clean up the parent element by parsing and serializing it - // This will remove invalid elements/attributes and fix nesting issues - dom.setOuterHTML(parent, - new tinymce.html.Serializer({}, editor.schema).serialize( - editor.parser.parse(dom.getOuterHTML(parent)) - ) - ); + // If parser says valid we can insert the contents into that parent + if (!args.invalid) { + value = serializer.serialize(fragment); - break; - } + // 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 - parent = caretNode; - caretNode = caretNode.parentNode; - } + // Insert bookmark node and get the parent + selection.setContent(bookmarkHtml); + parentNode = editor.selection.getNode(); + rootNode = editor.getBody(); - // Find caret after cleanup and move selection to that location - caretNode = dom.select('#__mce')[0]; - if (caretNode) { - node = findSuitableCaretNode(caretNode, rootNode) || findSuitableCaretNode(caretNode, rootNode, true); - dom.remove(caretNode); + // Opera will return the document node when selection is in root + if (parentNode.nodeType == 9) + parentNode = node = rootNode; + else + node = parentNode; - if (node) { - rng = dom.createRng(); + // Find the ancestor just before the root element + while (node !== rootNode) { + parentNode = node; + node = node.parentNode; + } - if (node.nodeType == 3) { - rng.setStart(node, node.length); - rng.setEnd(node, node.length); - } else { - if (node.nodeName == 'BR') { - rng.setStartBefore(node); - rng.setEndBefore(node); - } else { - rng.setStartAfter(node); - rng.setEndAfter(node); - } - } + // 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); + } - selection.setRng(rng); + marker = dom.get('mce_marker'); - // Scroll range into view scrollIntoView on element can't be used since it will scroll the main view port as well - if (!tinymce.isIE) { - node = dom.create('span', null, '\u00a0'); - rng.insertNode(node); - nodeRect = dom.getRect(node); - viewPortRect = dom.getViewPort(editor.getWin()); - - // Check if node is out side the viewport if it is then scroll to it - if ((nodeRect.y > viewPortRect.y + viewPortRect.h || nodeRect.y < viewPortRect.y) || - (nodeRect.x > viewPortRect.x + viewPortRect.w || nodeRect.x < viewPortRect.x)) { - editor.getBody().scrollLeft = nodeRect.x; - editor.getBody().scrollTop = nodeRect.y; - } + // 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()); - dom.remove(node); - } + // 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; + } - // Make sure that the selection is collapsed after we removed the node fixes a WebKit bug - // where WebKit would place the endContainer/endOffset at a different location than the startContainer/startOffset - selection.collapse(true); - } + // Move selection before marker and remove it + rng = dom.createRng(); + + // 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); } + // Remove the marker node and set the new range + dom.remove(marker); + selection.setRng(rng); + + // Dispatch after event and add any visual elements needed selection.onSetContent.dispatch(selection, args); editor.addVisual(); }, @@ -12847,7 +13481,7 @@ tinymce.create('tinymce.ui.Toolbar:tinymce.ui.Container', { }, mceToggleFormat : function(command, ui, value) { - editor.formatter.toggle(value); + formatter.toggle(value); }, InsertHorizontalRule : function() { @@ -12864,42 +13498,27 @@ tinymce.create('tinymce.ui.Toolbar:tinymce.ui.Container', { }, mceInsertLink : function(command, ui, value) { - var link = dom.getParent(selection.getNode(), 'a'), img, floatVal; + var anchor; - if (tinymce.is(value, 'string')) + if (typeof(value) == 'string') value = {href : value}; + anchor = dom.getParent(selection.getNode(), 'a'); + // 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'); - if (!link) { - // WebKit can't create links on float images for some odd reason so just remove it and restore it later - if (tinymce.isWebKit) { - img = dom.getParent(selection.getNode(), 'img'); + // Remove existing links if there could be child links or that the href isn't specified + if (!anchor || !value.href) { + formatter.remove('link'); + } - if (img) { - floatVal = img.style.cssFloat; - img.style.cssFloat = null; - } - } - - execNativeCommand('CreateLink', FALSE, 'javascript:mctmp(0);'); - - // Restore float value - if (floatVal) - img.style.cssFloat = floatVal; - - 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); + // Apply new link to selection + if (value.href) { + formatter.apply('link', value, anchor); } }, - + selectAll : function() { var root = dom.getRoot(), rng = dom.createRng(); @@ -12979,7 +13598,7 @@ tinymce.create('tinymce.ui.Toolbar:tinymce.ui.Container', { var Dispatcher = tinymce.util.Dispatcher; tinymce.UndoManager = function(editor) { - var self, index = 0, data = []; + var self, index = 0, data = [], beforeBookmark; function getContent() { return tinymce.trim(editor.getContent({format : 'raw', no_events : 1})); @@ -12995,9 +13614,7 @@ tinymce.create('tinymce.ui.Toolbar:tinymce.ui.Container', { onRedo : new Dispatcher(self), beforeChange : function() { - // Set before bookmark on previous level - if (data[index]) - data[index].beforeBookmark = editor.selection.getBookmark(2, true); + beforeBookmark = editor.selection.getBookmark(2, true); }, add : function(level) { @@ -13011,6 +13628,10 @@ tinymce.create('tinymce.ui.Toolbar:tinymce.ui.Container', { if (lastLevel && lastLevel.content == level.content) return null; + // Set before bookmark on previous level + if (data[index]) + data[index].beforeBookmark = beforeBookmark; + // Time to compress if (settings.custom_undo_redo_levels) { if (data.length > settings.custom_undo_redo_levels) { @@ -13162,28 +13783,94 @@ tinymce.create('tinymce.ui.Toolbar:tinymce.ui.Container', { s.element = elm.toUpperCase(); ed.onPreInit.add(t.setup, t); - - if (s.forced_root_block) { - ed.onInit.add(t.forceRoots, t); - ed.onSetContent.add(t.forceRoots, t); - ed.onBeforeGetContent.add(t.forceRoots, t); - ed.onExecCommand.add(function(ed, cmd) { - if (cmd == 'mceInsertContent') { - t.forceRoots(); - ed.nodeChanged(); - } - }); - } }, setup : function() { - var t = this, ed = t.editor, s = ed.settings, dom = ed.dom, selection = ed.selection; + var t = this, ed = t.editor, s = ed.settings, dom = ed.dom, selection = ed.selection, blockElements = ed.schema.getBlockElements(); - // Force root blocks when typing and when getting output + // Force root blocks if (s.forced_root_block) { - ed.onBeforeExecCommand.add(t.forceRoots, t); - ed.onKeyUp.add(t.forceRoots, t); - ed.onPreProcess.add(t.forceRoots, t); + function addRootBlocks() { + var node = selection.getStart(), rootNode = ed.getBody(), rng, startContainer, startOffset, endContainer, endOffset, rootBlockNode, tempNode, offset = -0xFFFFFF; + + if (!node || node.nodeType !== 1) + return; + + // Check if node is wrapped in block + while (node != rootNode) { + if (blockElements[node.nodeName]) + return; + + node = node.parentNode; + } + + // Get current selection + rng = selection.getRng(); + if (rng.setStart) { + startContainer = rng.startContainer; + startOffset = rng.startOffset; + endContainer = rng.endContainer; + endOffset = rng.endOffset; + } else { + // Force control range into text range + if (rng.item) { + rng = ed.getDoc().body.createTextRange(); + rng.moveToElementText(rng.item(0)); + } + + tmpRng = rng.duplicate(); + tmpRng.collapse(true); + startOffset = tmpRng.move('character', offset) * -1; + + if (!tmpRng.collapsed) { + tmpRng = rng.duplicate(); + tmpRng.collapse(false); + endOffset = (tmpRng.move('character', offset) * -1) - startOffset; + } + } + + // Wrap non block elements and text nodes + for (node = rootNode.firstChild; node; node) { + if (node.nodeType === 3 || (node.nodeType == 1 && !blockElements[node.nodeName])) { + if (!rootBlockNode) { + rootBlockNode = dom.create(s.forced_root_block); + node.parentNode.insertBefore(rootBlockNode, node); + } + + tempNode = node; + node = node.nextSibling; + rootBlockNode.appendChild(tempNode); + } else { + rootBlockNode = null; + node = node.nextSibling; + } + } + + if (rng.setStart) { + rng.setStart(startContainer, startOffset); + rng.setEnd(endContainer, endOffset); + selection.setRng(rng); + } else { + try { + rng = ed.getDoc().body.createTextRange(); + rng.moveToElementText(rootNode); + rng.collapse(true); + rng.moveStart('character', startOffset); + + if (endOffset > 0) + rng.moveEnd('character', endOffset); + + rng.select(); + } catch (ex) { + // Ignore + } + } + + ed.nodeChanged(); + }; + + ed.onKeyUp.add(addRootBlocks); + ed.onClick.add(addRootBlocks); } if (s.force_br_newlines) { @@ -13330,155 +14017,6 @@ tinymce.create('tinymce.ui.Toolbar:tinymce.ui.Container', { } }, - find : function(n, t, s) { - var ed = this.editor, w = ed.getDoc().createTreeWalker(n, 4, null, FALSE), c = -1; - - while (n = w.nextNode()) { - c++; - - // Index by node - if (t == 0 && n == s) - return c; - - // Node by index - if (t == 1 && c == s) - return n; - } - - return -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; - - // Fix for bug #1863847 - //if (e && e.keyCode == 13) - // return TRUE; - - // Wrap non blocks into blocks - for (i = nl.length - 1; i >= 0; i--) { - nx = nl[i]; - - // Ignore internal elements - if (nx.nodeType === 1 && nx.getAttribute('data-mce-type')) { - bl = null; - continue; - } - - // 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 || r.setStart) { - // 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; - } - - tr = d.body.createTextRange(); - tr.moveToElementText(b); - tr.collapse(1); - bp = tr.move('character', c) * -1; - - tr = r.duplicate(); - tr.collapse(1); - sp = tr.move('character', c) * -1; - - tr = r.duplicate(); - tr.collapse(0); - le = (tr.move('character', c) * -1) - sp; - - si = sp - bp; - ei = le; - } - } - - // 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 - } - - // Restore selection - if (si != -2) { - if (!isIE || r.setStart) { - bl = b.getElementsByTagName(ed.settings.element)[0]; - r = d.createRange(); - - // Select last location or generated block - if (si != -1) - r.setStart(t.find(b, 1, si), so); - else - r.setStart(bl, 0); - - // Select last location or generated block - if (ei != -1) - r.setEnd(t.find(b, 1, ei), eo); - else - r.setEnd(bl, 0); - - 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 || r.setStart) && (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); - } - }, - getParentBlock : function(n) { var d = this.dom; @@ -13552,6 +14090,11 @@ tinymce.create('tinymce.ui.Toolbar:tinymce.ui.Container', { ra.setStart(en, 0); } + // If the body is totally empty add a BR element this might happen on webkit + if (!d.body.hasChildNodes()) { + d.body.appendChild(dom.create('br')); + } + // 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; @@ -13666,10 +14209,6 @@ tinymce.create('tinymce.ui.Toolbar:tinymce.ui.Container', { if (aft.firstChild && aft.firstChild.nodeName == bn) aft.innerHTML = aft.firstChild.innerHTML; - // Padd empty blocks - if (dom.isEmpty(bef)) - bef.innerHTML = '
    '; - function appendStyles(e, en) { var nl = [], nn, n, i; @@ -13699,6 +14238,10 @@ tinymce.create('tinymce.ui.Toolbar:tinymce.ui.Container', { } else e.innerHTML = isOpera ? '\u00a0' : '
    '; // Extra space for Opera so that the caret can move there }; + + // Padd empty blocks + if (dom.isEmpty(bef)) + appendStyles(bef, sn); // Fill empty afterblook with current style if (dom.isEmpty(aft)) @@ -13717,16 +14260,9 @@ tinymce.create('tinymce.ui.Toolbar:tinymce.ui.Container', { aft.normalize(); bef.normalize(); - function first(n) { - return d.createTreeWalker(n, NodeFilter.SHOW_TEXT, null, FALSE).nextNode() || n; - }; - // 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); + ed.selection.select(aft, true); + ed.selection.collapse(true); // 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; @@ -13954,7 +14490,12 @@ 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; @@ -14017,7 +14558,7 @@ tinymce.create('tinymce.ui.Toolbar:tinymce.ui.Container', { 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); @@ -14418,6 +14959,10 @@ tinymce.create('tinymce.ui.Toolbar:tinymce.ui.Container', { 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)); }); @@ -14434,8 +14979,89 @@ 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 = wrapElm.cloneNode(FALSE); + + // create a list of the nodes on the same side of the list as the selection + each(tinymce.grep(node.childNodes), function(n, index) { + if ((startIndex < listIndex && index < listIndex) || (startIndex > listIndex && index > listIndex)) { + nodes.push(n); + n.parentNode.removeChild(n); + } + }); + + // insert the wrapping element either before or after the list. + if (startIndex < listIndex) { + node.insertBefore(currentWrapElm, list); + } else if (startIndex > listIndex) { + node.insertBefore(currentWrapElm, list.nextSibling); + } + + // add the new nodes to the list. + newWrappers.push(currentWrapElm); + + each(nodes, function(node) { + currentWrapElm.appendChild(node); + }); - function applyRngStyle(rng) { + return currentWrapElm; + } + }; + + function applyRngStyle(rng, bookmark) { var newWrappers = [], wrapName, wrapElm; // Setup wrapper element @@ -14509,6 +15135,9 @@ tinymce.create('tinymce.ui.Toolbar:tinymce.ui.Container', { } 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; @@ -14641,7 +15270,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)); } @@ -14663,8 +15292,9 @@ tinymce.create('tinymce.ui.Toolbar:tinymce.ui.Container', { var curSelNode = ed.selection.getNode(); // 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)) { @@ -14683,7 +15313,6 @@ tinymce.create('tinymce.ui.Toolbar:tinymce.ui.Container', { function remove(name, vars, node) { var formatList = get(name), format = formatList[0], bookmark, i, rng; - function moveStart(rng) { var container = rng.startContainer, offset = rng.startOffset, @@ -14883,7 +15512,7 @@ tinymce.create('tinymce.ui.Toolbar:tinymce.ui.Container', { selection.moveToBookmark(bookmark); // Check if start element still has formatting then we are at: "text|text" and need to move the start into the next text node - if (match(name, vars, selection.getStart())) { + if (format.inline && match(name, vars, selection.getStart())) { moveStart(selection.getRng(true)); } @@ -14907,6 +15536,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 @@ -15716,34 +16350,67 @@ tinymce.create('tinymce.ui.Toolbar:tinymce.ui.Container', { // Only register listeners once if we need to if (!pendingFormats.isListening && hasPending()) { pendingFormats.isListening = true; + function performPendingFormat(node, textNode) { + var rng = dom.createRng(); + perform(node); + + rng.setStart(textNode, textNode.nodeValue.length); + rng.setEnd(textNode, textNode.nodeValue.length); + selection.setRng(rng); + ed.nodeChanged(); + } + var enterKeyPressed = false; each('onKeyDown,onKeyUp,onKeyPress,onMouseUp'.split(','), function(event) { ed[event].addToTop(function(ed, e) { + if (e.keyCode==13 && !e.shiftKey) { + enterKeyPressed = true; + return; + } // Do we have pending formats and is the selection moved has moved if (hasPending() && !tinymce.dom.RangeUtils.compareRanges(pendingFormats.lastRng, selection.getRng())) { + var foundCaret = false; each(dom.select('font,span'), function(node) { var textNode, rng; // Look for marker if (isCaretNode(node)) { + foundCaret = true; textNode = node.firstChild; - if (textNode) { - perform(node); + // Find the first text node within node + while (textNode && textNode.nodeType != 3) + textNode = textNode.firstChild; - rng = dom.createRng(); - rng.setStart(textNode, textNode.nodeValue.length); - rng.setEnd(textNode, textNode.nodeValue.length); - selection.setRng(rng); - ed.nodeChanged(); - } else + if (textNode) + performPendingFormat(node, textNode); + else dom.remove(node); } }); + + // no caret - so we are + if (enterKeyPressed && !foundCaret) { + var node = selection.getNode(); + var textNode = node; + + // Find the first text node within node + while (textNode && textNode.nodeType != 3) + textNode = textNode.firstChild; + if (textNode) { + node=textNode.parentNode; + while (!isBlock(node)){ + node=node.parentNode; + } + performPendingFormat(node, textNode); + } + } // Always unbind and clear pending styles on keyup - if (e.type == 'keyup' || e.type == 'mouseup') + if (e.type == 'keyup' || e.type == 'mouseup') { resetPending(); + enterKeyPressed=false; + } } }); }); -- cgit v1.2.3