app/assets/source/tinymce/tinymce.jquery.js in tinymce-rails-4.0.19 vs app/assets/source/tinymce/tinymce.jquery.js in tinymce-rails-4.0.26

- old
+ new

@@ -1,6 +1,6 @@ -// 4.0.19 (2014-03-11) +// 4.0.26 (2014-05-06) /** * Compiled inline version. (Library mode) */ @@ -312,12 +312,20 @@ return "'" + str.replace(/\'/g, "\\'") + "'"; } url = decode(url || url2 || url3); - if (!settings.allow_script_urls && /(java|vb)script:/i.test(url.replace(/[\s\r\n]+/, ''))) { - return ""; + if (!settings.allow_script_urls) { + var scriptUrl = url.replace(/[\s\r\n]+/, ''); + + if (/(java|vb)script:/i.test(scriptUrl)) { + return ""; + } + + if (!settings.allow_svg_data_urls && /^data:image\/svg/i.test(scriptUrl)) { + return ""; + } } // Convert the URL to relative/absolute depending on config if (urlConverter) { url = urlConverter.call(urlConverterScope, url, 'style'); @@ -338,12 +346,20 @@ // Parse styles while ((matches = styleRegExp.exec(css))) { name = matches[1].replace(trimRightRegExp, '').toLowerCase(); value = matches[2].replace(trimRightRegExp, ''); + // Decode escaped sequences like \65 -> e + /*jshint loopfunc:true*/ + /*eslint no-loop-func:0 */ + value = value.replace(/\\[0-9a-f]+/g, function(e) { + return String.fromCharCode(parseInt(e.substr(1), 16)); + }); + if (name && value.length > 0) { - if (!settings.allow_script_urls && (name == "behavior" || /expression\s*\(/.test(value))) { + // Don't allow behavior name or expression/comments within the values + if (!settings.allow_script_urls && (name == "behavior" || /expression\s*\(|\/\*|\*\//.test(value))) { continue; } // Opera will produce 700 instead of bold in their style values if (name === 'font-weight' && value === '700') { @@ -838,14 +854,17 @@ if (callback) { ci = callbackList.length; while (ci--) { if (callbackList[ci].func === callback) { var nativeHandler = callbackList.nativeHandler; + var fakeName = callbackList.fakeName, capture = callbackList.capture; // Clone callbackList since unbind inside a callback would otherwise break the handlers loop callbackList = callbackList.slice(0, ci).concat(callbackList.slice(ci + 1)); callbackList.nativeHandler = nativeHandler; + callbackList.fakeName = fakeName; + callbackList.capture = capture; eventMap[name] = callbackList; } } } @@ -2386,11 +2405,11 @@ "tinymce/util/Tools" ], function(Tools) { var makeMap = Tools.makeMap; var namedEntities, baseEntities, reverseEntities, - attrsCharsRegExp = /[&<>\"\u007E-\uD7FF\uE000-\uFFEF]|[\uD800-\uDBFF][\uDC00-\uDFFF]/g, + attrsCharsRegExp = /[&<>\"\u0060\u007E-\uD7FF\uE000-\uFFEF]|[\uD800-\uDBFF][\uDC00-\uDFFF]/g, textCharsRegExp = /[<>&\u007E-\uD7FF\uE000-\uFFEF]|[\uD800-\uDBFF][\uDC00-\uDFFF]/g, rawCharsRegExp = /[<>&\"\']/g, entityRegExp = /&(#x|#)?([\w]+);/g, asciiMap = { 128: "\u20AC", 130: "\u201A", 131: "\u0192", 132: "\u201E", 133: "\u2026", 134: "\u2020", @@ -2404,11 +2423,12 @@ baseEntities = { '\"': '&quot;', // Needs to be escaped since the YUI compressor would otherwise break the code "'": '&#39;', '<': '&lt;', '>': '&gt;', - '&': '&amp;' + '&': '&amp;', + '\u0060': '&#96;' }; // Reverse lookup table for raw entities reverseEntities = { '&lt;': '<', @@ -3388,11 +3408,12 @@ // Is non element if (elm.nodeType && elm.nodeType != 1) { return false; } - return Sizzle.matches(selector, elm.nodeType ? [elm] : elm).length > 0; + var elms = elm.nodeType ? [elm] : elm; + return Sizzle(selector, elms[0].ownerDocument || elms[0], null, elms).length > 0; }, // #endif /** @@ -4676,13 +4697,13 @@ return false; } // Keep elements with data-bookmark attributes or name attribute like <a name="1"></a> attributes = self.getAttribs(node); - i = node.attributes.length; + i = attributes.length; while (i--) { - name = node.attributes[i].nodeName; + name = attributes[i].nodeName; if (name === "name" || name === 'data-mce-bookmark') { return false; } } } @@ -4936,11 +4957,11 @@ // Returns the content editable state of a node getContentEditable: function(node) { var contentEditable; // Check type - if (node.nodeType != 1) { + if (!node || node.nodeType != 1) { return null; } // Check for fake content editable contentEditable = node.getAttribute("data-mce-contenteditable"); @@ -4950,10 +4971,24 @@ // Check for real content editable return node.contentEditable !== "inherit" ? node.contentEditable : null; }, + getContentEditableParent: function(node) { + var root = this.getRoot(), state = null; + + for (; node && node !== root; node = node.parentNode) { + state = this.getContentEditable(node); + + if (state !== null) { + break; + } + } + + return state; + }, + /** * Destroys all internal references to the DOM to solve IE leak issues. * * @method destroy */ @@ -4979,10 +5014,22 @@ } self.win = self.doc = self.root = self.events = self.frag = null; }, + isChildOf: function(node, parent) { + while (node) { + if (parent === node) { + return true; + } + + node = node.parentNode; + } + + return false; + }, + // #ifdef debug dumpRng: function(r) { return ( 'startContainer: ' + r.startContainer.nodeName + @@ -5353,16 +5400,25 @@ * @method requireLangPack * @param {String} name Short name of the add-on. * @param {String} languages Optional comma or space separated list of languages to check if it matches the name. */ requireLangPack: function(name, languages) { - if (AddOnManager.language && AddOnManager.languageLoad !== false) { - if (languages && new RegExp('([, ]|\\b)' + AddOnManager.language + '([, ]|\\b)').test(languages) === false) { - return; + var language = AddOnManager.language; + + if (language && AddOnManager.languageLoad !== false) { + if (languages) { + languages = ',' + languages + ','; + + // Load short form sv.js or long form sv_SE.js + if (languages.indexOf(',' + language.substr(0, 2) + ',') != -1) { + language = language.substr(0, 2); + } else if (languages.indexOf(',' + language + ',') == -1) { + return; + } } - ScriptLoader.ScriptLoader.add(this.urls[name] + '/langs/' + AddOnManager.language + '.js'); + ScriptLoader.ScriptLoader.add(this.urls[name] + '/langs/' + language + '.js'); } }, /** * Adds a instance of the add-on by it's short name. @@ -6272,12 +6328,12 @@ add("ruby", "", phrasingContent, "rt rp"); add("figcaption", "", flowContent); add("mark rt rp summary bdi", "", phrasingContent); add("canvas", "width height", flowContent); add("video", "src crossorigin poster preload autoplay mediagroup loop " + - "muted controls width height", flowContent, "track source"); - add("audio", "src crossorigin preload autoplay mediagroup loop muted controls", flowContent, "track source"); + "muted controls width height buffered", flowContent, "track source"); + add("audio", "src crossorigin preload autoplay mediagroup loop muted controls buffered volume", flowContent, "track source"); add("source", "src type media"); add("track", "kind src srclang label default"); add("datalist", "", phrasingContent, "option"); add("article section nav aside header footer", "", flowContent); add("hgroup", "", "h1 h2 h3 h4 h5 h6"); @@ -6332,11 +6388,11 @@ if (type != "html4") { addAttrs("input button select textarea", "autofocus"); addAttrs("input textarea", "placeholder"); addAttrs("a", "download"); addAttrs("link script img", "crossorigin"); - addAttrs("iframe", "srcdoc sandbox seamless allowfullscreen"); + addAttrs("iframe", "sandbox seamless allowfullscreen"); // Excluded: srcdoc } // Special: iframe, ruby, video, audio, label // Delete children of the same name from it's parent @@ -6391,11 +6447,11 @@ mapCache[option] = value; } } else { // Create custom map - value = makeMap(value, ',', makeMap(value.toUpperCase(), ' ')); + value = makeMap(value, /[, ]/, makeMap(value.toUpperCase(), /[, ]/)); } return value; } @@ -6605,10 +6661,13 @@ // Adds custom non HTML elements to the schema function addCustomElements(custom_elements) { var customElementRegExp = /^(~)?(.+)$/; if (custom_elements) { + // Flush cached items since we are altering the default maps + mapCache.text_block_elements = mapCache.block_elements = null; + each(split(custom_elements, ','), function(rule) { var matches = customElementRegExp.exec(rule), inline = matches[1] === '~', cloneName = inline ? 'span' : 'div', name = matches[2]; @@ -6632,12 +6691,13 @@ elements[name] = customRule; } // Add custom elements at span/div positions - each(children, function(element) { + each(children, function(element, elmName) { if (element[cloneName]) { + children[elmName] = element = extend({}, children[elmName]); element[name] = element[cloneName]; } }); }); } @@ -6663,10 +6723,14 @@ parent = children[matches[2]]; each(split(matches[3], '|'), function(child) { if (prefix === '-') { + // Clone the element before we delete + // things in it to not mess up default schemas + children[matches[2]] = parent = extend({}, children[matches[2]]); + delete parent[child]; } else { parent[child] = {}; } }); @@ -7081,12 +7145,12 @@ var self = this, matches, index = 0, value, endRegExp, stack = [], attrList, i, text, name; var isInternalElement, removeInternalElements, shortEndedElements, fillAttrsMap, isShortEnded; var validate, elementRule, isValidElement, attr, attribsValue, validAttributesMap, validAttributePatterns; var attributesRequired, attributesDefault, attributesForced; var anyAttributesRequired, selfClosing, tokenRegExp, attrRegExp, specialElements, attrValue, idCount = 0; - var decode = Entities.decode, fixSelfClosing, filteredUrlAttrs = Tools.makeMap('src,href'); - var scriptUriRegExp = /(java|vb)script:/i; + var decode = Entities.decode, fixSelfClosing, filteredUrlAttrs = Tools.makeMap('src,href,data,background,formaction,poster'); + var scriptUriRegExp = /((java|vb)script|mhtml):/i, dataUriRegExp = /^data:/i; function processEndTag(name) { var pos, i; // Find position of parent of the same type @@ -7148,27 +7212,29 @@ if (attrRule.validValues && !(value in attrRule.validValues)) { return; } } - // Block any javascript: urls + // Block any javascript: urls or non image data uris if (filteredUrlAttrs[name] && !settings.allow_script_urls) { var uri = value.replace(trimRegExp, ''); try { // Might throw malformed URI sequence uri = decodeURIComponent(uri); - if (scriptUriRegExp.test(uri)) { - return; - } } catch (ex) { // Fallback to non UTF-8 decoder uri = unescape(uri); - if (scriptUriRegExp.test(uri)) { - return; - } } + + if (scriptUriRegExp.test(uri)) { + return; + } + + if (!settings.allow_html_data_urls && dataUriRegExp.test(uri) && !/^data:image\//i.test(uri)) { + return; + } } // Add attribute to list and map attrList.map[name] = value; attrList.push({ @@ -8580,10 +8646,21 @@ settings.entity_encoding = settings.entity_encoding || 'named'; settings.remove_trailing_brs = "remove_trailing_brs" in settings ? settings.remove_trailing_brs : true; htmlParser = new DomParser(settings, schema); + // Convert tabindex back to elements when serializing contents + htmlParser.addAttributeFilter('data-mce-tabindex', function(nodes, name) { + var i = nodes.length, node; + + while (i--) { + node = nodes[i]; + node.attr('tabindex', node.attributes.map['data-mce-tabindex']); + node.attr(name, null); + } + }); + // Convert move data-mce-src, data-mce-href and data-mce-style into nodes or process them if needed htmlParser.addAttributeFilter('src,href,style', function(nodes, name) { var i = nodes.length, node, value, internalName = 'data-mce-' + name; var urlConverter = settings.url_converter, urlConverterScope = settings.url_converter_scope, undef; @@ -9631,10 +9708,12 @@ } function showResizeRect(targetElm, mouseDownHandleName, mouseDownEvent) { var position, targetWidth, targetHeight, e, rect, offsetParent = editor.getBody(); + unbindResizeHandleEvents(); + // Get position and size of target position = dom.getPos(targetElm, offsetParent); selectedElmX = position.x; selectedElmY = position.y; rect = targetElm.getBoundingClientRect(); // Fix for Gecko offsetHeight for table with caption @@ -9710,18 +9789,22 @@ // Hides IE move layer cursor // If we set it on Chrome we get this wounderful bug: #6725 if (Env.ie) { handleElm.contentEditable = false; } + } else { + dom.show(handleElm); + } + if (!handle.elm) { dom.bind(handleElm, 'mousedown', function(e) { e.stopImmediatePropagation(); e.preventDefault(); startDrag(e); }); - } else { - dom.show(handleElm); + + handle.elm = handleElm; } /* var halfHandleW = handleElm.offsetWidth / 2; var halfHandleH = handleElm.offsetHeight / 2; @@ -9747,10 +9830,12 @@ } function hideResizeRect() { var name, handleElm; + unbindResizeHandleEvents(); + if (selectedElm) { selectedElm.removeAttribute('data-mce-selected'); } for (name in resizeHandles) { @@ -9856,10 +9941,21 @@ function detachResizeStartListener() { detachEvent(selectedElm, 'resizestart', resizeNativeStart); } + function unbindResizeHandleEvents() { + for (var name in resizeHandles) { + var handle = resizeHandles[name]; + + if (handle.elm) { + dom.unbind(handle.elm); + delete handle.elm; + } + } + } + function disableGeckoResize() { try { // Disable object resizing on Gecko editor.getDoc().execCommand('enableObjectResizing', false, false); } catch (ex) { @@ -9937,14 +10033,18 @@ if (selectedElm && selectedElm.nodeName == "TABLE") { updateResizeRect(e); } }); + editor.on('hide', hideResizeRect); + // Hide rect on focusout since it would float on top of windows otherwise //editor.on('focusout', hideResizeRect); }); + editor.on('remove', unbindResizeHandleEvents); + function destroy() { selectedElm = selectedElmGhost = null; if (isIE) { detachResizeStartListener(); @@ -10217,11 +10317,11 @@ */ this.normalize = function(rng) { var normalized, collapsed; function normalizeEndPoint(start) { - var container, offset, walker, body = dom.getRoot(), node, nonEmptyElementsMap, nodeName; + var container, offset, walker, body = dom.getRoot(), node, nonEmptyElementsMap; var directionLeft, isAfterNode; function hasBrBeforeAfter(node, left) { var walker = new TreeWalker(node, dom.getParent(node.parentNode, dom.isBlock) || body); @@ -10254,10 +10354,15 @@ } // Walk left until we hit a text node we can move to or a block/br/img walker = new TreeWalker(startNode, parentBlockContainer); while ((node = walker[left ? 'prev' : 'next']())) { + // Break if we hit a non content editable node + if (dom.getContentEditableParent(node) === "false") { + return; + } + // Found text node that has a length if (node.nodeType === 3 && node.nodeValue.length > 0) { container = node; offset = left ? node.nodeValue.length : 0; normalized = true; @@ -10300,11 +10405,10 @@ if (container === body) { // If start is before/after a image, table etc if (directionLeft) { node = container.childNodes[offset > 0 ? offset - 1 : 0]; if (node) { - nodeName = node.nodeName.toLowerCase(); if (nonEmptyElementsMap[node.nodeName] || node.nodeName == "TABLE") { return; } } } @@ -11702,10 +11806,164 @@ }; return Selection; }); +// Included from: js/tinymce/classes/fmt/Preview.js + +/** + * Preview.js + * + * Copyright, Moxiecode Systems AB + * Released under LGPL License. + * + * License: http://www.tinymce.com/license + * Contributing: http://www.tinymce.com/contributing + */ + +/** + * Internal class for generating previews styles for formats. + * + * Example: + * Preview.getCssText(editor, 'bold'); + * + * @class tinymce.fmt.Preview + * @private + */ +define("tinymce/fmt/Preview", [ + "tinymce/util/Tools" +], function(Tools) { + var each = Tools.each; + + function getCssText(editor, format) { + var name, previewElm, dom = editor.dom; + var previewCss = '', parentFontSize, previewStyles; + + previewStyles = editor.settings.preview_styles; + + // No preview forced + if (previewStyles === false) { + return ''; + } + + // Default preview + if (!previewStyles) { + previewStyles = 'font-family font-size font-weight font-style text-decoration ' + + 'text-transform color background-color border border-radius outline text-shadow'; + } + + // Removes any variables since these can't be previewed + function removeVars(val) { + return val.replace(/%(\w+)/g, ''); + } + + // Create block/inline element to use for preview + if (typeof(format) == "string") { + format = editor.formatter.get(format); + if (!format) { + return; + } + + format = format[0]; + } + + name = format.block || format.inline || 'span'; + previewElm = dom.create(name); + + // Add format styles to preview element + each(format.styles, function(value, name) { + value = removeVars(value); + + if (value) { + dom.setStyle(previewElm, name, value); + } + }); + + // Add attributes to preview element + each(format.attributes, function(value, name) { + value = removeVars(value); + + if (value) { + dom.setAttrib(previewElm, name, value); + } + }); + + // Add classes to preview element + each(format.classes, function(value) { + value = removeVars(value); + + if (!dom.hasClass(previewElm, value)) { + dom.addClass(previewElm, value); + } + }); + + editor.fire('PreviewFormats'); + + // Add the previewElm outside the visual area + dom.setStyles(previewElm, {position: 'absolute', left: -0xFFFF}); + editor.getBody().appendChild(previewElm); + + // Get parent container font size so we can compute px values out of em/% for older IE:s + parentFontSize = dom.getStyle(editor.getBody(), 'fontSize', true); + parentFontSize = /px$/.test(parentFontSize) ? parseInt(parentFontSize, 10) : 0; + + each(previewStyles.split(' '), function(name) { + var value = dom.getStyle(previewElm, name, true); + + // If background is transparent then check if the body has a background color we can use + if (name == 'background-color' && /transparent|rgba\s*\([^)]+,\s*0\)/.test(value)) { + value = dom.getStyle(editor.getBody(), name, true); + + // Ignore white since it's the default color, not the nicest fix + // TODO: Fix this by detecting runtime style + if (dom.toHex(value).toLowerCase() == '#ffffff') { + return; + } + } + + if (name == 'color') { + // Ignore black since it's the default color, not the nicest fix + // TODO: Fix this by detecting runtime style + if (dom.toHex(value).toLowerCase() == '#000000') { + return; + } + } + + // Old IE won't calculate the font size so we need to do that manually + if (name == 'font-size') { + if (/em|%$/.test(value)) { + if (parentFontSize === 0) { + return; + } + + // Convert font size from em/% to px + value = parseFloat(value, 10) / (/%$/.test(value) ? 100 : 1); + value = (value * parentFontSize) + 'px'; + } + } + + if (name == "border" && value) { + previewCss += 'padding:0 2px;'; + } + + previewCss += name + ':' + value + ';'; + }); + + editor.fire('AfterPreviewFormats'); + + //previewCss += 'line-height:normal'; + + dom.remove(previewElm); + + return previewCss; + } + + return { + getCssText: getCssText + }; +}); + // Included from: js/tinymce/classes/Formatter.js /** * Formatter.js * @@ -11731,12 +11989,13 @@ * tinymce.activeEditor.formatter.apply('mycustomformat'); */ define("tinymce/Formatter", [ "tinymce/dom/TreeWalker", "tinymce/dom/RangeUtils", - "tinymce/util/Tools" -], function(TreeWalker, RangeUtils, Tools) { + "tinymce/util/Tools", + "tinymce/fmt/Preview" +], function(TreeWalker, RangeUtils, Tools, Preview) { /** * Constructs a new formatter instance. * * @constructor Formatter * @param {tinymce.Editor} ed Editor instance to construct the formatter engine to. @@ -11781,10 +12040,23 @@ return node.nodeType === 1 && node.id === '_mce_caret'; } function defaultFormats() { register({ + + valigntop: [ + {selector: 'td,th', styles: {'verticalAlign': 'top'}} + ], + + valignmiddle: [ + {selector: 'td,th', styles: {'verticalAlign': 'middle'}} + ], + + valignbottom: [ + {selector: 'td,th', styles: {'verticalAlign': 'bottom'}} + ], + alignleft: [ {selector: 'figure,p,h1,h2,h3,h4,h5,h6,td,th,tr,div,ul,ol,li', styles: {textAlign: 'left'}, defaultBlock: 'div'}, {selector: 'img,table', collapsed: false, styles: {'float': 'left'}} ], @@ -11995,10 +12267,20 @@ each(fmt.styles, function(value, name) { dom.setStyle(elm, name, replaceVars(value, vars)); }); + // Needed for the WebKit span spam bug + // TODO: Remove this once WebKit/Blink fixes this + if (fmt.styles) { + var styleVal = dom.getAttrib(elm, 'style'); + + if (styleVal) { + elm.setAttribute('data-mce-style', styleVal); + } + } + each(fmt.attributes, function(value, name) { dom.setAttrib(elm, name, replaceVars(value, vars)); }); each(fmt.classes, function(value) { @@ -12897,10 +13179,24 @@ }); return this; } + /** + * Returns a preview css text for the specified format. + * + * @method getCssText + * @param {String/Object} format Format to generate preview css text for. + * @return {String} Css text for the specified format. + * @example + * var cssText1 = editor.formatter.getCssText('bold'); + * var cssText2 = editor.formatter.getCssText({inline: 'b'}); + */ + function getCssText(format) { + return Preview.getCssText(ed, format); + } + // Expose to public extend(this, { get: get, register: register, apply: apply, @@ -12908,11 +13204,12 @@ toggle: toggle, match: match, matchAll: matchAll, matchNode: matchNode, canApply: canApply, - formatChanged: formatChanged + formatChanged: formatChanged, + getCssText: getCssText }); // Initialize defaultFormats(); addKeyboardShortcuts(); @@ -14161,11 +14458,11 @@ '<div[^>]+data-mce-bogus[^>]+><\\/div>', // Trim bogus divs like resize handles '\\s?data-mce-selected="[^"]+"' // Trim temporaty data-mce prefixed attributes like data-mce-selected ].join('|'), 'gi'); return function(editor) { - var self = this, index = 0, data = [], beforeBookmark, isFirstTypedCharacter, lock; + var self = this, index = 0, data = [], beforeBookmark, isFirstTypedCharacter, locks = 0; // Returns a trimmed version of the current editor contents function getContent() { return trim(editor.getContent({format: 'raw', no_events: 1}).replace(trimContentRegExp, '')); } @@ -14201,11 +14498,11 @@ editor.on('ObjectResizeStart', function() { self.beforeChange(); }); editor.on('SaveContent ObjectResized blur', addNonTypingUndoLevel); - editor.dom.bind(editor.dom.getRoot(), 'dragend', addNonTypingUndoLevel); + editor.on('DragEnd', addNonTypingUndoLevel); editor.on('KeyUp', function(e) { var keyCode = e.keyCode; if ((keyCode >= 33 && keyCode <= 36) || (keyCode >= 37 && keyCode <= 40) || keyCode == 45 || keyCode == 13 || e.ctrlKey) { @@ -14289,11 +14586,11 @@ * the change has been made. * * @method beforeChange */ beforeChange: function() { - if (!lock) { + if (!locks) { beforeBookmark = editor.selection.getBookmark(2, true); } }, /** @@ -14308,20 +14605,20 @@ var i, settings = editor.settings, lastLevel; level = level || {}; level.content = getContent(); - if (lock || editor.removed) { + if (locks || editor.removed) { return null; } - if (editor.fire('BeforeAddUndo', {level: level, originalEvent: event}).isDefaultPrevented()) { + lastLevel = data[index]; + if (editor.fire('BeforeAddUndo', {level: level, lastLevel: lastLevel, originalEvent: event}).isDefaultPrevented()) { return null; } // Add undo level if needed - lastLevel = data[index]; if (lastLevel && lastLevel.content == level.content) { return null; } // Set before bookmark on previous level @@ -14459,13 +14756,16 @@ * @param {function} callback Function to execute dom manipulation logic in. */ transact: function(callback) { self.beforeChange(); - lock = true; - callback(); - lock = false; + try { + locks++; + callback(); + } finally { + locks--; + } self.add(); } }; @@ -16022,11 +16322,17 @@ * @class tinymce.util.URI */ define("tinymce/util/URI", [ "tinymce/util/Tools" ], function(Tools) { - var each = Tools.each, trim = Tools.trim; + var each = Tools.each, trim = Tools.trim, + DEFAULT_PORTS = { + 'ftp': 21, + 'http': 80, + 'https': 443, + 'mailto': 25 + }; /** * Constructs a new URI instance. * * @constructor @@ -16194,14 +16500,38 @@ * var url = new tinymce.util.URI('http://www.site.com/dir/').toAbsolute('somedir/somefile.htm'); */ toAbsolute: function(uri, noHost) { uri = new URI(uri, {base_uri: this}); - return uri.getURI(this.host == uri.host && this.protocol == uri.protocol ? noHost : 0); + return uri.getURI(noHost && this.isSameOrigin(uri)); }, /** + * Determine whether the given URI has the same origin as this URI. Based on RFC-6454. + * Supports default ports for protocols listed in DEFAULT_PORTS. Unsupported protocols will fail safe: they + * won't match, if the port specifications differ. + * + * @method isSameOrigin + * @param {tinymce.util.URI} uri Uri instance to compare. + * @returns {Boolean} True if the origins are the same. + */ + isSameOrigin: function(uri) { + if (this.host == uri.host && this.protocol == uri.protocol){ + if (this.port == uri.port) { + return true; + } + + var defaultPort = DEFAULT_PORTS[this.protocol]; + if (defaultPort && ((this.port || defaultPort) == (uri.port || defaultPort))) { + return true; + } + } + + return false; + }, + + /** * Converts a absolute path into a relative path. * * @method toRelPath * @param {String} base Base point to convert the path from. * @param {String} path Absolute path to convert into a relative path. @@ -16452,10 +16782,12 @@ } // Instantiate a base class (but only create the instance, // don't run the init constructor) initializing = true; + + /*eslint new-cap:0 */ prototype = new self(); initializing = false; // Add mixins if (prop.Mixins) { @@ -16538,10 +16870,273 @@ }; return Class; }); +// Included from: js/tinymce/classes/util/EventDispatcher.js + +/** + * EventDispatcher.js + * + * Copyright, Moxiecode Systems AB + * Released under LGPL License. + * + * License: http://www.tinymce.com/license + * Contributing: http://www.tinymce.com/contributing + */ + +/** + * This class lets you add/remove and fire events by name on the specified scope. This makes + * it easy to add event listener logic to any class. + * + * @class tinymce.util.EventDispatcher + * @example + * var eventDispatcher = new EventDispatcher(); + * + * eventDispatcher.on('click', function() {console.log('data');}); + * eventDispatcher.fire('click', {data: 123}); + */ +define("tinymce/util/EventDispatcher", [ + "tinymce/util/Tools" +], function(Tools) { + var nativeEvents = Tools.makeMap( + "focus blur focusin focusout click dblclick mousedown mouseup mousemove mouseover beforepaste paste cut copy selectionchange " + + "mouseout mouseenter mouseleave wheel keydown keypress keyup input contextmenu dragstart dragend dragover " + + "draggesture dragdrop drop drag submit", + ' ' + ); + + function Dispatcher(settings) { + var self = this, scope, bindings = {}, toggleEvent; + + function returnFalse() { + return false; + } + + function returnTrue() { + return true; + } + + settings = settings || {}; + scope = settings.scope || self; + toggleEvent = settings.toggleEvent || returnFalse; + + /** + * Fires the specified event by name. + * + * @method fire + * @param {String} name Name of the event to fire. + * @param {Object?} args Event arguments. + * @return {Object} Event args instance passed in. + * @example + * instance.fire('event', {...}); + */ + function fire(name, args) { + var handlers, i, l, callback; + + name = name.toLowerCase(); + args = args || {}; + args.type = name; + + // Setup target is there isn't one + if (!args.target) { + args.target = scope; + } + + // Add event delegation methods if they are missing + if (!args.preventDefault) { + // Add preventDefault method + args.preventDefault = function() { + args.isDefaultPrevented = returnTrue; + }; + + // Add stopPropagation + args.stopPropagation = function() { + args.isPropagationStopped = returnTrue; + }; + + // Add stopImmediatePropagation + args.stopImmediatePropagation = function() { + args.isImmediatePropagationStopped = returnTrue; + }; + + // Add event delegation states + args.isDefaultPrevented = returnFalse; + args.isPropagationStopped = returnFalse; + args.isImmediatePropagationStopped = returnFalse; + } + + if (settings.beforeFire) { + settings.beforeFire(args); + } + + handlers = bindings[name]; + if (handlers) { + for (i = 0, l = handlers.length; i < l; i++) { + handlers[i] = callback = handlers[i]; + + // Stop immediate propagation if needed + if (args.isImmediatePropagationStopped()) { + args.stopPropagation(); + return args; + } + + // If callback returns false then prevent default and stop all propagation + if (callback.call(scope, args) === false) { + args.preventDefault(); + return args; + } + } + } + + return args; + } + + /** + * Binds an event listener to a specific event by name. + * + * @method on + * @param {String} name Event name or space separated list of events to bind. + * @param {callback} callback Callback to be executed when the event occurs. + * @param {Boolean} first Optional flag if the event should be prepended. Use this with care. + * @return {Object} Current class instance. + * @example + * instance.on('event', function(e) { + * // Callback logic + * }); + */ + function on(name, callback, prepend) { + var handlers, names, i; + + if (callback === false) { + callback = returnFalse; + } + + if (callback) { + names = name.toLowerCase().split(' '); + i = names.length; + while (i--) { + name = names[i]; + handlers = bindings[name]; + if (!handlers) { + handlers = bindings[name] = []; + toggleEvent(name, true); + } + + if (prepend) { + handlers.unshift(callback); + } else { + handlers.push(callback); + } + } + } + + return self; + } + + /** + * Unbinds an event listener to a specific event by name. + * + * @method off + * @param {String?} name Name of the event to unbind. + * @param {callback?} callback Callback to unbind. + * @return {Object} Current class instance. + * @example + * // Unbind specific callback + * instance.off('event', handler); + * + * // Unbind all listeners by name + * instance.off('event'); + * + * // Unbind all events + * instance.off(); + */ + function off(name, callback) { + var i, handlers, bindingName, names, hi; + + if (name) { + names = name.toLowerCase().split(' '); + i = names.length; + while (i--) { + name = names[i]; + handlers = bindings[name]; + + // Unbind all handlers + if (!name) { + for (bindingName in bindings) { + toggleEvent(bindingName, false); + delete bindings[bindingName]; + } + + return self; + } + + if (handlers) { + // Unbind all by name + if (!callback) { + handlers.length = 0; + } else { + // Unbind specific ones + hi = handlers.length; + while (hi--) { + if (handlers[hi] === callback) { + handlers.splice(hi, 1); + } + } + } + + if (!handlers.length) { + toggleEvent(name, false); + delete bindings[name]; + } + } + } + } else { + for (name in bindings) { + toggleEvent(name, false); + } + + bindings = {}; + } + + return self; + } + + /** + * Returns true/false if the dispatcher has a event of the specified name. + * + * @method has + * @param {String} name Name of the event to check for. + * @return {Boolean} true/false if the event exists or not. + */ + function has(name) { + name = name.toLowerCase(); + return !(!bindings[name] || bindings[name].length === 0); + } + + // Expose + self.fire = fire; + self.on = on; + self.off = off; + self.has = has; + } + + /** + * Returns true/false if the specified event name is a native browser event or not. + * + * @method isNative + * @param {String} name Name to check if it's native. + * @return {Boolean} true/false if the event is native or not. + * @static + */ + Dispatcher.isNative = function(name) { + return !!nativeEvents[name.toLowerCase()]; + }; + + return Dispatcher; +}); + // Included from: js/tinymce/classes/ui/Selector.js /** * Selector.js * @@ -17464,25 +18059,48 @@ * @class tinymce.ui.Control */ define("tinymce/ui/Control", [ "tinymce/util/Class", "tinymce/util/Tools", + "tinymce/util/EventDispatcher", "tinymce/ui/Collection", "tinymce/ui/DomUtils" -], function(Class, Tools, Collection, DomUtils) { +], function(Class, Tools, EventDispatcher, Collection, DomUtils) { "use strict"; - var nativeEvents = Tools.makeMap("focusin focusout scroll click dblclick mousedown mouseup mousemove mouseover" + - " mouseout mouseenter mouseleave wheel keydown keypress keyup contextmenu", " "); - var elementIdCache = {}; var hasMouseWheelEventSupport = "onmousewheel" in document; var hasWheelEventSupport = false; + var classPrefix = "mce-"; + function getEventDispatcher(obj) { + if (!obj._eventDispatcher) { + obj._eventDispatcher = new EventDispatcher({ + scope: obj, + toggleEvent: function(name, state) { + if (state && EventDispatcher.isNative(name)) { + if (!obj._nativeEvents) { + obj._nativeEvents = {}; + } + + obj._nativeEvents[name] = true; + + if (obj._rendered) { + obj.bindPendingEvents(); + } + } + } + }); + } + + return obj._eventDispatcher; + } + var Control = Class.extend({ Statics: { - elementIdCache: elementIdCache + elementIdCache: elementIdCache, + classPrefix: classPrefix }, isRtl: function() { return Control.rtl; }, @@ -17491,11 +18109,11 @@ * Class/id prefix to use for all controls. * * @final * @field {String} classPrefix */ - classPrefix: "mce-", + classPrefix: classPrefix, /** * Constructs a new control instance with the specified settings. * * @constructor @@ -17948,18 +18566,22 @@ * @param {String} name Name of the event to bind. For example "click". * @param {String/function} callback Callback function to execute ones the event occurs. * @return {tinymce.ui.Control} Current control object. */ on: function(name, callback) { - var self = this, bindings, handlers, names, i; + var self = this; function resolveCallbackName(name) { var callback, scope; + if (typeof(name) != 'string') { + return name; + } + return function(e) { if (!callback) { - self.parents().each(function(ctrl) { + self.parentsAndSelf().each(function(ctrl) { var callbacks = ctrl.settings.callbacks; if (callbacks && (callback = callbacks[name])) { scope = ctrl; return false; @@ -17969,46 +18591,12 @@ return callback.call(scope, e); }; } - if (callback) { - if (typeof(callback) == 'string') { - callback = resolveCallbackName(callback); - } + getEventDispatcher(self).on(name, resolveCallbackName(callback)); - names = name.toLowerCase().split(' '); - i = names.length; - while (i--) { - name = names[i]; - - bindings = self._bindings; - if (!bindings) { - bindings = self._bindings = {}; - } - - handlers = bindings[name]; - if (!handlers) { - handlers = bindings[name] = []; - } - - handlers.push(callback); - - if (nativeEvents[name]) { - if (!self._nativeEvents) { - self._nativeEvents = {name: true}; - } else { - self._nativeEvents[name] = true; - } - - if (self._rendered) { - self.bindPendingEvents(); - } - } - } - } - return self; }, /** * Unbinds the specified event and optionally a specific callback. If you omit the name @@ -18019,50 +18607,12 @@ * @param {String} [name] Name for the event to unbind. * @param {function} [callback] Callback function to unbind. * @return {mxex.ui.Control} Current control object. */ off: function(name, callback) { - var self = this, i, bindings = self._bindings, handlers, bindingName, names, hi; - - if (bindings) { - if (name) { - names = name.toLowerCase().split(' '); - i = names.length; - while (i--) { - name = names[i]; - handlers = bindings[name]; - - // Unbind all handlers - if (!name) { - for (bindingName in bindings) { - bindings[bindingName].length = 0; - } - - return self; - } - - if (handlers) { - // Unbind all by name - if (!callback) { - handlers.length = 0; - } else { - // Unbind specific ones - hi = handlers.length; - while (hi--) { - if (handlers[hi] === callback) { - handlers.splice(hi, 1); - } - } - } - } - } - } else { - self._bindings = []; - } - } - - return self; + getEventDispatcher(this).off(name, callback); + return this; }, /** * Fires the specified event by name and arguments on the control. This will execute all * bound event handlers. @@ -18072,82 +18622,29 @@ * @param {Object} [args] Arguments to pass to the event. * @param {Boolean} [bubble] Value to control bubbeling. Defaults to true. * @return {Object} Current arguments object. */ fire: function(name, args, bubble) { - var self = this, i, l, handlers, parentCtrl; + var self = this; - name = name.toLowerCase(); - - // Dummy function that gets replaced on the delegation state functions - function returnFalse() { - return false; - } - - // Dummy function that gets replaced on the delegation state functions - function returnTrue() { - return true; - } - - // Setup empty object if args is omited args = args || {}; - // Stick type into event object - if (!args.type) { - args.type = name; - } - - // Stick control into event if (!args.control) { args.control = self; } - // Add event delegation methods if they are missing - if (!args.preventDefault) { - // Add preventDefault method - args.preventDefault = function() { - args.isDefaultPrevented = returnTrue; - }; + args = getEventDispatcher(self).fire(name, args); - // Add stopPropagation - args.stopPropagation = function() { - args.isPropagationStopped = returnTrue; - }; - - // Add stopImmediatePropagation - args.stopImmediatePropagation = function() { - args.isImmediatePropagationStopped = returnTrue; - }; - - // Add event delegation states - args.isDefaultPrevented = returnFalse; - args.isPropagationStopped = returnFalse; - args.isImmediatePropagationStopped = returnFalse; - } - - if (self._bindings) { - handlers = self._bindings[name]; - - if (handlers) { - for (i = 0, l = handlers.length; i < l; i++) { - // Execute callback and break if the callback returns a false - if (!args.isImmediatePropagationStopped() && handlers[i].call(self, args) === false) { - break; - } - } + // Bubble event up to parents + if (bubble !== false && self.parent) { + var parent = self.parent(); + while (parent && !args.isPropagationStopped()) { + parent.fire(name, args, false); + parent = parent.parent(); } } - // Bubble event up to parent controls - if (bubble !== false) { - parentCtrl = self.parent(); - while (parentCtrl && !args.isPropagationStopped()) { - parentCtrl.fire(name, args, false); - parentCtrl = parentCtrl.parent(); - } - } - return args; }, /** * Returns true/false if the specified event has any listeners. @@ -18155,11 +18652,11 @@ * @method hasEventListeners * @param {String} name Name of the event to check for. * @return {Boolean} True/false state if the event has listeners. */ hasEventListeners: function(name) { - return name in this._bindings; + return getEventDispatcher(this).has(name); }, /** * Returns a control collection with all parent controls. * @@ -18182,10 +18679,21 @@ return parents; }, /** + * Returns the current control and it's parents. + * + * @method parentsAndSelf + * @param {String} selector Optional selector expression to find parents. + * @return {tinymce.ui.Collection} Collection with all parent controls. + */ + parentsAndSelf: function(selector) { + return new Collection(this).add(this.parents(selector)); + }, + + /** * Returns the control next to the current control. * * @method next * @return {tinymce.ui.Control} Next control instance. */ @@ -18497,20 +19005,31 @@ * @param {String/Object/Array} text Text to entity encode. * @param {Boolean} [translate=true] False if the contents shouldn't be translated. * @return {String} Encoded and possible traslated string. */ encode: function(text, translate) { - if (translate !== false && Control.translate) { - text = Control.translate(text); + if (translate !== false) { + text = this.translate(text); } return (text || '').replace(/[&<>"]/g, function(match) { return '&#' + match.charCodeAt(0) + ';'; }); }, /** + * Returns the translated string. + * + * @method translate + * @param {String} text Text to translate. + * @return {String} Translated string or the same as the input. + */ + translate: function(text) { + return Control.translate ? Control.translate(text) : text; + }, + + /** * Adds items before the current control. * * @method before * @param {Array/tinymce.ui.Collection} items Array of items to prepend before this control. * @return {tinymce.ui.Control} Current control instance. @@ -18849,10 +19368,15 @@ self._eventsRoot = eventRootCtrl; for (l = i, i = 0; i < l; i++) { parents[i]._eventsRoot = eventRootCtrl; } + var eventRootDelegates = eventRootCtrl._delegates; + if (!eventRootDelegates) { + eventRootDelegates = eventRootCtrl._delegates = {}; + } + // Bind native event delegates for (name in nativeEvents) { if (!nativeEvents) { return false; } @@ -18873,13 +19397,13 @@ if (!eventRootCtrl._hasMouseEnter) { DomUtils.on(eventRootCtrl.getEl(), "mouseleave", mouseLeaveHandler); DomUtils.on(eventRootCtrl.getEl(), "mouseover", mouseEnterHandler); eventRootCtrl._hasMouseEnter = 1; } - } else if (!eventRootCtrl[name]) { + } else if (!eventRootDelegates[name]) { DomUtils.on(eventRootCtrl.getEl(), name, delegate); - eventRootCtrl[name] = true; + eventRootDelegates[name] = true; } // Remove the event once it's bound nativeEvents[name] = false; } @@ -19199,13 +19723,15 @@ * @private * @param {Element} elm Element to check if it's an text input element or not. * @return {Boolean} True/false if the element is a text element or not. */ function isTextInputElement(elm) { + var tagName = elm.tagName.toUpperCase(); + // Notice: since type can be "email" etc we don't check the type // So all input elements gets treated as text input elements - return elm.tagName == "INPUT" || elm.tagName == "TEXTAREA"; + return tagName == "INPUT" || tagName == "TEXTAREA"; } /** * Returns true/false if the specified element can be focused or not. * @@ -19443,11 +19969,11 @@ aria = aria || {}; focusedControl.fire('click', {target: focusedElement, aria: aria}); } root.on('keydown', function(e) { - function handleNonTabEvent(e, handler) { + function handleNonTabOrEscEvent(e, handler) { // Ignore non tab keys for text elements if (isTextInputElement(focusedElement)) { return; } @@ -19460,33 +19986,33 @@ return; } switch (e.keyCode) { case 37: // DOM_VK_LEFT - handleNonTabEvent(e, left); + handleNonTabOrEscEvent(e, left); break; case 39: // DOM_VK_RIGHT - handleNonTabEvent(e, right); + handleNonTabOrEscEvent(e, right); break; case 38: // DOM_VK_UP - handleNonTabEvent(e, up); + handleNonTabOrEscEvent(e, up); break; case 40: // DOM_VK_DOWN - handleNonTabEvent(e, down); + handleNonTabOrEscEvent(e, down); break; case 27: // DOM_VK_ESCAPE - handleNonTabEvent(e, cancel); + cancel(); break; case 14: // DOM_VK_ENTER case 13: // DOM_VK_RETURN case 32: // DOM_VK_SPACE - handleNonTabEvent(e, enter); + handleNonTabOrEscEvent(e, enter); break; case 9: // DOM_VK_TAB if (tab(e) !== false) { e.preventDefault(); @@ -20354,11 +20880,11 @@ self._hasBody = false; } return ( - '<div id="' + self._id + '" class="' + self.classes() + '" hideFocus="1" tabIndex="-1" role="group">' + + '<div id="' + self._id + '" class="' + self.classes() + '" hidefocus="1" tabindex="-1" role="group">' + (self._preBodyHtml || '') + innerHtml + '</div>' ); } @@ -21208,11 +21734,11 @@ if (settings.title) { headerHtml = ( '<div id="' + id + '-head" class="' + prefix + 'window-head">' + '<div id="' + id + '-title" class="' + prefix + 'title">' + self.encode(settings.title) + '</div>' + - '<button type="button" class="' + prefix + 'close" aria-hidden="true">&times;</button>' + + '<button type="button" class="' + prefix + 'close" aria-hidden="true">\u00d7</button>' + '<div id="' + id + '-dragh" class="' + prefix + 'dragh"></div>' + '</div>' ); } @@ -21227,11 +21753,11 @@ if (self.statusbar) { footerHtml = self.statusbar.renderHtml(); } return ( - '<div id="' + id + '" class="' + self.classes() + '" hideFocus="1">' + + '<div id="' + id + '" class="' + self.classes() + '" hidefocus="1">' + '<div class="' + self.classPrefix + 'reset" role="application">' + headerHtml + '<div id="' + id + '-body" class="' + self.classes('body') + '">' + html + '</div>' + @@ -21376,10 +21902,21 @@ if (self._fullscreen) { DomUtils.removeClass(document.documentElement, prefix + 'fullscreen'); DomUtils.removeClass(document.body, prefix + 'fullscreen'); } + }, + + /** + * Returns the contentWindow object of the iframe if it exists. + * + * @method getContentWindow + * @return {Window} window object or null. + */ + getContentWindow: function() { + var ifr = this.getEl().getElementsByTagName('iframe')[0]; + return ifr ? ifr.contentWindow : null; } }); return Window; }); @@ -21684,11 +22221,10 @@ if (!args.url && !args.buttons) { args.buttons = [ {text: 'Ok', subtype: 'primary', onclick: function() { win.find('form')[0].submit(); - win.close(); }}, {text: 'Cancel', onclick: function() { win.close(); }} @@ -21728,11 +22264,11 @@ win.params = params || {}; // Takes a snapshot in the FocusManager of the selection before focus is lost to dialog editor.nodeChanged(); - return win.renderTo(document.body).reflow(); + return win.renderTo().reflow(); }; /** * Creates a alert dialog. Please don't use the blocking behavior of this * native version use the callback method instead then it can be extended. @@ -21812,10 +22348,20 @@ self.setParams = function(params) { if (getTopMostWindow()) { getTopMostWindow().params = params; } }; + + /** + * Returns the currently opened window objects. + * + * @method getWindows + * @return {Array} Array of the currently opened windows. + */ + self.getWindows = function() { + return windows; + }; }; }); // Included from: js/tinymce/classes/util/Quirks.js @@ -21908,13 +22454,49 @@ * This code is a ugly hack since writing full custom delete logic for just this bug * fix seemed like a huge task. I hope we can remove this before the year 2030. */ function cleanupStylesWhenDeleting() { var doc = editor.getDoc(), urlPrefix = 'data:text/mce-internal,'; + var MutationObserver = window.MutationObserver, olderWebKit, dragStartRng; - if (!window.MutationObserver) { - return; + // Add mini polyfill for older WebKits + // TODO: Remove this when old Safari versions gets updated + if (!MutationObserver) { + olderWebKit = true; + + MutationObserver = function() { + var records = [], target; + + function nodeInsert(e) { + var target = e.relatedNode || e.target; + records.push({target: target, addedNodes: [target]}); + } + + function attrModified(e) { + var target = e.relatedNode || e.target; + records.push({target: target, attributeName: e.attrName}); + } + + this.observe = function(node) { + target = node; + target.addEventListener('DOMSubtreeModified', nodeInsert, false); + target.addEventListener('DOMNodeInsertedIntoDocument', nodeInsert, false); + target.addEventListener('DOMNodeInserted', nodeInsert, false); + target.addEventListener('DOMAttrModified', attrModified, false); + }; + + this.disconnect = function() { + target.removeEventListener('DOMSubtreeModified', nodeInsert, false); + target.removeEventListener('DOMNodeInsertedIntoDocument', nodeInsert, false); + target.removeEventListener('DOMNodeInserted', nodeInsert, false); + target.removeEventListener('DOMAttrModified', attrModified, false); + }; + + this.takeRecords = function() { + return records; + }; + }; } function customDelete(isForward) { var mutationObserver = new MutationObserver(function() {}); @@ -21942,10 +22524,14 @@ var rng = editor.selection.getRng(); var caretElement = rng.startContainer.parentNode; Tools.each(mutationObserver.takeRecords(), function(record) { + if (!dom.isChildOf(record.target, editor.getBody())) { + return; + } + // Restore style attribute to previous value if (record.attributeName == "style") { var oldValue = record.target.getAttribute('data-mce-style'); if (oldValue) { @@ -22021,13 +22607,29 @@ editor.addCommand('ForwardDelete', function() { customDelete(true); }); + // Older WebKits doesn't properly handle the clipboard so we can't add the rest + if (olderWebKit) { + return; + } + editor.on('dragstart', function(e) { + var selectionHtml; + + if (editor.selection.isCollapsed() && e.target.tagName == 'IMG') { + selection.select(e.target); + } + + dragStartRng = selection.getRng(); + selectionHtml = editor.selection.getContent(); + // Safari doesn't support custom dataTransfer items so we can only use URL and Text - e.dataTransfer.setData('URL', 'data:text/mce-internal,' + escape(editor.selection.getContent())); + if (selectionHtml.length > 0) { + e.dataTransfer.setData('URL', 'data:text/mce-internal,' + escape(selectionHtml)); + } }); editor.on('drop', function(e) { if (!isDefaultPrevented(e)) { var internalContent = e.dataTransfer.getData('URL'); @@ -22037,14 +22639,30 @@ } internalContent = unescape(internalContent.substr(urlPrefix.length)); if (doc.caretRangeFromPoint) { e.preventDefault(); - customDelete(); - editor.selection.setRng(doc.caretRangeFromPoint(e.x, e.y)); - editor.insertContent(internalContent); + + // Safari has a weird issue where drag/dropping images sometimes + // produces a green plus icon. When this happens the caretRangeFromPoint + // will return "null" even though the x, y coordinate is correct. + // But if we detach the insert from the drop event we will get a proper range + window.setTimeout(function() { + var pointRng = doc.caretRangeFromPoint(e.x, e.y); + + if (dragStartRng) { + selection.setRng(dragStartRng); + dragStartRng = null; + } + + customDelete(); + + selection.setRng(pointRng); + editor.insertContent(internalContent); + }, 0); } + } }); editor.on('cut', function(e) { if (!isDefaultPrevented(e) && e.clipboardData) { @@ -22321,10 +22939,14 @@ clearTimeout(selectionTimer); selectionTimer = 0; } selectionTimer = window.setTimeout(function() { + if (editor.removed) { + return; + } + var rng = selection.getRng(); // Compare the ranges to see if it was a real change or not if (!lastRng || !RangeUtils.compareRanges(rng, lastRng)) { editor.nodeChanged(); @@ -22733,20 +23355,20 @@ } }); } /** - * Fixes selection issues on Gecko where the caret can be placed between two inline elements like <b>a</b>|<b>b</b> + * Fixes selection issues where the caret can be placed between two inline elements like <b>a</b>|<b>b</b> * this fix will lean the caret right into the closest inline element. */ function normalizeSelection() { // Normalize selection for example <b>a</b><i>|a</i> becomes <b>a|</b><i>a</i> except for Ctrl+A since it selects everything - editor.on('keyup focusin', function(e) { + editor.on('keyup focusin mouseup', function(e) { if (e.keyCode != 65 || !VK.metaKeyPressed(e)) { selection.normalize(); } - }); + }, true); } /** * Forces Gecko to render a broken image icon if it fails to load an image. */ @@ -22830,18 +23452,50 @@ * * Example of what happens: <body>text</body> becomes <body>text<br><br></body> */ function doubleTrailingBrElements() { if (!editor.inline) { - editor.on('focus blur', function() { + editor.on('focus blur beforegetcontent', function() { var br = editor.dom.create('br'); editor.getBody().appendChild(br); br.parentNode.removeChild(br); }, true); } } + /** + * iOS 7.1 introduced two new bugs: + * 1) It's possible to open links within a contentEditable area by clicking on them. + * 2) If you hold down the finger it will display the link/image touch callout menu. + */ + function tapLinksAndImages() { + editor.on('click', function(e) { + var elm = e.target; + + do { + if (elm.tagName === 'A') { + e.preventDefault(); + return; + } + } while ((elm = elm.parentNode)); + }); + + editor.contentStyles.push('.mce-content-body {-webkit-touch-callout: none}'); + } + + /** + * WebKit has a bug where it will allow forms to be submitted if they are inside a contentEditable element. + * For example this: <form><button></form> + */ + function blockFormSubmitInsideEditor() { + editor.on('init', function() { + editor.dom.bind(editor.getBody(), 'submit', function(e) { + e.preventDefault(); + }); + }); + } + // All browsers disableBackspaceIntoATable(); removeBlockQuoteOnBackSpace(); emptyEditorWhenDeleting(); normalizeSelection(); @@ -22850,16 +23504,18 @@ if (isWebKit) { cleanupStylesWhenDeleting(); inputMethodFocus(); selectControlElements(); setDefaultBlockType(); + blockFormSubmitInsideEditor(); // iOS if (Env.iOS) { selectionChangeNodeChanged(); restoreFocusOnKeyDown(); bodyHeight(); + tapLinksAndImages(); } else { selectAll(); } } @@ -22915,103 +23571,52 @@ * This mixin will add event binding logic to classes. * * @mixin tinymce.util.Observable */ define("tinymce/util/Observable", [ - "tinymce/util/Tools" -], function(Tools) { - var bindingsName = "__bindings"; - var nativeEvents = Tools.makeMap( - "focusin focusout click dblclick mousedown mouseup mousemove mouseover beforepaste paste cut copy selectionchange" + - " mouseout mouseenter mouseleave keydown keypress keyup contextmenu dragstart dragend dragover draggesture dragdrop drop drag", ' ' - ); + "tinymce/util/EventDispatcher" +], function(EventDispatcher) { + function getEventDispatcher(obj) { + if (!obj._eventDispatcher) { + obj._eventDispatcher = new EventDispatcher({ + scope: obj, + toggleEvent: function(name, state) { + if (EventDispatcher.isNative(name) && obj.toggleNativeEvent) { + obj.toggleNativeEvent(name, state); + } + } + }); + } - function returnFalse() { - return false; + return obj._eventDispatcher; } - function returnTrue() { - return true; - } - return { /** * Fires the specified event by name. * * @method fire * @param {String} name Name of the event to fire. - * @param {tinymce.Event/Object?} args Event arguments. + * @param {Object?} args Event arguments. * @param {Boolean?} bubble True/false if the event is to be bubbled. - * @return {tinymce.Event} Event instance passed in converted into tinymce.Event instance. + * @return {Object} Event args instance passed in. * @example * instance.fire('event', {...}); */ fire: function(name, args, bubble) { - var self = this, handlers, i, l, callback, parent; + var self = this; - if (self.removed) { - return; + // Prevent all events except the remove event after the instance has been removed + if (self.removed && name !== "remove") { + return args; } - name = name.toLowerCase(); - args = args || {}; - args.type = name; + args = getEventDispatcher(self).fire(name, args, bubble); - // Setup target is there isn't one - if (!args.target) { - args.target = self; - } - - // Add event delegation methods if they are missing - if (!args.preventDefault) { - // Add preventDefault method - args.preventDefault = function() { - args.isDefaultPrevented = returnTrue; - }; - - // Add stopPropagation - args.stopPropagation = function() { - args.isPropagationStopped = returnTrue; - }; - - // Add stopImmediatePropagation - args.stopImmediatePropagation = function() { - args.isImmediatePropagationStopped = returnTrue; - }; - - // Add event delegation states - args.isDefaultPrevented = returnFalse; - args.isPropagationStopped = returnFalse; - args.isImmediatePropagationStopped = returnFalse; - } - - //console.log(name, args); - - if (self[bindingsName]) { - handlers = self[bindingsName][name]; - - if (handlers) { - for (i = 0, l = handlers.length; i < l; i++) { - handlers[i] = callback = handlers[i]; - - // Stop immediate propagation if needed - if (args.isImmediatePropagationStopped()) { - break; - } - - // If callback returns false then prevent default and stop all propagation - if (callback.call(self, args) === false) { - args.preventDefault(); - return args; - } - } - } - } - // Bubble event up to parents if (bubble !== false && self.parent) { - parent = self.parent(); + var parent = self.parent(); while (parent && !args.isPropagationStopped()) { parent.fire(name, args, false); parent = parent.parent(); } } @@ -23031,46 +23636,11 @@ * instance.on('event', function(e) { * // Callback logic * }); */ on: function(name, callback, prepend) { - var self = this, bindings, handlers, names, i; - - if (callback === false) { - callback = function() { - return false; - }; - } - - if (callback) { - names = name.toLowerCase().split(' '); - i = names.length; - while (i--) { - name = names[i]; - - bindings = self[bindingsName]; - if (!bindings) { - bindings = self[bindingsName] = {}; - } - - handlers = bindings[name]; - if (!handlers) { - handlers = bindings[name] = []; - if (self.bindNative && nativeEvents[name]) { - self.bindNative(name); - } - } - - if (prepend) { - handlers.unshift(callback); - } else { - handlers.push(callback); - } - } - } - - return self; + return getEventDispatcher(this).on(name, callback, prepend); }, /** * Unbinds an event listener to a specific event by name. * @@ -23087,71 +23657,154 @@ * * // Unbind all events * instance.off(); */ off: function(name, callback) { - var self = this, i, bindings = self[bindingsName], handlers, bindingName, names, hi; + return getEventDispatcher(this).off(name, callback); + }, - if (bindings) { - if (name) { - names = name.toLowerCase().split(' '); - i = names.length; - while (i--) { - name = names[i]; - handlers = bindings[name]; + /** + * Returns true/false if the object has a event of the specified name. + * + * @method hasEventListeners + * @param {String} name Name of the event to check for. + * @return {Boolean} true/false if the event exists or not. + */ + hasEventListeners: function(name) { + return getEventDispatcher(this).has(name); + } + }; +}); - // Unbind all handlers - if (!name) { - for (bindingName in bindings) { - bindings[name].length = 0; - } +// Included from: js/tinymce/classes/EditorObservable.js - return self; - } +/** + * EditorObservable.js + * + * Copyright, Moxiecode Systems AB + * Released under LGPL License. + * + * License: http://www.tinymce.com/license + * Contributing: http://www.tinymce.com/contributing + */ - if (handlers) { - // Unbind all by name - if (!callback) { - handlers.length = 0; - } else { - // Unbind specific ones - hi = handlers.length; - while (hi--) { - if (handlers[hi] === callback) { - handlers.splice(hi, 1); - } - } - } +/** + * This mixin contains the event logic for the tinymce.Editor class. + * + * @mixin tinymce.EditorObservable + * @extends tinymce.util.Observable + */ +define("tinymce/EditorObservable", [ + "tinymce/util/Observable", + "tinymce/dom/DOMUtils", + "tinymce/util/Tools" +], function(Observable, DOMUtils, Tools) { + var DOM = DOMUtils.DOM; - if (!handlers.length && self.unbindNative && nativeEvents[name]) { - self.unbindNative(name); - delete bindings[name]; - } - } + function getEventTarget(editor, eventName) { + if (eventName == 'selectionchange') { + return editor.getDoc(); + } + + // Need to bind mousedown/mouseup etc to document not body in iframe mode + // Since the user might click on the HTML element not the BODY + if (!editor.inline && /^mouse|click|contextmenu|drop/.test(eventName)) { + return editor.getDoc(); + } + + return editor.getBody(); + } + + function bindEventDelegate(editor, name) { + var eventRootSelector = editor.settings.event_root, editorManager = editor.editorManager; + var eventRootElm = editorManager.eventRootElm || getEventTarget(editor, name); + + if (eventRootSelector) { + if (!editorManager.rootEvents) { + editorManager.rootEvents = {}; + + editorManager.on('RemoveEditor', function() { + if (!editorManager.activeEditor) { + DOM.unbind(eventRootElm); + delete editorManager.rootEvents; } - } else { - if (self.unbindNative) { - for (name in bindings) { - self.unbindNative(name); + }); + } + + if (editorManager.rootEvents[name]) { + return; + } + + if (eventRootElm == editor.getBody()) { + eventRootElm = DOM.select(eventRootSelector)[0]; + editorManager.eventRootElm = eventRootElm; + } + + editorManager.rootEvents[name] = true; + + DOM.bind(eventRootElm, name, function(e) { + var target = e.target, editors = editorManager.editors, i = editors.length; + + while (i--) { + var body = editors[i].getBody(); + + if (body === target || DOM.isChildOf(target, body)) { + if (!editors[i].hidden) { + editors[i].fire(name, e); } } - - self[bindingsName] = []; } - } + }); + } else { + editor.dom.bind(eventRootElm, name, function(e) { + if (!editor.hidden) { + editor.fire(name, e); + } + }); + } + } - return self; + var EditorObservable = { + bindPendingEventDelegates: function() { + var self = this; + + Tools.each(self._pendingNativeEvents, function(name) { + bindEventDelegate(self, name); + }); }, - hasEventListeners: function(name) { - var bindings = this[bindingsName]; + toggleNativeEvent: function(name, state) { + var self = this; - name = name.toLowerCase(); + if (self.settings.readonly) { + return; + } - return !(!bindings || !bindings[name] || bindings[name].length === 0); + // Never bind focus/blur since the FocusManager fakes those + if (name == "focus" || name == "blur") { + return; + } + + if (state) { + if (self.initialized) { + bindEventDelegate(self, name); + } else { + if (!self._pendingNativeEvents) { + self._pendingNativeEvents = [name]; + } else { + self._pendingNativeEvents.push(name); + } + } + } else if (self.initialized) { + self.dom.unbind(getEventTarget(self, name), name); + } } }; + + EditorObservable = Tools.extend({}, Observable, EditorObservable); + + return EditorObservable; }); // Included from: js/tinymce/classes/Shortcuts.js /** @@ -23329,39 +23982,25 @@ "tinymce/html/Schema", "tinymce/html/DomParser", "tinymce/util/Quirks", "tinymce/Env", "tinymce/util/Tools", - "tinymce/util/Observable", + "tinymce/EditorObservable", "tinymce/Shortcuts" ], function( DOMUtils, AddOnManager, Node, DomSerializer, Serializer, Selection, Formatter, UndoManager, EnterKey, ForceBlocks, EditorCommands, URI, ScriptLoader, EventUtils, WindowManager, - Schema, DomParser, Quirks, Env, Tools, Observable, Shortcuts + Schema, DomParser, Quirks, Env, Tools, EditorObservable, Shortcuts ) { // Shorten these names var DOM = DOMUtils.DOM, ThemeManager = AddOnManager.ThemeManager, PluginManager = AddOnManager.PluginManager; var extend = Tools.extend, each = Tools.each, explode = Tools.explode; var inArray = Tools.inArray, trim = Tools.trim, resolve = Tools.resolve; var Event = EventUtils.Event; var isGecko = Env.gecko, ie = Env.ie; - function getEventTarget(editor, eventName) { - if (eventName == 'selectionchange') { - return editor.getDoc(); - } - - // Need to bind mousedown/mouseup etc to document not body in iframe mode - // Since the user might click on the HTML element not the BODY - if (!editor.inline && /^mouse|click|contextmenu|drop/.test(eventName)) { - return editor.getDoc(); - } - - return editor.getBody(); - } - /** * Include documentation for all the events. * * @include ../../../tools/docs/tinymce.Editor.js */ @@ -23987,16 +24626,14 @@ self.on('remove', function() { var bodyEl = this.getBody(); DOM.removeClass(bodyEl, 'mce-content-body'); DOM.removeClass(bodyEl, 'mce-edit-focus'); - DOM.setAttrib(bodyEl, 'tabIndex', null); DOM.setAttrib(bodyEl, 'contentEditable', null); }); DOM.addClass(targetElm, 'mce-content-body'); - targetElm.tabIndex = -1; self.contentDocument = doc = settings.content_document || document; self.contentWindow = settings.content_window || window; self.bodyElement = targetElm; // Prevent leak in IE @@ -24059,11 +24696,11 @@ * @type tinymce.html.DomParser */ self.parser = new DomParser(settings, self.schema); // Convert src and href into data-mce-src, data-mce-href and data-mce-style - self.parser.addAttributeFilter('src,href,style', function(nodes, name) { + self.parser.addAttributeFilter('src,href,style,tabindex', function(nodes, name) { var i = nodes.length, node, dom = self.dom, value, internalName; while (i--) { node = nodes[i]; value = node.attr(name); @@ -24071,10 +24708,13 @@ // 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 if (name === "tabindex") { + node.attr(internalName, value); + node.attr(name, null); } else { node.attr(internalName, self.convertURL(value, name, node.name)); } } } @@ -24216,17 +24856,12 @@ * function isEditorInitialized(editor) { * return editor && editor.initialized; * } */ self.initialized = true; + self.bindPendingEventDelegates(); - each(self._pendingNativeEvents, function(name) { - self.dom.bind(getEventTarget(self, name), name, function(e) { - self.fire(e.type, e); - }); - }); - self.fire('init'); self.focus(true); self.nodeChanged({initial: true}); self.execCallback('init_instance_callback', self); @@ -24299,12 +24934,17 @@ // Focus the body as well since it's contentEditable if (isGecko || contentEditable) { body = self.getBody(); // Check for setActive since it doesn't scroll to the element - if (body.setActive && Env.ie < 11) { - body.setActive(); + if (body.setActive) { + // IE 11 sometimes throws "Invalid function" then fallback to focus + try { + body.setActive(); + } catch (ex) { + body.focus(); + } } else { body.focus(); } if (contentEditable) { @@ -24474,13 +25114,11 @@ self.fire('NodeChange', {element: node, parents: parents}); } }, /** - * Adds a button that later gets created by the ControlManager. This is a shorter and easier method - * of adding buttons without the need to deal with the ControlManager directly. But it's also less - * powerfull if you need more control use the ControlManagers factory methods instead. + * Adds a button that later gets created by the theme in the editors toolbars. * * @method addButton * @param {String} name Button name to add. * @param {Object} settings Settings object with title, cmd etc. * @example @@ -24518,11 +25156,13 @@ settings.tooltip = settings.tooltip || settings.title; self.buttons[name] = settings; }, /** - * Adds a menu item to be used in the menus of the modern theme. + * Adds a menu item to be used in the menus of the theme. There might be multiple instances + * of this menu item for example it might be used in the main menus of the theme but also in + * the context menu so make sure that it's self contained and supports multiple instances. * * @method addMenuItem * @param {String} name Menu item name to add. * @param {Object} settings Settings object with title, cmd etc. * @example @@ -24788,46 +25428,68 @@ * @method show */ show: function() { var self = this; - DOM.show(self.getContainer()); - DOM.hide(self.id); - self.load(); - self.fire('show'); + if (self.hidden) { + self.hidden = false; + + if (self.inline) { + self.getBody().contentEditable = true; + } else { + DOM.show(self.getContainer()); + DOM.hide(self.id); + } + + self.load(); + self.fire('show'); + } }, /** * Hides the editor and shows any textarea/div that the editor is supposed to replace. * * @method hide */ hide: function() { var self = this, doc = self.getDoc(); - // Fixed bug where IE has a blinking cursor left from the editor - if (ie && doc && !self.inline) { - doc.execCommand('SelectAll'); - } + if (!self.hidden) { + self.hidden = true; - // We must save before we hide so Safari doesn't crash - self.save(); + // Fixed bug where IE has a blinking cursor left from the editor + if (ie && doc && !self.inline) { + doc.execCommand('SelectAll'); + } - // defer the call to hide to prevent an IE9 crash #4921 - DOM.hide(self.getContainer()); - DOM.setStyle(self.id, 'display', self.orgDisplay); - self.fire('hide'); + // We must save before we hide so Safari doesn't crash + self.save(); + + if (self.inline) { + self.getBody().contentEditable = false; + + // Make sure the editor gets blurred + if (self == self.editorManager.focusedEditor) { + self.editorManager.focusedEditor = null; + } + } else { + DOM.hide(self.getContainer()); + DOM.setStyle(self.id, 'display', self.orgDisplay); + } + + self.fire('hide'); + } }, /** * Returns true/false if the editor is hidden or not. * * @method isHidden * @return {Boolean} True/false if the editor is hidden or not. */ isHidden: function() { - return !DOM.isHidden(this.id); + return !!this.hidden; }, /** * Sets the progress state, this will display a throbber/progess for the editor. * This is ideal for asycronous operations like an AJAX save call. @@ -25279,69 +25941,46 @@ */ remove: function() { var self = this; if (!self.removed) { - self.fire('remove'); - self.off(); - self.removed = 1; // Cancels post remove event execution + self.removed = 1; + self.save(); // Remove any hidden input if (self.hasHiddenInput) { DOM.remove(self.getElement().nextSibling); } - // We must save before we hide so Safari doesn't crash - self.save(); + if (!self.inline) { + // IE 9 has a bug where the selection stops working if you place the + // caret inside the editor then remove the iframe + if (ie && ie < 10) { + self.getDoc().execCommand('SelectAll', false, null); + } - DOM.setStyle(self.id, 'display', self.orgDisplay); + DOM.setStyle(self.id, 'display', self.orgDisplay); + self.getBody().onload = null; // Prevent #6816 - // Don't clear the window or document if content editable - // is enabled since other instances might still be present - if (!self.settings.content_editable) { + // Don't clear the window or document if content editable + // is enabled since other instances might still be present Event.unbind(self.getWin()); Event.unbind(self.getDoc()); } var elm = self.getContainer(); Event.unbind(self.getBody()); Event.unbind(elm); + self.fire('remove'); + self.editorManager.remove(self); DOM.remove(elm); self.destroy(); } }, - bindNative: function(name) { - var self = this; - - if (self.settings.readonly) { - return; - } - - if (self.initialized) { - self.dom.bind(getEventTarget(self, name), name, function(e) { - self.fire(name, e); - }); - } else { - if (!self._pendingNativeEvents) { - self._pendingNativeEvents = [name]; - } else { - self._pendingNativeEvents.push(name); - } - } - }, - - unbindNative: function(name) { - var self = this; - - if (self.initialized) { - self.dom.unbind(name); - } - }, - /** * Destroys the editor instance by removing all events, element references or other resources * that could leak memory. This method will be called automatically when the page is unloaded * but you can also call it directly if you know what you are doing. * @@ -25432,11 +26071,11 @@ sel = this.selection.getSel(); return (!sel || !sel.rangeCount || sel.rangeCount === 0); } }; - extend(Editor.prototype, Observable); + extend(Editor.prototype, EditorObservable); return Editor; }); // Included from: js/tinymce/classes/util/I18n.js @@ -25545,11 +26184,11 @@ */ define("tinymce/FocusManager", [ "tinymce/dom/DOMUtils", "tinymce/Env" ], function(DOMUtils, Env) { - var selectionChangeHandler, documentFocusInHandler, DOM = DOMUtils.DOM; + var selectionChangeHandler, documentFocusInHandler, documentMouseUpHandler, DOM = DOMUtils.DOM; /** * Constructs a new focus manager instance. * * @constructor FocusManager @@ -25566,12 +26205,17 @@ } } // We can't store a real range on IE 11 since it gets mutated so we need to use a bookmark object // TODO: Move this to a separate range utils class since it's it's logic is present in Selection as well. - function createBookmark(rng) { + function createBookmark(dom, rng) { if (rng && rng.startContainer) { + // Verify that the range is within the root of the editor + if (!dom.isChildOf(rng.startContainer, dom.getRoot()) || !dom.isChildOf(rng.endContainer, dom.getRoot())) { + return; + } + return { startContainer: rng.startContainer, startOffset: rng.startOffset, endContainer: rng.endContainer, endOffset: rng.endOffset @@ -25597,58 +26241,26 @@ function isUIElement(elm) { return !!DOM.getParent(elm, FocusManager.isEditorUIElement); } - function isNodeInBodyOfEditor(node, editor) { - var body = editor.getBody(); - - while (node) { - if (node == body) { - return true; - } - - node = node.parentNode; - } - } - function registerEvents(e) { var editor = e.editor; editor.on('init', function() { - // On IE take selection snapshot onbeforedeactivate - if ("onbeforedeactivate" in document && Env.ie < 11) { - // Gets fired when the editor is about to be blurred but also when the selection - // is moved into a table cell so we need to add the range as a pending range then - // use that pending range on the blur event of the editor body - editor.dom.bind(editor.getBody(), 'beforedeactivate', function() { - try { - editor.pendingRng = editor.selection.getRng(); - } catch (ex) { - // IE throws "Unexcpected call to method or property access" some times so lets ignore it - } - }); - - // Set the pending range as the current last range if the blur event occurs - editor.dom.bind(editor.getBody(), 'blur', function() { - if (editor.pendingRng) { - editor.lastRng = editor.pendingRng; - editor.selection.lastFocusBookmark = createBookmark(editor.lastRng); - editor.pendingRng = null; - } - }); - } else if (editor.inline || Env.ie > 10) { + // Gecko/WebKit has ghost selections in iframes and IE only has one selection per browser tab + if (editor.inline || Env.ie) { // On other browsers take snapshot on nodechange in inline mode since they have Ghost selections for iframes editor.on('nodechange keyup', function() { var node = document.activeElement; // IE 11 reports active element as iframe not body of iframe if (node && node.id == editor.id + '_ifr') { node = editor.getBody(); } - if (isNodeInBodyOfEditor(node, editor)) { + if (editor.dom.isChildOf(node, editor.getBody())) { editor.lastRng = editor.selection.getRng(); } }); // Handles the issue with WebKit not retaining selection within inline document @@ -25718,39 +26330,63 @@ } } }, 0); }); + // Check if focus is moved to an element outside the active editor by checking if the target node + // isn't within the body of the activeEditor nor a UI element such as a dialog child control if (!documentFocusInHandler) { documentFocusInHandler = function(e) { var activeEditor = editorManager.activeEditor; if (activeEditor && e.target.ownerDocument == document) { // Check to make sure we have a valid selection if (activeEditor.selection) { - activeEditor.selection.lastFocusBookmark = createBookmark(activeEditor.lastRng); + activeEditor.selection.lastFocusBookmark = createBookmark(activeEditor.dom, activeEditor.lastRng); } // Fire a blur event if the element isn't a UI element if (!isUIElement(e.target) && editorManager.focusedEditor == activeEditor) { activeEditor.fire('blur', {focusedEditor: null}); editorManager.focusedEditor = null; } } }; - // Check if focus is moved to an element outside the active editor by checking if the target node - // isn't within the body of the activeEditor nor a UI element such as a dialog child control DOM.bind(document, 'focusin', documentFocusInHandler); } + + // Handle edge case when user starts the selection inside the editor and releases + // the mouse outside the editor producing a new selection. This weird workaround is needed since + // Gecko doesn't have the "selectionchange" event we need to do this. Fixes: #6843 + if (editor.inline && !documentMouseUpHandler) { + documentMouseUpHandler = function(e) { + var activeEditor = editorManager.activeEditor; + + if (activeEditor.inline && !activeEditor.dom.isChildOf(e.target, activeEditor.getBody())) { + var rng = activeEditor.selection.getRng(); + + if (!rng.collapsed) { + activeEditor.lastRng = rng; + } + } + }; + + DOM.bind(document, 'mouseup', documentMouseUpHandler); + } } - function unregisterDocumentEvents() { + function unregisterDocumentEvents(e) { + if (editorManager.focusedEditor == e.editor) { + editorManager.focusedEditor = null; + } + if (!editorManager.activeEditor) { DOM.unbind(document, 'selectionchange', selectionChangeHandler); DOM.unbind(document, 'focusin', documentFocusInHandler); - selectionChangeHandler = documentFocusInHandler = null; + DOM.unbind(document, 'mouseup', documentMouseUpHandler); + selectionChangeHandler = documentFocusInHandler = documentMouseUpHandler = null; } } editorManager.on('AddEditor', registerEvents); editorManager.on('RemoveEditor', unregisterDocumentEvents); @@ -25762,11 +26398,12 @@ * @method isEditorUIElement * @param {Element} elm Element to check if it's part of the UI or not. * @return {Boolean} True/false state if the element is part of the UI or not. */ FocusManager.isEditorUIElement = function(elm) { - return elm.className.indexOf('mce-') !== -1; + // Needs to be converted to string since svg can have focus: #6776 + return elm.className.toString().indexOf('mce-') !== -1; }; return FocusManager; }); @@ -25802,13 +26439,50 @@ "tinymce/util/I18n", "tinymce/FocusManager" ], function(Editor, DOMUtils, URI, Env, Tools, Observable, I18n, FocusManager) { var DOM = DOMUtils.DOM; var explode = Tools.explode, each = Tools.each, extend = Tools.extend; - var instanceCounter = 0, beforeUnloadDelegate; + var instanceCounter = 0, beforeUnloadDelegate, EditorManager; - var EditorManager = { + function removeEditorFromList(editor) { + var editors = EditorManager.editors, removedFromList; + + delete editors[editor.id]; + + for (var i = 0; i < editors.length; i++) { + if (editors[i] == editor) { + editors.splice(i, 1); + removedFromList = true; + break; + } + } + + // Select another editor since the active one was removed + if (EditorManager.activeEditor == editor) { + EditorManager.activeEditor = editors[0]; + } + + // Clear focusedEditor if necessary, so that we don't try to blur the destroyed editor + if (EditorManager.focusedEditor == editor) { + EditorManager.focusedEditor = null; + } + + return removedFromList; + } + + function purgeDestroyedEditor(editor) { + // User has manually destroyed the editor lets clean up the mess + if (editor && !(editor.getContainer() || editor.getBody()).parentNode) { + removeEditorFromList(editor); + editor.destroy(true); + editor = null; + } + + return editor; + } + + EditorManager = { /** * Major version of TinyMCE build. * * @property majorVersion * @type String @@ -25819,19 +26493,19 @@ * Minor version of TinyMCE build. * * @property minorVersion * @type String */ - minorVersion : '0.19', + minorVersion : '0.26', /** * Release date of TinyMCE build. * * @property releaseDate * @type String */ - releaseDate: '2014-03-11', + releaseDate: '2014-05-06', /** * Collection of editor instances. * * @property editors @@ -25860,11 +26534,11 @@ * tinymce.EditorManager.activeEditor.selection.getContent(); */ activeEditor: null, setup: function() { - var self = this, baseURL, documentBaseURL, suffix = "", preInit; + var self = this, baseURL, documentBaseURL, suffix = "", preInit, src; // Get base URL for the current document documentBaseURL = document.location.href.replace(/[\?#].*$/, '').replace(/[\/\\][^\/]+$/, ''); if (!/[\/\\]$/.test(documentBaseURL)) { documentBaseURL += '/'; @@ -25877,11 +26551,11 @@ suffix = preInit.suffix; } else { // Get base where the tinymce script is located var scripts = document.getElementsByTagName('script'); for (var i = 0; i < scripts.length; i++) { - var src = scripts[i].src; + src = scripts[i].src; // Script types supported: // tinymce.js tinymce.min.js tinymce.dev.js // tinymce.jquery.js tinymce.jquery.min.js tinymce.jquery.dev.js // tinymce.full.js tinymce.full.min.js tinymce.full.dev.js @@ -25892,10 +26566,22 @@ baseURL = src.substring(0, src.lastIndexOf('/')); break; } } + + // We didn't find any baseURL by looking at the script elements + // Try to use the document.currentScript as a fallback + if (!baseURL && document.currentScript) { + src = document.currentScript.src; + + if (src.indexOf('.min') != -1) { + suffix = '.min'; + } + + baseURL = src.substring(0, src.lastIndexOf('/')); + } } /** * Base URL where the root directory if TinyMCE is located. * @@ -25968,10 +26654,18 @@ } return id; } + function createEditor(id, settings) { + if (!purgeDestroyedEditor(self.get(id))) { + var editor = new Editor(id, settings, self); + editors.push(editor); + editor.render(); + } + } + function execCallback(se, n, s) { var f = se[n]; if (!f) { return; @@ -25993,49 +26687,42 @@ if (settings.types) { // Process type specific selector each(settings.types, function(type) { each(DOM.select(type.selector), function(elm) { - var editor = new Editor(createId(elm), extend({}, settings, type), self); - editors.push(editor); - editor.render(1); + createEditor(createId(elm), extend({}, settings, type)); }); }); return; } else if (settings.selector) { // Process global selector each(DOM.select(settings.selector), function(elm) { - var editor = new Editor(createId(elm), settings, self); - editors.push(editor); - editor.render(1); + createEditor(createId(elm), settings); }); return; } // Fallback to old setting switch (settings.mode) { case "exact": l = settings.elements || ''; - if(l.length > 0) { + if (l.length > 0) { each(explode(l), function(v) { if (DOM.get(v)) { editor = new Editor(v, settings, self); editors.push(editor); - editor.render(true); + editor.render(); } else { each(document.forms, function(f) { each(f.elements, function(e) { if (e.name === v) { v = 'mce_editor_' + instanceCounter++; DOM.setAttrib(e, 'id', v); - - editor = new Editor(v, settings, self); - editors.push(editor); - editor.render(1); + createEditor(v, settings); } }); }); } }); @@ -26048,13 +26735,11 @@ if (settings.editor_deselector && hasClass(elm, settings.editor_deselector)) { return; } if (!settings.editor_selector || hasClass(elm, settings.editor_selector)) { - editor = new Editor(createId(elm), settings, self); - editors.push(editor); - editor.render(true); + createEditor(createId(elm), settings); } }); break; } @@ -26108,15 +26793,15 @@ * tinymce.EditorManager.get('mytextbox').on('click', function(e) { * ed.windowManager.alert('Hello world!'); * }); */ get: function(id) { - if (id === undefined) { + if (!arguments.length) { return this.editors; } - return this.editors[id]; + return id in this.editors ? this.editors[id] : null; }, /** * Adds an editor instance to the editor collection. This will also set it as the active editor. * @@ -26183,11 +26868,11 @@ * @method remove * @param {tinymce.Editor/String/Object} [selector] CSS selector or editor instance to remove. * @return {tinymce.Editor} The editor that got passed in will be return if it was found otherwise null. */ remove: function(selector) { - var self = this, i, editors = self.editors, editor, removedFromList; + var self = this, i, editors = self.editors, editor; // Remove all editors if (!selector) { for (i = editors.length - 1; i >= 0; i--) { self.remove(editors[i]); @@ -26213,32 +26898,17 @@ // Not in the collection if (!editors[editor.id]) { return null; } - delete editors[editor.id]; - - for (i = 0; i < editors.length; i++) { - if (editors[i] == editor) { - editors.splice(i, 1); - removedFromList = true; - break; - } - } - - // Select another editor since the active one was removed - if (self.activeEditor == editor) { - self.activeEditor = editors[0]; - } - /** * Fires when an editor is removed from EditorManager collection. * * @event RemoveEditor * @param {Object} e Event arguments. */ - if (removedFromList) { + if (removeEditorFromList(editor)) { self.fire('RemoveEditor', {editor: editor}); } if (!editors.length) { DOM.unbind(window, 'beforeunload', beforeUnloadDelegate); @@ -27389,10 +28059,11 @@ */ init: function(settings) { var self = this; self._super(settings); + settings = self.settings; self.canFocus = true; if (settings.tooltip && Widget.tooltips !== false) { self.on('mouseenter', function(e) { var tooltip = self.tooltip().moveTo(-0xFFFF); @@ -27624,22 +28295,51 @@ this._super(); }, /** + * Sets/gets the current button text. + * + * @method text + * @param {String} [text] New button text. + * @return {String|tinymce.ui.Button} Current text or current Button instance. + */ + text: function(text) { + var self = this; + + if (self._rendered) { + var textNode = self.getEl().lastChild.lastChild; + if (textNode) { + textNode.data = self.translate(text); + } + } + + return self._super(text); + }, + + /** * Renders the control as a HTML string. * * @method renderHtml * @return {String} HTML representing the control. */ renderHtml: function() { var self = this, id = self._id, prefix = self.classPrefix; - var icon = self.settings.icon, image = ''; + var icon = self.settings.icon, image; - if (self.settings.image) { + image = self.settings.image; + if (image) { icon = 'none'; - image = ' style="background-image: url(\'' + self.settings.image + '\')"'; + + // Support for [high dpi, low dpi] image sources + if (typeof image != "string") { + image = window.getSelection ? image[0] : image[1]; + } + + image = ' style="background-image: url(\'' + image + '\')"'; + } else { + image = ''; } icon = self.settings.icon ? prefix + 'ico ' + prefix + 'i-' + icon : ''; return ( @@ -28018,16 +28718,16 @@ var icon = self.settings.icon ? prefix + 'ico ' + prefix + 'i-' + self.settings.icon : ''; var image = self.settings.image ? ' style="background-image: url(\'' + self.settings.image + '\')"' : ''; return ( '<div id="' + id + '" class="' + self.classes() + '" role="button" tabindex="-1" aria-haspopup="true">' + - '<button role="presentation" hidefocus type="button" tabindex="-1">' + + '<button role="presentation" hidefocus="1" type="button" tabindex="-1">' + (icon ? '<i class="' + icon + '"' + image + '></i>' : '') + '<span id="' + id + '-preview" class="' + prefix + 'preview"></span>' + (self._text ? (icon ? ' ' : '') + (self._text) : '') + '</button>' + - '<button type="button" class="' + prefix + 'open" hidefocus tabindex="-1">' + + '<button type="button" class="' + prefix + 'open" hidefocus="1" tabindex="-1">' + ' <i class="' + prefix + 'caret"></i>' + '</button>' + '</div>' ); }, @@ -28205,11 +28905,11 @@ self.menu.on('select', function(e) { self.value(e.control.value()); }); self.on('focusin', function(e) { - if (e.target.tagName == 'INPUT') { + if (e.target.tagName.toUpperCase() == 'INPUT') { self.menu.hide(); } }); self.aria('expanded', true); @@ -28371,11 +29071,11 @@ text = self._text; if (icon || text) { openBtnHtml = ( '<div id="' + id + '-open" class="' + prefix + 'btn ' + prefix + 'open" tabIndex="-1" role="button">' + - '<button id="' + id + '-action" type="button" hidefocus tabindex="-1">' + + '<button id="' + id + '-action" type="button" hidefocus="1" tabindex="-1">' + (icon != 'caret' ? '<i class="' + icon + '"></i>' : '<i class="' + prefix + 'caret"></i>') + (text ? (icon ? ' ' : '') + text : '') + '</button>' + '</div>' ); @@ -28384,11 +29084,11 @@ } return ( '<div id="' + id + '" class="' + self.classes() + '">' + '<input id="' + id + '-inp" class="' + prefix + 'textbox ' + prefix + 'placeholder" value="' + - value + '" hidefocus="true"' + extraAttrs + '>' + + value + '" hidefocus="1"' + extraAttrs + ' />' + openBtnHtml + '</div>' ); } }); @@ -28527,11 +29227,11 @@ i + '" tabindex="-1" id="' + self._id + '-' + i + '" aria-level="' + i + '">' + parts[i].name + '</div>' ); } if (!html) { - html = '<div class="' + prefix + 'path-item">&nbsp;</div>'; + html = '<div class="' + prefix + 'path-item">\u00a0</div>'; } return html; } }); @@ -28668,11 +29368,11 @@ self.addClass('formitem'); layout.preRender(self); return ( - '<div id="' + self._id + '" class="' + self.classes() + '" hideFocus="1" tabIndex="-1">' + + '<div id="' + self._id + '" class="' + self.classes() + '" hidefocus="1" tabindex="-1">' + (self.settings.title ? ('<div id="' + self._id + '-title" class="' + prefix + 'title">' + self.settings.title + '</div>') : '') + '<div id="' + self._id + '-body" class="' + self.classes('body') + '">' + (self.settings.html || '') + layout.renderHtml(self) + '</div>' + @@ -28887,11 +29587,11 @@ self.preRender(); layout.preRender(self); return ( - '<fieldset id="' + self._id + '" class="' + self.classes() + '" hideFocus="1" tabIndex="-1">' + + '<fieldset id="' + self._id + '" class="' + self.classes() + '" hidefocus="1" tabindex="-1">' + (self.settings.title ? ('<legend id="' + self._id + '-title" class="' + prefix + 'fieldset-title">' + self.settings.title + '</legend>') : '') + '<div id="' + self._id + '-body" class="' + self.classes('body') + '">' + (self.settings.html || '') + layout.renderHtml(self) + '</div>' + @@ -29345,131 +30045,10 @@ Widget.tooltips = !Env.iOS; function registerControls(editor) { var formatMenu; - // Generates a preview for a format - function getPreviewCss(format) { - var name, previewElm, dom = editor.dom; - var previewCss = '', parentFontSize, previewStyles; - - previewStyles = editor.settings.preview_styles; - - // No preview forced - if (previewStyles === false) { - return ''; - } - - // Default preview - if (!previewStyles) { - previewStyles = 'font-family font-size font-weight font-style text-decoration ' + - 'text-transform color background-color border border-radius outline text-shadow'; - } - - // Removes any variables since these can't be previewed - function removeVars(val) { - return val.replace(/%(\w+)/g, ''); - } - - // Create block/inline element to use for preview - format = editor.formatter.get(format); - if (!format) { - return; - } - - format = format[0]; - name = format.block || format.inline || 'span'; - previewElm = dom.create(name); - - // Add format styles to preview element - each(format.styles, function(value, name) { - value = removeVars(value); - - if (value) { - dom.setStyle(previewElm, name, value); - } - }); - - // Add attributes to preview element - each(format.attributes, function(value, name) { - value = removeVars(value); - - if (value) { - dom.setAttrib(previewElm, name, value); - } - }); - - // Add classes to preview element - each(format.classes, function(value) { - value = removeVars(value); - - if (!dom.hasClass(previewElm, value)) { - dom.addClass(previewElm, value); - } - }); - - editor.fire('PreviewFormats'); - - // Add the previewElm outside the visual area - dom.setStyles(previewElm, {position: 'absolute', left: -0xFFFF}); - editor.getBody().appendChild(previewElm); - - // Get parent container font size so we can compute px values out of em/% for older IE:s - parentFontSize = dom.getStyle(editor.getBody(), 'fontSize', true); - parentFontSize = /px$/.test(parentFontSize) ? parseInt(parentFontSize, 10) : 0; - - each(previewStyles.split(' '), function(name) { - var value = dom.getStyle(previewElm, name, true); - - // If background is transparent then check if the body has a background color we can use - if (name == 'background-color' && /transparent|rgba\s*\([^)]+,\s*0\)/.test(value)) { - value = dom.getStyle(editor.getBody(), name, true); - - // Ignore white since it's the default color, not the nicest fix - // TODO: Fix this by detecting runtime style - if (dom.toHex(value).toLowerCase() == '#ffffff') { - return; - } - } - - if (name == 'color') { - // Ignore black since it's the default color, not the nicest fix - // TODO: Fix this by detecting runtime style - if (dom.toHex(value).toLowerCase() == '#000000') { - return; - } - } - - // Old IE won't calculate the font size so we need to do that manually - if (name == 'font-size') { - if (/em|%$/.test(value)) { - if (parentFontSize === 0) { - return; - } - - // Convert font size from em/% to px - value = parseFloat(value, 10) / (/%$/.test(value) ? 100 : 1); - value = (value * parentFontSize) + 'px'; - } - } - - if (name == "border" && value) { - previewCss += 'padding:0 2px;'; - } - - previewCss += name + ':' + value + ';'; - }); - - editor.fire('AfterPreviewFormats'); - - //previewCss += 'line-height:normal'; - - dom.remove(previewElm); - - return previewCss; - } - function createListBoxChangeHandler(items, formatName) { return function() { var self = this; editor.on('nodeChange', function(e) { @@ -29502,11 +30081,11 @@ }); }; } function createFormats(formats) { - formats = formats.split(';'); + formats = formats.replace(/;$/, '').split(';'); var i = formats.length; while (i--) { formats[i] = formats[i].split('='); } @@ -29516,17 +30095,17 @@ function createFormatMenu() { var count = 0, newFormats = []; var defaultStyleFormats = [ - {title: 'Headers', items: [ - {title: 'Header 1', format: 'h1'}, - {title: 'Header 2', format: 'h2'}, - {title: 'Header 3', format: 'h3'}, - {title: 'Header 4', format: 'h4'}, - {title: 'Header 5', format: 'h5'}, - {title: 'Header 6', format: 'h6'} + {title: 'Headings', items: [ + {title: 'Heading 1', format: 'h1'}, + {title: 'Heading 2', format: 'h2'}, + {title: 'Heading 3', format: 'h3'}, + {title: 'Heading 4', format: 'h4'}, + {title: 'Heading 5', format: 'h5'}, + {title: 'Heading 6', format: 'h6'} ]}, {title: 'Inline', items: [ {title: 'Bold', icon: 'bold', format: 'bold'}, {title: 'Italic', icon: 'italic', format: 'italic'}, @@ -29615,11 +30194,11 @@ itemDefaults: { preview: true, textStyle: function() { if (this.settings.format) { - return getPreviewCss(this.settings.format); + return editor.formatter.getCssText(this.settings.format); } }, onPostRender: function() { var self = this, formatName = this.settings.format; @@ -29852,24 +30431,24 @@ editor.addButton('formatselect', function() { var items = [], blocks = createFormats(editor.settings.block_formats || 'Paragraph=p;' + 'Address=address;' + 'Pre=pre;' + - 'Header 1=h1;' + - 'Header 2=h2;' + - 'Header 3=h3;' + - 'Header 4=h4;' + - 'Header 5=h5;' + - 'Header 6=h6' + 'Heading 1=h1;' + + 'Heading 2=h2;' + + 'Heading 3=h3;' + + 'Heading 4=h4;' + + 'Heading 5=h5;' + + 'Heading 6=h6' ); each(blocks, function(block) { items.push({ text: block[0], value: block[1], textStyle: function() { - return getPreviewCss(block[1]); + return editor.formatter.getCssText(block[1]); } }); }); return { @@ -30225,10 +30804,11 @@ var self = this; self.addClass('iframe'); self.canFocus = false; + /*eslint no-script-url:0 */ return ( '<iframe id="' + self._id + '" class="' + self.classes() + '" tabindex="-1" src="' + (self.settings.url || "javascript:\'\'") + '" frameborder="0"></iframe>' ); }, @@ -30946,11 +31526,11 @@ self.on('mousedown', function(e) { e.preventDefault(); }); - if (settings.menu) { + if (settings.menu && !settings.ariaHideMenu) { self.aria('haspopup', true); } }, /** @@ -31090,11 +31670,11 @@ icon = prefix + 'ico ' + prefix + 'i-' + (self.settings.icon || 'none'); return ( '<div id="' + id + '" class="' + self.classes() + '" tabindex="-1">' + - (text !== '-' ? '<i class="' + icon + '"' + image + '></i>&nbsp;' : '') + + (text !== '-' ? '<i class="' + icon + '"' + image + '></i>\u00a0' : '') + (text !== '-' ? '<span id="' + id + '-text" class="' + prefix + 'text">' + text + '</span>' : '') + (shortcut ? '<div id="' + id + '-shortcut" class="' + prefix + 'menu-shortcut">' + shortcut + '</div>' : '') + (settings.menu ? '<div class="' + prefix + 'caret"></div>' : '') + '</div>' ); @@ -31544,15 +32124,15 @@ var self = this, id = self._id, prefix = self.classPrefix; var icon = self.settings.icon ? prefix + 'ico ' + prefix + 'i-' + self.settings.icon : ''; return ( '<div id="' + id + '" class="' + self.classes() + '" role="button" tabindex="-1">' + - '<button type="button" hidefocus tabindex="-1">' + + '<button type="button" hidefocus="1" tabindex="-1">' + (icon ? '<i class="' + icon + '"></i>' : '') + (self._text ? (icon ? ' ' : '') + self._text : '') + '</button>' + - '<button type="button" class="' + prefix + 'open" hidefocus tabindex="-1">' + + '<button type="button" class="' + prefix + 'open" hidefocus="1" tabindex="-1">' + //(icon ? '<i class="' + icon + '"></i>' : '') + (self._menuBtnText ? (icon ? '\u00a0' : '') + self._menuBtnText : '') + ' <i class="' + prefix + 'caret"></i>' + '</button>' + '</div>' @@ -31716,11 +32296,11 @@ '</div>' ); }); return ( - '<div id="' + self._id + '" class="' + self.classes() + '" hideFocus="1" tabIndex="-1">' + + '<div id="' + self._id + '" class="' + self.classes() + '" hidefocus="1" tabindex="-1">' + '<div id="' + self._id + '-head" class="' + prefix + 'tabs" role="tablist">' + tabsHtml + '</div>' + '<div id="' + self._id + '-body" class="' + self.classes('body') + '">' + layout.renderHtml(self) + @@ -31992,16 +32572,16 @@ if (settings.multiline) { return ( '<textarea id="' + id + '" class="' + self.classes() + '" ' + (settings.rows ? ' rows="' + settings.rows + '"' : '') + - ' hidefocus="true"' + extraAttrs + '>' + value + + ' hidefocus="1"' + extraAttrs + '>' + value + '</textarea>' ); } - return '<input id="' + id + '" class="' + self.classes() + '" value="' + value + '" hidefocus="true"' + extraAttrs + '>'; + return '<input id="' + id + '" class="' + self.classes() + '" value="' + value + '" hidefocus="1"' + extraAttrs + ' />'; }, /** * Called after the control has been rendered. * @@ -32041,22 +32621,24 @@ * * @-x-less Throbber.less * @class tinymce.ui.Throbber */ define("tinymce/ui/Throbber", [ - "tinymce/ui/DomUtils" -], function(DomUtils) { + "tinymce/ui/DomUtils", + "tinymce/ui/Control" +], function(DomUtils, Control) { "use strict"; /** * Constructs a new throbber. * * @constructor * @param {Element} elm DOM Html element to display throbber in. + * @param {Boolean} inline Optional true/false state if the throbber should be appended to end of element for infinite scroll. */ - return function(elm) { - var self = this, state; + return function(elm, inline) { + var self = this, state, classPrefix = Control.classPrefix; /** * Shows the throbber. * * @method show @@ -32068,11 +32650,13 @@ state = true; window.setTimeout(function() { if (state) { - elm.appendChild(DomUtils.createFragment('<div class="mce-throbber"></div>')); + elm.appendChild(DomUtils.createFragment( + '<div class="' + classPrefix + 'throbber' + (inline ? ' ' + classPrefix + 'throbber-inline' : '') + '"></div>' + )); } }, time || 0); return self; }; @@ -32095,7 +32679,7 @@ return self; }; }; }); -expose(["tinymce/dom/Sizzle","tinymce/html/Styles","tinymce/dom/EventUtils","tinymce/dom/TreeWalker","tinymce/util/Tools","tinymce/dom/Range","tinymce/html/Entities","tinymce/Env","tinymce/dom/StyleSheetLoader","tinymce/dom/DOMUtils","tinymce/dom/ScriptLoader","tinymce/AddOnManager","tinymce/html/Node","tinymce/html/Schema","tinymce/html/SaxParser","tinymce/html/DomParser","tinymce/html/Writer","tinymce/html/Serializer","tinymce/dom/Serializer","tinymce/dom/TridentSelection","tinymce/util/VK","tinymce/dom/ControlSelection","tinymce/dom/RangeUtils","tinymce/dom/Selection","tinymce/Formatter","tinymce/UndoManager","tinymce/EnterKey","tinymce/ForceBlocks","tinymce/EditorCommands","tinymce/util/URI","tinymce/util/Class","tinymce/ui/Selector","tinymce/ui/Collection","tinymce/ui/DomUtils","tinymce/ui/Control","tinymce/ui/Factory","tinymce/ui/KeyboardNavigation","tinymce/ui/Container","tinymce/ui/DragHelper","tinymce/ui/Scrollable","tinymce/ui/Panel","tinymce/ui/Movable","tinymce/ui/Resizable","tinymce/ui/FloatPanel","tinymce/ui/Window","tinymce/ui/MessageBox","tinymce/WindowManager","tinymce/util/Quirks","tinymce/util/Observable","tinymce/Shortcuts","tinymce/Editor","tinymce/util/I18n","tinymce/FocusManager","tinymce/EditorManager","tinymce/LegacyInput","tinymce/util/XHR","tinymce/util/JSON","tinymce/util/JSONRequest","tinymce/util/JSONP","tinymce/util/LocalStorage","tinymce/Compat","tinymce/ui/Layout","tinymce/ui/AbsoluteLayout","tinymce/ui/Tooltip","tinymce/ui/Widget","tinymce/ui/Button","tinymce/ui/ButtonGroup","tinymce/ui/Checkbox","tinymce/ui/PanelButton","tinymce/ui/ColorButton","tinymce/ui/ComboBox","tinymce/ui/Path","tinymce/ui/ElementPath","tinymce/ui/FormItem","tinymce/ui/Form","tinymce/ui/FieldSet","tinymce/ui/FilePicker","tinymce/ui/FitLayout","tinymce/ui/FlexLayout","tinymce/ui/FlowLayout","tinymce/ui/FormatControls","tinymce/ui/GridLayout","tinymce/ui/Iframe","tinymce/ui/Label","tinymce/ui/Toolbar","tinymce/ui/MenuBar","tinymce/ui/MenuButton","tinymce/ui/ListBox","tinymce/ui/MenuItem","tinymce/ui/Menu","tinymce/ui/Radio","tinymce/ui/ResizeHandle","tinymce/ui/Spacer","tinymce/ui/SplitButton","tinymce/ui/StackLayout","tinymce/ui/TabPanel","tinymce/ui/TextBox","tinymce/ui/Throbber"]); +expose(["tinymce/dom/Sizzle","tinymce/html/Styles","tinymce/dom/EventUtils","tinymce/dom/TreeWalker","tinymce/util/Tools","tinymce/dom/Range","tinymce/html/Entities","tinymce/Env","tinymce/dom/StyleSheetLoader","tinymce/dom/DOMUtils","tinymce/dom/ScriptLoader","tinymce/AddOnManager","tinymce/html/Node","tinymce/html/Schema","tinymce/html/SaxParser","tinymce/html/DomParser","tinymce/html/Writer","tinymce/html/Serializer","tinymce/dom/Serializer","tinymce/dom/TridentSelection","tinymce/util/VK","tinymce/dom/ControlSelection","tinymce/dom/RangeUtils","tinymce/dom/Selection","tinymce/fmt/Preview","tinymce/Formatter","tinymce/UndoManager","tinymce/EnterKey","tinymce/ForceBlocks","tinymce/EditorCommands","tinymce/util/URI","tinymce/util/Class","tinymce/util/EventDispatcher","tinymce/ui/Selector","tinymce/ui/Collection","tinymce/ui/DomUtils","tinymce/ui/Control","tinymce/ui/Factory","tinymce/ui/KeyboardNavigation","tinymce/ui/Container","tinymce/ui/DragHelper","tinymce/ui/Scrollable","tinymce/ui/Panel","tinymce/ui/Movable","tinymce/ui/Resizable","tinymce/ui/FloatPanel","tinymce/ui/Window","tinymce/ui/MessageBox","tinymce/WindowManager","tinymce/util/Quirks","tinymce/util/Observable","tinymce/EditorObservable","tinymce/Shortcuts","tinymce/Editor","tinymce/util/I18n","tinymce/FocusManager","tinymce/EditorManager","tinymce/LegacyInput","tinymce/util/XHR","tinymce/util/JSON","tinymce/util/JSONRequest","tinymce/util/JSONP","tinymce/util/LocalStorage","tinymce/Compat","tinymce/ui/Layout","tinymce/ui/AbsoluteLayout","tinymce/ui/Tooltip","tinymce/ui/Widget","tinymce/ui/Button","tinymce/ui/ButtonGroup","tinymce/ui/Checkbox","tinymce/ui/PanelButton","tinymce/ui/ColorButton","tinymce/ui/ComboBox","tinymce/ui/Path","tinymce/ui/ElementPath","tinymce/ui/FormItem","tinymce/ui/Form","tinymce/ui/FieldSet","tinymce/ui/FilePicker","tinymce/ui/FitLayout","tinymce/ui/FlexLayout","tinymce/ui/FlowLayout","tinymce/ui/FormatControls","tinymce/ui/GridLayout","tinymce/ui/Iframe","tinymce/ui/Label","tinymce/ui/Toolbar","tinymce/ui/MenuBar","tinymce/ui/MenuButton","tinymce/ui/ListBox","tinymce/ui/MenuItem","tinymce/ui/Menu","tinymce/ui/Radio","tinymce/ui/ResizeHandle","tinymce/ui/Spacer","tinymce/ui/SplitButton","tinymce/ui/StackLayout","tinymce/ui/TabPanel","tinymce/ui/TextBox","tinymce/ui/Throbber"]); })(this); \ No newline at end of file