abcabc123
would produceabc
abc123
. + * + * @method split + * @param {Element} parentElm Parent element to split. + * @param {Element} splitElm Element to split at. + * @param {Element} replacementElm Optional replacement element to replace the split element with. + * @return {Element} Returns the split element or the replacement element if that is specified. + */ + split: function(parentElm, splitElm, replacementElm) { + var self = this, r = self.createRng(), bef, aft, pa; + + // W3C valid browsers tend to leave empty nodes to the left/right side of the contents - this makes sense + // but we don't want that in our code since it serves no purpose for the end user + // For example splitting this html at the bold element: + //text 1CHOPtext 2
+ // would produce: + //text 1
CHOPtext 2
+ // this function will then trim off empty edges and produce: + //text 1
CHOPtext 2
+ function trimNode(node) { + var i, children = node.childNodes, type = node.nodeType; + + function surroundedBySpans(node) { + var previousIsSpan = node.previousSibling && node.previousSibling.nodeName == 'SPAN'; + var nextIsSpan = node.nextSibling && node.nextSibling.nodeName == 'SPAN'; + return previousIsSpan && nextIsSpan; + } + + if (type == 1 && node.getAttribute('data-mce-type') == 'bookmark') { + return; + } + + for (i = children.length - 1; i >= 0; i--) { + trimNode(children[i]); + } + + if (type != 9) { + // Keep non whitespace text nodes + if (type == 3 && node.nodeValue.length > 0) { + // If parent element isn't a block or there isn't any useful contents for example "" + // Also keep text nodes with only spaces if surrounded by spans. + // eg. "
a b
" should keep space between a and b + var trimmedLength = trim(node.nodeValue).length; + if (!self.isBlock(node.parentNode) || trimmedLength > 0 || trimmedLength === 0 && surroundedBySpans(node)) { + return; + } + } else if (type == 1) { + // If the only child is a bookmark then move it up + children = node.childNodes; + + // TODO fix this complex if + if (children.length == 1 && children[0] && children[0].nodeType == 1 && + children[0].getAttribute('data-mce-type') == 'bookmark') { + node.parentNode.insertBefore(children[0], node); + } + + // Keep non empty elements or img, hr etc + if (children.length || /^(br|hr|input|img)$/i.test(node.nodeName)) { + return; + } + } + + self.remove(node); + } + + return node; + } + + if (parentElm && splitElm) { + // Get before chunk + r.setStart(parentElm.parentNode, self.nodeIndex(parentElm)); + r.setEnd(splitElm.parentNode, self.nodeIndex(splitElm)); + bef = r.extractContents(); + + // Get after chunk + r = self.createRng(); + r.setStart(splitElm.parentNode, self.nodeIndex(splitElm) + 1); + r.setEnd(parentElm.parentNode, self.nodeIndex(parentElm) + 1); + aft = r.extractContents(); + + // Insert before chunk + pa = parentElm.parentNode; + pa.insertBefore(trimNode(bef), parentElm); + + // Insert middle chunk + if (replacementElm) { + pa.insertBefore(replacementElm, parentElm); + //pa.replaceChild(replacementElm, splitElm); + } else { + pa.insertBefore(splitElm, parentElm); + } + + // Insert after chunk + pa.insertBefore(trimNode(aft), parentElm); + self.remove(parentElm); + + return replacementElm || splitElm; + } + }, + + /** + * Adds an event handler to the specified object. + * + * @method bind + * @param {Element/Document/Window/Array} target Target element to bind events to. + * handler to or an array of elements/ids/documents. + * @param {String} name Name of event handler to add, for example: click. + * @param {function} func Function to execute when the event occurs. + * @param {Object} scope Optional scope to execute the function in. + * @return {function} Function callback handler the same as the one passed in. + */ + bind: function(target, name, func, scope) { + var self = this; + + if (Tools.isArray(target)) { + var i = target.length; + + while (i--) { + target[i] = self.bind(target[i], name, func, scope); + } + + return target; + } + + // Collect all window/document events bound by editor instance + if (self.settings.collect && (target === self.doc || target === self.win)) { + self.boundEvents.push([target, name, func, scope]); + } + + return self.events.bind(target, name, func, scope || self); + }, + + /** + * Removes the specified event handler by name and function from an element or collection of elements. + * + * @method unbind + * @param {Element/Document/Window/Array} target Target element to unbind events on. + * @param {String} name Event handler name, for example: "click" + * @param {function} func Function to remove. + * @return {bool/Array} Bool state of true if the handler was removed, or an array of states if multiple input elements + * were passed in. + */ + unbind: function(target, name, func) { + var self = this, i; + + if (Tools.isArray(target)) { + i = target.length; + + while (i--) { + target[i] = self.unbind(target[i], name, func); + } + + return target; + } + + // Remove any bound events matching the input + if (self.boundEvents && (target === self.doc || target === self.win)) { + i = self.boundEvents.length; + + while (i--) { + var item = self.boundEvents[i]; + + if (target == item[0] && (!name || name == item[1]) && (!func || func == item[2])) { + this.events.unbind(item[0], item[1], item[2]); + } + } + } + + return this.events.unbind(target, name, func); + }, + + /** + * Fires the specified event name with object on target. + * + * @method fire + * @param {Node/Document/Window} target Target element or object to fire event on. + * @param {String} name Name of the event to fire. + * @param {Object} evt Event object to send. + * @return {Event} Event object. + */ + fire: function(target, name, evt) { + return this.events.fire(target, name, evt); + }, + + // Returns the content editable state of a node + getContentEditable: function(node) { + var contentEditable; + + // Check type + if (!node || node.nodeType != 1) { + return null; + } + + // Check for fake content editable + contentEditable = node.getAttribute("data-mce-contenteditable"); + if (contentEditable && contentEditable !== "inherit") { + return contentEditable; + } + + // 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 + */ + destroy: function() { + var self = this; + + // Unbind all events bound to window/document by editor instance + if (self.boundEvents) { + var i = self.boundEvents.length; + + while (i--) { + var item = self.boundEvents[i]; + this.events.unbind(item[0], item[1], item[2]); + } + + self.boundEvents = null; + } + + // Restore sizzle document to window.document + // Since the current document might be removed producing "Permission denied" on IE see #6325 + if (Sizzle.setDocument) { + Sizzle.setDocument(); + } + + 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 + + ', startOffset: ' + r.startOffset + + ', endContainer: ' + r.endContainer.nodeName + + ', endOffset: ' + r.endOffset + ); + }, + + // #endif + + _findSib: function(node, selector, name) { + var self = this, func = selector; + + if (node) { + // If expression make a function of it using is + if (typeof func == 'string') { + func = function(node) { + return self.is(node, selector); + }; + } + + // Loop all siblings + for (node = node[name]; node; node = node[name]) { + if (func(node)) { + return node; + } + } + } + + return null; + } + }; + + /** + * Instance of DOMUtils for the current document. + * + * @static + * @property DOM + * @type tinymce.dom.DOMUtils + * @example + * // Example of how to add a class to some element by id + * tinymce.DOM.addClass('someid', 'someclass'); + */ + DOMUtils.DOM = new DOMUtils(document); + DOMUtils.nodeIndex = nodeIndex; + + return DOMUtils; +}); + +// Included from: js/tinymce/classes/dom/ScriptLoader.js + +/** + * ScriptLoader.js + * + * Released under LGPL License. + * Copyright (c) 1999-2015 Ephox Corp. All rights reserved + * + * License: http://www.tinymce.com/license + * Contributing: http://www.tinymce.com/contributing + */ + +/*globals console*/ + +/** + * This class handles asynchronous/synchronous loading of JavaScript files it will execute callbacks + * when various items gets loaded. This class is useful to load external JavaScript files. + * + * @class tinymce.dom.ScriptLoader + * @example + * // Load a script from a specific URL using the global script loader + * tinymce.ScriptLoader.load('somescript.js'); + * + * // Load a script using a unique instance of the script loader + * var scriptLoader = new tinymce.dom.ScriptLoader(); + * + * scriptLoader.load('somescript.js'); + * + * // Load multiple scripts + * var scriptLoader = new tinymce.dom.ScriptLoader(); + * + * scriptLoader.add('somescript1.js'); + * scriptLoader.add('somescript2.js'); + * scriptLoader.add('somescript3.js'); + * + * scriptLoader.loadQueue(function() { + * alert('All scripts are now loaded.'); + * }); + */ +define("tinymce/dom/ScriptLoader", [ + "tinymce/dom/DOMUtils", + "tinymce/util/Tools" +], function(DOMUtils, Tools) { + var DOM = DOMUtils.DOM; + var each = Tools.each, grep = Tools.grep; + + var isFunction = function (f) { + return typeof f === 'function'; + }; + + function ScriptLoader() { + var QUEUED = 0, + LOADING = 1, + LOADED = 2, + FAILED = 3, + states = {}, + queue = [], + scriptLoadedCallbacks = {}, + queueLoadedCallbacks = [], + loading = 0, + undef; + + /** + * Loads a specific script directly without adding it to the load queue. + * + * @method load + * @param {String} url Absolute URL to script to add. + * @param {function} callback Optional success callback function when the script loaded successfully. + * @param {function} callback Optional failure callback function when the script failed to load. + */ + function loadScript(url, success, failure) { + var dom = DOM, elm, id; + + // Execute callback when script is loaded + function done() { + dom.remove(id); + + if (elm) { + elm.onreadystatechange = elm.onload = elm = null; + } + + success(); + } + + function error() { + /*eslint no-console:0 */ + + // We can't mark it as done if there is a load error since + // A) We don't want to produce 404 errors on the server and + // B) the onerror event won't fire on all browsers. + // done(); + + if (isFunction(failure)) { + failure(); + } else { + // Report the error so it's easier for people to spot loading errors + if (typeof console !== "undefined" && console.log) { + console.log("Failed to load script: " + url); + } + } + } + + id = dom.uniqueId(); + + // Create new script element + elm = document.createElement('script'); + elm.id = id; + elm.type = 'text/javascript'; + elm.src = Tools._addCacheSuffix(url); + + // Seems that onreadystatechange works better on IE 10 onload seems to fire incorrectly + if ("onreadystatechange" in elm) { + elm.onreadystatechange = function() { + if (/loaded|complete/.test(elm.readyState)) { + done(); + } + }; + } else { + elm.onload = done; + } + + // Add onerror event will get fired on some browsers but not all of them + elm.onerror = error; + + // Add script to document + (document.getElementsByTagName('head')[0] || document.body).appendChild(elm); + } + + /** + * Returns true/false if a script has been loaded or not. + * + * @method isDone + * @param {String} url URL to check for. + * @return {Boolean} true/false if the URL is loaded. + */ + this.isDone = function(url) { + return states[url] == LOADED; + }; + + /** + * Marks a specific script to be loaded. This can be useful if a script got loaded outside + * the script loader or to skip it from loading some script. + * + * @method markDone + * @param {string} url Absolute URL to the script to mark as loaded. + */ + this.markDone = function(url) { + states[url] = LOADED; + }; + + /** + * Adds a specific script to the load queue of the script loader. + * + * @method add + * @param {String} url Absolute URL to script to add. + * @param {function} success Optional success callback function to execute when the script loades successfully. + * @param {Object} scope Optional scope to execute callback in. + * @param {function} failure Optional failure callback function to execute when the script failed to load. + */ + this.add = this.load = function(url, success, scope, failure) { + var state = states[url]; + + // Add url to load queue + if (state == undef) { + queue.push(url); + states[url] = QUEUED; + } + + if (success) { + // Store away callback for later execution + if (!scriptLoadedCallbacks[url]) { + scriptLoadedCallbacks[url] = []; + } + + scriptLoadedCallbacks[url].push({ + success: success, + failure: failure, + scope: scope || this + }); + } + }; + + this.remove = function(url) { + delete states[url]; + delete scriptLoadedCallbacks[url]; + }; + + /** + * Starts the loading of the queue. + * + * @method loadQueue + * @param {function} success Optional callback to execute when all queued items are loaded. + * @param {function} failure Optional callback to execute when queued items failed to load. + * @param {Object} scope Optional scope to execute the callback in. + */ + this.loadQueue = function(success, scope, failure) { + this.loadScripts(queue, success, scope, failure); + }; + + /** + * Loads the specified queue of files and executes the callback ones they are loaded. + * This method is generally not used outside this class but it might be useful in some scenarios. + * + * @method loadScripts + * @param {Array} scripts Array of queue items to load. + * @param {function} callback Optional callback to execute when scripts is loaded successfully. + * @param {Object} scope Optional scope to execute callback in. + * @param {function} callback Optional callback to execute if scripts failed to load. + */ + this.loadScripts = function(scripts, success, scope, failure) { + var loadScripts, failures = []; + + function execCallbacks(name, url) { + // Execute URL callback functions + each(scriptLoadedCallbacks[url], function(callback) { + if (isFunction(callback[name])) { + callback[name].call(callback.scope); + } + }); + + scriptLoadedCallbacks[url] = undef; + } + + queueLoadedCallbacks.push({ + success: success, + failure: failure, + scope: scope || this + }); + + loadScripts = function() { + var loadingScripts = grep(scripts); + + // Current scripts has been handled + scripts.length = 0; + + // Load scripts that needs to be loaded + each(loadingScripts, function(url) { + // Script is already loaded then execute script callbacks directly + if (states[url] === LOADED) { + execCallbacks('success', url); + return; + } + + if (states[url] === FAILED) { + execCallbacks('failure', url); + return; + } + + // Is script not loading then start loading it + if (states[url] !== LOADING) { + states[url] = LOADING; + loading++; + + loadScript(url, function() { + states[url] = LOADED; + loading--; + + execCallbacks('success', url); + + // Load more scripts if they where added by the recently loaded script + loadScripts(); + }, function () { + states[url] = FAILED; + loading--; + + failures.push(url); + execCallbacks('failure', url); + + // Load more scripts if they where added by the recently loaded script + loadScripts(); + }); + } + }); + + // No scripts are currently loading then execute all pending queue loaded callbacks + if (!loading) { + each(queueLoadedCallbacks, function(callback) { + if (failures.length === 0) { + if (isFunction(callback.success)) { + callback.success.call(callback.scope); + } + } else { + if (isFunction(callback.failure)) { + callback.failure.call(callback.scope, failures); + } + } + }); + + queueLoadedCallbacks.length = 0; + } + }; + + loadScripts(); + }; + } + + ScriptLoader.ScriptLoader = new ScriptLoader(); + + return ScriptLoader; +}); + +// Included from: js/tinymce/classes/AddOnManager.js + +/** + * AddOnManager.js + * + * Released under LGPL License. + * Copyright (c) 1999-2015 Ephox Corp. All rights reserved + * + * License: http://www.tinymce.com/license + * Contributing: http://www.tinymce.com/contributing + */ + +/** + * This class handles the loading of themes/plugins or other add-ons and their language packs. + * + * @class tinymce.AddOnManager + */ +define("tinymce/AddOnManager", [ + "tinymce/dom/ScriptLoader", + "tinymce/util/Tools" +], function(ScriptLoader, Tools) { + var each = Tools.each; + + function AddOnManager() { + var self = this; + + self.items = []; + self.urls = {}; + self.lookup = {}; + } + + AddOnManager.prototype = { + /** + * Returns the specified add on by the short name. + * + * @method get + * @param {String} name Add-on to look for. + * @return {tinymce.Theme/tinymce.Plugin} Theme or plugin add-on instance or undefined. + */ + get: function(name) { + if (this.lookup[name]) { + return this.lookup[name].instance; + } + + return undefined; + }, + + dependencies: function(name) { + var result; + + if (this.lookup[name]) { + result = this.lookup[name].dependencies; + } + + return result || []; + }, + + /** + * Loads a language pack for the specified add-on. + * + * @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) { + 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/' + language + '.js'); + } + }, + + /** + * Adds a instance of the add-on by it's short name. + * + * @method add + * @param {String} id Short name/id for the add-on. + * @param {tinymce.Theme/tinymce.Plugin} addOn Theme or plugin to add. + * @return {tinymce.Theme/tinymce.Plugin} The same theme or plugin instance that got passed in. + * @example + * // Create a simple plugin + * tinymce.create('tinymce.plugins.TestPlugin', { + * TestPlugin: function(ed, url) { + * ed.on('click', function(e) { + * ed.windowManager.alert('Hello World!'); + * }); + * } + * }); + * + * // Register plugin using the add method + * tinymce.PluginManager.add('test', tinymce.plugins.TestPlugin); + * + * // Initialize TinyMCE + * tinymce.init({ + * ... + * plugins: '-test' // Init the plugin but don't try to load it + * }); + */ + add: function(id, addOn, dependencies) { + this.items.push(addOn); + this.lookup[id] = {instance: addOn, dependencies: dependencies}; + + return addOn; + }, + + remove: function(name) { + delete this.urls[name]; + delete this.lookup[name]; + }, + + createUrl: function(baseUrl, dep) { + if (typeof dep === "object") { + return dep; + } + + return {prefix: baseUrl.prefix, resource: dep, suffix: baseUrl.suffix}; + }, + + /** + * Add a set of components that will make up the add-on. Using the url of the add-on name as the base url. + * This should be used in development mode. A new compressor/javascript munger process will ensure that the + * components are put together into the plugin.js file and compressed correctly. + * + * @method addComponents + * @param {String} pluginName name of the plugin to load scripts from (will be used to get the base url for the plugins). + * @param {Array} scripts Array containing the names of the scripts to load. + */ + addComponents: function(pluginName, scripts) { + var pluginUrl = this.urls[pluginName]; + + each(scripts, function(script) { + ScriptLoader.ScriptLoader.add(pluginUrl + "/" + script); + }); + }, + + /** + * Loads an add-on from a specific url. + * + * @method load + * @param {String} name Short name of the add-on that gets loaded. + * @param {String} addOnUrl URL to the add-on that will get loaded. + * @param {function} success Optional success callback to execute when an add-on is loaded. + * @param {Object} scope Optional scope to execute the callback in. + * @param {function} failure Optional failure callback to execute when an add-on failed to load. + * @example + * // Loads a plugin from an external URL + * tinymce.PluginManager.load('myplugin', '/some/dir/someplugin/plugin.js'); + * + * // Initialize TinyMCE + * tinymce.init({ + * ... + * plugins: '-myplugin' // Don't try to load it again + * }); + */ + load: function(name, addOnUrl, success, scope, failure) { + var self = this, url = addOnUrl; + + function loadDependencies() { + var dependencies = self.dependencies(name); + + each(dependencies, function(dep) { + var newUrl = self.createUrl(addOnUrl, dep); + + self.load(newUrl.resource, newUrl, undefined, undefined); + }); + + if (success) { + if (scope) { + success.call(scope); + } else { + success.call(ScriptLoader); + } + } + } + + if (self.urls[name]) { + return; + } + + if (typeof addOnUrl === "object") { + url = addOnUrl.prefix + addOnUrl.resource + addOnUrl.suffix; + } + + if (url.indexOf('/') !== 0 && url.indexOf('://') == -1) { + url = AddOnManager.baseURL + '/' + url; + } + + self.urls[name] = url.substring(0, url.lastIndexOf('/')); + + if (self.lookup[name]) { + loadDependencies(); + } else { + ScriptLoader.ScriptLoader.add(url, loadDependencies, scope, failure); + } + } + }; + + AddOnManager.PluginManager = new AddOnManager(); + AddOnManager.ThemeManager = new AddOnManager(); + + return AddOnManager; +}); + +/** + * TinyMCE theme class. + * + * @class tinymce.Theme + */ + +/** + * This method is responsible for rendering/generating the overall user interface with toolbars, buttons, iframe containers etc. + * + * @method renderUI + * @param {Object} obj Object parameter containing the targetNode DOM node that will be replaced visually with an editor instance. + * @return {Object} an object with items like iframeContainer, editorContainer, sizeContainer, deltaWidth, deltaHeight. + */ + +/** + * Plugin base class, this is a pseudo class that describes how a plugin is to be created for TinyMCE. The methods below are all optional. + * + * @class tinymce.Plugin + * @example + * tinymce.PluginManager.add('example', function(editor, url) { + * // Add a button that opens a window + * editor.addButton('example', { + * text: 'My button', + * icon: false, + * onclick: function() { + * // Open window + * editor.windowManager.open({ + * title: 'Example plugin', + * body: [ + * {type: 'textbox', name: 'title', label: 'Title'} + * ], + * onsubmit: function(e) { + * // Insert content when the window form is submitted + * editor.insertContent('Title: ' + e.data.title); + * } + * }); + * } + * }); + * + * // Adds a menu item to the tools menu + * editor.addMenuItem('example', { + * text: 'Example plugin', + * context: 'tools', + * onclick: function() { + * // Open window with a specific url + * editor.windowManager.open({ + * title: 'TinyMCE site', + * url: 'http://www.tinymce.com', + * width: 800, + * height: 600, + * buttons: [{ + * text: 'Close', + * onclick: 'close' + * }] + * }); + * } + * }); + * }); + */ + +// Included from: js/tinymce/classes/dom/NodeType.js + +/** + * NodeType.js + * + * Released under LGPL License. + * Copyright (c) 1999-2015 Ephox Corp. All rights reserved + * + * License: http://www.tinymce.com/license + * Contributing: http://www.tinymce.com/contributing + */ + +/** + * Contains various node validation functions. + * + * @private + * @class tinymce.dom.NodeType + */ +define("tinymce/dom/NodeType", [], function() { + function isNodeType(type) { + return function(node) { + return !!node && node.nodeType == type; + }; + } + + var isElement = isNodeType(1); + + function matchNodeNames(names) { + names = names.toLowerCase().split(' '); + + return function(node) { + var i, name; + + if (node && node.nodeType) { + name = node.nodeName.toLowerCase(); + + for (i = 0; i < names.length; i++) { + if (name === names[i]) { + return true; + } + } + } + + return false; + }; + } + + function matchStyleValues(name, values) { + values = values.toLowerCase().split(' '); + + return function(node) { + var i, cssValue; + + if (isElement(node)) { + for (i = 0; i < values.length; i++) { + cssValue = getComputedStyle(node, null).getPropertyValue(name); + if (cssValue === values[i]) { + return true; + } + } + } + + return false; + }; + } + + function hasPropValue(propName, propValue) { + return function(node) { + return isElement(node) && node[propName] === propValue; + }; + } + + function hasAttributeValue(attrName, attrValue) { + return function(node) { + return isElement(node) && node.getAttribute(attrName) === attrValue; + }; + } + + function isBogus(node) { + return isElement(node) && node.hasAttribute('data-mce-bogus'); + } + + function hasContentEditableState(value) { + return function(node) { + if (isElement(node)) { + if (node.contentEditable === value) { + return true; + } + + if (node.getAttribute('data-mce-contenteditable') === value) { + return true; + } + } + + return false; + }; + } + + return { + isText: isNodeType(3), + isElement: isElement, + isComment: isNodeType(8), + isBr: matchNodeNames('br'), + isContentEditableTrue: hasContentEditableState('true'), + isContentEditableFalse: hasContentEditableState('false'), + matchNodeNames: matchNodeNames, + hasPropValue: hasPropValue, + hasAttributeValue: hasAttributeValue, + matchStyleValues: matchStyleValues, + isBogus: isBogus + }; +}); + +// Included from: js/tinymce/classes/text/Zwsp.js + +/** + * Zwsp.js + * + * Released under LGPL License. + * Copyright (c) 1999-2015 Ephox Corp. All rights reserved + * + * License: http://www.tinymce.com/license + * Contributing: http://www.tinymce.com/contributing + */ + +/** + * Utility functions for working with zero width space + * characters used as character containers etc. + * + * @private + * @class tinymce.text.Zwsp + * @example + * var isZwsp = Zwsp.isZwsp('\uFEFF'); + * var abc = Zwsp.trim('a\uFEFFc'); + */ +define("tinymce/text/Zwsp", [], function() { + var ZWSP = '\uFEFF'; + + function isZwsp(chr) { + return chr == ZWSP; + } + + function trim(str) { + return str.replace(new RegExp(ZWSP, 'g'), ''); + } + + return { + isZwsp: isZwsp, + ZWSP: ZWSP, + trim: trim + }; +}); + +// Included from: js/tinymce/classes/caret/CaretContainer.js + +/** + * CaretContainer.js + * + * Released under LGPL License. + * Copyright (c) 1999-2015 Ephox Corp. All rights reserved + * + * License: http://www.tinymce.com/license + * Contributing: http://www.tinymce.com/contributing + */ + +/** + * This module handles caret containers. A caret container is a node that + * holds the caret for positional purposes. + * + * @private + * @class tinymce.caret.CaretContainer + */ +define("tinymce/caret/CaretContainer", [ + "tinymce/dom/NodeType", + "tinymce/text/Zwsp" +], function(NodeType, Zwsp) { + var isElement = NodeType.isElement, + isText = NodeType.isText; + + function isCaretContainerBlock(node) { + if (isText(node)) { + node = node.parentNode; + } + + return isElement(node) && node.hasAttribute('data-mce-caret'); + } + + function isCaretContainerInline(node) { + return isText(node) && Zwsp.isZwsp(node.data); + } + + function isCaretContainer(node) { + return isCaretContainerBlock(node) || isCaretContainerInline(node); + } + + function removeNode(node) { + var parentNode = node.parentNode; + if (parentNode) { + parentNode.removeChild(node); + } + } + + function getNodeValue(node) { + try { + return node.nodeValue; + } catch (ex) { + // IE sometimes produces "Invalid argument" on nodes + return ""; + } + } + + function setNodeValue(node, text) { + if (text.length === 0) { + removeNode(node); + } else { + node.nodeValue = text; + } + } + + function insertInline(node, before) { + var doc, sibling, textNode, parentNode; + + doc = node.ownerDocument; + textNode = doc.createTextNode(Zwsp.ZWSP); + parentNode = node.parentNode; + + if (!before) { + sibling = node.nextSibling; + if (isText(sibling)) { + if (isCaretContainer(sibling)) { + return sibling; + } + + if (startsWithCaretContainer(sibling)) { + sibling.splitText(1); + return sibling; + } + } + + if (node.nextSibling) { + parentNode.insertBefore(textNode, node.nextSibling); + } else { + parentNode.appendChild(textNode); + } + } else { + sibling = node.previousSibling; + if (isText(sibling)) { + if (isCaretContainer(sibling)) { + return sibling; + } + + if (endsWithCaretContainer(sibling)) { + return sibling.splitText(sibling.data.length - 1); + } + } + + parentNode.insertBefore(textNode, node); + } + + return textNode; + } + + function createBogusBr() { + var br = document.createElement('br'); + br.setAttribute('data-mce-bogus', '1'); + return br; + } + + function insertBlock(blockName, node, before) { + var doc, blockNode, parentNode; + + doc = node.ownerDocument; + blockNode = doc.createElement(blockName); + blockNode.setAttribute('data-mce-caret', before ? 'before' : 'after'); + blockNode.setAttribute('data-mce-bogus', 'all'); + blockNode.appendChild(createBogusBr()); + parentNode = node.parentNode; + + if (!before) { + if (node.nextSibling) { + parentNode.insertBefore(blockNode, node.nextSibling); + } else { + parentNode.appendChild(blockNode); + } + } else { + parentNode.insertBefore(blockNode, node); + } + + return blockNode; + } + + function hasContent(node) { + return node.firstChild !== node.lastChild || !NodeType.isBr(node.firstChild); + } + + function remove(caretContainerNode) { + if (isElement(caretContainerNode) && isCaretContainer(caretContainerNode)) { + if (hasContent(caretContainerNode)) { + caretContainerNode.removeAttribute('data-mce-caret'); + } else { + removeNode(caretContainerNode); + } + } + + if (isText(caretContainerNode)) { + var text = Zwsp.trim(getNodeValue(caretContainerNode)); + setNodeValue(caretContainerNode, text); + } + } + + function startsWithCaretContainer(node) { + return isText(node) && node.data[0] == Zwsp.ZWSP; + } + + function endsWithCaretContainer(node) { + return isText(node) && node.data[node.data.length - 1] == Zwsp.ZWSP; + } + + function trimBogusBr(elm) { + var brs = elm.getElementsByTagName('br'); + var lastBr = brs[brs.length - 1]; + if (NodeType.isBogus(lastBr)) { + lastBr.parentNode.removeChild(lastBr); + } + } + + function showCaretContainerBlock(caretContainer) { + if (caretContainer && caretContainer.hasAttribute('data-mce-caret')) { + trimBogusBr(caretContainer); + caretContainer.removeAttribute('data-mce-caret'); + caretContainer.removeAttribute('data-mce-bogus'); + caretContainer.removeAttribute('style'); + caretContainer.removeAttribute('_moz_abspos'); + return caretContainer; + } + + return null; + } + + return { + isCaretContainer: isCaretContainer, + isCaretContainerBlock: isCaretContainerBlock, + isCaretContainerInline: isCaretContainerInline, + showCaretContainerBlock: showCaretContainerBlock, + insertInline: insertInline, + insertBlock: insertBlock, + hasContent: hasContent, + remove: remove, + startsWithCaretContainer: startsWithCaretContainer, + endsWithCaretContainer: endsWithCaretContainer + }; +}); + +// Included from: js/tinymce/classes/dom/RangeUtils.js + +/** + * RangeUtils.js + * + * Released under LGPL License. + * Copyright (c) 1999-2015 Ephox Corp. All rights reserved + * + * License: http://www.tinymce.com/license + * Contributing: http://www.tinymce.com/contributing + */ + +/** + * This class contains a few utility methods for ranges. + * + * @class tinymce.dom.RangeUtils + */ +define("tinymce/dom/RangeUtils", [ + "tinymce/util/Tools", + "tinymce/dom/TreeWalker", + "tinymce/dom/NodeType", + "tinymce/dom/Range", + "tinymce/caret/CaretContainer" +], function(Tools, TreeWalker, NodeType, Range, CaretContainer) { + var each = Tools.each, + isContentEditableTrue = NodeType.isContentEditableTrue, + isContentEditableFalse = NodeType.isContentEditableFalse, + isCaretContainer = CaretContainer.isCaretContainer; + + function hasCeProperty(node) { + return isContentEditableTrue(node) || isContentEditableFalse(node); + } + + function getEndChild(container, index) { + var childNodes = container.childNodes; + + index--; + + if (index > childNodes.length - 1) { + index = childNodes.length - 1; + } else if (index < 0) { + index = 0; + } + + return childNodes[index] || container; + } + + function findParent(node, rootNode, predicate) { + while (node && node !== rootNode) { + if (predicate(node)) { + return node; + } + + node = node.parentNode; + } + + return null; + } + + function hasParent(node, rootNode, predicate) { + return findParent(node, rootNode, predicate) !== null; + } + + function isFormatterCaret(node) { + return node.id === '_mce_caret'; + } + + function isCeFalseCaretContainer(node, rootNode) { + return isCaretContainer(node) && hasParent(node, rootNode, isFormatterCaret) === false; + } + + function RangeUtils(dom) { + /** + * Walks the specified range like object and executes the callback for each sibling collection it finds. + * + * @private + * @method walk + * @param {Object} rng Range like object. + * @param {function} callback Callback function to execute for each sibling collection. + */ + this.walk = function(rng, callback) { + var startContainer = rng.startContainer, + startOffset = rng.startOffset, + endContainer = rng.endContainer, + endOffset = rng.endOffset, + ancestor, startPoint, + endPoint, node, parent, siblings, nodes; + + // Handle table cell selection the table plugin enables + // you to fake select table cells and perform formatting actions on them + nodes = dom.select('td[data-mce-selected],th[data-mce-selected]'); + if (nodes.length > 0) { + each(nodes, function(node) { + callback([node]); + }); + + return; + } + + /** + * Excludes start/end text node if they are out side the range + * + * @private + * @param {Array} nodes Nodes to exclude items from. + * @return {Array} Array with nodes excluding the start/end container if needed. + */ + function exclude(nodes) { + var node; + + // First node is excluded + node = nodes[0]; + if (node.nodeType === 3 && node === startContainer && startOffset >= node.nodeValue.length) { + nodes.splice(0, 1); + } + + // Last node is excluded + node = nodes[nodes.length - 1]; + if (endOffset === 0 && nodes.length > 0 && node === endContainer && node.nodeType === 3) { + nodes.splice(nodes.length - 1, 1); + } + + return nodes; + } + + /** + * Collects siblings + * + * @private + * @param {Node} node Node to collect siblings from. + * @param {String} name Name of the sibling to check for. + * @param {Node} end_node + * @return {Array} Array of collected siblings. + */ + function collectSiblings(node, name, end_node) { + var siblings = []; + + for (; node && node != end_node; node = node[name]) { + siblings.push(node); + } + + return siblings; + } + + /** + * Find an end point this is the node just before the common ancestor root. + * + * @private + * @param {Node} node Node to start at. + * @param {Node} root Root/ancestor element to stop just before. + * @return {Node} Node just before the root element. + */ + function findEndPoint(node, root) { + do { + if (node.parentNode == root) { + return node; + } + + node = node.parentNode; + } while (node); + } + + function walkBoundary(start_node, end_node, next) { + var siblingName = next ? 'nextSibling' : 'previousSibling'; + + for (node = start_node, parent = node.parentNode; node && node != end_node; node = parent) { + parent = node.parentNode; + siblings = collectSiblings(node == start_node ? node : node[siblingName], siblingName); + + if (siblings.length) { + if (!next) { + siblings.reverse(); + } + + callback(exclude(siblings)); + } + } + } + + // If index based start position then resolve it + if (startContainer.nodeType == 1 && startContainer.hasChildNodes()) { + startContainer = startContainer.childNodes[startOffset]; + } + + // If index based end position then resolve it + if (endContainer.nodeType == 1 && endContainer.hasChildNodes()) { + endContainer = getEndChild(endContainer, endOffset); + } + + // Same container + if (startContainer == endContainer) { + return callback(exclude([startContainer])); + } + + // Find common ancestor and end points + ancestor = dom.findCommonAncestor(startContainer, endContainer); + + // Process left side + for (node = startContainer; node; node = node.parentNode) { + if (node === endContainer) { + return walkBoundary(startContainer, ancestor, true); + } + + if (node === ancestor) { + break; + } + } + + // Process right side + for (node = endContainer; node; node = node.parentNode) { + if (node === startContainer) { + return walkBoundary(endContainer, ancestor); + } + + if (node === ancestor) { + break; + } + } + + // Find start/end point + startPoint = findEndPoint(startContainer, ancestor) || startContainer; + endPoint = findEndPoint(endContainer, ancestor) || endContainer; + + // Walk left leaf + walkBoundary(startContainer, startPoint, true); + + // Walk the middle from start to end point + siblings = collectSiblings( + startPoint == startContainer ? startPoint : startPoint.nextSibling, + 'nextSibling', + endPoint == endContainer ? endPoint.nextSibling : endPoint + ); + + if (siblings.length) { + callback(exclude(siblings)); + } + + // Walk right leaf + walkBoundary(endContainer, endPoint); + }; + + /** + * Splits the specified range at it's start/end points. + * + * @private + * @param {Range/RangeObject} rng Range to split. + * @return {Object} Range position object. + */ + this.split = function(rng) { + var startContainer = rng.startContainer, + startOffset = rng.startOffset, + endContainer = rng.endContainer, + endOffset = rng.endOffset; + + function splitText(node, offset) { + return node.splitText(offset); + } + + // Handle single text node + if (startContainer == endContainer && startContainer.nodeType == 3) { + if (startOffset > 0 && startOffset < startContainer.nodeValue.length) { + endContainer = splitText(startContainer, startOffset); + startContainer = endContainer.previousSibling; + + if (endOffset > startOffset) { + endOffset = endOffset - startOffset; + startContainer = endContainer = splitText(endContainer, endOffset).previousSibling; + endOffset = endContainer.nodeValue.length; + startOffset = 0; + } else { + endOffset = 0; + } + } + } else { + // Split startContainer text node if needed + if (startContainer.nodeType == 3 && startOffset > 0 && startOffset < startContainer.nodeValue.length) { + startContainer = splitText(startContainer, startOffset); + startOffset = 0; + } + + // Split endContainer text node if needed + if (endContainer.nodeType == 3 && endOffset > 0 && endOffset < endContainer.nodeValue.length) { + endContainer = splitText(endContainer, endOffset).previousSibling; + endOffset = endContainer.nodeValue.length; + } + } + + return { + startContainer: startContainer, + startOffset: startOffset, + endContainer: endContainer, + endOffset: endOffset + }; + }; + + /** + * Normalizes the specified range by finding the closest best suitable caret location. + * + * @private + * @param {Range} rng Range to normalize. + * @return {Boolean} True/false if the specified range was normalized or not. + */ + this.normalize = function(rng) { + var normalized, collapsed; + + function normalizeEndPoint(start) { + var container, offset, walker, body = dom.getRoot(), node, nonEmptyElementsMap; + var directionLeft, isAfterNode; + + function isTableCell(node) { + return node && /^(TD|TH|CAPTION)$/.test(node.nodeName); + } + + function hasBrBeforeAfter(node, left) { + var walker = new TreeWalker(node, dom.getParent(node.parentNode, dom.isBlock) || body); + + while ((node = walker[left ? 'prev' : 'next']())) { + if (node.nodeName === "BR") { + return true; + } + } + } + + function hasContentEditableFalseParent(node) { + while (node && node != body) { + if (isContentEditableFalse(node)) { + return true; + } + + node = node.parentNode; + } + + return false; + } + + function isPrevNode(node, name) { + return node.previousSibling && node.previousSibling.nodeName == name; + } + + // Walks the dom left/right to find a suitable text node to move the endpoint into + // It will only walk within the current parent block or body and will stop if it hits a block or a BR/IMG + function findTextNodeRelative(left, startNode) { + var walker, lastInlineElement, parentBlockContainer; + + startNode = startNode || container; + parentBlockContainer = dom.getParent(startNode.parentNode, dom.isBlock) || body; + + // Lean left before the BR element if it's the only BR within a block element. Gecko bug: #6680 + // This:
|
|
[a
x|
|
+ if (node.nodeName == 'IMG' && selection.isCollapsed()) { + node = node.parentNode; + } + + // Get parents and add them to object + parents = []; + editor.dom.getParent(node, function(node) { + if (node === root) { + return true; + } + + parents.push(node); + }); + + args = args || {}; + args.element = node; + args.parents = parents; + + editor.fire('NodeChange', args); + } + }; + }; +}); + +// Included from: js/tinymce/classes/html/Node.js + +/** + * Node.js + * + * Released under LGPL License. + * Copyright (c) 1999-2015 Ephox Corp. All rights reserved + * + * License: http://www.tinymce.com/license + * Contributing: http://www.tinymce.com/contributing + */ + +/** + * This class is a minimalistic implementation of a DOM like node used by the DomParser class. + * + * @example + * var node = new tinymce.html.Node('strong', 1); + * someRoot.append(node); + * + * @class tinymce.html.Node + * @version 3.4 + */ +define("tinymce/html/Node", [], function() { + var whiteSpaceRegExp = /^[ \t\r\n]*$/, typeLookup = { + '#text': 3, + '#comment': 8, + '#cdata': 4, + '#pi': 7, + '#doctype': 10, + '#document-fragment': 11 + }; + + // Walks the tree left/right + function walk(node, root_node, prev) { + var sibling, parent, startName = prev ? 'lastChild' : 'firstChild', siblingName = prev ? 'prev' : 'next'; + + // Walk into nodes if it has a start + if (node[startName]) { + return node[startName]; + } + + // Return the sibling if it has one + if (node !== root_node) { + sibling = node[siblingName]; + + if (sibling) { + return sibling; + } + + // Walk up the parents to look for siblings + for (parent = node.parent; parent && parent !== root_node; parent = parent.parent) { + sibling = parent[siblingName]; + + if (sibling) { + return sibling; + } + } + } + } + + /** + * Constructs a new Node instance. + * + * @constructor + * @method Node + * @param {String} name Name of the node type. + * @param {Number} type Numeric type representing the node. + */ + function Node(name, type) { + this.name = name; + this.type = type; + + if (type === 1) { + this.attributes = []; + this.attributes.map = {}; + } + } + + Node.prototype = { + /** + * Replaces the current node with the specified one. + * + * @example + * someNode.replace(someNewNode); + * + * @method replace + * @param {tinymce.html.Node} node Node to replace the current node with. + * @return {tinymce.html.Node} The old node that got replaced. + */ + replace: function(node) { + var self = this; + + if (node.parent) { + node.remove(); + } + + self.insert(node, self); + self.remove(); + + return self; + }, + + /** + * Gets/sets or removes an attribute by name. + * + * @example + * someNode.attr("name", "value"); // Sets an attribute + * console.log(someNode.attr("name")); // Gets an attribute + * someNode.attr("name", null); // Removes an attribute + * + * @method attr + * @param {String} name Attribute name to set or get. + * @param {String} value Optional value to set. + * @return {String/tinymce.html.Node} String or undefined on a get operation or the current node on a set operation. + */ + attr: function(name, value) { + var self = this, attrs, i, undef; + + if (typeof name !== "string") { + for (i in name) { + self.attr(i, name[i]); + } + + return self; + } + + if ((attrs = self.attributes)) { + if (value !== undef) { + // Remove attribute + if (value === null) { + if (name in attrs.map) { + delete attrs.map[name]; + + i = attrs.length; + while (i--) { + if (attrs[i].name === name) { + attrs = attrs.splice(i, 1); + return self; + } + } + } + + return self; + } + + // Set attribute + if (name in attrs.map) { + // Set attribute + i = attrs.length; + while (i--) { + if (attrs[i].name === name) { + attrs[i].value = value; + break; + } + } + } else { + attrs.push({name: name, value: value}); + } + + attrs.map[name] = value; + + return self; + } + + return attrs.map[name]; + } + }, + + /** + * Does a shallow clones the node into a new node. It will also exclude id attributes since + * there should only be one id per document. + * + * @example + * var clonedNode = node.clone(); + * + * @method clone + * @return {tinymce.html.Node} New copy of the original node. + */ + clone: function() { + var self = this, clone = new Node(self.name, self.type), i, l, selfAttrs, selfAttr, cloneAttrs; + + // Clone element attributes + if ((selfAttrs = self.attributes)) { + cloneAttrs = []; + cloneAttrs.map = {}; + + for (i = 0, l = selfAttrs.length; i < l; i++) { + selfAttr = selfAttrs[i]; + + // Clone everything except id + if (selfAttr.name !== 'id') { + cloneAttrs[cloneAttrs.length] = {name: selfAttr.name, value: selfAttr.value}; + cloneAttrs.map[selfAttr.name] = selfAttr.value; + } + } + + clone.attributes = cloneAttrs; + } + + clone.value = self.value; + clone.shortEnded = self.shortEnded; + + return clone; + }, + + /** + * Wraps the node in in another node. + * + * @example + * node.wrap(wrapperNode); + * + * @method wrap + */ + wrap: function(wrapper) { + var self = this; + + self.parent.insert(wrapper, self); + wrapper.append(self); + + return self; + }, + + /** + * Unwraps the node in other words it removes the node but keeps the children. + * + * @example + * node.unwrap(); + * + * @method unwrap + */ + unwrap: function() { + var self = this, node, next; + + for (node = self.firstChild; node;) { + next = node.next; + self.insert(node, self, true); + node = next; + } + + self.remove(); + }, + + /** + * Removes the node from it's parent. + * + * @example + * node.remove(); + * + * @method remove + * @return {tinymce.html.Node} Current node that got removed. + */ + remove: function() { + var self = this, parent = self.parent, next = self.next, prev = self.prev; + + if (parent) { + if (parent.firstChild === self) { + parent.firstChild = next; + + if (next) { + next.prev = null; + } + } else { + prev.next = next; + } + + if (parent.lastChild === self) { + parent.lastChild = prev; + + if (prev) { + prev.next = null; + } + } else { + next.prev = prev; + } + + self.parent = self.next = self.prev = null; + } + + return self; + }, + + /** + * Appends a new node as a child of the current node. + * + * @example + * node.append(someNode); + * + * @method append + * @param {tinymce.html.Node} node Node to append as a child of the current one. + * @return {tinymce.html.Node} The node that got appended. + */ + append: function(node) { + var self = this, last; + + if (node.parent) { + node.remove(); + } + + last = self.lastChild; + if (last) { + last.next = node; + node.prev = last; + self.lastChild = node; + } else { + self.lastChild = self.firstChild = node; + } + + node.parent = self; + + return node; + }, + + /** + * Inserts a node at a specific position as a child of the current node. + * + * @example + * parentNode.insert(newChildNode, oldChildNode); + * + * @method insert + * @param {tinymce.html.Node} node Node to insert as a child of the current node. + * @param {tinymce.html.Node} ref_node Reference node to set node before/after. + * @param {Boolean} before Optional state to insert the node before the reference node. + * @return {tinymce.html.Node} The node that got inserted. + */ + insert: function(node, ref_node, before) { + var parent; + + if (node.parent) { + node.remove(); + } + + parent = ref_node.parent || this; + + if (before) { + if (ref_node === parent.firstChild) { + parent.firstChild = node; + } else { + ref_node.prev.next = node; + } + + node.prev = ref_node.prev; + node.next = ref_node; + ref_node.prev = node; + } else { + if (ref_node === parent.lastChild) { + parent.lastChild = node; + } else { + ref_node.next.prev = node; + } + + node.next = ref_node.next; + node.prev = ref_node; + ref_node.next = node; + } + + node.parent = parent; + + return node; + }, + + /** + * Get all children by name. + * + * @method getAll + * @param {String} name Name of the child nodes to collect. + * @return {Array} Array with child nodes matchin the specified name. + */ + getAll: function(name) { + var self = this, node, collection = []; + + for (node = self.firstChild; node; node = walk(node, self)) { + if (node.name === name) { + collection.push(node); + } + } + + return collection; + }, + + /** + * Removes all children of the current node. + * + * @method empty + * @return {tinymce.html.Node} The current node that got cleared. + */ + empty: function() { + var self = this, nodes, i, node; + + // Remove all children + if (self.firstChild) { + nodes = []; + + // Collect the children + for (node = self.firstChild; node; node = walk(node, self)) { + nodes.push(node); + } + + // Remove the children + i = nodes.length; + while (i--) { + node = nodes[i]; + node.parent = node.firstChild = node.lastChild = node.next = node.prev = null; + } + } + + self.firstChild = self.lastChild = null; + + return self; + }, + + /** + * Returns true/false if the node is to be considered empty or not. + * + * @example + * node.isEmpty({img: true}); + * @method isEmpty + * @param {Object} elements Name/value object with elements that are automatically treated as non empty elements. + * @return {Boolean} true/false if the node is empty or not. + */ + isEmpty: function(elements) { + var self = this, node = self.firstChild, i, name; + + if (node) { + do { + if (node.type === 1) { + // Ignore bogus elements + if (node.attributes.map['data-mce-bogus']) { + continue; + } + + // Keep empty elements like + if (elements[node.name]) { + return false; + } + + // Keep bookmark nodes and name attribute like + i = node.attributes.length; + while (i--) { + name = node.attributes[i].name; + if (name === "name" || name.indexOf('data-mce-bookmark') === 0) { + return false; + } + } + } + + // Keep comments + if (node.type === 8) { + return false; + } + + // Keep non whitespace text nodes + if ((node.type === 3 && !whiteSpaceRegExp.test(node.value))) { + return false; + } + } while ((node = walk(node, self))); + } + + return true; + }, + + /** + * Walks to the next or previous node and returns that node or null if it wasn't found. + * + * @method walk + * @param {Boolean} prev Optional previous node state defaults to false. + * @return {tinymce.html.Node} Node that is next to or previous of the current node. + */ + walk: function(prev) { + return walk(this, null, prev); + } + }; + + /** + * Creates a node of a specific type. + * + * @static + * @method create + * @param {String} name Name of the node type to create for example "b" or "#text". + * @param {Object} attrs Name/value collection of attributes that will be applied to elements. + */ + Node.create = function(name, attrs) { + var node, attrName; + + // Create node + node = new Node(name, typeLookup[name] || 1); + + // Add attributes if needed + if (attrs) { + for (attrName in attrs) { + node.attr(attrName, attrs[attrName]); + } + } + + return node; + }; + + return Node; +}); + +// Included from: js/tinymce/classes/html/Schema.js + +/** + * Schema.js + * + * Released under LGPL License. + * Copyright (c) 1999-2015 Ephox Corp. All rights reserved + * + * License: http://www.tinymce.com/license + * Contributing: http://www.tinymce.com/contributing + */ + +/** + * Schema validator class. + * + * @class tinymce.html.Schema + * @example + * if (tinymce.activeEditor.schema.isValidChild('p', 'span')) + * alert('span is valid child of p.'); + * + * if (tinymce.activeEditor.schema.getElementRule('p')) + * alert('P is a valid element.'); + * + * @class tinymce.html.Schema + * @version 3.4 + */ +define("tinymce/html/Schema", [ + "tinymce/util/Tools" +], function(Tools) { + var mapCache = {}, dummyObj = {}; + var makeMap = Tools.makeMap, each = Tools.each, extend = Tools.extend, explode = Tools.explode, inArray = Tools.inArray; + + function split(items, delim) { + items = Tools.trim(items); + return items ? items.split(delim || ' ') : []; + } + + /** + * Builds a schema lookup table + * + * @private + * @param {String} type html4, html5 or html5-strict schema type. + * @return {Object} Schema lookup table. + */ + function compileSchema(type) { + var schema = {}, globalAttributes, blockContent; + var phrasingContent, flowContent, html4BlockContent, html4PhrasingContent; + + function add(name, attributes, children) { + var ni, attributesOrder, element; + + function arrayToMap(array, obj) { + var map = {}, i, l; + + for (i = 0, l = array.length; i < l; i++) { + map[array[i]] = obj || {}; + } + + return map; + } + + children = children || []; + attributes = attributes || ""; + + if (typeof children === "string") { + children = split(children); + } + + name = split(name); + ni = name.length; + while (ni--) { + attributesOrder = split([globalAttributes, attributes].join(' ')); + + element = { + attributes: arrayToMap(attributesOrder), + attributesOrder: attributesOrder, + children: arrayToMap(children, dummyObj) + }; + + schema[name[ni]] = element; + } + } + + function addAttrs(name, attributes) { + var ni, schemaItem, i, l; + + name = split(name); + ni = name.length; + attributes = split(attributes); + while (ni--) { + schemaItem = schema[name[ni]]; + for (i = 0, l = attributes.length; i < l; i++) { + schemaItem.attributes[attributes[i]] = {}; + schemaItem.attributesOrder.push(attributes[i]); + } + } + } + + // Use cached schema + if (mapCache[type]) { + return mapCache[type]; + } + + // Attributes present on all elements + globalAttributes = "id accesskey class dir lang style tabindex title"; + + // Event attributes can be opt-in/opt-out + /*eventAttributes = split("onabort onblur oncancel oncanplay oncanplaythrough onchange onclick onclose oncontextmenu oncuechange " + + "ondblclick ondrag ondragend ondragenter ondragleave ondragover ondragstart ondrop ondurationchange onemptied onended " + + "onerror onfocus oninput oninvalid onkeydown onkeypress onkeyup onload onloadeddata onloadedmetadata onloadstart " + + "onmousedown onmousemove onmouseout onmouseover onmouseup onmousewheel onpause onplay onplaying onprogress onratechange " + + "onreset onscroll onseeked onseeking onseeking onselect onshow onstalled onsubmit onsuspend ontimeupdate onvolumechange " + + "onwaiting" + );*/ + + // Block content elements + blockContent = + "address blockquote div dl fieldset form h1 h2 h3 h4 h5 h6 hr menu ol p pre table ul"; + + // Phrasing content elements from the HTML5 spec (inline) + phrasingContent = + "a abbr b bdo br button cite code del dfn em embed i iframe img input ins kbd " + + "label map noscript object q s samp script select small span strong sub sup " + + "textarea u var #text #comment" + ; + + // Add HTML5 items to globalAttributes, blockContent, phrasingContent + if (type != "html4") { + globalAttributes += " contenteditable contextmenu draggable dropzone " + + "hidden spellcheck translate"; + blockContent += " article aside details dialog figure header footer hgroup section nav"; + phrasingContent += " audio canvas command datalist mark meter output picture " + + "progress time wbr video ruby bdi keygen"; + } + + // Add HTML4 elements unless it's html5-strict + if (type != "html5-strict") { + globalAttributes += " xml:lang"; + + html4PhrasingContent = "acronym applet basefont big font strike tt"; + phrasingContent = [phrasingContent, html4PhrasingContent].join(' '); + + each(split(html4PhrasingContent), function(name) { + add(name, "", phrasingContent); + }); + + html4BlockContent = "center dir isindex noframes"; + blockContent = [blockContent, html4BlockContent].join(' '); + + // Flow content elements from the HTML5 spec (block+inline) + flowContent = [blockContent, phrasingContent].join(' '); + + each(split(html4BlockContent), function(name) { + add(name, "", flowContent); + }); + } + + // Flow content elements from the HTML5 spec (block+inline) + flowContent = flowContent || [blockContent, phrasingContent].join(" "); + + // HTML4 base schema TODO: Move HTML5 specific attributes to HTML5 specific if statement + // Schema itemsa
b
c will becomea
b
c
+ * + * @example + * var parser = new tinymce.html.DomParser({validate: true}, schema); + * var rootNode = parser.parse('x
->x
+ function trim(rootBlockNode) { + if (rootBlockNode) { + node = rootBlockNode.firstChild; + if (node && node.type == 3) { + node.value = node.value.replace(startWhiteSpaceRegExp, ''); + } + + node = rootBlockNode.lastChild; + if (node && node.type == 3) { + node.value = node.value.replace(endWhiteSpaceRegExp, ''); + } + } + } + + // Check if rootBlock is valid within rootNode for example if P is valid in H1 if H1 is the contentEditabe root + if (!schema.isValidChild(rootNode.name, rootBlockName.toLowerCase())) { + return; + } + + while (node) { + next = node.next; + + if (node.type == 3 || (node.type == 1 && node.name !== 'p' && + !blockElements[node.name] && !node.attr('data-mce-type'))) { + if (!rootBlockNode) { + // Create a new root block element + rootBlockNode = createNode(rootBlockName, 1); + rootBlockNode.attr(settings.forced_root_block_attrs); + rootNode.insert(rootBlockNode, node); + rootBlockNode.append(node); + } else { + rootBlockNode.append(node); + } + } else { + trim(rootBlockNode); + rootBlockNode = null; + } + + node = next; + } + + trim(rootBlockNode); + } + + function createNode(name, type) { + var node = new Node(name, type), list; + + if (name in nodeFilters) { + list = matchedNodes[name]; + + if (list) { + list.push(node); + } else { + matchedNodes[name] = [node]; + } + } + + return node; + } + + function removeWhitespaceBefore(node) { + var textNode, textNodeNext, textVal, sibling, blockElements = schema.getBlockElements(); + + for (textNode = node.prev; textNode && textNode.type === 3;) { + textVal = textNode.value.replace(endWhiteSpaceRegExp, ''); + + // Found a text node with non whitespace then trim that and break + if (textVal.length > 0) { + textNode.value = textVal; + return; + } + + textNodeNext = textNode.next; + + // Fix for bug #7543 where bogus nodes would produce empty + // text nodes and these would be removed if a nested list was before it + if (textNodeNext) { + if (textNodeNext.type == 3 && textNodeNext.value.length) { + textNode = textNode.prev; + continue; + } + + if (!blockElements[textNodeNext.name] && textNodeNext.name != 'script' && textNodeNext.name != 'style') { + textNode = textNode.prev; + continue; + } + } + + sibling = textNode.prev; + textNode.remove(); + textNode = sibling; + } + } + + function cloneAndExcludeBlocks(input) { + var name, output = {}; + + for (name in input) { + if (name !== 'li' && name != 'p') { + output[name] = input[name]; + } + } + + return output; + } + + parser = new SaxParser({ + validate: validate, + allow_script_urls: settings.allow_script_urls, + allow_conditional_comments: settings.allow_conditional_comments, + + // Exclude P and LI from DOM parsing since it's treated better by the DOM parser + self_closing_elements: cloneAndExcludeBlocks(schema.getSelfClosingElements()), + + cdata: function(text) { + node.append(createNode('#cdata', 4)).value = text; + }, + + text: function(text, raw) { + var textNode; + + // Trim all redundant whitespace on non white space elements + if (!isInWhiteSpacePreservedElement) { + text = text.replace(allWhiteSpaceRegExp, ' '); + + if (node.lastChild && blockElements[node.lastChild.name]) { + text = text.replace(startWhiteSpaceRegExp, ''); + } + } + + // Do we need to create the node + if (text.length !== 0) { + textNode = createNode('#text', 3); + textNode.raw = !!raw; + node.append(textNode).value = text; + } + }, + + comment: function(text) { + node.append(createNode('#comment', 8)).value = text; + }, + + pi: function(name, text) { + node.append(createNode(name, 7)).value = text; + removeWhitespaceBefore(node); + }, + + doctype: function(text) { + var newNode; + + newNode = node.append(createNode('#doctype', 10)); + newNode.value = text; + removeWhitespaceBefore(node); + }, + + start: function(name, attrs, empty) { + var newNode, attrFiltersLen, elementRule, attrName, parent; + + elementRule = validate ? schema.getElementRule(name) : {}; + if (elementRule) { + newNode = createNode(elementRule.outputName || name, 1); + newNode.attributes = attrs; + newNode.shortEnded = empty; + + node.append(newNode); + + // Check if node is valid child of the parent node is the child is + // unknown we don't collect it since it's probably a custom element + parent = children[node.name]; + if (parent && children[newNode.name] && !parent[newNode.name]) { + invalidChildren.push(newNode); + } + + attrFiltersLen = attributeFilters.length; + while (attrFiltersLen--) { + attrName = attributeFilters[attrFiltersLen].name; + + if (attrName in attrs.map) { + list = matchedAttributes[attrName]; + + if (list) { + list.push(newNode); + } else { + matchedAttributes[attrName] = [newNode]; + } + } + } + + // Trim whitespace before block + if (blockElements[name]) { + removeWhitespaceBefore(newNode); + } + + // Change current node if the element wasn't empty i.e nota
+ lastParent = node; + while (parent && parent.firstChild === lastParent && parent.lastChild === lastParent) { + lastParent = parent; + + if (blockElements[parent.name]) { + break; + } + + parent = parent.parent; + } + + if (lastParent === parent) { + textNode = new Node('#text', 3); + textNode.value = '\u00a0'; + node.replace(textNode); + } + } + } + }); + } + + if (!settings.allow_unsafe_link_target) { + self.addAttributeFilter('href', function(nodes) { + var i = nodes.length, node, rel; + var rules = 'noopener noreferrer'; + + function addTargetRules(rel) { + rel = removeTargetRules(rel); + return rel ? [rel, rules].join(' ') : rules; + } + + function removeTargetRules(rel) { + var regExp = new RegExp('(' + rules.replace(' ', '|') + ')', 'g'); + if (rel) { + rel = Tools.trim(rel.replace(regExp, '')); + } + return rel ? rel : null; + } + + function toggleTargetRules(rel, isUnsafe) { + return isUnsafe ? addTargetRules(rel) : removeTargetRules(rel); + } + + while (i--) { + node = nodes[i]; + rel = node.attr('rel'); + if (node.name === 'a') { + node.attr('rel', toggleTargetRules(rel, node.attr('target') == '_blank')); + } + } + }); + } + + // Force anchor names closed, unless the setting "allow_html_in_named_anchor" is explicitly included. + if (!settings.allow_html_in_named_anchor) { + self.addAttributeFilter('id,name', function(nodes) { + var i = nodes.length, sibling, prevSibling, parent, node; + + while (i--) { + node = nodes[i]; + if (node.name === 'a' && node.firstChild && !node.attr('href')) { + parent = node.parent; + + // Move children after current node + sibling = node.lastChild; + do { + prevSibling = sibling.prev; + parent.insert(sibling, node); + sibling = prevSibling; + } while (sibling); + } + } + }); + } + + if (settings.validate && schema.getValidClasses()) { + self.addAttributeFilter('class', function(nodes) { + var i = nodes.length, node, classList, ci, className, classValue; + var validClasses = schema.getValidClasses(), validClassesMap, valid; + + while (i--) { + node = nodes[i]; + classList = node.attr('class').split(' '); + classValue = ''; + + for (ci = 0; ci < classList.length; ci++) { + className = classList[ci]; + valid = false; + + validClassesMap = validClasses['*']; + if (validClassesMap && validClassesMap[className]) { + valid = true; + } + + validClassesMap = validClasses[node.name]; + if (!valid && validClassesMap && validClassesMap[className]) { + valid = true; + } + + if (valid) { + if (classValue) { + classValue += ' '; + } + + classValue += className; + } + } + + if (!classValue.length) { + classValue = null; + } + + node.attr('class', classValue); + } + }); + } + }; +}); + +// Included from: js/tinymce/classes/html/Writer.js + +/** + * Writer.js + * + * Released under LGPL License. + * Copyright (c) 1999-2015 Ephox Corp. All rights reserved + * + * License: http://www.tinymce.com/license + * Contributing: http://www.tinymce.com/contributing + */ + +/** + * This class is used to write HTML tags out it can be used with the Serializer or the SaxParser. + * + * @class tinymce.html.Writer + * @example + * var writer = new tinymce.html.Writer({indent: true}); + * var parser = new tinymce.html.SaxParser(writer).parse('
.
+ *
+ * @method start
+ * @param {String} name Name of the element.
+ * @param {Array} attrs Optional attribute array or undefined if it hasn't any.
+ * @param {Boolean} empty Optional empty state if the tag should end like
.
+ */
+ start: function(name, attrs, empty) {
+ var i, l, attr, value;
+
+ if (indent && indentBefore[name] && html.length > 0) {
+ value = html[html.length - 1];
+
+ if (value.length > 0 && value !== '\n') {
+ html.push('\n');
+ }
+ }
+
+ html.push('<', name);
+
+ if (attrs) {
+ for (i = 0, l = attrs.length; i < l; i++) {
+ attr = attrs[i];
+ html.push(' ', attr.name, '="', encode(attr.value, true), '"');
+ }
+ }
+
+ if (!empty || htmlOutput) {
+ html[html.length] = '>';
+ } else {
+ html[html.length] = ' />';
+ }
+
+ if (empty && indent && indentAfter[name] && html.length > 0) {
+ value = html[html.length - 1];
+
+ if (value.length > 0 && value !== '\n') {
+ html.push('\n');
+ }
+ }
+ },
+
+ /**
+ * Writes the a end element such as
text
')); + * @class tinymce.html.Serializer + * @version 3.4 + */ +define("tinymce/html/Serializer", [ + "tinymce/html/Writer", + "tinymce/html/Schema" +], function(Writer, Schema) { + /** + * Constructs a new Serializer instance. + * + * @constructor + * @method Serializer + * @param {Object} settings Name/value settings object. + * @param {tinymce.html.Schema} schema Schema instance to use. + */ + return function(settings, schema) { + var self = this, writer = new Writer(settings); + + settings = settings || {}; + settings.validate = "validate" in settings ? settings.validate : true; + + self.schema = schema = schema || new Schema(); + self.writer = writer; + + /** + * Serializes the specified node into a string. + * + * @example + * new tinymce.html.Serializer().serialize(new tinymce.html.DomParser().parse('text
')); + * @method serialize + * @param {tinymce.html.Node} node Node instance to serialize. + * @return {String} String with HTML based on DOM tree. + */ + self.serialize = function(node) { + var handlers, validate; + + validate = settings.validate; + + handlers = { + // #text + 3: function(node) { + writer.text(node.value, node.raw); + }, + + // #comment + 8: function(node) { + writer.comment(node.value); + }, + + // Processing instruction + 7: function(node) { + writer.pi(node.name, node.value); + }, + + // Doctype + 10: function(node) { + writer.doctype(node.value); + }, + + // CDATA + 4: function(node) { + writer.cdata(node.value); + }, + + // Document fragment + 11: function(node) { + if ((node = node.firstChild)) { + do { + walk(node); + } while ((node = node.next)); + } + } + }; + + writer.reset(); + + function walk(node) { + var handler = handlers[node.type], name, isEmpty, attrs, attrName, attrValue, sortedAttrs, i, l, elementRule; + + if (!handler) { + name = node.name; + isEmpty = node.shortEnded; + attrs = node.attributes; + + // Sort attributes + if (validate && attrs && attrs.length > 1) { + sortedAttrs = []; + sortedAttrs.map = {}; + + elementRule = schema.getElementRule(node.name); + if (elementRule) { + for (i = 0, l = elementRule.attributesOrder.length; i < l; i++) { + attrName = elementRule.attributesOrder[i]; + + if (attrName in attrs.map) { + attrValue = attrs.map[attrName]; + sortedAttrs.map[attrName] = attrValue; + sortedAttrs.push({name: attrName, value: attrValue}); + } + } + + for (i = 0, l = attrs.length; i < l; i++) { + attrName = attrs[i].name; + + if (!(attrName in sortedAttrs.map)) { + attrValue = attrs.map[attrName]; + sortedAttrs.map[attrName] = attrValue; + sortedAttrs.push({name: attrName, value: attrValue}); + } + } + + attrs = sortedAttrs; + } + } + + writer.start(node.name, attrs, isEmpty); + + if (!isEmpty) { + if ((node = node.firstChild)) { + do { + walk(node); + } while ((node = node.next)); + } + + writer.end(name); + } + } else { + handler(node); + } + } + + // Serialize element and treat all non elements as fragments + if (node.type == 1 && !settings.inner) { + walk(node); + } else { + handlers[11](node); + } + + return writer.getContent(); + }; + }; +}); + +// Included from: js/tinymce/classes/dom/Serializer.js + +/** + * Serializer.js + * + * Released under LGPL License. + * Copyright (c) 1999-2015 Ephox Corp. All rights reserved + * + * License: http://www.tinymce.com/license + * Contributing: http://www.tinymce.com/contributing + */ + +/** + * This class is used to serialize DOM trees into a string. Consult the TinyMCE Wiki API for + * more details and examples on how to use this class. + * + * @class tinymce.dom.Serializer + */ +define("tinymce/dom/Serializer", [ + "tinymce/dom/DOMUtils", + "tinymce/html/DomParser", + "tinymce/html/SaxParser", + "tinymce/html/Entities", + "tinymce/html/Serializer", + "tinymce/html/Node", + "tinymce/html/Schema", + "tinymce/Env", + "tinymce/util/Tools", + "tinymce/text/Zwsp" +], function(DOMUtils, DomParser, SaxParser, Entities, Serializer, Node, Schema, Env, Tools, Zwsp) { + var each = Tools.each, trim = Tools.trim; + var DOM = DOMUtils.DOM; + + /** + * IE 11 has a fantastic bug where it will produce two trailing BR elements to iframe bodies when + * the iframe is hidden by display: none on a parent container. The DOM is actually out of sync + * with innerHTML in this case. It's like IE adds shadow DOM BR elements that appears on innerHTML + * but not as the lastChild of the body. So this fix simply removes the last two + * BR elements at the end of the document. + * + * Example of what happens: text becomes text|
would become this:|
+ sibling = startContainer.previousSibling; + if (sibling && !sibling.hasChildNodes() && dom.isBlock(sibling)) { + sibling.innerHTML = ''; + } else { + sibling = null; + } + + startContainer.innerHTML = ''; + ieRng.moveToElementText(startContainer.lastChild); + ieRng.select(); + dom.doc.selection.clear(); + startContainer.innerHTML = ''; + + if (sibling) { + sibling.innerHTML = ''; + } + return; + } + + startOffset = dom.nodeIndex(startContainer); + startContainer = startContainer.parentNode; + } + + if (startOffset == endOffset - 1) { + try { + ctrlElm = startContainer.childNodes[startOffset]; + ctrlRng = body.createControlRange(); + ctrlRng.addElement(ctrlElm); + ctrlRng.select(); + + // Check if the range produced is on the correct element and is a control range + // On IE 8 it will select the parent contentEditable container if you select an inner element see: #5398 + nativeRng = selection.getRng(); + if (nativeRng.item && ctrlElm === nativeRng.item(0)) { + return; + } + } catch (ex) { + // Ignore + } + } + } + + // Set start/end point of selection + setEndPoint(true); + setEndPoint(); + + // Select the new range and scroll it into view + ieRng.select(); + }; + + // Expose range method + this.getRangeAt = getRange; + } + + return Selection; +}); + +// Included from: js/tinymce/classes/util/VK.js + +/** + * VK.js + * + * Released under LGPL License. + * Copyright (c) 1999-2015 Ephox Corp. All rights reserved + * + * License: http://www.tinymce.com/license + * Contributing: http://www.tinymce.com/contributing + */ + +/** + * This file exposes a set of the common KeyCodes for use. Please grow it as needed. + */ +define("tinymce/util/VK", [ + "tinymce/Env" +], function(Env) { + return { + BACKSPACE: 8, + DELETE: 46, + DOWN: 40, + ENTER: 13, + LEFT: 37, + RIGHT: 39, + SPACEBAR: 32, + TAB: 9, + UP: 38, + + modifierPressed: function(e) { + return e.shiftKey || e.ctrlKey || e.altKey || this.metaKeyPressed(e); + }, + + metaKeyPressed: function(e) { + // Check if ctrl or meta key is pressed. Edge case for AltGr on Windows where it produces ctrlKey+altKey states + return (Env.mac ? e.metaKey : e.ctrlKey && !e.altKey); + } + }; +}); + +// Included from: js/tinymce/classes/dom/ControlSelection.js + +/** + * ControlSelection.js + * + * Released under LGPL License. + * Copyright (c) 1999-2015 Ephox Corp. All rights reserved + * + * License: http://www.tinymce.com/license + * Contributing: http://www.tinymce.com/contributing + */ + +/** + * This class handles control selection of elements. Controls are elements + * that can be resized and needs to be selected as a whole. It adds custom resize handles + * to all browser engines that support properly disabling the built in resize logic. + * + * @class tinymce.dom.ControlSelection + */ +define("tinymce/dom/ControlSelection", [ + "tinymce/util/VK", + "tinymce/util/Tools", + "tinymce/util/Delay", + "tinymce/Env", + "tinymce/dom/NodeType" +], function(VK, Tools, Delay, Env, NodeType) { + var isContentEditableFalse = NodeType.isContentEditableFalse; + var isContentEditableTrue = NodeType.isContentEditableTrue; + + function getContentEditableRoot(root, node) { + while (node && node != root) { + if (isContentEditableTrue(node) || isContentEditableFalse(node)) { + return node; + } + + node = node.parentNode; + } + + return null; + } + + return function(selection, editor) { + var dom = editor.dom, each = Tools.each; + var selectedElm, selectedElmGhost, resizeHelper, resizeHandles, selectedHandle, lastMouseDownEvent; + var startX, startY, selectedElmX, selectedElmY, startW, startH, ratio, resizeStarted; + var width, height, editableDoc = editor.getDoc(), rootDocument = document, isIE = Env.ie && Env.ie < 11; + var abs = Math.abs, round = Math.round, rootElement = editor.getBody(), startScrollWidth, startScrollHeight; + + // Details about each resize handle how to scale etc + resizeHandles = { + // Name: x multiplier, y multiplier, delta size x, delta size y + /*n: [0.5, 0, 0, -1], + e: [1, 0.5, 1, 0], + s: [0.5, 1, 0, 1], + w: [0, 0.5, -1, 0],*/ + nw: [0, 0, -1, -1], + ne: [1, 0, 1, -1], + se: [1, 1, 1, 1], + sw: [0, 1, -1, 1] + }; + + // Add CSS for resize handles, cloned element and selected + var rootClass = '.mce-content-body'; + editor.contentStyles.push( + rootClass + ' div.mce-resizehandle {' + + 'position: absolute;' + + 'border: 1px solid black;' + + 'box-sizing: box-sizing;' + + 'background: #FFF;' + + 'width: 7px;' + + 'height: 7px;' + + 'z-index: 10000' + + '}' + + rootClass + ' .mce-resizehandle:hover {' + + 'background: #000' + + '}' + + rootClass + ' img[data-mce-selected],' + rootClass + ' hr[data-mce-selected] {' + + 'outline: 1px solid black;' + + 'resize: none' + // Have been talks about implementing this in browsers + '}' + + rootClass + ' .mce-clonedresizable {' + + 'position: absolute;' + + (Env.gecko ? '' : 'outline: 1px dashed black;') + // Gecko produces trails while resizing + 'opacity: .5;' + + 'filter: alpha(opacity=50);' + + 'z-index: 10000' + + '}' + + rootClass + ' .mce-resize-helper {' + + 'background: #555;' + + 'background: rgba(0,0,0,0.75);' + + 'border-radius: 3px;' + + 'border: 1px;' + + 'color: white;' + + 'display: none;' + + 'font-family: sans-serif;' + + 'font-size: 12px;' + + 'white-space: nowrap;' + + 'line-height: 14px;' + + 'margin: 5px 10px;' + + 'padding: 5px;' + + 'position: absolute;' + + 'z-index: 10001' + + '}' + ); + + function isResizable(elm) { + var selector = editor.settings.object_resizing; + + if (selector === false || Env.iOS) { + return false; + } + + if (typeof selector != 'string') { + selector = 'table,img,div'; + } + + if (elm.getAttribute('data-mce-resize') === 'false') { + return false; + } + + if (elm == editor.getBody()) { + return false; + } + + return editor.dom.is(elm, selector); + } + + function resizeGhostElement(e) { + var deltaX, deltaY, proportional; + var resizeHelperX, resizeHelperY; + + // Calc new width/height + deltaX = e.screenX - startX; + deltaY = e.screenY - startY; + + // Calc new size + width = deltaX * selectedHandle[2] + startW; + height = deltaY * selectedHandle[3] + startH; + + // Never scale down lower than 5 pixels + width = width < 5 ? 5 : width; + height = height < 5 ? 5 : height; + + if (selectedElm.nodeName == "IMG" && editor.settings.resize_img_proportional !== false) { + proportional = !VK.modifierPressed(e); + } else { + proportional = VK.modifierPressed(e) || (selectedElm.nodeName == "IMG" && selectedHandle[2] * selectedHandle[3] !== 0); + } + + // Constrain proportions + if (proportional) { + if (abs(deltaX) > abs(deltaY)) { + height = round(width * ratio); + width = round(height / ratio); + } else { + width = round(height / ratio); + height = round(width * ratio); + } + } + + // Update ghost size + dom.setStyles(selectedElmGhost, { + width: width, + height: height + }); + + // Update resize helper position + resizeHelperX = selectedHandle.startPos.x + deltaX; + resizeHelperY = selectedHandle.startPos.y + deltaY; + resizeHelperX = resizeHelperX > 0 ? resizeHelperX : 0; + resizeHelperY = resizeHelperY > 0 ? resizeHelperY : 0; + + dom.setStyles(resizeHelper, { + left: resizeHelperX, + top: resizeHelperY, + display: 'block' + }); + + resizeHelper.innerHTML = width + ' × ' + height; + + // Update ghost X position if needed + if (selectedHandle[2] < 0 && selectedElmGhost.clientWidth <= width) { + dom.setStyle(selectedElmGhost, 'left', selectedElmX + (startW - width)); + } + + // Update ghost Y position if needed + if (selectedHandle[3] < 0 && selectedElmGhost.clientHeight <= height) { + dom.setStyle(selectedElmGhost, 'top', selectedElmY + (startH - height)); + } + + // Calculate how must overflow we got + deltaX = rootElement.scrollWidth - startScrollWidth; + deltaY = rootElement.scrollHeight - startScrollHeight; + + // Re-position the resize helper based on the overflow + if (deltaX + deltaY !== 0) { + dom.setStyles(resizeHelper, { + left: resizeHelperX - deltaX, + top: resizeHelperY - deltaY + }); + } + + if (!resizeStarted) { + editor.fire('ObjectResizeStart', {target: selectedElm, width: startW, height: startH}); + resizeStarted = true; + } + } + + function endGhostResize() { + resizeStarted = false; + + function setSizeProp(name, value) { + if (value) { + // Resize by using style or attribute + if (selectedElm.style[name] || !editor.schema.isValid(selectedElm.nodeName.toLowerCase(), name)) { + dom.setStyle(selectedElm, name, value); + } else { + dom.setAttrib(selectedElm, name, value); + } + } + } + + // Set width/height properties + setSizeProp('width', width); + setSizeProp('height', height); + + dom.unbind(editableDoc, 'mousemove', resizeGhostElement); + dom.unbind(editableDoc, 'mouseup', endGhostResize); + + if (rootDocument != editableDoc) { + dom.unbind(rootDocument, 'mousemove', resizeGhostElement); + dom.unbind(rootDocument, 'mouseup', endGhostResize); + } + + // Remove ghost/helper and update resize handle positions + dom.remove(selectedElmGhost); + dom.remove(resizeHelper); + + if (!isIE || selectedElm.nodeName == "TABLE") { + showResizeRect(selectedElm); + } + + editor.fire('ObjectResized', {target: selectedElm, width: width, height: height}); + dom.setAttrib(selectedElm, 'style', dom.getAttrib(selectedElm, 'style')); + editor.nodeChanged(); + } + + function showResizeRect(targetElm, mouseDownHandleName, mouseDownEvent) { + var position, targetWidth, targetHeight, e, rect; + + hideResizeRect(); + unbindResizeHandleEvents(); + + // Get position and size of target + position = dom.getPos(targetElm, rootElement); + selectedElmX = position.x; + selectedElmY = position.y; + rect = targetElm.getBoundingClientRect(); // Fix for Gecko offsetHeight for table with caption + targetWidth = rect.width || (rect.right - rect.left); + targetHeight = rect.height || (rect.bottom - rect.top); + + // Reset width/height if user selects a new image/table + if (selectedElm != targetElm) { + detachResizeStartListener(); + selectedElm = targetElm; + width = height = 0; + } + + // Makes it possible to disable resizing + e = editor.fire('ObjectSelected', {target: targetElm}); + + if (isResizable(targetElm) && !e.isDefaultPrevented()) { + each(resizeHandles, function(handle, name) { + var handleElm; + + function startDrag(e) { + startX = e.screenX; + startY = e.screenY; + startW = selectedElm.clientWidth; + startH = selectedElm.clientHeight; + ratio = startH / startW; + selectedHandle = handle; + + handle.startPos = { + x: targetWidth * handle[0] + selectedElmX, + y: targetHeight * handle[1] + selectedElmY + }; + + startScrollWidth = rootElement.scrollWidth; + startScrollHeight = rootElement.scrollHeight; + + selectedElmGhost = selectedElm.cloneNode(true); + dom.addClass(selectedElmGhost, 'mce-clonedresizable'); + dom.setAttrib(selectedElmGhost, 'data-mce-bogus', 'all'); + selectedElmGhost.contentEditable = false; // Hides IE move layer cursor + selectedElmGhost.unSelectabe = true; + dom.setStyles(selectedElmGhost, { + left: selectedElmX, + top: selectedElmY, + margin: 0 + }); + + selectedElmGhost.removeAttribute('data-mce-selected'); + rootElement.appendChild(selectedElmGhost); + + dom.bind(editableDoc, 'mousemove', resizeGhostElement); + dom.bind(editableDoc, 'mouseup', endGhostResize); + + if (rootDocument != editableDoc) { + dom.bind(rootDocument, 'mousemove', resizeGhostElement); + dom.bind(rootDocument, 'mouseup', endGhostResize); + } + + resizeHelper = dom.add(rootElement, 'div', { + 'class': 'mce-resize-helper', + 'data-mce-bogus': 'all' + }, startW + ' × ' + startH); + } + + if (mouseDownHandleName) { + // Drag started by IE native resizestart + if (name == mouseDownHandleName) { + startDrag(mouseDownEvent); + } + + return; + } + + // Get existing or render resize handle + handleElm = dom.get('mceResizeHandle' + name); + if (handleElm) { + dom.remove(handleElm); + } + + handleElm = dom.add(rootElement, 'div', { + id: 'mceResizeHandle' + name, + 'data-mce-bogus': 'all', + 'class': 'mce-resizehandle', + unselectable: true, + style: 'cursor:' + name + '-resize; margin:0; padding:0' + }); + + // Hides IE move layer cursor + // If we set it on Chrome we get this wounderful bug: #6725 + if (Env.ie) { + handleElm.contentEditable = false; + } + + dom.bind(handleElm, 'mousedown', function(e) { + e.stopImmediatePropagation(); + e.preventDefault(); + startDrag(e); + }); + + handle.elm = handleElm; + + // Position element + dom.setStyles(handleElm, { + left: (targetWidth * handle[0] + selectedElmX) - (handleElm.offsetWidth / 2), + top: (targetHeight * handle[1] + selectedElmY) - (handleElm.offsetHeight / 2) + }); + }); + } else { + hideResizeRect(); + } + + selectedElm.setAttribute('data-mce-selected', '1'); + } + + function hideResizeRect() { + var name, handleElm; + + unbindResizeHandleEvents(); + + if (selectedElm) { + selectedElm.removeAttribute('data-mce-selected'); + } + + for (name in resizeHandles) { + handleElm = dom.get('mceResizeHandle' + name); + if (handleElm) { + dom.unbind(handleElm); + dom.remove(handleElm); + } + } + } + + function updateResizeRect(e) { + var startElm, controlElm; + + function isChildOrEqual(node, parent) { + if (node) { + do { + if (node === parent) { + return true; + } + } while ((node = node.parentNode)); + } + } + + // Ignore all events while resizing or if the editor instance was removed + if (resizeStarted || editor.removed) { + return; + } + + // Remove data-mce-selected from all elements since they might have been copied using Ctrl+c/v + each(dom.select('img[data-mce-selected],hr[data-mce-selected]'), function(img) { + img.removeAttribute('data-mce-selected'); + }); + + controlElm = e.type == 'mousedown' ? e.target : selection.getNode(); + controlElm = dom.$(controlElm).closest(isIE ? 'table' : 'table,img,hr')[0]; + + if (isChildOrEqual(controlElm, rootElement)) { + disableGeckoResize(); + startElm = selection.getStart(true); + + if (isChildOrEqual(startElm, controlElm) && isChildOrEqual(selection.getEnd(true), controlElm)) { + if (!isIE || (controlElm != startElm && startElm.nodeName !== 'IMG')) { + showResizeRect(controlElm); + return; + } + } + } + + hideResizeRect(); + } + + function attachEvent(elm, name, func) { + if (elm && elm.attachEvent) { + elm.attachEvent('on' + name, func); + } + } + + function detachEvent(elm, name, func) { + if (elm && elm.detachEvent) { + elm.detachEvent('on' + name, func); + } + } + + function resizeNativeStart(e) { + var target = e.srcElement, pos, name, corner, cornerX, cornerY, relativeX, relativeY; + + pos = target.getBoundingClientRect(); + relativeX = lastMouseDownEvent.clientX - pos.left; + relativeY = lastMouseDownEvent.clientY - pos.top; + + // Figure out what corner we are draging on + for (name in resizeHandles) { + corner = resizeHandles[name]; + + cornerX = target.offsetWidth * corner[0]; + cornerY = target.offsetHeight * corner[1]; + + if (abs(cornerX - relativeX) < 8 && abs(cornerY - relativeY) < 8) { + selectedHandle = corner; + break; + } + } + + // Remove native selection and let the magic begin + resizeStarted = true; + editor.fire('ObjectResizeStart', { + target: selectedElm, + width: selectedElm.clientWidth, + height: selectedElm.clientHeight + }); + editor.getDoc().selection.empty(); + showResizeRect(target, name, lastMouseDownEvent); + } + + function preventDefault(e) { + if (e.preventDefault) { + e.preventDefault(); + } else { + e.returnValue = false; // IE + } + } + + function isWithinContentEditableFalse(elm) { + return isContentEditableFalse(getContentEditableRoot(editor.getBody(), elm)); + } + + function nativeControlSelect(e) { + var target = e.srcElement; + + if (isWithinContentEditableFalse(target)) { + preventDefault(e); + return; + } + + if (target != selectedElm) { + editor.fire('ObjectSelected', {target: target}); + detachResizeStartListener(); + + if (target.id.indexOf('mceResizeHandle') === 0) { + e.returnValue = false; + return; + } + + if (target.nodeName == 'IMG' || target.nodeName == 'TABLE') { + hideResizeRect(); + selectedElm = target; + attachEvent(target, 'resizestart', resizeNativeStart); + } + } + } + + 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) { + // Ignore + } + } + + function controlSelect(elm) { + var ctrlRng; + + if (!isIE) { + return; + } + + ctrlRng = editableDoc.body.createControlRange(); + + try { + ctrlRng.addElement(elm); + ctrlRng.select(); + return true; + } catch (ex) { + // Ignore since the element can't be control selected for example a P tag + } + } + + editor.on('init', function() { + if (isIE) { + // Hide the resize rect on resize and reselect the image + editor.on('ObjectResized', function(e) { + if (e.target.nodeName != 'TABLE') { + hideResizeRect(); + controlSelect(e.target); + } + }); + + attachEvent(rootElement, 'controlselect', nativeControlSelect); + + editor.on('mousedown', function(e) { + lastMouseDownEvent = e; + }); + } else { + disableGeckoResize(); + + // Sniff sniff, hard to feature detect this stuff + if (Env.ie >= 11) { + // Needs to be mousedown for drag/drop to work on IE 11 + // Needs to be click on Edge to properly select images + editor.on('mousedown click', function(e) { + var target = e.target, nodeName = target.nodeName; + + if (!resizeStarted && /^(TABLE|IMG|HR)$/.test(nodeName) && !isWithinContentEditableFalse(target)) { + editor.selection.select(target, nodeName == 'TABLE'); + + // Only fire once since nodeChange is expensive + if (e.type == 'mousedown') { + editor.nodeChanged(); + } + } + }); + + editor.dom.bind(rootElement, 'mscontrolselect', function(e) { + function delayedSelect(node) { + Delay.setEditorTimeout(editor, function() { + editor.selection.select(node); + }); + } + + if (isWithinContentEditableFalse(e.target)) { + e.preventDefault(); + delayedSelect(e.target); + return; + } + + if (/^(TABLE|IMG|HR)$/.test(e.target.nodeName)) { + e.preventDefault(); + + // This moves the selection from being a control selection to a text like selection like in WebKit #6753 + // TODO: Fix this the day IE works like other browsers without this nasty native ugly control selections. + if (e.target.tagName == 'IMG') { + delayedSelect(e.target); + } + } + }); + } + } + + var throttledUpdateResizeRect = Delay.throttle(function(e) { + if (!editor.composing) { + updateResizeRect(e); + } + }); + + editor.on('nodechange ResizeEditor ResizeWindow drop', throttledUpdateResizeRect); + + // Update resize rect while typing in a table + editor.on('keyup compositionend', function(e) { + // Don't update the resize rect while composing since it blows away the IME see: #2710 + if (selectedElm && selectedElm.nodeName == "TABLE") { + throttledUpdateResizeRect(e); + } + }); + + editor.on('hide blur', 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(); + detachEvent(rootElement, 'controlselect', nativeControlSelect); + } + } + + return { + isResizable: isResizable, + showResizeRect: showResizeRect, + hideResizeRect: hideResizeRect, + updateResizeRect: updateResizeRect, + controlSelect: controlSelect, + destroy: destroy + }; + }; +}); + +// Included from: js/tinymce/classes/util/Fun.js + +/** + * Fun.js + * + * Released under LGPL License. + * Copyright (c) 1999-2015 Ephox Corp. All rights reserved + * + * License: http://www.tinymce.com/license + * Contributing: http://www.tinymce.com/contributing + */ + +/** + * Functional utility class. + * + * @private + * @class tinymce.util.Fun + */ +define("tinymce/util/Fun", [], function() { + var slice = [].slice; + + function constant(value) { + return function() { + return value; + }; + } + + function negate(predicate) { + return function(x) { + return !predicate(x); + }; + } + + function compose(f, g) { + return function(x) { + return f(g(x)); + }; + } + + function or() { + var args = slice.call(arguments); + + return function(x) { + for (var i = 0; i < args.length; i++) { + if (args[i](x)) { + return true; + } + } + + return false; + }; + } + + function and() { + var args = slice.call(arguments); + + return function(x) { + for (var i = 0; i < args.length; i++) { + if (!args[i](x)) { + return false; + } + } + + return true; + }; + } + + function curry(fn) { + var args = slice.call(arguments); + + if (args.length - 1 >= fn.length) { + return fn.apply(this, args.slice(1)); + } + + return function() { + var tempArgs = args.concat([].slice.call(arguments)); + return curry.apply(this, tempArgs); + }; + } + + function noop() { + } + + return { + constant: constant, + negate: negate, + and: and, + or: or, + curry: curry, + compose: compose, + noop: noop + }; +}); + +// Included from: js/tinymce/classes/caret/CaretCandidate.js + +/** + * CaretCandidate.js + * + * Released under LGPL License. + * Copyright (c) 1999-2015 Ephox Corp. All rights reserved + * + * License: http://www.tinymce.com/license + * Contributing: http://www.tinymce.com/contributing + */ + +/** + * This module contains logic for handling caret candidates. A caret candidate is + * for example text nodes, images, input elements, cE=false elements etc. + * + * @private + * @class tinymce.caret.CaretCandidate + */ +define("tinymce/caret/CaretCandidate", [ + "tinymce/dom/NodeType", + "tinymce/util/Arr", + "tinymce/caret/CaretContainer" +], function(NodeType, Arr, CaretContainer) { + var isContentEditableTrue = NodeType.isContentEditableTrue, + isContentEditableFalse = NodeType.isContentEditableFalse, + isBr = NodeType.isBr, + isText = NodeType.isText, + isInvalidTextElement = NodeType.matchNodeNames('script style textarea'), + isAtomicInline = NodeType.matchNodeNames('img input textarea hr iframe video audio object'), + isTable = NodeType.matchNodeNames('table'), + isCaretContainer = CaretContainer.isCaretContainer; + + function isCaretCandidate(node) { + if (isCaretContainer(node)) { + return false; + } + + if (isText(node)) { + if (isInvalidTextElement(node.parentNode)) { + return false; + } + + return true; + } + + return isAtomicInline(node) || isBr(node) || isTable(node) || isContentEditableFalse(node); + } + + function isInEditable(node, rootNode) { + for (node = node.parentNode; node && node != rootNode; node = node.parentNode) { + if (isContentEditableFalse(node)) { + return false; + } + + if (isContentEditableTrue(node)) { + return true; + } + } + + return true; + } + + function isAtomicContentEditableFalse(node) { + if (!isContentEditableFalse(node)) { + return false; + } + + return Arr.reduce(node.getElementsByTagName('*'), function(result, elm) { + return result || isContentEditableTrue(elm); + }, false) !== true; + } + + function isAtomic(node) { + return isAtomicInline(node) || isAtomicContentEditableFalse(node); + } + + function isEditableCaretCandidate(node, rootNode) { + return isCaretCandidate(node) && isInEditable(node, rootNode); + } + + return { + isCaretCandidate: isCaretCandidate, + isInEditable: isInEditable, + isAtomic: isAtomic, + isEditableCaretCandidate: isEditableCaretCandidate + }; +}); + +// Included from: js/tinymce/classes/geom/ClientRect.js + +/** + * ClientRect.js + * + * Released under LGPL License. + * Copyright (c) 1999-2015 Ephox Corp. All rights reserved + * + * License: http://www.tinymce.com/license + * Contributing: http://www.tinymce.com/contributing + */ + +/** + * Utility functions for working with client rects. + * + * @private + * @class tinymce.geom.ClientRect + */ +define("tinymce/geom/ClientRect", [], function() { + var round = Math.round; + + function clone(rect) { + if (!rect) { + return {left: 0, top: 0, bottom: 0, right: 0, width: 0, height: 0}; + } + + return { + left: round(rect.left), + top: round(rect.top), + bottom: round(rect.bottom), + right: round(rect.right), + width: round(rect.width), + height: round(rect.height) + }; + } + + function collapse(clientRect, toStart) { + clientRect = clone(clientRect); + + if (toStart) { + clientRect.right = clientRect.left; + } else { + clientRect.left = clientRect.left + clientRect.width; + clientRect.right = clientRect.left; + } + + clientRect.width = 0; + + return clientRect; + } + + function isEqual(rect1, rect2) { + return ( + rect1.left === rect2.left && + rect1.top === rect2.top && + rect1.bottom === rect2.bottom && + rect1.right === rect2.right + ); + } + + function isValidOverflow(overflowY, clientRect1, clientRect2) { + return overflowY >= 0 && overflowY <= Math.min(clientRect1.height, clientRect2.height) / 2; + + } + + function isAbove(clientRect1, clientRect2) { + if (clientRect1.bottom < clientRect2.top) { + return true; + } + + if (clientRect1.top > clientRect2.bottom) { + return false; + } + + return isValidOverflow(clientRect2.top - clientRect1.bottom, clientRect1, clientRect2); + } + + function isBelow(clientRect1, clientRect2) { + if (clientRect1.top > clientRect2.bottom) { + return true; + } + + if (clientRect1.bottom < clientRect2.top) { + return false; + } + + return isValidOverflow(clientRect2.bottom - clientRect1.top, clientRect1, clientRect2); + } + + function isLeft(clientRect1, clientRect2) { + return clientRect1.left < clientRect2.left; + } + + function isRight(clientRect1, clientRect2) { + return clientRect1.right > clientRect2.right; + } + + function compare(clientRect1, clientRect2) { + if (isAbove(clientRect1, clientRect2)) { + return -1; + } + + if (isBelow(clientRect1, clientRect2)) { + return 1; + } + + if (isLeft(clientRect1, clientRect2)) { + return -1; + } + + if (isRight(clientRect1, clientRect2)) { + return 1; + } + + return 0; + } + + function containsXY(clientRect, clientX, clientY) { + return ( + clientX >= clientRect.left && + clientX <= clientRect.right && + clientY >= clientRect.top && + clientY <= clientRect.bottom + ); + } + + return { + clone: clone, + collapse: collapse, + isEqual: isEqual, + isAbove: isAbove, + isBelow: isBelow, + isLeft: isLeft, + isRight: isRight, + compare: compare, + containsXY: containsXY + }; +}); + +// Included from: js/tinymce/classes/text/ExtendingChar.js + +/** + * ExtendingChar.js + * + * Released under LGPL License. + * Copyright (c) 1999-2015 Ephox Corp. All rights reserved + * + * License: http://www.tinymce.com/license + * Contributing: http://www.tinymce.com/contributing + */ + +/** + * This class contains logic for detecting extending characters. + * + * @private + * @class tinymce.text.ExtendingChar + * @example + * var isExtending = ExtendingChar.isExtendingChar('a'); + */ +define("tinymce/text/ExtendingChar", [], function() { + // Generated from: http://www.unicode.org/Public/UNIDATA/DerivedCoreProperties.txt + // Only includes the characters in that fit into UCS-2 16 bit + var extendingChars = new RegExp( + "[\u0300-\u036F\u0483-\u0487\u0488-\u0489\u0591-\u05BD\u05BF\u05C1-\u05C2\u05C4-\u05C5\u05C7\u0610-\u061A" + + "\u064B-\u065F\u0670\u06D6-\u06DC\u06DF-\u06E4\u06E7-\u06E8\u06EA-\u06ED\u0711\u0730-\u074A\u07A6-\u07B0" + + "\u07EB-\u07F3\u0816-\u0819\u081B-\u0823\u0825-\u0827\u0829-\u082D\u0859-\u085B\u08E3-\u0902\u093A\u093C" + + "\u0941-\u0948\u094D\u0951-\u0957\u0962-\u0963\u0981\u09BC\u09BE\u09C1-\u09C4\u09CD\u09D7\u09E2-\u09E3" + + "\u0A01-\u0A02\u0A3C\u0A41-\u0A42\u0A47-\u0A48\u0A4B-\u0A4D\u0A51\u0A70-\u0A71\u0A75\u0A81-\u0A82\u0ABC" + + "\u0AC1-\u0AC5\u0AC7-\u0AC8\u0ACD\u0AE2-\u0AE3\u0B01\u0B3C\u0B3E\u0B3F\u0B41-\u0B44\u0B4D\u0B56\u0B57" + + "\u0B62-\u0B63\u0B82\u0BBE\u0BC0\u0BCD\u0BD7\u0C00\u0C3E-\u0C40\u0C46-\u0C48\u0C4A-\u0C4D\u0C55-\u0C56" + + "\u0C62-\u0C63\u0C81\u0CBC\u0CBF\u0CC2\u0CC6\u0CCC-\u0CCD\u0CD5-\u0CD6\u0CE2-\u0CE3\u0D01\u0D3E\u0D41-\u0D44" + + "\u0D4D\u0D57\u0D62-\u0D63\u0DCA\u0DCF\u0DD2-\u0DD4\u0DD6\u0DDF\u0E31\u0E34-\u0E3A\u0E47-\u0E4E\u0EB1\u0EB4-\u0EB9" + + "\u0EBB-\u0EBC\u0EC8-\u0ECD\u0F18-\u0F19\u0F35\u0F37\u0F39\u0F71-\u0F7E\u0F80-\u0F84\u0F86-\u0F87\u0F8D-\u0F97" + + "\u0F99-\u0FBC\u0FC6\u102D-\u1030\u1032-\u1037\u1039-\u103A\u103D-\u103E\u1058-\u1059\u105E-\u1060\u1071-\u1074" + + "\u1082\u1085-\u1086\u108D\u109D\u135D-\u135F\u1712-\u1714\u1732-\u1734\u1752-\u1753\u1772-\u1773\u17B4-\u17B5" + + "\u17B7-\u17BD\u17C6\u17C9-\u17D3\u17DD\u180B-\u180D\u18A9\u1920-\u1922\u1927-\u1928\u1932\u1939-\u193B\u1A17-\u1A18" + + "\u1A1B\u1A56\u1A58-\u1A5E\u1A60\u1A62\u1A65-\u1A6C\u1A73-\u1A7C\u1A7F\u1AB0-\u1ABD\u1ABE\u1B00-\u1B03\u1B34" + + "\u1B36-\u1B3A\u1B3C\u1B42\u1B6B-\u1B73\u1B80-\u1B81\u1BA2-\u1BA5\u1BA8-\u1BA9\u1BAB-\u1BAD\u1BE6\u1BE8-\u1BE9" + + "\u1BED\u1BEF-\u1BF1\u1C2C-\u1C33\u1C36-\u1C37\u1CD0-\u1CD2\u1CD4-\u1CE0\u1CE2-\u1CE8\u1CED\u1CF4\u1CF8-\u1CF9" + + "\u1DC0-\u1DF5\u1DFC-\u1DFF\u200C-\u200D\u20D0-\u20DC\u20DD-\u20E0\u20E1\u20E2-\u20E4\u20E5-\u20F0\u2CEF-\u2CF1" + + "\u2D7F\u2DE0-\u2DFF\u302A-\u302D\u302E-\u302F\u3099-\u309A\uA66F\uA670-\uA672\uA674-\uA67D\uA69E-\uA69F\uA6F0-\uA6F1" + + "\uA802\uA806\uA80B\uA825-\uA826\uA8C4\uA8E0-\uA8F1\uA926-\uA92D\uA947-\uA951\uA980-\uA982\uA9B3\uA9B6-\uA9B9\uA9BC" + + "\uA9E5\uAA29-\uAA2E\uAA31-\uAA32\uAA35-\uAA36\uAA43\uAA4C\uAA7C\uAAB0\uAAB2-\uAAB4\uAAB7-\uAAB8\uAABE-\uAABF\uAAC1" + + "\uAAEC-\uAAED\uAAF6\uABE5\uABE8\uABED\uFB1E\uFE00-\uFE0F\uFE20-\uFE2F\uFF9E-\uFF9F]" + ); + + function isExtendingChar(ch) { + return typeof ch == "string" && ch.charCodeAt(0) >= 768 && extendingChars.test(ch); + } + + return { + isExtendingChar: isExtendingChar + }; +}); + +// Included from: js/tinymce/classes/caret/CaretPosition.js + +/** + * CaretPosition.js + * + * Released under LGPL License. + * Copyright (c) 1999-2015 Ephox Corp. All rights reserved + * + * License: http://www.tinymce.com/license + * Contributing: http://www.tinymce.com/contributing + */ + +/** + * This module contains logic for creating caret positions within a document a caretposition + * is similar to a DOMRange object but it doesn't have two endpoints and is also more lightweight + * since it's now updated live when the DOM changes. + * + * @private + * @class tinymce.caret.CaretPosition + * @example + * var caretPos1 = new CaretPosition(container, offset); + * var caretPos2 = CaretPosition.fromRangeStart(someRange); + */ +define("tinymce/caret/CaretPosition", [ + "tinymce/util/Fun", + "tinymce/dom/NodeType", + "tinymce/dom/DOMUtils", + "tinymce/dom/RangeUtils", + "tinymce/caret/CaretCandidate", + "tinymce/geom/ClientRect", + "tinymce/text/ExtendingChar" +], function(Fun, NodeType, DOMUtils, RangeUtils, CaretCandidate, ClientRect, ExtendingChar) { + var isElement = NodeType.isElement, + isCaretCandidate = CaretCandidate.isCaretCandidate, + isBlock = NodeType.matchStyleValues('display', 'block table'), + isFloated = NodeType.matchStyleValues('float', 'left right'), + isValidElementCaretCandidate = Fun.and(isElement, isCaretCandidate, Fun.negate(isFloated)), + isNotPre = Fun.negate(NodeType.matchStyleValues('white-space', 'pre pre-line pre-wrap')), + isText = NodeType.isText, + isBr = NodeType.isBr, + nodeIndex = DOMUtils.nodeIndex, + resolveIndex = RangeUtils.getNode; + + function createRange(doc) { + return "createRange" in doc ? doc.createRange() : DOMUtils.DOM.createRng(); + } + + function isWhiteSpace(chr) { + return chr && /[\r\n\t ]/.test(chr); + } + + function isHiddenWhiteSpaceRange(range) { + var container = range.startContainer, + offset = range.startOffset, + text; + + if (isWhiteSpace(range.toString()) && isNotPre(container.parentNode)) { + text = container.data; + + if (isWhiteSpace(text[offset - 1]) || isWhiteSpace(text[offset + 1])) { + return true; + } + } + + return false; + } + + function getCaretPositionClientRects(caretPosition) { + var clientRects = [], beforeNode, node; + + // Hack for older WebKit versions that doesn't + // support getBoundingClientRect on BR elements + function getBrClientRect(brNode) { + var doc = brNode.ownerDocument, + rng = createRange(doc), + nbsp = doc.createTextNode('\u00a0'), + parentNode = brNode.parentNode, + clientRect; + + parentNode.insertBefore(nbsp, brNode); + rng.setStart(nbsp, 0); + rng.setEnd(nbsp, 1); + clientRect = ClientRect.clone(rng.getBoundingClientRect()); + parentNode.removeChild(nbsp); + + return clientRect; + } + + function getBoundingClientRect(item) { + var clientRect, clientRects; + + clientRects = item.getClientRects(); + if (clientRects.length > 0) { + clientRect = ClientRect.clone(clientRects[0]); + } else { + clientRect = ClientRect.clone(item.getBoundingClientRect()); + } + + if (isBr(item) && clientRect.left === 0) { + return getBrClientRect(item); + } + + return clientRect; + } + + function collapseAndInflateWidth(clientRect, toStart) { + clientRect = ClientRect.collapse(clientRect, toStart); + clientRect.width = 1; + clientRect.right = clientRect.left + 1; + + return clientRect; + } + + function addUniqueAndValidRect(clientRect) { + if (clientRect.height === 0) { + return; + } + + if (clientRects.length > 0) { + if (ClientRect.isEqual(clientRect, clientRects[clientRects.length - 1])) { + return; + } + } + + clientRects.push(clientRect); + } + + function addCharacterOffset(container, offset) { + var range = createRange(container.ownerDocument); + + if (offset < container.data.length) { + if (ExtendingChar.isExtendingChar(container.data[offset])) { + return clientRects; + } + + // WebKit returns two client rects for a position after an extending + // character a\uxxx|b so expand on "b" and collapse to start of "b" box + if (ExtendingChar.isExtendingChar(container.data[offset - 1])) { + range.setStart(container, offset); + range.setEnd(container, offset + 1); + + if (!isHiddenWhiteSpaceRange(range)) { + addUniqueAndValidRect(collapseAndInflateWidth(getBoundingClientRect(range), false)); + return clientRects; + } + } + } + + if (offset > 0) { + range.setStart(container, offset - 1); + range.setEnd(container, offset); + + if (!isHiddenWhiteSpaceRange(range)) { + addUniqueAndValidRect(collapseAndInflateWidth(getBoundingClientRect(range), false)); + } + } + + if (offset < container.data.length) { + range.setStart(container, offset); + range.setEnd(container, offset + 1); + + if (!isHiddenWhiteSpaceRange(range)) { + addUniqueAndValidRect(collapseAndInflateWidth(getBoundingClientRect(range), true)); + } + } + } + + if (isText(caretPosition.container())) { + addCharacterOffset(caretPosition.container(), caretPosition.offset()); + return clientRects; + } + + if (isElement(caretPosition.container())) { + if (caretPosition.isAtEnd()) { + node = resolveIndex(caretPosition.container(), caretPosition.offset()); + if (isText(node)) { + addCharacterOffset(node, node.data.length); + } + + if (isValidElementCaretCandidate(node) && !isBr(node)) { + addUniqueAndValidRect(collapseAndInflateWidth(getBoundingClientRect(node), false)); + } + } else { + node = resolveIndex(caretPosition.container(), caretPosition.offset()); + if (isText(node)) { + addCharacterOffset(node, 0); + } + + if (isValidElementCaretCandidate(node) && caretPosition.isAtEnd()) { + addUniqueAndValidRect(collapseAndInflateWidth(getBoundingClientRect(node), false)); + return clientRects; + } + + beforeNode = resolveIndex(caretPosition.container(), caretPosition.offset() - 1); + if (isValidElementCaretCandidate(beforeNode) && !isBr(beforeNode)) { + if (isBlock(beforeNode) || isBlock(node) || !isValidElementCaretCandidate(node)) { + addUniqueAndValidRect(collapseAndInflateWidth(getBoundingClientRect(beforeNode), false)); + } + } + + if (isValidElementCaretCandidate(node)) { + addUniqueAndValidRect(collapseAndInflateWidth(getBoundingClientRect(node), true)); + } + } + } + + return clientRects; + } + + /** + * Represents a location within the document by a container and an offset. + * + * @constructor + * @param {Node} container Container node. + * @param {Number} offset Offset within that container node. + * @param {Array} clientRects Optional client rects array for the position. + */ + function CaretPosition(container, offset, clientRects) { + function isAtStart() { + if (isText(container)) { + return offset === 0; + } + + return offset === 0; + } + + function isAtEnd() { + if (isText(container)) { + return offset >= container.data.length; + } + + return offset >= container.childNodes.length; + } + + function toRange() { + var range; + + range = createRange(container.ownerDocument); + range.setStart(container, offset); + range.setEnd(container, offset); + + return range; + } + + function getClientRects() { + if (!clientRects) { + clientRects = getCaretPositionClientRects(new CaretPosition(container, offset)); + } + + return clientRects; + } + + function isVisible() { + return getClientRects().length > 0; + } + + function isEqual(caretPosition) { + return caretPosition && container === caretPosition.container() && offset === caretPosition.offset(); + } + + function getNode(before) { + return resolveIndex(container, before ? offset - 1 : offset); + } + + return { + /** + * Returns the container node. + * + * @method container + * @return {Node} Container node. + */ + container: Fun.constant(container), + + /** + * Returns the offset within the container node. + * + * @method offset + * @return {Number} Offset within the container node. + */ + offset: Fun.constant(offset), + + /** + * Returns a range out of a the caret position. + * + * @method toRange + * @return {DOMRange} range for the caret position. + */ + toRange: toRange, + + /** + * Returns the client rects for the caret position. Might be multiple rects between + * block elements. + * + * @method getClientRects + * @return {Array} Array of client rects. + */ + getClientRects: getClientRects, + + /** + * Returns true if the caret location is visible/displayed on screen. + * + * @method isVisible + * @return {Boolean} true/false if the position is visible or not. + */ + isVisible: isVisible, + + /** + * Returns true if the caret location is at the beginning of text node or container. + * + * @method isVisible + * @return {Boolean} true/false if the position is at the beginning. + */ + isAtStart: isAtStart, + + /** + * Returns true if the caret location is at the end of text node or container. + * + * @method isVisible + * @return {Boolean} true/false if the position is at the end. + */ + isAtEnd: isAtEnd, + + /** + * Compares the caret position to another caret position. This will only compare the + * container and offset not it's visual position. + * + * @method isEqual + * @param {tinymce.caret.CaretPosition} caretPosition Caret position to compare with. + * @return {Boolean} true if the caret positions are equal. + */ + isEqual: isEqual, + + /** + * Returns the closest resolved node from a node index. That means if you have an offset after the + * last node in a container it will return that last node. + * + * @method getNode + * @return {Node} Node that is closest to the index. + */ + getNode: getNode + }; + } + + /** + * Creates a caret position from the start of a range. + * + * @method fromRangeStart + * @param {DOMRange} range DOM Range to create caret position from. + * @return {tinymce.caret.CaretPosition} Caret position from the start of DOM range. + */ + CaretPosition.fromRangeStart = function(range) { + return new CaretPosition(range.startContainer, range.startOffset); + }; + + /** + * Creates a caret position from the end of a range. + * + * @method fromRangeEnd + * @param {DOMRange} range DOM Range to create caret position from. + * @return {tinymce.caret.CaretPosition} Caret position from the end of DOM range. + */ + CaretPosition.fromRangeEnd = function(range) { + return new CaretPosition(range.endContainer, range.endOffset); + }; + + /** + * Creates a caret position from a node and places the offset after it. + * + * @method after + * @param {Node} node Node to get caret position from. + * @return {tinymce.caret.CaretPosition} Caret position from the node. + */ + CaretPosition.after = function(node) { + return new CaretPosition(node.parentNode, nodeIndex(node) + 1); + }; + + /** + * Creates a caret position from a node and places the offset before it. + * + * @method before + * @param {Node} node Node to get caret position from. + * @return {tinymce.caret.CaretPosition} Caret position from the node. + */ + CaretPosition.before = function(node) { + return new CaretPosition(node.parentNode, nodeIndex(node)); + }; + + return CaretPosition; +}); + +// Included from: js/tinymce/classes/caret/CaretBookmark.js + +/** + * CaretBookmark.js + * + * Released under LGPL License. + * Copyright (c) 1999-2015 Ephox Corp. All rights reserved + * + * License: http://www.tinymce.com/license + * Contributing: http://www.tinymce.com/contributing + */ + +/** + * This module creates or resolves xpath like string representation of a CaretPositions. + * + * The format is a / separated list of chunks with: + *a|c
+ * p[0]/img[0],before =|
+ * p[0]/img[0],after =|
+ * + * @private + * @static + * @class tinymce.caret.CaretBookmark + * @example + * var bookmark = CaretBookmark.create(rootElm, CaretPosition.before(rootElm.firstChild)); + * var caretPosition = CaretBookmark.resolve(bookmark); + */ +define('tinymce/caret/CaretBookmark', [ + 'tinymce/dom/NodeType', + 'tinymce/dom/DOMUtils', + 'tinymce/util/Fun', + 'tinymce/util/Arr', + 'tinymce/caret/CaretPosition' +], function(NodeType, DomUtils, Fun, Arr, CaretPosition) { + var isText = NodeType.isText, + isBogus = NodeType.isBogus, + nodeIndex = DomUtils.nodeIndex; + + function normalizedParent(node) { + var parentNode = node.parentNode; + + if (isBogus(parentNode)) { + return normalizedParent(parentNode); + } + + return parentNode; + } + + function getChildNodes(node) { + if (!node) { + return []; + } + + return Arr.reduce(node.childNodes, function(result, node) { + if (isBogus(node) && node.nodeName != 'BR') { + result = result.concat(getChildNodes(node)); + } else { + result.push(node); + } + + return result; + }, []); + } + + function normalizedTextOffset(textNode, offset) { + while ((textNode = textNode.previousSibling)) { + if (!isText(textNode)) { + break; + } + + offset += textNode.data.length; + } + + return offset; + } + + function equal(targetValue) { + return function(value) { + return targetValue === value; + }; + } + + function normalizedNodeIndex(node) { + var nodes, index, numTextFragments; + + nodes = getChildNodes(normalizedParent(node)); + index = Arr.findIndex(nodes, equal(node), node); + nodes = nodes.slice(0, index + 1); + numTextFragments = Arr.reduce(nodes, function(result, node, i) { + if (isText(node) && isText(nodes[i - 1])) { + result++; + } + + return result; + }, 0); + + nodes = Arr.filter(nodes, NodeType.matchNodeNames(node.nodeName)); + index = Arr.findIndex(nodes, equal(node), node); + + return index - numTextFragments; + } + + function createPathItem(node) { + var name; + + if (isText(node)) { + name = 'text()'; + } else { + name = node.nodeName.toLowerCase(); + } + + return name + '[' + normalizedNodeIndex(node) + ']'; + } + + function parentsUntil(rootNode, node, predicate) { + var parents = []; + + for (node = node.parentNode; node != rootNode; node = node.parentNode) { + if (predicate && predicate(node)) { + break; + } + + parents.push(node); + } + + return parents; + } + + function create(rootNode, caretPosition) { + var container, offset, path = [], + outputOffset, childNodes, parents; + + container = caretPosition.container(); + offset = caretPosition.offset(); + + if (isText(container)) { + outputOffset = normalizedTextOffset(container, offset); + } else { + childNodes = container.childNodes; + if (offset >= childNodes.length) { + outputOffset = 'after'; + offset = childNodes.length - 1; + } else { + outputOffset = 'before'; + } + + container = childNodes[offset]; + } + + path.push(createPathItem(container)); + parents = parentsUntil(rootNode, container); + parents = Arr.filter(parents, Fun.negate(NodeType.isBogus)); + path = path.concat(Arr.map(parents, function(node) { + return createPathItem(node); + })); + + return path.reverse().join('/') + ',' + outputOffset; + } + + function resolvePathItem(node, name, index) { + var nodes = getChildNodes(node); + + nodes = Arr.filter(nodes, function(node, index) { + return !isText(node) || !isText(nodes[index - 1]); + }); + + nodes = Arr.filter(nodes, NodeType.matchNodeNames(name)); + return nodes[index]; + } + + function findTextPosition(container, offset) { + var node = container, targetOffset = 0, dataLen; + + while (isText(node)) { + dataLen = node.data.length; + + if (offset >= targetOffset && offset <= targetOffset + dataLen) { + container = node; + offset = offset - targetOffset; + break; + } + + if (!isText(node.nextSibling)) { + container = node; + offset = dataLen; + break; + } + + targetOffset += dataLen; + node = node.nextSibling; + } + + if (offset > container.data.length) { + offset = container.data.length; + } + + return new CaretPosition(container, offset); + } + + function resolve(rootNode, path) { + var parts, container, offset; + + if (!path) { + return null; + } + + parts = path.split(','); + path = parts[0].split('/'); + offset = parts.length > 1 ? parts[1] : 'before'; + + container = Arr.reduce(path, function(result, value) { + value = /([\w\-\(\)]+)\[([0-9]+)\]/.exec(value); + if (!value) { + return null; + } + + if (value[1] === 'text()') { + value[1] = '#text'; + } + + return resolvePathItem(result, value[1], parseInt(value[2], 10)); + }, rootNode); + + if (!container) { + return null; + } + + if (!isText(container)) { + if (offset === 'after') { + offset = nodeIndex(container) + 1; + } else { + offset = nodeIndex(container); + } + + return new CaretPosition(container.parentNode, offset); + } + + return findTextPosition(container, parseInt(offset, 10)); + } + + return { + /** + * Create a xpath bookmark location for the specified caret position. + * + * @method create + * @param {Node} rootNode Root node to create bookmark within. + * @param {tinymce.caret.CaretPosition} caretPosition Caret position within the root node. + * @return {String} String xpath like location of caret position. + */ + create: create, + + /** + * Resolves a xpath like bookmark location to the a caret position. + * + * @method resolve + * @param {Node} rootNode Root node to resolve xpath bookmark within. + * @param {String} bookmark Bookmark string to resolve. + * @return {tinymce.caret.CaretPosition} Caret position resolved from xpath like bookmark. + */ + resolve: resolve + }; +}); + +// Included from: js/tinymce/classes/dom/BookmarkManager.js + +/** + * BookmarkManager.js + * + * Released under LGPL License. + * Copyright (c) 1999-2015 Ephox Corp. All rights reserved + * + * License: http://www.tinymce.com/license + * Contributing: http://www.tinymce.com/contributing + */ + +/** + * This class handles selection bookmarks. + * + * @class tinymce.dom.BookmarkManager + */ +define("tinymce/dom/BookmarkManager", [ + "tinymce/Env", + "tinymce/util/Tools", + "tinymce/caret/CaretContainer", + "tinymce/caret/CaretBookmark", + "tinymce/caret/CaretPosition", + "tinymce/dom/NodeType", + "tinymce/dom/RangeUtils" +], function(Env, Tools, CaretContainer, CaretBookmark, CaretPosition, NodeType, RangeUtils) { + var isContentEditableFalse = NodeType.isContentEditableFalse; + + /** + * Constructs a new BookmarkManager instance for a specific selection instance. + * + * @constructor + * @method BookmarkManager + * @param {tinymce.dom.Selection} selection Selection instance to handle bookmarks for. + */ + function BookmarkManager(selection) { + var dom = selection.dom; + + /** + * Returns a bookmark location for the current selection. This bookmark object + * can then be used to restore the selection after some content modification to the document. + * + * @method getBookmark + * @param {Number} type Optional state if the bookmark should be simple or not. Default is complex. + * @param {Boolean} normalized Optional state that enables you to get a position that it would be after normalization. + * @return {Object} Bookmark object, use moveToBookmark with this object to restore the selection. + * @example + * // Stores a bookmark of the current selection + * var bm = tinymce.activeEditor.selection.getBookmark(); + * + * tinymce.activeEditor.setContent(tinymce.activeEditor.getContent() + 'Some new content'); + * + * // Restore the selection bookmark + * tinymce.activeEditor.selection.moveToBookmark(bm); + */ + this.getBookmark = function(type, normalized) { + var rng, rng2, id, collapsed, name, element, chr = '', styles; + + function findIndex(name, element) { + var count = 0; + + Tools.each(dom.select(name), function(node) { + if (node.getAttribute('data-mce-bogus') === 'all') { + return; + } + + if (node == element) { + return false; + } + + count++; + }); + + return count; + } + + function normalizeTableCellSelection(rng) { + function moveEndPoint(start) { + var container, offset, childNodes, prefix = start ? 'start' : 'end'; + + container = rng[prefix + 'Container']; + offset = rng[prefix + 'Offset']; + + if (container.nodeType == 1 && container.nodeName == "TR") { + childNodes = container.childNodes; + container = childNodes[Math.min(start ? offset : offset - 1, childNodes.length - 1)]; + if (container) { + offset = start ? 0 : container.childNodes.length; + rng['set' + (start ? 'Start' : 'End')](container, offset); + } + } + } + + moveEndPoint(true); + moveEndPoint(); + + return rng; + } + + function getLocation(rng) { + var root = dom.getRoot(), bookmark = {}; + + function getPoint(rng, start) { + var container = rng[start ? 'startContainer' : 'endContainer'], + offset = rng[start ? 'startOffset' : 'endOffset'], point = [], node, childNodes, after = 0; + + if (container.nodeType == 3) { + if (normalized) { + for (node = container.previousSibling; node && node.nodeType == 3; node = node.previousSibling) { + offset += node.nodeValue.length; + } + } + + point.push(offset); + } else { + childNodes = container.childNodes; + + if (offset >= childNodes.length && childNodes.length) { + after = 1; + offset = Math.max(0, childNodes.length - 1); + } + + point.push(dom.nodeIndex(childNodes[offset], normalized) + after); + } + + for (; container && container != root; container = container.parentNode) { + point.push(dom.nodeIndex(container, normalized)); + } + + return point; + } + + bookmark.start = getPoint(rng, true); + + if (!selection.isCollapsed()) { + bookmark.end = getPoint(rng); + } + + return bookmark; + } + + function findAdjacentContentEditableFalseElm(rng) { + function findSibling(node, offset) { + var sibling; + + if (NodeType.isElement(node)) { + node = RangeUtils.getNode(node, offset); + if (isContentEditableFalse(node)) { + return node; + } + } + + if (CaretContainer.isCaretContainer(node)) { + if (NodeType.isText(node) && CaretContainer.isCaretContainerBlock(node)) { + node = node.parentNode; + } + + sibling = node.previousSibling; + if (isContentEditableFalse(sibling)) { + return sibling; + } + + sibling = node.nextSibling; + if (isContentEditableFalse(sibling)) { + return sibling; + } + } + } + + return findSibling(rng.startContainer, rng.startOffset) || findSibling(rng.endContainer, rng.endOffset); + } + + if (type == 2) { + element = selection.getNode(); + name = element ? element.nodeName : null; + rng = selection.getRng(); + + if (isContentEditableFalse(element) || name == 'IMG') { + return {name: name, index: findIndex(name, element)}; + } + + if (selection.tridentSel) { + return selection.tridentSel.getBookmark(type); + } + + element = findAdjacentContentEditableFalseElm(rng); + if (element) { + name = element.tagName; + return {name: name, index: findIndex(name, element)}; + } + + return getLocation(rng); + } + + if (type == 3) { + rng = selection.getRng(); + + return { + start: CaretBookmark.create(dom.getRoot(), CaretPosition.fromRangeStart(rng)), + end: CaretBookmark.create(dom.getRoot(), CaretPosition.fromRangeEnd(rng)) + }; + } + + // Handle simple range + if (type) { + return {rng: selection.getRng()}; + } + + rng = selection.getRng(); + id = dom.uniqueId(); + collapsed = selection.isCollapsed(); + styles = 'overflow:hidden;line-height:0px'; + + // Explorer method + if (rng.duplicate || rng.item) { + // Text selection + if (!rng.item) { + rng2 = rng.duplicate(); + + try { + // Insert start marker + rng.collapse(); + rng.pasteHTML('' + chr + ''); + + // Insert end marker + if (!collapsed) { + rng2.collapse(false); + + // Detect the empty space after block elements in IE and move the + // end back one character ] becomes]
+ rng.moveToElementText(rng2.parentElement()); + if (rng.compareEndPoints('StartToEnd', rng2) === 0) { + rng2.move('character', -1); + } + + rng2.pasteHTML('' + chr + ''); + } + } catch (ex) { + // IE might throw unspecified error so lets ignore it + return null; + } + } else { + // Control selection + element = rng.item(0); + name = element.nodeName; + + return {name: name, index: findIndex(name, element)}; + } + } else { + element = selection.getNode(); + name = element.nodeName; + if (name == 'IMG') { + return {name: name, index: findIndex(name, element)}; + } + + // W3C method + rng2 = normalizeTableCellSelection(rng.cloneRange()); + + // Insert end marker + if (!collapsed) { + rng2.collapse(false); + rng2.insertNode(dom.create('span', {'data-mce-type': "bookmark", id: id + '_end', style: styles}, chr)); + } + + rng = normalizeTableCellSelection(rng); + rng.collapse(true); + rng.insertNode(dom.create('span', {'data-mce-type': "bookmark", id: id + '_start', style: styles}, chr)); + } + + selection.moveToBookmark({id: id, keep: 1}); + + return {id: id}; + }; + + /** + * Restores the selection to the specified bookmark. + * + * @method moveToBookmark + * @param {Object} bookmark Bookmark to restore selection from. + * @return {Boolean} true/false if it was successful or not. + * @example + * // Stores a bookmark of the current selection + * var bm = tinymce.activeEditor.selection.getBookmark(); + * + * tinymce.activeEditor.setContent(tinymce.activeEditor.getContent() + 'Some new content'); + * + * // Restore the selection bookmark + * tinymce.activeEditor.selection.moveToBookmark(bm); + */ + this.moveToBookmark = function(bookmark) { + var rng, root, startContainer, endContainer, startOffset, endOffset; + + function setEndPoint(start) { + var point = bookmark[start ? 'start' : 'end'], i, node, offset, children; + + if (point) { + offset = point[0]; + + // Find container node + for (node = root, i = point.length - 1; i >= 1; i--) { + children = node.childNodes; + + if (point[i] > children.length - 1) { + return; + } + + node = children[point[i]]; + } + + // Move text offset to best suitable location + if (node.nodeType === 3) { + offset = Math.min(point[0], node.nodeValue.length); + } + + // Move element offset to best suitable location + if (node.nodeType === 1) { + offset = Math.min(point[0], node.childNodes.length); + } + + // Set offset within container node + if (start) { + rng.setStart(node, offset); + } else { + rng.setEnd(node, offset); + } + } + + return true; + } + + function restoreEndPoint(suffix) { + var marker = dom.get(bookmark.id + '_' + suffix), node, idx, next, prev, keep = bookmark.keep; + + if (marker) { + node = marker.parentNode; + + if (suffix == 'start') { + if (!keep) { + idx = dom.nodeIndex(marker); + } else { + node = marker.firstChild; + idx = 1; + } + + startContainer = endContainer = node; + startOffset = endOffset = idx; + } else { + if (!keep) { + idx = dom.nodeIndex(marker); + } else { + node = marker.firstChild; + idx = 1; + } + + endContainer = node; + endOffset = idx; + } + + if (!keep) { + prev = marker.previousSibling; + next = marker.nextSibling; + + // Remove all marker text nodes + Tools.each(Tools.grep(marker.childNodes), function(node) { + if (node.nodeType == 3) { + node.nodeValue = node.nodeValue.replace(/\uFEFF/g, ''); + } + }); + + // Remove marker but keep children if for example contents where inserted into the marker + // Also remove duplicated instances of the marker for example by a + // split operation or by WebKit auto split on paste feature + while ((marker = dom.get(bookmark.id + '_' + suffix))) { + dom.remove(marker, 1); + } + + // If siblings are text nodes then merge them unless it's Opera since it some how removes the node + // and we are sniffing since adding a lot of detection code for a browser with 3% of the market + // isn't worth the effort. Sorry, Opera but it's just a fact + if (prev && next && prev.nodeType == next.nodeType && prev.nodeType == 3 && !Env.opera) { + idx = prev.nodeValue.length; + prev.appendData(next.nodeValue); + dom.remove(next); + + if (suffix == 'start') { + startContainer = endContainer = prev; + startOffset = endOffset = idx; + } else { + endContainer = prev; + endOffset = idx; + } + } + } + } + } + + function addBogus(node) { + // Adds a bogus BR element for empty block elements + if (dom.isBlock(node) && !node.innerHTML && !Env.ie) { + node.innerHTML = '[a
]b
->[a]
b
+ function adjustSelectionToVisibleSelection() { + function findSelectionEnd(start, end) { + var walker = new TreeWalker(end); + for (node = walker.prev2(); node; node = walker.prev2()) { + if (node.nodeType == 3 && node.data.length > 0) { + return node; + } + + if (node.childNodes.length > 1 || node == start || node.tagName == 'BR') { + return node; + } + } + } + + // Adjust selection so that a end container with a end offset of zero is not included in the selection + // as this isn't visible to the user. + var rng = ed.selection.getRng(); + var start = rng.startContainer; + var end = rng.endContainer; + + if (start != end && rng.endOffset === 0) { + var newEnd = findSelectionEnd(start, end); + var endOffset = newEnd.nodeType == 3 ? newEnd.data.length : newEnd.childNodes.length; + + rng.setEnd(newEnd, endOffset); + } + + return rng; + } + + function applyRngStyle(rng, bookmark, node_specific) { + var newWrappers = [], wrapName, wrapElm, contentEditable = true; + + // Setup wrapper element + wrapName = format.inline || format.block; + wrapElm = dom.create(wrapName); + setElementFormat(wrapElm); + + rangeUtils.walk(rng, function(nodes) { + var currentWrapElm; + + /** + * Process a list of nodes wrap them. + */ + function process(node) { + var nodeName, parentName, hasContentEditableState, lastContentEditable; + + lastContentEditable = contentEditable; + nodeName = node.nodeName.toLowerCase(); + parentName = node.parentNode.nodeName.toLowerCase(); + + // Node has a contentEditable value + if (node.nodeType === 1 && getContentEditable(node)) { + lastContentEditable = contentEditable; + contentEditable = getContentEditable(node) === "true"; + hasContentEditableState = true; // We don't want to wrap the container only it's children + } + + // Stop wrapping on br elements + if (isEq(nodeName, 'br')) { + currentWrapElm = 0; + + // Remove any br elements when we wrap things + if (format.block) { + dom.remove(node); + } + + return; + } + + // If node is wrapper type + if (format.wrapper && matchNode(node, name, vars)) { + currentWrapElm = 0; + return; + } + + // Can we rename the block + // TODO: Break this if up, too complex + if (contentEditable && !hasContentEditableState && format.block && + !format.wrapper && isTextBlock(nodeName) && isValid(parentName, wrapName)) { + node = dom.rename(node, wrapName); + setElementFormat(node); + newWrappers.push(node); + currentWrapElm = 0; + return; + } + + // Handle selector patterns + if (format.selector) { + var found = applyNodeStyle(formatList, node); + + // Continue processing if a selector match wasn't found and a inline element is defined + if (!format.inline || found) { + currentWrapElm = 0; + return; + } + } + + // Is it valid to wrap this item + // TODO: Break this if up, too complex + if (contentEditable && !hasContentEditableState && isValid(wrapName, nodeName) && isValid(parentName, wrapName) && + !(!node_specific && node.nodeType === 3 && + node.nodeValue.length === 1 && + node.nodeValue.charCodeAt(0) === 65279) && + !isCaretNode(node) && + (!format.inline || !isBlock(node))) { + // Start wrapping + if (!currentWrapElm) { + // Wrap the node + currentWrapElm = dom.clone(wrapElm, FALSE); + node.parentNode.insertBefore(currentWrapElm, node); + newWrappers.push(currentWrapElm); + } + + currentWrapElm.appendChild(node); + } else { + // Start a new wrapper for possible children + currentWrapElm = 0; + + each(grep(node.childNodes), process); + + if (hasContentEditableState) { + contentEditable = lastContentEditable; // Restore last contentEditable state from stack + } + + // End the last wrapper + currentWrapElm = 0; + } + } + + // Process siblings from range + each(nodes, process); + }); + + // Apply formats to links as well to get the color of the underline to change as well + if (format.links === true) { + each(newWrappers, function(node) { + function process(node) { + if (node.nodeName === 'A') { + setElementFormat(node, format); + } + + each(grep(node.childNodes), process); + } + + process(node); + }); + } + + // Cleanup + each(newWrappers, function(node) { + var childCount; + + function getChildCount(node) { + var count = 0; + + each(node.childNodes, function(node) { + if (!isWhiteSpaceNode(node) && !isBookmarkNode(node)) { + count++; + } + }); + + return count; + } + + function mergeStyles(node) { + var child, clone; + + each(node.childNodes, function(node) { + if (node.nodeType == 1 && !isBookmarkNode(node) && !isCaretNode(node)) { + child = node; + return FALSE; // break loop + } + }); + + // If child was found and of the same type as the current node + if (child && !isBookmarkNode(child) && matchName(child, format)) { + clone = dom.clone(child, FALSE); + setElementFormat(clone); + + dom.replace(clone, node, TRUE); + dom.remove(child, 1); + } + + return clone || node; + } + + childCount = getChildCount(node); + + // Remove empty nodes but only if there is multiple wrappers and they are not block + // elements so never remove single since that would remove the + // current empty block element where the caret is at + if ((newWrappers.length > 1 || !isBlock(node)) && childCount === 0) { + dom.remove(node, 1); + return; + } + + if (format.inline || format.wrapper) { + // Merges the current node with it's children of similar type to reduce the number of elements + if (!format.exact && childCount === 1) { + node = mergeStyles(node); + } + + // Remove/merge children + each(formatList, function(format) { + // Merge all children of similar type will move styles from child to parent + // this: text + // will become: text + each(dom.select(format.inline, node), function(child) { + if (isBookmarkNode(child)) { + return; + } + + removeFormat(format, vars, child, format.exact ? child : null); + }); + }); + + // Remove child if direct parent is of same type + if (matchNode(node.parentNode, name, vars)) { + dom.remove(node, 1); + node = 0; + return TRUE; + } + + // Look for parent with similar style format + if (format.merge_with_parents) { + dom.getParent(node.parentNode, function(parent) { + if (matchNode(parent, name, vars)) { + dom.remove(node, 1); + node = 0; + return TRUE; + } + }); + } + + // Merge next and previous siblings if they are similar texttext becomes texttext + if (node && format.merge_siblings !== false) { + node = mergeSiblings(getNonWhiteSpaceSibling(node), node); + node = mergeSiblings(node, getNonWhiteSpaceSibling(node, TRUE)); + } + } + }); + } + + if (getContentEditable(selection.getNode()) === "false") { + node = selection.getNode(); + for (var i = 0, l = formatList.length; i < l; i++) { + if (formatList[i].ceFalseOverride && dom.is(node, formatList[i].selector)) { + setElementFormat(node, formatList[i]); + return; + } + } + + return; + } + + if (format) { + if (node) { + if (node.nodeType) { + if (!applyNodeStyle(formatList, node)) { + rng = dom.createRng(); + rng.setStartBefore(node); + rng.setEndAfter(node); + applyRngStyle(expandRng(rng, formatList), null, true); + } + } else { + applyRngStyle(node, null, true); + } + } else { + if (!isCollapsed || !format.inline || dom.select('td[data-mce-selected],th[data-mce-selected]').length) { + // Obtain selection node before selection is unselected by applyRngStyle() + var curSelNode = ed.selection.getNode(); + + // If the formats have a default block and we can't find a parent block then + // start wrapping it with a DIV this is for forced_root_blocks: false + // It's kind of a hack but people should be using the default block type P since all desktop editors work that way + if (!forcedRootBlock && formatList[0].defaultBlock && !dom.getParent(curSelNode, dom.isBlock)) { + apply(formatList[0].defaultBlock); + } + + // Apply formatting to selection + ed.selection.setRng(adjustSelectionToVisibleSelection()); + bookmark = selection.getBookmark(); + applyRngStyle(expandRng(selection.getRng(TRUE), formatList), bookmark); + + // Colored nodes should be underlined so that the color of the underline matches the text color. + if (format.styles && (format.styles.color || format.styles.textDecoration)) { + walk(curSelNode, processUnderlineAndColor, 'childNodes'); + processUnderlineAndColor(curSelNode); + } + + selection.moveToBookmark(bookmark); + moveStart(selection.getRng(TRUE)); + ed.nodeChanged(); + } else { + performCaretAction('apply', name, vars); + } + } + + Hooks.postProcess(name, ed); + } + } + + /** + * Removes the specified format from the current selection or specified node. + * + * @method remove + * @param {String} name Name of format to remove. + * @param {Object} vars Optional list of variables to replace within format before removing it. + * @param {Node/Range} node Optional node or DOM range to remove the format from defaults to current selection. + */ + function remove(name, vars, node, similar) { + var formatList = get(name), format = formatList[0], bookmark, rng, contentEditable = true; + + // Merges the styles for each node + function process(node) { + var children, i, l, lastContentEditable, hasContentEditableState; + + // Node has a contentEditable value + if (node.nodeType === 1 && getContentEditable(node)) { + lastContentEditable = contentEditable; + contentEditable = getContentEditable(node) === "true"; + hasContentEditableState = true; // We don't want to wrap the container only it's children + } + + // Grab the children first since the nodelist might be changed + children = grep(node.childNodes); + + // Process current node + if (contentEditable && !hasContentEditableState) { + for (i = 0, l = formatList.length; i < l; i++) { + if (removeFormat(formatList[i], vars, node, node)) { + break; + } + } + } + + // Process the children + if (format.deep) { + if (children.length) { + for (i = 0, l = children.length; i < l; i++) { + process(children[i]); + } + + if (hasContentEditableState) { + contentEditable = lastContentEditable; // Restore last contentEditable state from stack + } + } + } + } + + function findFormatRoot(container) { + var formatRoot; + + // Find format root + each(getParents(container.parentNode).reverse(), function(parent) { + var format; + + // Find format root element + if (!formatRoot && parent.id != '_start' && parent.id != '_end') { + // Is the node matching the format we are looking for + format = matchNode(parent, name, vars, similar); + if (format && format.split !== false) { + formatRoot = parent; + } + } + }); + + return formatRoot; + } + + function wrapAndSplit(formatRoot, container, target, split) { + var parent, clone, lastClone, firstClone, i, formatRootParent; + + // Format root found then clone formats and split it + if (formatRoot) { + formatRootParent = formatRoot.parentNode; + + for (parent = container.parentNode; parent && parent != formatRootParent; parent = parent.parentNode) { + clone = dom.clone(parent, FALSE); + + for (i = 0; i < formatList.length; i++) { + if (removeFormat(formatList[i], vars, clone, clone)) { + clone = 0; + break; + } + } + + // Build wrapper node + if (clone) { + if (lastClone) { + clone.appendChild(lastClone); + } + + if (!firstClone) { + firstClone = clone; + } + + lastClone = clone; + } + } + + // Never split block elements if the format is mixed + if (split && (!format.mixed || !isBlock(formatRoot))) { + container = dom.split(formatRoot, container); + } + + // Wrap container in cloned formats + if (lastClone) { + target.parentNode.insertBefore(lastClone, target); + firstClone.appendChild(target); + } + } + + return container; + } + + function splitToFormatRoot(container) { + return wrapAndSplit(findFormatRoot(container), container, container, true); + } + + function unwrap(start) { + var node = dom.get(start ? '_start' : '_end'), + out = node[start ? 'firstChild' : 'lastChild']; + + // If the end is placed within the start the result will be removed + // So this checks if the out node is a bookmark node if it is it + // checks for another more suitable node + if (isBookmarkNode(out)) { + out = out[start ? 'firstChild' : 'lastChild']; + } + + // Since dom.remove removes empty text nodes then we need to try to find a better node + if (out.nodeType == 3 && out.data.length === 0) { + out = start ? node.previousSibling || node.nextSibling : node.nextSibling || node.previousSibling; + } + + dom.remove(node, true); + + return out; + } + + function removeRngStyle(rng) { + var startContainer, endContainer; + var commonAncestorContainer = rng.commonAncestorContainer; + + rng = expandRng(rng, formatList, TRUE); + + if (format.split) { + startContainer = getContainer(rng, TRUE); + endContainer = getContainer(rng); + + if (startContainer != endContainer) { + // WebKit will render the table incorrectly if we wrap a TH or TD in a SPAN + // so let's see if we can use the first child instead + // This will happen if you triple click a table cell and use remove formatting + if (/^(TR|TH|TD)$/.test(startContainer.nodeName) && startContainer.firstChild) { + if (startContainer.nodeName == "TR") { + startContainer = startContainer.firstChild.firstChild || startContainer; + } else { + startContainer = startContainer.firstChild || startContainer; + } + } + + // Try to adjust endContainer as well if cells on the same row were selected - bug #6410 + if (commonAncestorContainer && + /^T(HEAD|BODY|FOOT|R)$/.test(commonAncestorContainer.nodeName) && + isTableCell(endContainer) && endContainer.firstChild) { + endContainer = endContainer.firstChild || endContainer; + } + + if (dom.isChildOf(startContainer, endContainer) && !isBlock(endContainer) && + !isTableCell(startContainer) && !isTableCell(endContainer)) { + startContainer = wrap(startContainer, 'span', {id: '_start', 'data-mce-type': 'bookmark'}); + splitToFormatRoot(startContainer); + startContainer = unwrap(TRUE); + return; + } + + // Wrap start/end nodes in span element since these might be cloned/moved + startContainer = wrap(startContainer, 'span', {id: '_start', 'data-mce-type': 'bookmark'}); + endContainer = wrap(endContainer, 'span', {id: '_end', 'data-mce-type': 'bookmark'}); + + // Split start/end + splitToFormatRoot(startContainer); + splitToFormatRoot(endContainer); + + // Unwrap start/end to get real elements again + startContainer = unwrap(TRUE); + endContainer = unwrap(); + } else { + startContainer = endContainer = splitToFormatRoot(startContainer); + } + + // Update range positions since they might have changed after the split operations + rng.startContainer = startContainer.parentNode ? startContainer.parentNode : startContainer; + rng.startOffset = nodeIndex(startContainer); + rng.endContainer = endContainer.parentNode ? endContainer.parentNode : endContainer; + rng.endOffset = nodeIndex(endContainer) + 1; + } + + // Remove items between start/end + rangeUtils.walk(rng, function(nodes) { + each(nodes, function(node) { + process(node); + + // Remove parent span if it only contains text-decoration: underline, yet a parent node is also underlined. + if (node.nodeType === 1 && ed.dom.getStyle(node, 'text-decoration') === 'underline' && + node.parentNode && getTextDecoration(node.parentNode) === 'underline') { + removeFormat({ + 'deep': false, + 'exact': true, + 'inline': 'span', + 'styles': { + 'textDecoration': 'underline' + } + }, null, node); + } + }); + }); + } + + // Handle node + if (node) { + if (node.nodeType) { + rng = dom.createRng(); + rng.setStartBefore(node); + rng.setEndAfter(node); + removeRngStyle(rng); + } else { + removeRngStyle(node); + } + + return; + } + + if (getContentEditable(selection.getNode()) === "false") { + node = selection.getNode(); + for (var i = 0, l = formatList.length; i < l; i++) { + if (formatList[i].ceFalseOverride) { + if (removeFormat(formatList[i], vars, node, node)) { + break; + } + } + } + + return; + } + + if (!selection.isCollapsed() || !format.inline || dom.select('td[data-mce-selected],th[data-mce-selected]').length) { + bookmark = selection.getBookmark(); + removeRngStyle(selection.getRng(TRUE)); + selection.moveToBookmark(bookmark); + + // Check if start element still has formatting then we are at: "text|text" + // and need to move the start into the next text node + if (format.inline && match(name, vars, selection.getStart())) { + moveStart(selection.getRng(true)); + } + + ed.nodeChanged(); + } else { + performCaretAction('remove', name, vars, similar); + } + } + + /** + * Toggles the specified format on/off. + * + * @method toggle + * @param {String} name Name of format to apply/remove. + * @param {Object} vars Optional list of variables to replace within format before applying/removing it. + * @param {Node} node Optional node to apply the format to or remove from. Defaults to current selection. + */ + function toggle(name, vars, node) { + var fmt = get(name); + + if (match(name, vars, node) && (!('toggle' in fmt[0]) || fmt[0].toggle)) { + remove(name, vars, node); + } else { + apply(name, vars, node); + } + } + + /** + * Return true/false if the specified node has the specified format. + * + * @method matchNode + * @param {Node} node Node to check the format on. + * @param {String} name Format name to check. + * @param {Object} vars Optional list of variables to replace before checking it. + * @param {Boolean} similar Match format that has similar properties. + * @return {Object} Returns the format object it matches or undefined if it doesn't match. + */ + function matchNode(node, name, vars, similar) { + var formatList = get(name), format, i, classes; + + function matchItems(node, format, item_name) { + var key, value, items = format[item_name], i; + + // Custom match + if (format.onmatch) { + return format.onmatch(node, format, item_name); + } + + // Check all items + if (items) { + // Non indexed object + if (items.length === undef) { + for (key in items) { + if (items.hasOwnProperty(key)) { + if (item_name === 'attributes') { + value = dom.getAttrib(node, key); + } else { + value = getStyle(node, key); + } + + if (similar && !value && !format.exact) { + return; + } + + if ((!similar || format.exact) && !isEq(value, normalizeStyleValue(replaceVars(items[key], vars), key))) { + return; + } + } + } + } else { + // Only one match needed for indexed arrays + for (i = 0; i < items.length; i++) { + if (item_name === 'attributes' ? dom.getAttrib(node, items[i]) : getStyle(node, items[i])) { + return format; + } + } + } + } + + return format; + } + + if (formatList && node) { + // Check each format in list + for (i = 0; i < formatList.length; i++) { + format = formatList[i]; + + // Name name, attributes, styles and classes + if (matchName(node, format) && matchItems(node, format, 'attributes') && matchItems(node, format, 'styles')) { + // Match classes + if ((classes = format.classes)) { + for (i = 0; i < classes.length; i++) { + if (!dom.hasClass(node, classes[i])) { + return; + } + } + } + + return format; + } + } + } + } + + /** + * Matches the current selection or specified node against the specified format name. + * + * @method match + * @param {String} name Name of format to match. + * @param {Object} vars Optional list of variables to replace before checking it. + * @param {Node} node Optional node to check. + * @return {boolean} true/false if the specified selection/node matches the format. + */ + function match(name, vars, node) { + var startNode; + + function matchParents(node) { + var root = dom.getRoot(); + + if (node === root) { + return false; + } + + // Find first node with similar format settings + node = dom.getParent(node, function(node) { + if (matchesUnInheritedFormatSelector(node, name)) { + return true; + } + + return node.parentNode === root || !!matchNode(node, name, vars, true); + }); + + // Do an exact check on the similar format element + return matchNode(node, name, vars); + } + + // Check specified node + if (node) { + return matchParents(node); + } + + // Check selected node + node = selection.getNode(); + if (matchParents(node)) { + return TRUE; + } + + // Check start node if it's different + startNode = selection.getStart(); + if (startNode != node) { + if (matchParents(startNode)) { + return TRUE; + } + } + + return FALSE; + } + + /** + * Matches the current selection against the array of formats and returns a new array with matching formats. + * + * @method matchAll + * @param {Array} names Name of format to match. + * @param {Object} vars Optional list of variables to replace before checking it. + * @return {Array} Array with matched formats. + */ + function matchAll(names, vars) { + var startElement, matchedFormatNames = [], checkedMap = {}; + + // Check start of selection for formats + startElement = selection.getStart(); + dom.getParent(startElement, function(node) { + var i, name; + + for (i = 0; i < names.length; i++) { + name = names[i]; + + if (!checkedMap[name] && matchNode(node, name, vars)) { + checkedMap[name] = true; + matchedFormatNames.push(name); + } + } + }, dom.getRoot()); + + return matchedFormatNames; + } + + /** + * Returns true/false if the specified format can be applied to the current selection or not. It + * will currently only check the state for selector formats, it returns true on all other format types. + * + * @method canApply + * @param {String} name Name of format to check. + * @return {boolean} true/false if the specified format can be applied to the current selection/node. + */ + function canApply(name) { + var formatList = get(name), startNode, parents, i, x, selector; + + if (formatList) { + startNode = selection.getStart(); + parents = getParents(startNode); + + for (x = formatList.length - 1; x >= 0; x--) { + selector = formatList[x].selector; + + // Format is not selector based then always return TRUE + // Is it has a defaultBlock then it's likely it can be applied for example align on a non block element line + if (!selector || formatList[x].defaultBlock) { + return TRUE; + } + + for (i = parents.length - 1; i >= 0; i--) { + if (dom.is(parents[i], selector)) { + return TRUE; + } + } + } + } + + return FALSE; + } + + /** + * Executes the specified callback when the current selection matches the formats or not. + * + * @method formatChanged + * @param {String} formats Comma separated list of formats to check for. + * @param {function} callback Callback with state and args when the format is changed/toggled on/off. + * @param {Boolean} similar True/false state if the match should handle similar or exact formats. + */ + function formatChanged(formats, callback, similar) { + var currentFormats; + + // Setup format node change logic + if (!formatChangeData) { + formatChangeData = {}; + currentFormats = {}; + + ed.on('NodeChange', function(e) { + var parents = getParents(e.element), matchedFormats = {}; + + // Ignore bogus nodes like the tag created by moveStart() + parents = Tools.grep(parents, function(node) { + return node.nodeType == 1 && !node.getAttribute('data-mce-bogus'); + }); + + // Check for new formats + each(formatChangeData, function(callbacks, format) { + each(parents, function(node) { + if (matchNode(node, format, {}, callbacks.similar)) { + if (!currentFormats[format]) { + // Execute callbacks + each(callbacks, function(callback) { + callback(true, {node: node, format: format, parents: parents}); + }); + + currentFormats[format] = callbacks; + } + + matchedFormats[format] = callbacks; + return false; + } + + if (matchesUnInheritedFormatSelector(node, format)) { + return false; + } + }); + }); + + // Check if current formats still match + each(currentFormats, function(callbacks, format) { + if (!matchedFormats[format]) { + delete currentFormats[format]; + + each(callbacks, function(callback) { + callback(false, {node: e.element, format: format, parents: parents}); + }); + } + }); + }); + } + + // Add format listeners + each(formats.split(','), function(format) { + if (!formatChangeData[format]) { + formatChangeData[format] = []; + formatChangeData[format].similar = similar; + } + + formatChangeData[format].push(callback); + }); + + 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, + unregister: unregister, + apply: apply, + remove: remove, + toggle: toggle, + match: match, + matchAll: matchAll, + matchNode: matchNode, + canApply: canApply, + formatChanged: formatChanged, + getCssText: getCssText + }); + + // Initialize + defaultFormats(); + addKeyboardShortcuts(); + ed.on('BeforeGetContent', function(e) { + if (markCaretContainersBogus && e.format != 'raw') { + markCaretContainersBogus(); + } + }); + ed.on('mouseup keydown', function(e) { + if (disableCaretContainer) { + disableCaretContainer(e); + } + }); + + // Private functions + + /** + * Checks if the specified nodes name matches the format inline/block or selector. + * + * @private + * @param {Node} node Node to match against the specified format. + * @param {Object} format Format object o match with. + * @return {boolean} true/false if the format matches. + */ + function matchName(node, format) { + // Check for inline match + if (isEq(node, format.inline)) { + return TRUE; + } + + // Check for block match + if (isEq(node, format.block)) { + return TRUE; + } + + // Check for selector match + if (format.selector) { + return node.nodeType == 1 && dom.is(node, format.selector); + } + } + + /** + * Compares two string/nodes regardless of their case. + * + * @private + * @param {String/Node} str1 Node or string to compare. + * @param {String/Node} str2 Node or string to compare. + * @return {boolean} True/false if they match. + */ + function isEq(str1, str2) { + str1 = str1 || ''; + str2 = str2 || ''; + + str1 = '' + (str1.nodeName || str1); + str2 = '' + (str2.nodeName || str2); + + return str1.toLowerCase() == str2.toLowerCase(); + } + + /** + * Returns the style by name on the specified node. This method modifies the style + * contents to make it more easy to match. This will resolve a few browser issues. + * + * @private + * @param {Node} node to get style from. + * @param {String} name Style name to get. + * @return {String} Style item value. + */ + function getStyle(node, name) { + return normalizeStyleValue(dom.getStyle(node, name), name); + } + + /** + * Normalize style value by name. This method modifies the style contents + * to make it more easy to match. This will resolve a few browser issues. + * + * @private + * @param {String} value Value to get style from. + * @param {String} name Style name to get. + * @return {String} Style item value. + */ + function normalizeStyleValue(value, name) { + // Force the format to hex + if (name == 'color' || name == 'backgroundColor') { + value = dom.toHex(value); + } + + // Opera will return bold as 700 + if (name == 'fontWeight' && value == 700) { + value = 'bold'; + } + + // Normalize fontFamily so "'Font name', Font" becomes: "Font name,Font" + if (name == 'fontFamily') { + value = value.replace(/[\'\"]/g, '').replace(/,\s+/g, ','); + } + + return '' + value; + } + + /** + * Replaces variables in the value. The variable format is %var. + * + * @private + * @param {String} value Value to replace variables in. + * @param {Object} vars Name/value array with variables to replace. + * @return {String} New value with replaced variables. + */ + function replaceVars(value, vars) { + if (typeof value != "string") { + value = value(vars); + } else if (vars) { + value = value.replace(/%(\w+)/g, function(str, name) { + return vars[name] || str; + }); + } + + return value; + } + + function isWhiteSpaceNode(node) { + return node && node.nodeType === 3 && /^([\t \r\n]+|)$/.test(node.nodeValue); + } + + function wrap(node, name, attrs) { + var wrapper = dom.create(name, attrs); + + node.parentNode.insertBefore(wrapper, node); + wrapper.appendChild(node); + + return wrapper; + } + + /** + * Expands the specified range like object to depending on format. + * + * For example on block formats it will move the start/end position + * to the beginning of the current block. + * + * @private + * @param {Object} rng Range like object. + * @param {Array} format Array with formats to expand by. + * @param {Boolean} remove + * @return {Object} Expanded range like object. + */ + function expandRng(rng, format, remove) { + var lastIdx, leaf, endPoint, + startContainer = rng.startContainer, + startOffset = rng.startOffset, + endContainer = rng.endContainer, + endOffset = rng.endOffset; + + // This function walks up the tree if there is no siblings before/after the node + function findParentContainer(start) { + var container, parent, sibling, siblingName, root; + + container = parent = start ? startContainer : endContainer; + siblingName = start ? 'previousSibling' : 'nextSibling'; + root = dom.getRoot(); + + function isBogusBr(node) { + return node.nodeName == "BR" && node.getAttribute('data-mce-bogus') && !node.nextSibling; + } + + // If it's a text node and the offset is inside the text + if (container.nodeType == 3 && !isWhiteSpaceNode(container)) { + if (start ? startOffset > 0 : endOffset < container.nodeValue.length) { + return container; + } + } + + /*eslint no-constant-condition:0 */ + while (true) { + // Stop expanding on block elements + if (!format[0].block_expand && isBlock(parent)) { + return parent; + } + + // Walk left/right + for (sibling = parent[siblingName]; sibling; sibling = sibling[siblingName]) { + if (!isBookmarkNode(sibling) && !isWhiteSpaceNode(sibling) && !isBogusBr(sibling)) { + return parent; + } + } + + // Check if we can move up are we at root level or body level + if (parent == root || parent.parentNode == root) { + container = parent; + break; + } + + parent = parent.parentNode; + } + + return container; + } + + // This function walks down the tree to find the leaf at the selection. + // The offset is also returned as if node initially a leaf, the offset may be in the middle of the text node. + function findLeaf(node, offset) { + if (offset === undef) { + offset = node.nodeType === 3 ? node.length : node.childNodes.length; + } + + while (node && node.hasChildNodes()) { + node = node.childNodes[offset]; + if (node) { + offset = node.nodeType === 3 ? node.length : node.childNodes.length; + } + } + return {node: node, offset: offset}; + } + + // If index based start position then resolve it + if (startContainer.nodeType == 1 && startContainer.hasChildNodes()) { + lastIdx = startContainer.childNodes.length - 1; + startContainer = startContainer.childNodes[startOffset > lastIdx ? lastIdx : startOffset]; + + if (startContainer.nodeType == 3) { + startOffset = 0; + } + } + + // If index based end position then resolve it + if (endContainer.nodeType == 1 && endContainer.hasChildNodes()) { + lastIdx = endContainer.childNodes.length - 1; + endContainer = endContainer.childNodes[endOffset > lastIdx ? lastIdx : endOffset - 1]; + + if (endContainer.nodeType == 3) { + endOffset = endContainer.nodeValue.length; + } + } + + // Expands the node to the closes contentEditable false element if it exists + function findParentContentEditable(node) { + var parent = node; + + while (parent) { + if (parent.nodeType === 1 && getContentEditable(parent)) { + return getContentEditable(parent) === "false" ? parent : node; + } + + parent = parent.parentNode; + } + + return node; + } + + function findWordEndPoint(container, offset, start) { + var walker, node, pos, lastTextNode; + + function findSpace(node, offset) { + var pos, pos2, str = node.nodeValue; + + if (typeof offset == "undefined") { + offset = start ? str.length : 0; + } + + if (start) { + pos = str.lastIndexOf(' ', offset); + pos2 = str.lastIndexOf('\u00a0', offset); + pos = pos > pos2 ? pos : pos2; + + // Include the space on remove to avoid tag soup + if (pos !== -1 && !remove) { + pos++; + } + } else { + pos = str.indexOf(' ', offset); + pos2 = str.indexOf('\u00a0', offset); + pos = pos !== -1 && (pos2 === -1 || pos < pos2) ? pos : pos2; + } + + return pos; + } + + if (container.nodeType === 3) { + pos = findSpace(container, offset); + + if (pos !== -1) { + return {container: container, offset: pos}; + } + + lastTextNode = container; + } + + // Walk the nodes inside the block + walker = new TreeWalker(container, dom.getParent(container, isBlock) || ed.getBody()); + while ((node = walker[start ? 'prev' : 'next']())) { + if (node.nodeType === 3) { + lastTextNode = node; + pos = findSpace(node); + + if (pos !== -1) { + return {container: node, offset: pos}; + } + } else if (isBlock(node)) { + break; + } + } + + if (lastTextNode) { + if (start) { + offset = 0; + } else { + offset = lastTextNode.length; + } + + return {container: lastTextNode, offset: offset}; + } + } + + function findSelectorEndPoint(container, sibling_name) { + var parents, i, y, curFormat; + + if (container.nodeType == 3 && container.nodeValue.length === 0 && container[sibling_name]) { + container = container[sibling_name]; + } + + parents = getParents(container); + for (i = 0; i < parents.length; i++) { + for (y = 0; y < format.length; y++) { + curFormat = format[y]; + + // If collapsed state is set then skip formats that doesn't match that + if ("collapsed" in curFormat && curFormat.collapsed !== rng.collapsed) { + continue; + } + + if (dom.is(parents[i], curFormat.selector)) { + return parents[i]; + } + } + } + + return container; + } + + function findBlockEndPoint(container, sibling_name) { + var node, root = dom.getRoot(); + + // Expand to block of similar type + if (!format[0].wrapper) { + node = dom.getParent(container, format[0].block, root); + } + + // Expand to first wrappable block element or any block element + if (!node) { + node = dom.getParent(container.nodeType == 3 ? container.parentNode : container, function(node) { + // Fixes #6183 where it would expand to editable parent element in inline mode + return node != root && isTextBlock(node); + }); + } + + // Exclude inner lists from wrapping + if (node && format[0].wrapper) { + node = getParents(node, 'ul,ol').reverse()[0] || node; + } + + // Didn't find a block element look for first/last wrappable element + if (!node) { + node = container; + + while (node[sibling_name] && !isBlock(node[sibling_name])) { + node = node[sibling_name]; + + // Break on BR but include it will be removed later on + // we can't remove it now since we need to check if it can be wrapped + if (isEq(node, 'br')) { + break; + } + } + } + + return node || container; + } + + // Expand to closest contentEditable element + startContainer = findParentContentEditable(startContainer); + endContainer = findParentContentEditable(endContainer); + + // Exclude bookmark nodes if possible + if (isBookmarkNode(startContainer.parentNode) || isBookmarkNode(startContainer)) { + startContainer = isBookmarkNode(startContainer) ? startContainer : startContainer.parentNode; + startContainer = startContainer.nextSibling || startContainer; + + if (startContainer.nodeType == 3) { + startOffset = 0; + } + } + + if (isBookmarkNode(endContainer.parentNode) || isBookmarkNode(endContainer)) { + endContainer = isBookmarkNode(endContainer) ? endContainer : endContainer.parentNode; + endContainer = endContainer.previousSibling || endContainer; + + if (endContainer.nodeType == 3) { + endOffset = endContainer.length; + } + } + + if (format[0].inline) { + if (rng.collapsed) { + // Expand left to closest word boundary + endPoint = findWordEndPoint(startContainer, startOffset, true); + if (endPoint) { + startContainer = endPoint.container; + startOffset = endPoint.offset; + } + + // Expand right to closest word boundary + endPoint = findWordEndPoint(endContainer, endOffset); + if (endPoint) { + endContainer = endPoint.container; + endOffset = endPoint.offset; + } + } + + // Avoid applying formatting to a trailing space. + leaf = findLeaf(endContainer, endOffset); + if (leaf.node) { + while (leaf.node && leaf.offset === 0 && leaf.node.previousSibling) { + leaf = findLeaf(leaf.node.previousSibling); + } + + if (leaf.node && leaf.offset > 0 && leaf.node.nodeType === 3 && + leaf.node.nodeValue.charAt(leaf.offset - 1) === ' ') { + + if (leaf.offset > 1) { + endContainer = leaf.node; + endContainer.splitText(leaf.offset - 1); + } + } + } + } + + // Move start/end point up the tree if the leaves are sharp and if we are in different containers + // Example * becomes !: !*texttext*
! + // This will reduce the number of wrapper elements that needs to be created + // Move start point up the tree + if (format[0].inline || format[0].block_expand) { + if (!format[0].inline || (startContainer.nodeType != 3 || startOffset === 0)) { + startContainer = findParentContainer(true); + } + + if (!format[0].inline || (endContainer.nodeType != 3 || endOffset === endContainer.nodeValue.length)) { + endContainer = findParentContainer(); + } + } + + // Expand start/end container to matching selector + if (format[0].selector && format[0].expand !== FALSE && !format[0].inline) { + // Find new startContainer/endContainer if there is better one + startContainer = findSelectorEndPoint(startContainer, 'previousSibling'); + endContainer = findSelectorEndPoint(endContainer, 'nextSibling'); + } + + // Expand start/end container to matching block element or text node + if (format[0].block || format[0].selector) { + // Find new startContainer/endContainer if there is better one + startContainer = findBlockEndPoint(startContainer, 'previousSibling'); + endContainer = findBlockEndPoint(endContainer, 'nextSibling'); + + // Non block element then try to expand up the leaf + if (format[0].block) { + if (!isBlock(startContainer)) { + startContainer = findParentContainer(true); + } + + if (!isBlock(endContainer)) { + endContainer = findParentContainer(); + } + } + } + + // Setup index for startContainer + if (startContainer.nodeType == 1) { + startOffset = nodeIndex(startContainer); + startContainer = startContainer.parentNode; + } + + // Setup index for endContainer + if (endContainer.nodeType == 1) { + endOffset = nodeIndex(endContainer) + 1; + endContainer = endContainer.parentNode; + } + + // Return new range like object + return { + startContainer: startContainer, + startOffset: startOffset, + endContainer: endContainer, + endOffset: endOffset + }; + } + + function isColorFormatAndAnchor(node, format) { + return format.links && node.tagName == 'A'; + } + + /** + * Removes the specified format for the specified node. It will also remove the node if it doesn't have + * any attributes if the format specifies it to do so. + * + * @private + * @param {Object} format Format object with items to remove from node. + * @param {Object} vars Name/value object with variables to apply to format. + * @param {Node} node Node to remove the format styles on. + * @param {Node} compare_node Optional compare node, if specified the styles will be compared to that node. + * @return {Boolean} True/false if the node was removed or not. + */ + function removeFormat(format, vars, node, compare_node) { + var i, attrs, stylesModified; + + // Check if node matches format + if (!matchName(node, format) && !isColorFormatAndAnchor(node, format)) { + return FALSE; + } + + // Should we compare with format attribs and styles + if (format.remove != 'all') { + // Remove styles + each(format.styles, function(value, name) { + value = normalizeStyleValue(replaceVars(value, vars), name); + + // Indexed array + if (typeof name === 'number') { + name = value; + compare_node = 0; + } + + if (format.remove_similar || (!compare_node || isEq(getStyle(compare_node, name), value))) { + dom.setStyle(node, name, ''); + } + + stylesModified = 1; + }); + + // Remove style attribute if it's empty + if (stylesModified && dom.getAttrib(node, 'style') === '') { + node.removeAttribute('style'); + node.removeAttribute('data-mce-style'); + } + + // Remove attributes + each(format.attributes, function(value, name) { + var valueOut; + + value = replaceVars(value, vars); + + // Indexed array + if (typeof name === 'number') { + name = value; + compare_node = 0; + } + + if (!compare_node || isEq(dom.getAttrib(compare_node, name), value)) { + // Keep internal classes + if (name == 'class') { + value = dom.getAttrib(node, name); + if (value) { + // Build new class value where everything is removed except the internal prefixed classes + valueOut = ''; + each(value.split(/\s+/), function(cls) { + if (/mce\-\w+/.test(cls)) { + valueOut += (valueOut ? ' ' : '') + cls; + } + }); + + // We got some internal classes left + if (valueOut) { + dom.setAttrib(node, name, valueOut); + return; + } + } + } + + // IE6 has a bug where the attribute doesn't get removed correctly + if (name == "class") { + node.removeAttribute('className'); + } + + // Remove mce prefixed attributes + if (MCE_ATTR_RE.test(name)) { + node.removeAttribute('data-mce-' + name); + } + + node.removeAttribute(name); + } + }); + + // Remove classes + each(format.classes, function(value) { + value = replaceVars(value, vars); + + if (!compare_node || dom.hasClass(compare_node, value)) { + dom.removeClass(node, value); + } + }); + + // Check for non internal attributes + attrs = dom.getAttribs(node); + for (i = 0; i < attrs.length; i++) { + var attrName = attrs[i].nodeName; + if (attrName.indexOf('_') !== 0 && attrName.indexOf('data-') !== 0) { + return FALSE; + } + } + } + + // Remove the inline child if it's empty for example or + if (format.remove != 'none') { + removeNode(node, format); + return TRUE; + } + } + + /** + * Removes the node and wrap it's children in paragraphs before doing so or + * appends BR elements to the beginning/end of the block element if forcedRootBlocks is disabled. + * + * If the div in the node below gets removed: + * text|
+ formatNode.parentNode.replaceChild(caretContainer, formatNode); + } else { + // Insert caret container after the formatted node + dom.insertAfter(caretContainer, formatNode); + } + + // Move selection to text node + selection.setCursorLocation(node, 1); + + // If the formatNode is empty, we can remove it safely. + if (dom.isEmpty(formatNode)) { + dom.remove(formatNode); + } + } + } + + // Checks if the parent caret container node isn't empty if that is the case it + // will remove the bogus state on all children that isn't empty + function unmarkBogusCaretParents() { + var caretContainer; + + caretContainer = getParentCaretContainer(selection.getStart()); + if (caretContainer && !dom.isEmpty(caretContainer)) { + walk(caretContainer, function(node) { + if (node.nodeType == 1 && node.id !== caretContainerId && !dom.isEmpty(node)) { + dom.setAttrib(node, 'data-mce-bogus', null); + } + }, 'childNodes'); + } + } + + // Only bind the caret events once + if (!ed._hasCaretEvents) { + // Mark current caret container elements as bogus when getting the contents so we don't end up with empty elements + markCaretContainersBogus = function() { + var nodes = [], i; + + if (isCaretContainerEmpty(getParentCaretContainer(selection.getStart()), nodes)) { + // Mark children + i = nodes.length; + while (i--) { + dom.setAttrib(nodes[i], 'data-mce-bogus', '1'); + } + } + }; + + disableCaretContainer = function(e) { + var keyCode = e.keyCode; + + removeCaretContainer(); + + // Remove caret container if it's empty + if (keyCode == 8 && selection.isCollapsed() && selection.getStart().innerHTML == INVISIBLE_CHAR) { + removeCaretContainer(getParentCaretContainer(selection.getStart())); + } + + // Remove caret container on keydown and it's left/right arrow keys + if (keyCode == 37 || keyCode == 39) { + removeCaretContainer(getParentCaretContainer(selection.getStart())); + } + + unmarkBogusCaretParents(); + }; + + // Remove bogus state if they got filled by contents using editor.selection.setContent + ed.on('SetContent', function(e) { + if (e.selection) { + unmarkBogusCaretParents(); + } + }); + ed._hasCaretEvents = true; + } + + // Do apply or remove caret format + if (type == "apply") { + applyCaretFormat(); + } else { + removeCaretFormat(); + } + } + + /** + * Moves the start to the first suitable text node. + */ + function moveStart(rng) { + var container = rng.startContainer, + offset = rng.startOffset, isAtEndOfText, + walker, node, nodes, tmpNode; + + if (rng.startContainer == rng.endContainer) { + if (isInlineBlock(rng.startContainer.childNodes[rng.startOffset])) { + return; + } + } + + // Convert text node into index if possible + if (container.nodeType == 3 && offset >= container.nodeValue.length) { + // Get the parent container location and walk from there + offset = nodeIndex(container); + container = container.parentNode; + isAtEndOfText = true; + } + + // Move startContainer/startOffset in to a suitable node + if (container.nodeType == 1) { + nodes = container.childNodes; + container = nodes[Math.min(offset, nodes.length - 1)]; + walker = new TreeWalker(container, dom.getParent(container, dom.isBlock)); + + // If offset is at end of the parent node walk to the next one + if (offset > nodes.length - 1 || isAtEndOfText) { + walker.next(); + } + + for (node = walker.current(); node; node = walker.next()) { + if (node.nodeType == 3 && !isWhiteSpaceNode(node)) { + // IE has a "neat" feature where it moves the start node into the closest element + // we can avoid this by inserting an element before it and then remove it after we set the selection + tmpNode = dom.create('a', {'data-mce-bogus': 'all'}, INVISIBLE_CHAR); + node.parentNode.insertBefore(tmpNode, node); + + // Set selection and remove tmpNode + rng.setStart(node, 0); + selection.setRng(rng); + dom.remove(tmpNode); + + return; + } + } + } + } + }; +}); + +// Included from: js/tinymce/classes/undo/Diff.js + +/** + * Diff.js + * + * Released under LGPL License. + * Copyright (c) 1999-2016 Ephox Corp. All rights reserved + * + * License: http://www.tinymce.com/license + * Contributing: http://www.tinymce.com/contributing + */ + +/** + * JS Implementation of the O(ND) Difference Algorithm by Eugene W. Myers. + * + * @class tinymce.undo.Diff + * @private + */ +define("tinymce/undo/Diff", [ +], function () { + var KEEP = 0, INSERT = 1, DELETE = 2; + + var diff = function (left, right) { + var size = left.length + right.length + 2; + var vDown = new Array(size); + var vUp = new Array(size); + + var snake = function (start, end, diag) { + return { + start: start, + end: end, + diag: diag + }; + }; + + var buildScript = function (start1, end1, start2, end2, script) { + var middle = getMiddleSnake(start1, end1, start2, end2); + + if (middle === null || middle.start === end1 && middle.diag === end1 - end2 || + middle.end === start1 && middle.diag === start1 - start2) { + var i = start1; + var j = start2; + while (i < end1 || j < end2) { + if (i < end1 && j < end2 && left[i] === right[j]) { + script.push([KEEP, left[i]]); + ++i; + ++j; + } else { + if (end1 - start1 > end2 - start2) { + script.push([DELETE, left[i]]); + ++i; + } else { + script.push([INSERT, right[j]]); + ++j; + } + } + } + } else { + buildScript(start1, middle.start, start2, middle.start - middle.diag, script); + for (var i2 = middle.start; i2 < middle.end; ++i2) { + script.push([KEEP, left[i2]]); + } + buildScript(middle.end, end1, middle.end - middle.diag, end2, script); + } + }; + + var buildSnake = function (start, diag, end1, end2) { + var end = start; + while (end - diag < end2 && end < end1 && left[end] === right[end - diag]) { + ++end; + } + return snake(start, end, diag); + }; + + var getMiddleSnake = function (start1, end1, start2, end2) { + // Myers Algorithm + // Initialisations + var m = end1 - start1; + var n = end2 - start2; + if (m === 0 || n === 0) { + return null; + } + + var delta = m - n; + var sum = n + m; + var offset = (sum % 2 === 0 ? sum : sum + 1) / 2; + vDown[1 + offset] = start1; + vUp[1 + offset] = end1 + 1; + + for (var d = 0; d <= offset; ++d) { + // Down + for (var k = -d; k <= d; k += 2) { + // First step + + var i = k + offset; + if (k === -d || k != d && vDown[i - 1] < vDown[i + 1]) { + vDown[i] = vDown[i + 1]; + } else { + vDown[i] = vDown[i - 1] + 1; + } + + var x = vDown[i]; + var y = x - start1 + start2 - k; + + while (x < end1 && y < end2 && left[x] === right[y]) { + vDown[i] = ++x; + ++y; + } + // Second step + if (delta % 2 != 0 && delta - d <= k && k <= delta + d) { + if (vUp[i - delta] <= vDown[i]) { + return buildSnake(vUp[i - delta], k + start1 - start2, end1, end2); + } + } + } + + // Up + for (k = delta - d; k <= delta + d; k += 2) { + // First step + i = k + offset - delta; + if (k === delta - d || k != delta + d && vUp[i + 1] <= vUp[i - 1]) { + vUp[i] = vUp[i + 1] - 1; + } else { + vUp[i] = vUp[i - 1]; + } + + x = vUp[i] - 1; + y = x - start1 + start2 - k; + while (x >= start1 && y >= start2 && left[x] === right[y]) { + vUp[i] = x--; + y--; + } + // Second step + if (delta % 2 === 0 && -d <= k && k <= d) { + if (vUp[i] <= vDown[i + delta]) { + return buildSnake(vUp[i], k + start1 - start2, end1, end2); + } + } + } + } + }; + + var script = []; + buildScript(0, left.length, 0, right.length, script); + return script; + }; + + return { + KEEP: KEEP, + DELETE: DELETE, + INSERT: INSERT, + diff: diff + }; +}); + +// Included from: js/tinymce/classes/undo/Fragments.js + +/** + * Fragments.js + * + * Released under LGPL License. + * Copyright (c) 1999-2016 Ephox Corp. All rights reserved + * + * License: http://www.tinymce.com/license + * Contributing: http://www.tinymce.com/contributing + */ + +/** + * This module reads and applies html fragments from/to dom nodes. + * + * @class tinymce.undo.Fragments + * @private + */ +define("tinymce/undo/Fragments", [ + "tinymce/util/Arr", + "tinymce/html/Entities", + "tinymce/undo/Diff" +], function (Arr, Entities, Diff) { + var getOuterHtml = function (elm) { + if (elm.nodeType === 1) { + return elm.outerHTML; + } else if (elm.nodeType === 3) { + return Entities.encodeRaw(elm.data, false); + } else if (elm.nodeType === 8) { + return ''; + } + + return ''; + }; + + var createFragment = function(html) { + var frag, node, container; + + container = document.createElement("div"); + frag = document.createDocumentFragment(); + + if (html) { + container.innerHTML = html; + } + + while ((node = container.firstChild)) { + frag.appendChild(node); + } + + return frag; + }; + + var insertAt = function (elm, html, index) { + var fragment = createFragment(html); + if (elm.hasChildNodes() && index < elm.childNodes.length) { + var target = elm.childNodes[index]; + target.parentNode.insertBefore(fragment, target); + } else { + elm.appendChild(fragment); + } + }; + + var removeAt = function (elm, index) { + if (elm.hasChildNodes() && index < elm.childNodes.length) { + var target = elm.childNodes[index]; + target.parentNode.removeChild(target); + } + }; + + var applyDiff = function (diff, elm) { + var index = 0; + Arr.each(diff, function (action) { + if (action[0] === Diff.KEEP) { + index++; + } else if (action[0] === Diff.INSERT) { + insertAt(elm, action[1], index); + index++; + } else if (action[0] === Diff.DELETE) { + removeAt(elm, index); + } + }); + }; + + var read = function (elm) { + return Arr.map(elm.childNodes, getOuterHtml); + }; + + var write = function (fragments, elm) { + var currentFragments = Arr.map(elm.childNodes, getOuterHtml); + applyDiff(Diff.diff(currentFragments, fragments), elm); + return elm; + }; + + return { + read: read, + write: write + }; +}); + +// Included from: js/tinymce/classes/undo/Levels.js + +/** + * Levels.js + * + * Released under LGPL License. + * Copyright (c) 1999-2016 Ephox Corp. All rights reserved + * + * License: http://www.tinymce.com/license + * Contributing: http://www.tinymce.com/contributing + */ + +/** + * This module handles getting/setting undo levels to/from editor instances. + * + * @class tinymce.undo.Levels + * @private + */ +define("tinymce/undo/Levels", [ + "tinymce/util/Arr", + "tinymce/undo/Fragments" +], function (Arr, Fragments) { + var hasIframes = function (html) { + return html.indexOf('') !== -1; + }; + + var createFragmentedLevel = function (fragments) { + return { + type: 'fragmented', + fragments: fragments, + content: '', + bookmark: null, + beforeBookmark: null + }; + }; + + var createCompleteLevel = function (content) { + return { + type: 'complete', + fragments: null, + content: content, + bookmark: null, + beforeBookmark: null + }; + }; + + var createFromEditor = function (editor) { + var fragments, content; + + fragments = Fragments.read(editor.getBody()); + content = Arr.map(fragments, function (html) { + return editor.serializer.trimContent(html); + }).join(''); + + return hasIframes(content) ? createFragmentedLevel(fragments) : createCompleteLevel(content); + }; + + var applyToEditor = function (editor, level, before) { + if (level.type === 'fragmented') { + Fragments.write(level.fragments, editor.getBody()); + } else { + editor.setContent(level.content, {format: 'raw'}); + } + + editor.selection.moveToBookmark(before ? level.beforeBookmark : level.bookmark); + }; + + var getLevelContent = function (level) { + return level.type === 'fragmented' ? level.fragments.join('') : level.content; + }; + + var isEq = function (level1, level2) { + return getLevelContent(level1) === getLevelContent(level2); + }; + + return { + createFragmentedLevel: createFragmentedLevel, + createCompleteLevel: createCompleteLevel, + createFromEditor: createFromEditor, + applyToEditor: applyToEditor, + isEq: isEq + }; +}); + +// Included from: js/tinymce/classes/UndoManager.js + +/** + * UndoManager.js + * + * Released under LGPL License. + * Copyright (c) 1999-2015 Ephox Corp. All rights reserved + * + * License: http://www.tinymce.com/license + * Contributing: http://www.tinymce.com/contributing + */ + +/** + * This class handles the undo/redo history levels for the editor. Since the built-in undo/redo has major drawbacks a custom one was needed. + * + * @class tinymce.UndoManager + */ +define("tinymce/UndoManager", [ + "tinymce/util/VK", + "tinymce/util/Tools", + "tinymce/undo/Levels", + "tinymce/Env" +], function(VK, Tools, Levels, Env) { + return function(editor) { + var self = this, index = 0, data = [], beforeBookmark, isFirstTypedCharacter, locks = 0; + + function setDirty(state) { + editor.setDirty(state); + } + + function addNonTypingUndoLevel(e) { + self.typing = false; + self.add({}, e); + } + + function endTyping() { + if (self.typing) { + self.typing = false; + self.add(); + } + } + + // Add initial undo level when the editor is initialized + editor.on('init', function() { + self.add(); + }); + + // Get position before an execCommand is processed + editor.on('BeforeExecCommand', function(e) { + var cmd = e.command; + + if (cmd !== 'Undo' && cmd !== 'Redo' && cmd !== 'mceRepaint') { + endTyping(); + self.beforeChange(); + } + }); + + // Add undo level after an execCommand call was made + editor.on('ExecCommand', function(e) { + var cmd = e.command; + + if (cmd !== 'Undo' && cmd !== 'Redo' && cmd !== 'mceRepaint') { + addNonTypingUndoLevel(e); + } + }); + + editor.on('ObjectResizeStart Cut', function() { + self.beforeChange(); + }); + + editor.on('SaveContent ObjectResized blur', addNonTypingUndoLevel); + editor.on('DragEnd', addNonTypingUndoLevel); + + editor.on('KeyUp', function(e) { + var keyCode = e.keyCode; + + // If key is prevented then don't add undo level + // This would happen on keyboard shortcuts for example + if (e.isDefaultPrevented()) { + return; + } + + if ((keyCode >= 33 && keyCode <= 36) || (keyCode >= 37 && keyCode <= 40) || keyCode === 45 || e.ctrlKey) { + addNonTypingUndoLevel(); + editor.nodeChanged(); + } + + if (keyCode === 46 || keyCode === 8 || (Env.mac && (keyCode === 91 || keyCode === 93))) { + editor.nodeChanged(); + } + + // Fire a TypingUndo event on the first character entered + if (isFirstTypedCharacter && self.typing) { + // Make it dirty if the content was changed after typing the first character + if (!editor.isDirty()) { + setDirty(data[0] && !Levels.isEq(Levels.createFromEditor(editor), data[0])); + + // Fire initial change event + if (editor.isDirty()) { + editor.fire('change', {level: data[0], lastLevel: null}); + } + } + + editor.fire('TypingUndo'); + isFirstTypedCharacter = false; + editor.nodeChanged(); + } + }); + + editor.on('KeyDown', function(e) { + var keyCode = e.keyCode; + + // If key is prevented then don't add undo level + // This would happen on keyboard shortcuts for example + if (e.isDefaultPrevented()) { + return; + } + + // Is character position keys left,right,up,down,home,end,pgdown,pgup,enter + if ((keyCode >= 33 && keyCode <= 36) || (keyCode >= 37 && keyCode <= 40) || keyCode === 45) { + if (self.typing) { + addNonTypingUndoLevel(e); + } + + return; + } + + // If key isn't Ctrl+Alt/AltGr + var modKey = (e.ctrlKey && !e.altKey) || e.metaKey; + if ((keyCode < 16 || keyCode > 20) && keyCode !== 224 && keyCode !== 91 && !self.typing && !modKey) { + self.beforeChange(); + self.typing = true; + self.add({}, e); + isFirstTypedCharacter = true; + } + }); + + editor.on('MouseDown', function(e) { + if (self.typing) { + addNonTypingUndoLevel(e); + } + }); + + // Add keyboard shortcuts for undo/redo keys + editor.addShortcut('meta+z', '', 'Undo'); + editor.addShortcut('meta+y,meta+shift+z', '', 'Redo'); + + editor.on('AddUndo Undo Redo ClearUndos', function(e) { + if (!e.isDefaultPrevented()) { + editor.nodeChanged(); + } + }); + + /*eslint consistent-this:0 */ + self = { + // Explode for debugging reasons + data: data, + + /** + * State if the user is currently typing or not. This will add a typing operation into one undo + * level instead of one new level for each keystroke. + * + * @field {Boolean} typing + */ + typing: false, + + /** + * Stores away a bookmark to be used when performing an undo action so that the selection is before + * the change has been made. + * + * @method beforeChange + */ + beforeChange: function() { + if (!locks) { + beforeBookmark = editor.selection.getBookmark(2, true); + } + }, + + /** + * Adds a new undo level/snapshot to the undo list. + * + * @method add + * @param {Object} level Optional undo level object to add. + * @param {DOMEvent} event Optional event responsible for the creation of the undo level. + * @return {Object} Undo level that got added or null it a level wasn't needed. + */ + add: function(level, event) { + var i, settings = editor.settings, lastLevel, currentLevel; + + currentLevel = Levels.createFromEditor(editor); + level = level || {}; + level = Tools.extend(level, currentLevel); + + if (locks || editor.removed) { + return null; + } + + lastLevel = data[index]; + if (editor.fire('BeforeAddUndo', {level: level, lastLevel: lastLevel, originalEvent: event}).isDefaultPrevented()) { + return null; + } + + // Add undo level if needed + if (lastLevel && Levels.isEq(lastLevel, level)) { + return null; + } + + // Set before bookmark on previous level + if (data[index]) { + data[index].beforeBookmark = beforeBookmark; + } + + // Time to compress + if (settings.custom_undo_redo_levels) { + if (data.length > settings.custom_undo_redo_levels) { + for (i = 0; i < data.length - 1; i++) { + data[i] = data[i + 1]; + } + + data.length--; + index = data.length; + } + } + + // Get a non intrusive normalized bookmark + level.bookmark = editor.selection.getBookmark(2, true); + + // Crop array if needed + if (index < data.length - 1) { + data.length = index + 1; + } + + data.push(level); + index = data.length - 1; + + var args = {level: level, lastLevel: lastLevel, originalEvent: event}; + + editor.fire('AddUndo', args); + + if (index > 0) { + setDirty(true); + editor.fire('change', args); + } + + return level; + }, + + /** + * Undoes the last action. + * + * @method undo + * @return {Object} Undo level or null if no undo was performed. + */ + undo: function() { + var level; + + if (self.typing) { + self.add(); + self.typing = false; + } + + if (index > 0) { + level = data[--index]; + Levels.applyToEditor(editor, level, true); + setDirty(true); + editor.fire('undo', {level: level}); + } + + return level; + }, + + /** + * Redoes the last action. + * + * @method redo + * @return {Object} Redo level or null if no redo was performed. + */ + redo: function() { + var level; + + if (index < data.length - 1) { + level = data[++index]; + Levels.applyToEditor(editor, level, false); + setDirty(true); + editor.fire('redo', {level: level}); + } + + return level; + }, + + /** + * Removes all undo levels. + * + * @method clear + */ + clear: function() { + data = []; + index = 0; + self.typing = false; + self.data = data; + editor.fire('ClearUndos'); + }, + + /** + * Returns true/false if the undo manager has any undo levels. + * + * @method hasUndo + * @return {Boolean} true/false if the undo manager has any undo levels. + */ + hasUndo: function() { + // Has undo levels or typing and content isn't the same as the initial level + return index > 0 || (self.typing && data[0] && !Levels.isEq(Levels.createFromEditor(editor), data[0])); + }, + + /** + * Returns true/false if the undo manager has any redo levels. + * + * @method hasRedo + * @return {Boolean} true/false if the undo manager has any redo levels. + */ + hasRedo: function() { + return index < data.length - 1 && !self.typing; + }, + + /** + * Executes the specified mutator function as an undo transaction. The selection + * before the modification will be stored to the undo stack and if the DOM changes + * it will add a new undo level. Any methods within the translation that adds undo levels will + * be ignored. So a translation can include calls to execCommand or editor.insertContent. + * + * @method transact + * @param {function} callback Function that gets executed and has dom manipulation logic in it. + * @return {Object} Undo level that got added or null it a level wasn't needed. + */ + transact: function(callback) { + endTyping(); + self.beforeChange(); + + try { + locks++; + callback(); + } finally { + locks--; + } + + return self.add(); + }, + + /** + * Adds an extra "hidden" undo level by first applying the first mutation and store that to the undo stack + * then roll back that change and do the second mutation on top of the stack. This will produce an extra + * undo level that the user doesn't see until they undo. + * + * @method extra + * @param {function} callback1 Function that does mutation but gets stored as a "hidden" extra undo level. + * @param {function} callback2 Function that does mutation but gets displayed to the user. + */ + extra: function (callback1, callback2) { + var lastLevel, bookmark; + + if (self.transact(callback1)) { + bookmark = data[index].bookmark; + lastLevel = data[index - 1]; + Levels.applyToEditor(editor, lastLevel, true); + + if (self.transact(callback2)) { + data[index - 1].beforeBookmark = bookmark; + } + } + } + }; + + return self; + }; +}); + +// Included from: js/tinymce/classes/EnterKey.js + +/** + * EnterKey.js + * + * Released under LGPL License. + * Copyright (c) 1999-2015 Ephox Corp. All rights reserved + * + * License: http://www.tinymce.com/license + * Contributing: http://www.tinymce.com/contributing + */ + +/** + * Contains logic for handling the enter key to split/generate block elements. + * + * @private + * @class tinymce.EnterKey + */ +define("tinymce/EnterKey", [ + "tinymce/dom/TreeWalker", + "tinymce/dom/RangeUtils", + "tinymce/caret/CaretContainer", + "tinymce/Env" +], function(TreeWalker, RangeUtils, CaretContainer, Env) { + var isIE = Env.ie && Env.ie < 11; + + return function(editor) { + var dom = editor.dom, selection = editor.selection, settings = editor.settings; + var undoManager = editor.undoManager, schema = editor.schema, nonEmptyElementsMap = schema.getNonEmptyElements(), + moveCaretBeforeOnEnterElementsMap = schema.getMoveCaretBeforeOnEnterElements(); + + function handleEnterKey(evt) { + var rng, tmpRng, editableRoot, container, offset, parentBlock, documentMode, shiftKey, + newBlock, fragment, containerBlock, parentBlockName, containerBlockName, newBlockName, isAfterLastNodeInContainer; + + // Returns true if the block can be split into two blocks or not + function canSplitBlock(node) { + return node && + dom.isBlock(node) && + !/^(TD|TH|CAPTION|FORM)$/.test(node.nodeName) && + !/^(fixed|absolute)/i.test(node.style.position) && + dom.getContentEditable(node) !== "true"; + } + + function isTableCell(node) { + return node && /^(TD|TH|CAPTION)$/.test(node.nodeName); + } + + // Renders empty block on IE + function renderBlockOnIE(block) { + var oldRng; + + if (dom.isBlock(block)) { + oldRng = selection.getRng(); + block.appendChild(dom.create('span', null, '\u00a0')); + selection.select(block); + block.lastChild.outerHTML = ''; + selection.setRng(oldRng); + } + } + + // Remove the first empty inline element of the block so this:x
becomes this:x
+ function trimInlineElementsOnLeftSideOfBlock(block) { + var node = block, firstChilds = [], i; + + if (!node) { + return; + } + + // Find inner most first child ex:*
+ while ((node = node.firstChild)) { + if (dom.isBlock(node)) { + return; + } + + if (node.nodeType == 1 && !nonEmptyElementsMap[node.nodeName.toLowerCase()]) { + firstChilds.push(node); + } + } + + i = firstChilds.length; + while (i--) { + node = firstChilds[i]; + if (!node.hasChildNodes() || (node.firstChild == node.lastChild && node.firstChild.nodeValue === '')) { + dom.remove(node); + } else { + // Remove see #5381 + if (node.nodeName == "A" && (node.innerText || node.textContent) === ' ') { + dom.remove(node); + } + } + } + } + + // Moves the caret to a suitable position within the root for example in the first non + // pure whitespace text node or before an image + function moveToCaretPosition(root) { + var walker, node, rng, lastNode = root, tempElm; + function firstNonWhiteSpaceNodeSibling(node) { + while (node) { + if (node.nodeType == 1 || (node.nodeType == 3 && node.data && /[\r\n\s]/.test(node.data))) { + return node; + } + + node = node.nextSibling; + } + } + + if (!root) { + return; + } + + // Old IE versions doesn't properly render blocks with br elements in them + // For exampletext|
text|text2
a |
+ if (dom.isEmpty(newBlock)) { + dom.remove(newBlock); + insertNewBlockAfter(); + } else { + moveToCaretPosition(newBlock); + } + } + + dom.setAttrib(newBlock, 'id', ''); // Remove ID since it needs to be document unique + + // Allow custom handling of new blocks + editor.fire('NewBlock', {newBlock: newBlock}); + + undoManager.typing = false; + undoManager.add(); + } + + editor.on('keydown', function(evt) { + if (evt.keyCode == 13) { + if (handleEnterKey(evt) !== false) { + evt.preventDefault(); + } + } + }); + }; +}); + +// Included from: js/tinymce/classes/ForceBlocks.js + +/** + * ForceBlocks.js + * + * Released under LGPL License. + * Copyright (c) 1999-2015 Ephox Corp. All rights reserved + * + * License: http://www.tinymce.com/license + * Contributing: http://www.tinymce.com/contributing + */ + +/** + * Makes sure that everything gets wrapped in paragraphs. + * + * @private + * @class tinymce.ForceBlocks + */ +define("tinymce/ForceBlocks", [], function() { + return function(editor) { + var settings = editor.settings, dom = editor.dom, selection = editor.selection; + var schema = editor.schema, blockElements = schema.getBlockElements(); + + function addRootBlocks() { + var node = selection.getStart(), rootNode = editor.getBody(), rng; + var startContainer, startOffset, endContainer, endOffset, rootBlockNode; + var tempNode, offset = -0xFFFFFF, wrapped, restoreSelection; + var tmpRng, rootNodeName, forcedRootBlock; + + forcedRootBlock = settings.forced_root_block; + + if (!node || node.nodeType !== 1 || !forcedRootBlock) { + return; + } + + // Check if node is wrapped in block + while (node && node != rootNode) { + if (blockElements[node.nodeName]) { + return; + } + + node = node.parentNode; + } + + // Get current selection + rng = selection.getRng(); + if (rng.setStart) { + startContainer = rng.startContainer; + startOffset = rng.startOffset; + endContainer = rng.endContainer; + endOffset = rng.endOffset; + + try { + restoreSelection = editor.getDoc().activeElement === rootNode; + } catch (ex) { + // IE throws unspecified error here sometimes + } + } else { + // Force control range into text range + if (rng.item) { + node = rng.item(0); + rng = editor.getDoc().body.createTextRange(); + rng.moveToElementText(node); + } + + restoreSelection = rng.parentElement().ownerDocument === editor.getDoc(); + tmpRng = rng.duplicate(); + tmpRng.collapse(true); + startOffset = tmpRng.move('character', offset) * -1; + + if (!tmpRng.collapsed) { + tmpRng = rng.duplicate(); + tmpRng.collapse(false); + endOffset = (tmpRng.move('character', offset) * -1) - startOffset; + } + } + + // Wrap non block elements and text nodes + node = rootNode.firstChild; + rootNodeName = rootNode.nodeName.toLowerCase(); + while (node) { + // TODO: Break this up, too complex + if (((node.nodeType === 3 || (node.nodeType == 1 && !blockElements[node.nodeName]))) && + schema.isValidChild(rootNodeName, forcedRootBlock.toLowerCase())) { + // Remove empty text nodes + if (node.nodeType === 3 && node.nodeValue.length === 0) { + tempNode = node; + node = node.nextSibling; + dom.remove(tempNode); + continue; + } + + if (!rootBlockNode) { + rootBlockNode = dom.create(forcedRootBlock, editor.settings.forced_root_block_attrs); + node.parentNode.insertBefore(rootBlockNode, node); + wrapped = true; + } + + tempNode = node; + node = node.nextSibling; + rootBlockNode.appendChild(tempNode); + } else { + rootBlockNode = null; + node = node.nextSibling; + } + } + + if (wrapped && restoreSelection) { + if (rng.setStart) { + rng.setStart(startContainer, startOffset); + rng.setEnd(endContainer, endOffset); + selection.setRng(rng); + } else { + // Only select if the previous selection was inside the document to prevent auto focus in quirks mode + try { + rng = editor.getDoc().body.createTextRange(); + rng.moveToElementText(rootNode); + rng.collapse(true); + rng.moveStart('character', startOffset); + + if (endOffset > 0) { + rng.moveEnd('character', endOffset); + } + + rng.select(); + } catch (ex) { + // Ignore + } + } + + editor.nodeChanged(); + } + } + + // Force root blocks + if (settings.forced_root_block) { + editor.on('NodeChange', addRootBlocks); + } + }; +}); + +// Included from: js/tinymce/classes/caret/CaretUtils.js + +/** + * CaretUtils.js + * + * Released under LGPL License. + * Copyright (c) 1999-2015 Ephox Corp. All rights reserved + * + * License: http://www.tinymce.com/license + * Contributing: http://www.tinymce.com/contributing + */ + +/** + * Utility functions shared by the caret logic. + * + * @private + * @class tinymce.caret.CaretUtils + */ +define("tinymce/caret/CaretUtils", [ + "tinymce/util/Fun", + "tinymce/dom/TreeWalker", + "tinymce/dom/NodeType", + "tinymce/caret/CaretPosition", + "tinymce/caret/CaretContainer", + "tinymce/caret/CaretCandidate" +], function(Fun, TreeWalker, NodeType, CaretPosition, CaretContainer, CaretCandidate) { + var isContentEditableTrue = NodeType.isContentEditableTrue, + isContentEditableFalse = NodeType.isContentEditableFalse, + isBlockLike = NodeType.matchStyleValues('display', 'block table table-cell table-caption'), + isCaretContainer = CaretContainer.isCaretContainer, + isCaretContainerBlock = CaretContainer.isCaretContainerBlock, + curry = Fun.curry, + isElement = NodeType.isElement, + isCaretCandidate = CaretCandidate.isCaretCandidate; + + function isForwards(direction) { + return direction > 0; + } + + function isBackwards(direction) { + return direction < 0; + } + + function skipCaretContainers(walk, shallow) { + var node; + + while ((node = walk(shallow))) { + if (!isCaretContainerBlock(node)) { + return node; + } + } + + return null; + } + + function findNode(node, direction, predicateFn, rootNode, shallow) { + var walker = new TreeWalker(node, rootNode); + + if (isBackwards(direction)) { + if (isContentEditableFalse(node) || isCaretContainerBlock(node)) { + node = skipCaretContainers(walker.prev, true); + if (predicateFn(node)) { + return node; + } + } + + while ((node = skipCaretContainers(walker.prev, shallow))) { + if (predicateFn(node)) { + return node; + } + } + } + + if (isForwards(direction)) { + if (isContentEditableFalse(node) || isCaretContainerBlock(node)) { + node = skipCaretContainers(walker.next, true); + if (predicateFn(node)) { + return node; + } + } + + while ((node = skipCaretContainers(walker.next, shallow))) { + if (predicateFn(node)) { + return node; + } + } + } + + return null; + } + + function getEditingHost(node, rootNode) { + for (node = node.parentNode; node && node != rootNode; node = node.parentNode) { + if (isContentEditableTrue(node)) { + return node; + } + } + + return rootNode; + } + + function getParentBlock(node, rootNode) { + while (node && node != rootNode) { + if (isBlockLike(node)) { + return node; + } + + node = node.parentNode; + } + + return null; + } + + function isInSameBlock(caretPosition1, caretPosition2, rootNode) { + return getParentBlock(caretPosition1.container(), rootNode) == getParentBlock(caretPosition2.container(), rootNode); + } + + function isInSameEditingHost(caretPosition1, caretPosition2, rootNode) { + return getEditingHost(caretPosition1.container(), rootNode) == getEditingHost(caretPosition2.container(), rootNode); + } + + function getChildNodeAtRelativeOffset(relativeOffset, caretPosition) { + var container, offset; + + if (!caretPosition) { + return null; + } + + container = caretPosition.container(); + offset = caretPosition.offset(); + + if (!isElement(container)) { + return null; + } + + return container.childNodes[offset + relativeOffset]; + } + + function beforeAfter(before, node) { + var range = node.ownerDocument.createRange(); + + if (before) { + range.setStartBefore(node); + range.setEndBefore(node); + } else { + range.setStartAfter(node); + range.setEndAfter(node); + } + + return range; + } + + function isNodesInSameBlock(rootNode, node1, node2) { + return getParentBlock(node1, rootNode) == getParentBlock(node2, rootNode); + } + + function lean(left, rootNode, node) { + var sibling, siblingName; + + if (left) { + siblingName = 'previousSibling'; + } else { + siblingName = 'nextSibling'; + } + + while (node && node != rootNode) { + sibling = node[siblingName]; + + if (isCaretContainer(sibling)) { + sibling = sibling[siblingName]; + } + + if (isContentEditableFalse(sibling)) { + if (isNodesInSameBlock(rootNode, sibling, node)) { + return sibling; + } + + break; + } + + if (isCaretCandidate(sibling)) { + break; + } + + node = node.parentNode; + } + + return null; + } + + var before = curry(beforeAfter, true); + var after = curry(beforeAfter, false); + + function normalizeRange(direction, rootNode, range) { + var node, container, offset, location; + var leanLeft = curry(lean, true, rootNode); + var leanRight = curry(lean, false, rootNode); + + container = range.startContainer; + offset = range.startOffset; + + if (CaretContainer.isCaretContainerBlock(container)) { + if (!isElement(container)) { + container = container.parentNode; + } + + location = container.getAttribute('data-mce-caret'); + + if (location == 'before') { + node = container.nextSibling; + if (isContentEditableFalse(node)) { + return before(node); + } + } + + if (location == 'after') { + node = container.previousSibling; + if (isContentEditableFalse(node)) { + return after(node); + } + } + } + + if (!range.collapsed) { + return range; + } + + if (NodeType.isText(container)) { + if (isCaretContainer(container)) { + if (direction === 1) { + node = leanRight(container); + if (node) { + return before(node); + } + + node = leanLeft(container); + if (node) { + return after(node); + } + } + + if (direction === -1) { + node = leanLeft(container); + if (node) { + return after(node); + } + + node = leanRight(container); + if (node) { + return before(node); + } + } + + return range; + } + + if (CaretContainer.endsWithCaretContainer(container) && offset >= container.data.length - 1) { + if (direction === 1) { + node = leanRight(container); + if (node) { + return before(node); + } + } + + return range; + } + + if (CaretContainer.startsWithCaretContainer(container) && offset <= 1) { + if (direction === -1) { + node = leanLeft(container); + if (node) { + return after(node); + } + } + + return range; + } + + if (offset === container.data.length) { + node = leanRight(container); + if (node) { + return before(node); + } + + return range; + } + + if (offset === 0) { + node = leanLeft(container); + if (node) { + return after(node); + } + + return range; + } + } + + return range; + } + + function isNextToContentEditableFalse(relativeOffset, caretPosition) { + return isContentEditableFalse(getChildNodeAtRelativeOffset(relativeOffset, caretPosition)); + } + + return { + isForwards: isForwards, + isBackwards: isBackwards, + findNode: findNode, + getEditingHost: getEditingHost, + getParentBlock: getParentBlock, + isInSameBlock: isInSameBlock, + isInSameEditingHost: isInSameEditingHost, + isBeforeContentEditableFalse: curry(isNextToContentEditableFalse, 0), + isAfterContentEditableFalse: curry(isNextToContentEditableFalse, -1), + normalizeRange: normalizeRange + }; +}); + +// Included from: js/tinymce/classes/caret/CaretWalker.js + +/** + * CaretWalker.js + * + * Released under LGPL License. + * Copyright (c) 1999-2015 Ephox Corp. All rights reserved + * + * License: http://www.tinymce.com/license + * Contributing: http://www.tinymce.com/contributing + */ + +/** + * This module contains logic for moving around a virtual caret in logical order within a DOM element. + * + * It ignores the most obvious invalid caret locations such as within a script element or within a + * contentEditable=false element but it will return locations that isn't possible to render visually. + * + * @private + * @class tinymce.caret.CaretWalker + * @example + * var caretWalker = new CaretWalker(rootElm); + * + * var prevLogicalCaretPosition = caretWalker.prev(CaretPosition.fromRangeStart(range)); + * var nextLogicalCaretPosition = caretWalker.next(CaretPosition.fromRangeEnd(range)); + */ +define("tinymce/caret/CaretWalker", [ + "tinymce/dom/NodeType", + "tinymce/caret/CaretCandidate", + "tinymce/caret/CaretPosition", + "tinymce/caret/CaretUtils", + "tinymce/util/Arr", + "tinymce/util/Fun" +], function(NodeType, CaretCandidate, CaretPosition, CaretUtils, Arr, Fun) { + var isContentEditableFalse = NodeType.isContentEditableFalse, + isText = NodeType.isText, + isElement = NodeType.isElement, + isBr = NodeType.isBr, + isForwards = CaretUtils.isForwards, + isBackwards = CaretUtils.isBackwards, + isCaretCandidate = CaretCandidate.isCaretCandidate, + isAtomic = CaretCandidate.isAtomic, + isEditableCaretCandidate = CaretCandidate.isEditableCaretCandidate; + + function getParents(node, rootNode) { + var parents = []; + + while (node && node != rootNode) { + parents.push(node); + node = node.parentNode; + } + + return parents; + } + + function nodeAtIndex(container, offset) { + if (container.hasChildNodes() && offset < container.childNodes.length) { + return container.childNodes[offset]; + } + + return null; + } + + function getCaretCandidatePosition(direction, node) { + if (isForwards(direction)) { + if (isCaretCandidate(node.previousSibling) && !isText(node.previousSibling)) { + return CaretPosition.before(node); + } + + if (isText(node)) { + return CaretPosition(node, 0); + } + } + + if (isBackwards(direction)) { + if (isCaretCandidate(node.nextSibling) && !isText(node.nextSibling)) { + return CaretPosition.after(node); + } + + if (isText(node)) { + return CaretPosition(node, node.data.length); + } + } + + if (isBackwards(direction)) { + if (isBr(node)) { + return CaretPosition.before(node); + } + + return CaretPosition.after(node); + } + + return CaretPosition.before(node); + } + + // Jumps over BR elements|
a
->|a
+ function isBrBeforeBlock(node, rootNode) { + var next; + + if (!NodeType.isBr(node)) { + return false; + } + + next = findCaretPosition(1, CaretPosition.after(node), rootNode); + if (!next) { + return false; + } + + return !CaretUtils.isInSameBlock(CaretPosition.before(node), CaretPosition.before(next), rootNode); + } + + function findCaretPosition(direction, startCaretPosition, rootNode) { + var container, offset, node, nextNode, innerNode, + rootContentEditableFalseElm, caretPosition; + + if (!isElement(rootNode) || !startCaretPosition) { + return null; + } + + caretPosition = startCaretPosition; + container = caretPosition.container(); + offset = caretPosition.offset(); + + if (isText(container)) { + if (isBackwards(direction) && offset > 0) { + return CaretPosition(container, --offset); + } + + if (isForwards(direction) && offset < container.length) { + return CaretPosition(container, ++offset); + } + + node = container; + } else { + if (isBackwards(direction) && offset > 0) { + nextNode = nodeAtIndex(container, offset - 1); + if (isCaretCandidate(nextNode)) { + if (!isAtomic(nextNode)) { + innerNode = CaretUtils.findNode(nextNode, direction, isEditableCaretCandidate, nextNode); + if (innerNode) { + if (isText(innerNode)) { + return CaretPosition(innerNode, innerNode.data.length); + } + + return CaretPosition.after(innerNode); + } + } + + if (isText(nextNode)) { + return CaretPosition(nextNode, nextNode.data.length); + } + + return CaretPosition.before(nextNode); + } + } + + if (isForwards(direction) && offset < container.childNodes.length) { + nextNode = nodeAtIndex(container, offset); + if (isCaretCandidate(nextNode)) { + if (isBrBeforeBlock(nextNode, rootNode)) { + return findCaretPosition(direction, CaretPosition.after(nextNode), rootNode); + } + + if (!isAtomic(nextNode)) { + innerNode = CaretUtils.findNode(nextNode, direction, isEditableCaretCandidate, nextNode); + if (innerNode) { + if (isText(innerNode)) { + return CaretPosition(innerNode, 0); + } + + return CaretPosition.before(innerNode); + } + } + + if (isText(nextNode)) { + return CaretPosition(nextNode, 0); + } + + return CaretPosition.after(nextNode); + } + } + + node = caretPosition.getNode(); + } + + if ((isForwards(direction) && caretPosition.isAtEnd()) || (isBackwards(direction) && caretPosition.isAtStart())) { + node = CaretUtils.findNode(node, direction, Fun.constant(true), rootNode, true); + if (isEditableCaretCandidate(node)) { + return getCaretCandidatePosition(direction, node); + } + } + + nextNode = CaretUtils.findNode(node, direction, isEditableCaretCandidate, rootNode); + + rootContentEditableFalseElm = Arr.last(Arr.filter(getParents(container, rootNode), isContentEditableFalse)); + if (rootContentEditableFalseElm && (!nextNode || !rootContentEditableFalseElm.contains(nextNode))) { + if (isForwards(direction)) { + caretPosition = CaretPosition.after(rootContentEditableFalseElm); + } else { + caretPosition = CaretPosition.before(rootContentEditableFalseElm); + } + + return caretPosition; + } + + if (nextNode) { + return getCaretCandidatePosition(direction, nextNode); + } + + return null; + } + + return function(rootNode) { + return { + /** + * Returns the next logical caret position from the specificed input + * caretPoisiton or null if there isn't any more positions left for example + * at the end specified root element. + * + * @method next + * @param {tinymce.caret.CaretPosition} caretPosition Caret position to start from. + * @return {tinymce.caret.CaretPosition} CaretPosition or null if no position was found. + */ + next: function(caretPosition) { + return findCaretPosition(1, caretPosition, rootNode); + }, + + /** + * Returns the previous logical caret position from the specificed input + * caretPoisiton or null if there isn't any more positions left for example + * at the end specified root element. + * + * @method prev + * @param {tinymce.caret.CaretPosition} caretPosition Caret position to start from. + * @return {tinymce.caret.CaretPosition} CaretPosition or null if no position was found. + */ + prev: function(caretPosition) { + return findCaretPosition(-1, caretPosition, rootNode); + } + }; + }; +}); + +// Included from: js/tinymce/classes/InsertList.js + +/** + * InsertList.js + * + * Released under LGPL License. + * Copyright (c) 1999-2016 Ephox Corp. All rights reserved + * + * License: http://www.tinymce.com/license + * Contributing: http://www.tinymce.com/contributing + */ + +/** + * Handles inserts of lists into the editor instance. + * + * @class tinymce.InsertList + * @private + */ +define("tinymce/InsertList", [ + "tinymce/util/Tools", + "tinymce/caret/CaretWalker", + "tinymce/caret/CaretPosition" +], function(Tools, CaretWalker, CaretPosition) { + var isListFragment = function(fragment) { + var firstChild = fragment.firstChild; + var lastChild = fragment.lastChild; + + // Skip meta since it's likely|
+ rng = selection.getRng(); + var caretElement = rng.startContainer || (rng.parentElement ? rng.parentElement() : null); + var body = editor.getBody(); + if (caretElement === body && selection.isCollapsed()) { + if (dom.isBlock(body.firstChild) && canHaveChildren(body.firstChild) && dom.isEmpty(body.firstChild)) { + rng = dom.createRng(); + rng.setStart(body.firstChild, 0); + rng.setEnd(body.firstChild, 0); + selection.setRng(rng); + } + } + + // Insert node maker where we will insert the new HTML and get it's parent + if (!selection.isCollapsed()) { + // Fix for #2595 seems that delete removes one extra character on + // WebKit for some odd reason if you double click select a word + editor.selection.setRng(editor.selection.getRng()); + editor.getDoc().execCommand('Delete', false, null); + trimNbspAfterDeleteAndPaddValue(); + } + + parentNode = selection.getNode(); + + // Parse the fragment within the context of the parent node + var parserArgs = {context: parentNode.nodeName.toLowerCase(), data: details.data}; + fragment = parser.parse(value, parserArgs); + + // Custom handling of lists + if (details.paste === true && InsertList.isListFragment(fragment) && InsertList.isParentBlockLi(dom, parentNode)) { + rng = InsertList.insertAtCaret(serializer, dom, editor.selection.getRng(true), fragment); + editor.selection.setRng(rng); + editor.fire('SetContent', args); + return; + } + + markFragmentElements(fragment); + + // Move the caret to a more suitable location + node = fragment.lastChild; + if (node.attr('id') == 'mce_marker') { + marker = node; + + for (node = node.prev; node; node = node.walk(true)) { + if (node.type == 3 || !dom.isBlock(node.name)) { + if (editor.schema.isValidChild(node.parent.name, 'span')) { + node.parent.insert(marker, node, node.name === 'br'); + } + break; + } + } + } + + editor._selectionOverrides.showBlockCaretContainer(parentNode); + + // If parser says valid we can insert the contents into that parent + if (!parserArgs.invalid) { + value = serializer.serialize(fragment); + + // Check if parent is empty or only has one BR element then set the innerHTML of that parent + node = parentNode.firstChild; + node2 = parentNode.lastChild; + if (!node || (node === node2 && node.nodeName === 'BR')) { + dom.setHTML(parentNode, value); + } else { + selection.setContent(value); + } + } else { + // If the fragment was invalid within that context then we need + // to parse and process the parent it's inserted into + + // Insert bookmark node and get the parent + selection.setContent(bookmarkHtml); + parentNode = selection.getNode(); + rootNode = editor.getBody(); + + // Opera will return the document node when selection is in root + if (parentNode.nodeType == 9) { + parentNode = node = rootNode; + } else { + node = parentNode; + } + + // Find the ancestor just before the root element + while (node !== rootNode) { + parentNode = node; + node = node.parentNode; + } + + // Get the outer/inner HTML depending on if we are in the root and parser and serialize that + value = parentNode == rootNode ? rootNode.innerHTML : dom.getOuterHTML(parentNode); + value = serializer.serialize( + parser.parse( + // Need to replace by using a function since $ in the contents would otherwise be a problem + value.replace(//i, function() { + return serializer.serialize(fragment); + }) + ) + ); + + // Set the inner/outer HTML depending on if we are in the root or not + if (parentNode == rootNode) { + dom.setHTML(rootNode, value); + } else { + dom.setOuterHTML(parentNode, value); + } + } + + reduceInlineTextElements(); + moveSelectionToMarker(dom.get('mce_marker')); + umarkFragmentElements(editor.getBody()); + editor.fire('SetContent', args); + editor.addVisual(); + }; + + var processValue = function (value) { + var details; + + if (typeof value !== 'string') { + details = Tools.extend({ + paste: value.paste, + data: { + paste: value.paste + } + }, value); + + return { + content: value.content, + details: details + }; + } + + return { + content: value, + details: {} + }; + }; + + var insertAtCaret = function (editor, value) { + var result = processValue(value); + insertHtmlAtCaret(editor, result.content, result.details); + }; + + return { + insertAtCaret: insertAtCaret + }; +}); + +// Included from: js/tinymce/classes/EditorCommands.js + +/** + * EditorCommands.js + * + * Released under LGPL License. + * Copyright (c) 1999-2015 Ephox Corp. All rights reserved + * + * License: http://www.tinymce.com/license + * Contributing: http://www.tinymce.com/contributing + */ + +/** + * This class enables you to add custom editor commands and it contains + * overrides for native browser commands to address various bugs and issues. + * + * @class tinymce.EditorCommands + */ +define("tinymce/EditorCommands", [ + "tinymce/Env", + "tinymce/util/Tools", + "tinymce/dom/RangeUtils", + "tinymce/dom/TreeWalker", + "tinymce/InsertContent" +], function(Env, Tools, RangeUtils, TreeWalker, InsertContent) { + // Added for compression purposes + var each = Tools.each, extend = Tools.extend; + var map = Tools.map, inArray = Tools.inArray, explode = Tools.explode; + var isOldIE = Env.ie && Env.ie < 11; + var TRUE = true, FALSE = false; + + return function(editor) { + var dom, selection, formatter, + commands = {state: {}, exec: {}, value: {}}, + settings = editor.settings, + bookmark; + + editor.on('PreInit', function() { + dom = editor.dom; + selection = editor.selection; + settings = editor.settings; + formatter = editor.formatter; + }); + + /** + * Executes the specified command. + * + * @method execCommand + * @param {String} command Command to execute. + * @param {Boolean} ui Optional user interface state. + * @param {Object} value Optional value for command. + * @param {Object} args Optional extra arguments to the execCommand. + * @return {Boolean} true/false if the command was found or not. + */ + function execCommand(command, ui, value, args) { + var func, customCommand, state = 0; + + if (!/^(mceAddUndoLevel|mceEndUndoLevel|mceBeginUndoLevel|mceRepaint)$/.test(command) && (!args || !args.skip_focus)) { + editor.focus(); + } + + args = editor.fire('BeforeExecCommand', {command: command, ui: ui, value: value}); + if (args.isDefaultPrevented()) { + return false; + } + + customCommand = command.toLowerCase(); + if ((func = commands.exec[customCommand])) { + func(customCommand, ui, value); + editor.fire('ExecCommand', {command: command, ui: ui, value: value}); + return true; + } + + // Plugin commands + each(editor.plugins, function(p) { + if (p.execCommand && p.execCommand(command, ui, value)) { + editor.fire('ExecCommand', {command: command, ui: ui, value: value}); + state = true; + return false; + } + }); + + if (state) { + return state; + } + + // Theme commands + if (editor.theme && editor.theme.execCommand && editor.theme.execCommand(command, ui, value)) { + editor.fire('ExecCommand', {command: command, ui: ui, value: value}); + return true; + } + + // Browser commands + try { + state = editor.getDoc().execCommand(command, ui, value); + } catch (ex) { + // Ignore old IE errors + } + + if (state) { + editor.fire('ExecCommand', {command: command, ui: ui, value: value}); + return true; + } + + return false; + } + + /** + * Queries the current state for a command for example if the current selection is "bold". + * + * @method queryCommandState + * @param {String} command Command to check the state of. + * @return {Boolean/Number} true/false if the selected contents is bold or not, -1 if it's not found. + */ + function queryCommandState(command) { + var func; + + // Is hidden then return undefined + if (editor.quirks.isHidden()) { + return; + } + + command = command.toLowerCase(); + if ((func = commands.state[command])) { + return func(command); + } + + // Browser commands + try { + return editor.getDoc().queryCommandState(command); + } catch (ex) { + // Fails sometimes see bug: 1896577 + } + + return false; + } + + /** + * Queries the command value for example the current fontsize. + * + * @method queryCommandValue + * @param {String} command Command to check the value of. + * @return {Object} Command value of false if it's not found. + */ + function queryCommandValue(command) { + var func; + + // Is hidden then return undefined + if (editor.quirks.isHidden()) { + return; + } + + command = command.toLowerCase(); + if ((func = commands.value[command])) { + return func(command); + } + + // Browser commands + try { + return editor.getDoc().queryCommandValue(command); + } catch (ex) { + // Fails sometimes see bug: 1896577 + } + } + + /** + * Adds commands to the command collection. + * + * @method addCommands + * @param {Object} command_list Name/value collection with commands to add, the names can also be comma separated. + * @param {String} type Optional type to add, defaults to exec. Can be value or state as well. + */ + function addCommands(command_list, type) { + type = type || 'exec'; + + each(command_list, function(callback, command) { + each(command.toLowerCase().split(','), function(command) { + commands[type][command] = callback; + }); + }); + } + + function addCommand(command, callback, scope) { + command = command.toLowerCase(); + commands.exec[command] = function(command, ui, value, args) { + return callback.call(scope || editor, ui, value, args); + }; + } + + /** + * Returns true/false if the command is supported or not. + * + * @method queryCommandSupported + * @param {String} command Command that we check support for. + * @return {Boolean} true/false if the command is supported or not. + */ + function queryCommandSupported(command) { + command = command.toLowerCase(); + + if (commands.exec[command]) { + return true; + } + + // Browser commands + try { + return editor.getDoc().queryCommandSupported(command); + } catch (ex) { + // Fails sometimes see bug: 1896577 + } + + return false; + } + + function addQueryStateHandler(command, callback, scope) { + command = command.toLowerCase(); + commands.state[command] = function() { + return callback.call(scope || editor); + }; + } + + function addQueryValueHandler(command, callback, scope) { + command = command.toLowerCase(); + commands.value[command] = function() { + return callback.call(scope || editor); + }; + } + + function hasCustomCommand(command) { + command = command.toLowerCase(); + return !!commands.exec[command]; + } + + // Expose public methods + extend(this, { + execCommand: execCommand, + queryCommandState: queryCommandState, + queryCommandValue: queryCommandValue, + queryCommandSupported: queryCommandSupported, + addCommands: addCommands, + addCommand: addCommand, + addQueryStateHandler: addQueryStateHandler, + addQueryValueHandler: addQueryValueHandler, + hasCustomCommand: hasCustomCommand + }); + + // Private methods + + function execNativeCommand(command, ui, value) { + if (ui === undefined) { + ui = FALSE; + } + + if (value === undefined) { + value = null; + } + + return editor.getDoc().execCommand(command, ui, value); + } + + function isFormatMatch(name) { + return formatter.match(name); + } + + function toggleFormat(name, value) { + formatter.toggle(name, value ? {value: value} : undefined); + editor.nodeChanged(); + } + + function storeSelection(type) { + bookmark = selection.getBookmark(type); + } + + function restoreSelection() { + selection.moveToBookmark(bookmark); + } + + // Add execCommand overrides + addCommands({ + // Ignore these, added for compatibility + 'mceResetDesignMode,mceBeginUndoLevel': function() {}, + + // Add undo manager logic + 'mceEndUndoLevel,mceAddUndoLevel': function() { + editor.undoManager.add(); + }, + + 'Cut,Copy,Paste': function(command) { + var doc = editor.getDoc(), failed; + + // Try executing the native command + try { + execNativeCommand(command); + } catch (ex) { + // Command failed + failed = TRUE; + } + + // Chrome reports the paste command as supported however older IE:s will return false for cut/paste + if (command === 'paste' && !doc.queryCommandEnabled(command)) { + failed = true; + } + + // Present alert message about clipboard access not being available + if (failed || !doc.queryCommandSupported(command)) { + var msg = editor.translate( + "Your browser doesn't support direct access to the clipboard. " + + "Please use the Ctrl+X/C/V keyboard shortcuts instead." + ); + + if (Env.mac) { + msg = msg.replace(/Ctrl\+/g, '\u2318+'); + } + + editor.notificationManager.open({text: msg, type: 'error'}); + } + }, + + // Override unlink command + unlink: function() { + if (selection.isCollapsed()) { + var elm = editor.dom.getParent(editor.selection.getStart(), 'a'); + if (elm) { + editor.dom.remove(elm, true); + } + + return; + } + + formatter.remove("link"); + }, + + // Override justify commands to use the text formatter engine + 'JustifyLeft,JustifyCenter,JustifyRight,JustifyFull,JustifyNone': function(command) { + var align = command.substring(7); + + if (align == 'full') { + align = 'justify'; + } + + // Remove all other alignments first + each('left,center,right,justify'.split(','), function(name) { + if (align != name) { + formatter.remove('align' + name); + } + }); + + if (align != 'none') { + toggleFormat('align' + align); + } + }, + + // Override list commands to fix WebKit bug + 'InsertUnorderedList,InsertOrderedList': function(command) { + var listElm, listParent; + + execNativeCommand(command); + + // WebKit produces lists within block elements so we need to split them + // we will replace the native list creation logic to custom logic later on + // TODO: Remove this when the list creation logic is removed + listElm = dom.getParent(selection.getNode(), 'ol,ul'); + if (listElm) { + listParent = listElm.parentNode; + + // If list is within a text block then split that block + if (/^(H[1-6]|P|ADDRESS|PRE)$/.test(listParent.nodeName)) { + storeSelection(); + dom.split(listParent, listElm); + restoreSelection(); + } + } + }, + + // Override commands to use the text formatter engine + 'Bold,Italic,Underline,Strikethrough,Superscript,Subscript': function(command) { + toggleFormat(command); + }, + + // Override commands to use the text formatter engine + 'ForeColor,HiliteColor,FontName': function(command, ui, value) { + toggleFormat(command, value); + }, + + FontSize: function(command, ui, value) { + var fontClasses, fontSizes; + + // Convert font size 1-7 to styles + if (value >= 1 && value <= 7) { + fontSizes = explode(settings.font_size_style_values); + fontClasses = explode(settings.font_size_classes); + + if (fontClasses) { + value = fontClasses[value - 1] || value; + } else { + value = fontSizes[value - 1] || value; + } + } + + toggleFormat(command, value); + }, + + RemoveFormat: function(command) { + formatter.remove(command); + }, + + mceBlockQuote: function() { + toggleFormat('blockquote'); + }, + + FormatBlock: function(command, ui, value) { + return toggleFormat(value || 'p'); + }, + + mceCleanup: function() { + var bookmark = selection.getBookmark(); + + editor.setContent(editor.getContent({cleanup: TRUE}), {cleanup: TRUE}); + + selection.moveToBookmark(bookmark); + }, + + mceRemoveNode: function(command, ui, value) { + var node = value || selection.getNode(); + + // Make sure that the body node isn't removed + if (node != editor.getBody()) { + storeSelection(); + editor.dom.remove(node, TRUE); + restoreSelection(); + } + }, + + mceSelectNodeDepth: function(command, ui, value) { + var counter = 0; + + dom.getParent(selection.getNode(), function(node) { + if (node.nodeType == 1 && counter++ == value) { + selection.select(node); + return FALSE; + } + }, editor.getBody()); + }, + + mceSelectNode: function(command, ui, value) { + selection.select(value); + }, + + mceInsertContent: function(command, ui, value) { + InsertContent.insertAtCaret(editor, value); + }, + + mceInsertRawHTML: function(command, ui, value) { + selection.setContent('tiny_mce_marker'); + editor.setContent( + editor.getContent().replace(/tiny_mce_marker/g, function() { + return value; + }) + ); + }, + + mceToggleFormat: function(command, ui, value) { + toggleFormat(value); + }, + + mceSetContent: function(command, ui, value) { + editor.setContent(value); + }, + + 'Indent,Outdent': function(command) { + var intentValue, indentUnit, value; + + // Setup indent level + intentValue = settings.indentation; + indentUnit = /[a-z%]+$/i.exec(intentValue); + intentValue = parseInt(intentValue, 10); + + if (!queryCommandState('InsertUnorderedList') && !queryCommandState('InsertOrderedList')) { + // If forced_root_blocks is set to false we don't have a block to indent so lets create a div + if (!settings.forced_root_block && !dom.getParent(selection.getNode(), dom.isBlock)) { + formatter.apply('div'); + } + + each(selection.getSelectedBlocks(), function(element) { + if (dom.getContentEditable(element) === "false") { + return; + } + + if (element.nodeName !== "LI") { + var indentStyleName = editor.getParam('indent_use_margin', false) ? 'margin' : 'padding'; + indentStyleName = element.nodeName === 'TABLE' ? 'margin' : indentStyleName; + indentStyleName += dom.getStyle(element, 'direction', true) == 'rtl' ? 'Right' : 'Left'; + + if (command == 'outdent') { + value = Math.max(0, parseInt(element.style[indentStyleName] || 0, 10) - intentValue); + dom.setStyle(element, indentStyleName, value ? value + indentUnit : ''); + } else { + value = (parseInt(element.style[indentStyleName] || 0, 10) + intentValue) + indentUnit; + dom.setStyle(element, indentStyleName, value); + } + } + }); + } else { + execNativeCommand(command); + } + }, + + mceRepaint: function() { + }, + + InsertHorizontalRule: function() { + editor.execCommand('mceInsertContent', false, '|
+ rng = selection.getRng(); + if (!rng.item) { + rng.moveToElementText(root); + rng.select(); + } + } + }, + + "delete": function() { + execNativeCommand("Delete"); + + // Check if body is empty after the delete call if so then set the contents + // to an empty string and move the caret to any block produced by that operation + // this fixes the issue with root blocks not being properly produced after a delete call on IE + var body = editor.getBody(); + + if (dom.isEmpty(body)) { + editor.setContent(''); + + if (body.firstChild && dom.isBlock(body.firstChild)) { + editor.selection.setCursorLocation(body.firstChild, 0); + } else { + editor.selection.setCursorLocation(body, 0); + } + } + }, + + mceNewDocument: function() { + editor.setContent(''); + }, + + InsertLineBreak: function(command, ui, value) { + // We load the current event in from EnterKey.js when appropriate to heed + // certain event-specific variations such as ctrl-enter in a list + var evt = value; + var brElm, extraBr, marker; + var rng = selection.getRng(true); + new RangeUtils(dom).normalize(rng); + + var offset = rng.startOffset; + var container = rng.startContainer; + + // Resolve node index + if (container.nodeType == 1 && container.hasChildNodes()) { + var isAfterLastNodeInContainer = offset > container.childNodes.length - 1; + + container = container.childNodes[Math.min(offset, container.childNodes.length - 1)] || container; + if (isAfterLastNodeInContainer && container.nodeType == 3) { + offset = container.nodeValue.length; + } else { + offset = 0; + } + } + + var parentBlock = dom.getParent(container, dom.isBlock); + var parentBlockName = parentBlock ? parentBlock.nodeName.toUpperCase() : ''; // IE < 9 & HTML5 + var containerBlock = parentBlock ? dom.getParent(parentBlock.parentNode, dom.isBlock) : null; + var containerBlockName = containerBlock ? containerBlock.nodeName.toUpperCase() : ''; // IE < 9 & HTML5 + + // Enter inside block contained within a LI then split or insert before/after LI + var isControlKey = evt && evt.ctrlKey; + if (containerBlockName == 'LI' && !isControlKey) { + parentBlock = containerBlock; + parentBlockName = containerBlockName; + } + + // Walks the parent block to the right and look for BR elements + function hasRightSideContent() { + var walker = new TreeWalker(container, parentBlock), node; + var nonEmptyElementsMap = editor.schema.getNonEmptyElements(); + + while ((node = walker.next())) { + if (nonEmptyElementsMap[node.nodeName.toLowerCase()] || node.length > 0) { + return true; + } + } + } + + if (container && container.nodeType == 3 && offset >= container.nodeValue.length) { + // Insert extra BR element at the end block elements + if (!isOldIE && !hasRightSideContent()) { + brElm = dom.create('br'); + rng.insertNode(brElm); + rng.setStartAfter(brElm); + rng.setEndAfter(brElm); + extraBr = true; + } + } + + brElm = dom.create('br'); + rng.insertNode(brElm); + + // Rendering modes below IE8 doesn't display BR elements in PRE unless we have a \n before it + var documentMode = dom.doc.documentMode; + if (isOldIE && parentBlockName == 'PRE' && (!documentMode || documentMode < 8)) { + brElm.parentNode.insertBefore(dom.doc.createTextNode('\r'), brElm); + } + + // Insert temp marker and scroll to that + marker = dom.create('span', {}, ' '); + brElm.parentNode.insertBefore(marker, brElm); + selection.scrollIntoView(marker); + dom.remove(marker); + + if (!extraBr) { + rng.setStartAfter(brElm); + rng.setEndAfter(brElm); + } else { + rng.setStartBefore(brElm); + rng.setEndBefore(brElm); + } + + selection.setRng(rng); + editor.undoManager.add(); + + return TRUE; + } + }); + + // Add queryCommandState overrides + addCommands({ + // Override justify commands + 'JustifyLeft,JustifyCenter,JustifyRight,JustifyFull': function(command) { + var name = 'align' + command.substring(7); + var nodes = selection.isCollapsed() ? [dom.getParent(selection.getNode(), dom.isBlock)] : selection.getSelectedBlocks(); + var matches = map(nodes, function(node) { + return !!formatter.matchNode(node, name); + }); + return inArray(matches, TRUE) !== -1; + }, + + 'Bold,Italic,Underline,Strikethrough,Superscript,Subscript': function(command) { + return isFormatMatch(command); + }, + + mceBlockQuote: function() { + return isFormatMatch('blockquote'); + }, + + Outdent: function() { + var node; + + if (settings.inline_styles) { + if ((node = dom.getParent(selection.getStart(), dom.isBlock)) && parseInt(node.style.paddingLeft, 10) > 0) { + return TRUE; + } + + if ((node = dom.getParent(selection.getEnd(), dom.isBlock)) && parseInt(node.style.paddingLeft, 10) > 0) { + return TRUE; + } + } + + return ( + queryCommandState('InsertUnorderedList') || + queryCommandState('InsertOrderedList') || + (!settings.inline_styles && !!dom.getParent(selection.getNode(), 'BLOCKQUOTE')) + ); + }, + + 'InsertUnorderedList,InsertOrderedList': function(command) { + var list = dom.getParent(selection.getNode(), 'ul,ol'); + + return list && + ( + command === 'insertunorderedlist' && list.tagName === 'UL' || + command === 'insertorderedlist' && list.tagName === 'OL' + ); + } + }, 'state'); + + // Add queryCommandValue overrides + addCommands({ + 'FontSize,FontName': function(command) { + var value = 0, parent; + + if ((parent = dom.getParent(selection.getNode(), 'span'))) { + if (command == 'fontsize') { + value = parent.style.fontSize; + } else { + value = parent.style.fontFamily.replace(/, /g, ',').replace(/[\'\"]/g, '').toLowerCase(); + } + } + + return value; + } + }, 'value'); + + // Add undo manager logic + addCommands({ + Undo: function() { + editor.undoManager.undo(); + }, + + Redo: function() { + editor.undoManager.redo(); + } + }); + }; +}); + +// Included from: js/tinymce/classes/util/URI.js + +/** + * URI.js + * + * Released under LGPL License. + * Copyright (c) 1999-2015 Ephox Corp. All rights reserved + * + * License: http://www.tinymce.com/license + * Contributing: http://www.tinymce.com/contributing + */ + +/** + * This class handles parsing, modification and serialization of URI/URL strings. + * @class tinymce.util.URI + */ +define("tinymce/util/URI", [ + "tinymce/util/Tools" +], function(Tools) { + var each = Tools.each, trim = Tools.trim; + var queryParts = "source protocol authority userInfo user password host port relative path directory file query anchor".split(' '); + var DEFAULT_PORTS = { + 'ftp': 21, + 'http': 80, + 'https': 443, + 'mailto': 25 + }; + + /** + * Constructs a new URI instance. + * + * @constructor + * @method URI + * @param {String} url URI string to parse. + * @param {Object} settings Optional settings object. + */ + function URI(url, settings) { + var self = this, baseUri, base_url; + + url = trim(url); + settings = self.settings = settings || {}; + baseUri = settings.base_uri; + + // Strange app protocol that isn't http/https or local anchor + // For example: mailto,skype,tel etc. + if (/^([\w\-]+):([^\/]{2})/i.test(url) || /^\s*#/.test(url)) { + self.source = url; + return; + } + + var isProtocolRelative = url.indexOf('//') === 0; + + // Absolute path with no host, fake host and protocol + if (url.indexOf('/') === 0 && !isProtocolRelative) { + url = (baseUri ? baseUri.protocol || 'http' : 'http') + '://mce_host' + url; + } + + // Relative path http:// or protocol relative //path + if (!/^[\w\-]*:?\/\//.test(url)) { + base_url = settings.base_uri ? settings.base_uri.path : new URI(location.href).directory; + if (settings.base_uri.protocol === "") { + url = '//mce_host' + self.toAbsPath(base_url, url); + } else { + url = /([^#?]*)([#?]?.*)/.exec(url); + url = ((baseUri && baseUri.protocol) || 'http') + '://mce_host' + self.toAbsPath(base_url, url[1]) + url[2]; + } + } + + // Parse URL (Credits goes to Steave, http://blog.stevenlevithan.com/archives/parseuri) + url = url.replace(/@@/g, '(mce_at)'); // Zope 3 workaround, they use @@something + + /*jshint maxlen: 255 */ + /*eslint max-len: 0 */ + url = /^(?:(?![^:@]+:[^:@\/]*@)([^:\/?#.]+):)?(?:\/\/)?((?:(([^:@\/]*):?([^:@\/]*))?@)?([^:\/?#]*)(?::(\d*))?)(((\/(?:[^?#](?![^?#\/]*\.[^?#\/.]+(?:[?#]|$)))*\/?)?([^?#\/]*))(?:\?([^#]*))?(?:#(.*))?)/.exec(url); + + each(queryParts, function(v, i) { + var part = url[i]; + + // Zope 3 workaround, they use @@something + if (part) { + part = part.replace(/\(mce_at\)/g, '@@'); + } + + self[v] = part; + }); + + if (baseUri) { + if (!self.protocol) { + self.protocol = baseUri.protocol; + } + + if (!self.userInfo) { + self.userInfo = baseUri.userInfo; + } + + if (!self.port && self.host === 'mce_host') { + self.port = baseUri.port; + } + + if (!self.host || self.host === 'mce_host') { + self.host = baseUri.host; + } + + self.source = ''; + } + + if (isProtocolRelative) { + self.protocol = ''; + } + + //t.path = t.path || '/'; + } + + URI.prototype = { + /** + * Sets the internal path part of the URI. + * + * @method setPath + * @param {string} path Path string to set. + */ + setPath: function(path) { + var self = this; + + path = /^(.*?)\/?(\w+)?$/.exec(path); + + // Update path parts + self.path = path[0]; + self.directory = path[1]; + self.file = path[2]; + + // Rebuild source + self.source = ''; + self.getURI(); + }, + + /** + * Converts the specified URI into a relative URI based on the current URI instance location. + * + * @method toRelative + * @param {String} uri URI to convert into a relative path/URI. + * @return {String} Relative URI from the point specified in the current URI instance. + * @example + * // Converts an absolute URL to an relative URL url will be somedir/somefile.htm + * var url = new tinymce.util.URI('http://www.site.com/dir/').toRelative('http://www.site.com/dir/somedir/somefile.htm'); + */ + toRelative: function(uri) { + var self = this, output; + + if (uri === "./") { + return uri; + } + + uri = new URI(uri, {base_uri: self}); + + // Not on same domain/port or protocol + if ((uri.host != 'mce_host' && self.host != uri.host && uri.host) || self.port != uri.port || + (self.protocol != uri.protocol && uri.protocol !== "")) { + return uri.getURI(); + } + + var tu = self.getURI(), uu = uri.getURI(); + + // Allow usage of the base_uri when relative_urls = true + if (tu == uu || (tu.charAt(tu.length - 1) == "/" && tu.substr(0, tu.length - 1) == uu)) { + return tu; + } + + output = self.toRelPath(self.path, uri.path); + + // Add query + if (uri.query) { + output += '?' + uri.query; + } + + // Add anchor + if (uri.anchor) { + output += '#' + uri.anchor; + } + + return output; + }, + + /** + * Converts the specified URI into a absolute URI based on the current URI instance location. + * + * @method toAbsolute + * @param {String} uri URI to convert into a relative path/URI. + * @param {Boolean} noHost No host and protocol prefix. + * @return {String} Absolute URI from the point specified in the current URI instance. + * @example + * // Converts an relative URL to an absolute URL url will be http://www.site.com/dir/somedir/somefile.htm + * 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(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. + */ + toRelPath: function(base, path) { + var items, breakPoint = 0, out = '', i, l; + + // Split the paths + base = base.substring(0, base.lastIndexOf('/')); + base = base.split('/'); + items = path.split('/'); + + if (base.length >= items.length) { + for (i = 0, l = base.length; i < l; i++) { + if (i >= items.length || base[i] != items[i]) { + breakPoint = i + 1; + break; + } + } + } + + if (base.length < items.length) { + for (i = 0, l = items.length; i < l; i++) { + if (i >= base.length || base[i] != items[i]) { + breakPoint = i + 1; + break; + } + } + } + + if (breakPoint === 1) { + return path; + } + + for (i = 0, l = base.length - (breakPoint - 1); i < l; i++) { + out += "../"; + } + + for (i = breakPoint - 1, l = items.length; i < l; i++) { + if (i != breakPoint - 1) { + out += "/" + items[i]; + } else { + out += items[i]; + } + } + + return out; + }, + + /** + * Converts a relative path into a absolute path. + * + * @method toAbsPath + * @param {String} base Base point to convert the path from. + * @param {String} path Relative path to convert into an absolute path. + */ + toAbsPath: function(base, path) { + var i, nb = 0, o = [], tr, outPath; + + // Split paths + tr = /\/$/.test(path) ? '/' : ''; + base = base.split('/'); + path = path.split('/'); + + // Remove empty chunks + each(base, function(k) { + if (k) { + o.push(k); + } + }); + + base = o; + + // Merge relURLParts chunks + for (i = path.length - 1, o = []; i >= 0; i--) { + // Ignore empty or . + if (path[i].length === 0 || path[i] === ".") { + continue; + } + + // Is parent + if (path[i] === '..') { + nb++; + continue; + } + + // Move up + if (nb > 0) { + nb--; + continue; + } + + o.push(path[i]); + } + + i = base.length - nb; + + // If /a/b/c or / + if (i <= 0) { + outPath = o.reverse().join('/'); + } else { + outPath = base.slice(0, i).join('/') + '/' + o.reverse().join('/'); + } + + // Add front / if it's needed + if (outPath.indexOf('/') !== 0) { + outPath = '/' + outPath; + } + + // Add traling / if it's needed + if (tr && outPath.lastIndexOf('/') !== outPath.length - 1) { + outPath += tr; + } + + return outPath; + }, + + /** + * Returns the full URI of the internal structure. + * + * @method getURI + * @param {Boolean} noProtoHost Optional no host and protocol part. Defaults to false. + */ + getURI: function(noProtoHost) { + var s, self = this; + + // Rebuild source + if (!self.source || noProtoHost) { + s = ''; + + if (!noProtoHost) { + if (self.protocol) { + s += self.protocol + '://'; + } else { + s += '//'; + } + + if (self.userInfo) { + s += self.userInfo + '@'; + } + + if (self.host) { + s += self.host; + } + + if (self.port) { + s += ':' + self.port; + } + } + + if (self.path) { + s += self.path; + } + + if (self.query) { + s += '?' + self.query; + } + + if (self.anchor) { + s += '#' + self.anchor; + } + + self.source = s; + } + + return self.source; + } + }; + + URI.parseDataUri = function(uri) { + var type, matches; + + uri = decodeURIComponent(uri).split(','); + + matches = /data:([^;]+)/.exec(uri[0]); + if (matches) { + type = matches[1]; + } + + return { + type: type, + data: uri[1] + }; + }; + + URI.getDocumentBaseUrl = function(loc) { + var baseUrl; + + // Pass applewebdata:// and other non web protocols though + if (loc.protocol.indexOf('http') !== 0 && loc.protocol !== 'file:') { + baseUrl = loc.href; + } else { + baseUrl = loc.protocol + '//' + loc.host + loc.pathname; + } + + if (/^[^:]+:\/\/\/?[^\/]+\//.test(baseUrl)) { + baseUrl = baseUrl.replace(/[\?#].*$/, '').replace(/[\/\\][^\/]+$/, ''); + + if (!/[\/\\]$/.test(baseUrl)) { + baseUrl += '/'; + } + } + + return baseUrl; + }; + + return URI; +}); + +// Included from: js/tinymce/classes/util/Class.js + +/** + * Class.js + * + * Released under LGPL License. + * Copyright (c) 1999-2015 Ephox Corp. All rights reserved + * + * License: http://www.tinymce.com/license + * Contributing: http://www.tinymce.com/contributing + */ + +/** + * This utilitiy class is used for easier inheritance. + * + * Features: + * * Exposed super functions: this._super(); + * * Mixins + * * Dummy functions + * * Property functions: var value = object.value(); and object.value(newValue); + * * Static functions + * * Defaults settings + */ +define("tinymce/util/Class", [ + "tinymce/util/Tools" +], function(Tools) { + var each = Tools.each, extend = Tools.extend; + + var extendClass, initializing; + + function Class() { + } + + // Provides classical inheritance, based on code made by John Resig + Class.extend = extendClass = function(prop) { + var self = this, _super = self.prototype, prototype, name, member; + + // The dummy class constructor + function Class() { + var i, mixins, mixin, self = this; + + // All construction is actually done in the init method + if (!initializing) { + // Run class constuctor + if (self.init) { + self.init.apply(self, arguments); + } + + // Run mixin constructors + mixins = self.Mixins; + if (mixins) { + i = mixins.length; + while (i--) { + mixin = mixins[i]; + if (mixin.init) { + mixin.init.apply(self, arguments); + } + } + } + } + } + + // Dummy function, needs to be extended in order to provide functionality + function dummy() { + return this; + } + + // Creates a overloaded method for the class + // this enables you to use this._super(); to call the super function + function createMethod(name, fn) { + return function() { + var self = this, tmp = self._super, ret; + + self._super = _super[name]; + ret = fn.apply(self, arguments); + self._super = tmp; + + return ret; + }; + } + + // 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) { + each(prop.Mixins, function(mixin) { + for (var name in mixin) { + if (name !== "init") { + prop[name] = mixin[name]; + } + } + }); + + if (_super.Mixins) { + prop.Mixins = _super.Mixins.concat(prop.Mixins); + } + } + + // Generate dummy methods + if (prop.Methods) { + each(prop.Methods.split(','), function(name) { + prop[name] = dummy; + }); + } + + // Generate property methods + if (prop.Properties) { + each(prop.Properties.split(','), function(name) { + var fieldName = '_' + name; + + prop[name] = function(value) { + var self = this, undef; + + // Set value + if (value !== undef) { + self[fieldName] = value; + + return self; + } + + // Get value + return self[fieldName]; + }; + }); + } + + // Static functions + if (prop.Statics) { + each(prop.Statics, function(func, name) { + Class[name] = func; + }); + } + + // Default settings + if (prop.Defaults && _super.Defaults) { + prop.Defaults = extend({}, _super.Defaults, prop.Defaults); + } + + // Copy the properties over onto the new prototype + for (name in prop) { + member = prop[name]; + + if (typeof member == "function" && _super[name]) { + prototype[name] = createMethod(name, member); + } else { + prototype[name] = member; + } + } + + // Populate our constructed prototype object + Class.prototype = prototype; + + // Enforce the constructor to be what we expect + Class.constructor = Class; + + // And make this class extendible + Class.extend = extendClass; + + return Class; + }; + + return Class; +}); + +// Included from: js/tinymce/classes/util/EventDispatcher.js + +/** + * EventDispatcher.js + * + * Released under LGPL License. + * Copyright (c) 1999-2015 Ephox Corp. All rights reserved + * + * 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 " + + "compositionstart compositionend compositionupdate touchstart touchmove touchend", + ' ' + ); + + 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++) { + callback = handlers[i]; + + // Unbind handlers marked with "once" + if (callback.once) { + off(name, callback.func); + } + + // 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.func.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, extra) { + var handlers, names, i; + + if (callback === false) { + callback = returnFalse; + } + + if (callback) { + callback = { + func: callback + }; + + if (extra) { + Tools.extend(callback, extra); + } + + 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].func === callback) { + handlers = handlers.slice(0, hi).concat(handlers.slice(hi + 1)); + bindings[name] = handlers; + } + } + } + + if (!handlers.length) { + toggleEvent(name, false); + delete bindings[name]; + } + } + } + } else { + for (name in bindings) { + toggleEvent(name, false); + } + + bindings = {}; + } + + return self; + } + + /** + * Binds an event listener to a specific event by name + * and automatically unbind the event once the callback fires. + * + * @method once + * @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.once('event', function(e) { + * // Callback logic + * }); + */ + function once(name, callback, prepend) { + return on(name, callback, prepend, {once: true}); + } + + /** + * 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.once = once; + 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/data/Binding.js + +/** + * Binding.js + * + * Released under LGPL License. + * Copyright (c) 1999-2015 Ephox Corp. All rights reserved + * + * License: http://www.tinymce.com/license + * Contributing: http://www.tinymce.com/contributing + */ + +/** + * This class gets dynamically extended to provide a binding between two models. This makes it possible to + * sync the state of two properties in two models by a layer of abstraction. + * + * @private + * @class tinymce.data.Binding + */ +define("tinymce/data/Binding", [], function() { + /** + * Constructs a new bidning. + * + * @constructor + * @method Binding + * @param {Object} settings Settings to the binding. + */ + function Binding(settings) { + this.create = settings.create; + } + + /** + * Creates a binding for a property on a model. + * + * @method create + * @param {tinymce.data.ObservableObject} model Model to create binding to. + * @param {String} name Name of property to bind. + * @return {tinymce.data.Binding} Binding instance. + */ + Binding.create = function(model, name) { + return new Binding({ + create: function(otherModel, otherName) { + var bindings; + + function fromSelfToOther(e) { + otherModel.set(otherName, e.value); + } + + function fromOtherToSelf(e) { + model.set(name, e.value); + } + + otherModel.on('change:' + otherName, fromOtherToSelf); + model.on('change:' + name, fromSelfToOther); + + // Keep track of the bindings + bindings = otherModel._bindings; + + if (!bindings) { + bindings = otherModel._bindings = []; + + otherModel.on('destroy', function() { + var i = bindings.length; + + while (i--) { + bindings[i](); + } + }); + } + + bindings.push(function() { + model.off('change:' + name, fromSelfToOther); + }); + + return model.get(name); + } + }); + }; + + return Binding; +}); + +// Included from: js/tinymce/classes/util/Observable.js + +/** + * Observable.js + * + * Released under LGPL License. + * Copyright (c) 1999-2015 Ephox Corp. All rights reserved + * + * License: http://www.tinymce.com/license + * Contributing: http://www.tinymce.com/contributing + */ + +/** + * This mixin will add event binding logic to classes. + * + * @mixin tinymce.util.Observable + */ +define("tinymce/util/Observable", [ + "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); + } + } + }); + } + + return obj._eventDispatcher; + } + + return { + /** + * Fires the specified event by name. Consult the + * event reference for more details on each event. + * + * @method fire + * @param {String} name Name of the event to fire. + * @param {Object?} args Event arguments. + * @param {Boolean?} bubble True/false if the event is to be bubbled. + * @return {Object} Event args instance passed in. + * @example + * instance.fire('event', {...}); + */ + fire: function(name, args, bubble) { + var self = this; + + // Prevent all events except the remove event after the instance has been removed + if (self.removed && name !== "remove") { + return args; + } + + args = getEventDispatcher(self).fire(name, args, bubble); + + // 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(); + } + } + + return args; + }, + + /** + * Binds an event listener to a specific event by name. Consult the + * event reference for more details on each event. + * + * @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 + * }); + */ + on: function(name, callback, prepend) { + return getEventDispatcher(this).on(name, callback, prepend); + }, + + /** + * Unbinds an event listener to a specific event by name. Consult the + * event reference for more details on each event. + * + * @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(); + */ + off: function(name, callback) { + return getEventDispatcher(this).off(name, callback); + }, + + /** + * Bind the event callback and once it fires the callback is removed. Consult the + * event reference for more details on each event. + * + * @method once + * @param {String} name Name of the event to bind. + * @param {callback} callback Callback to bind only once. + * @return {Object} Current class instance. + */ + once: function(name, callback) { + return getEventDispatcher(this).once(name, callback); + }, + + /** + * 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); + } + }; +}); + +// Included from: js/tinymce/classes/data/ObservableObject.js + +/** + * ObservableObject.js + * + * Released under LGPL License. + * Copyright (c) 1999-2015 Ephox Corp. All rights reserved + * + * License: http://www.tinymce.com/license + * Contributing: http://www.tinymce.com/contributing + */ + +/** + * This class is a object that is observable when properties changes a change event gets emitted. + * + * @private + * @class tinymce.data.ObservableObject + */ +define("tinymce/data/ObservableObject", [ + "tinymce/data/Binding", + "tinymce/util/Observable", + "tinymce/util/Class", + "tinymce/util/Tools" +], function(Binding, Observable, Class, Tools) { + function isNode(node) { + return node.nodeType > 0; + } + + // Todo: Maybe this should be shallow compare since it might be huge object references + function isEqual(a, b) { + var k, checked; + + // Strict equals + if (a === b) { + return true; + } + + // Compare null + if (a === null || b === null) { + return a === b; + } + + // Compare number, boolean, string, undefined + if (typeof a !== "object" || typeof b !== "object") { + return a === b; + } + + // Compare arrays + if (Tools.isArray(b)) { + if (a.length !== b.length) { + return false; + } + + k = a.length; + while (k--) { + if (!isEqual(a[k], b[k])) { + return false; + } + } + } + + // Shallow compare nodes + if (isNode(a) || isNode(b)) { + return a === b; + } + + // Compare objects + checked = {}; + for (k in b) { + if (!isEqual(a[k], b[k])) { + return false; + } + + checked[k] = true; + } + + for (k in a) { + if (!checked[k] && !isEqual(a[k], b[k])) { + return false; + } + } + + return true; + } + + return Class.extend({ + Mixins: [Observable], + + /** + * Constructs a new observable object instance. + * + * @constructor + * @param {Object} data Initial data for the object. + */ + init: function(data) { + var name, value; + + data = data || {}; + + for (name in data) { + value = data[name]; + + if (value instanceof Binding) { + data[name] = value.create(this, name); + } + } + + this.data = data; + }, + + /** + * Sets a property on the value this will call + * observers if the value is a change from the current value. + * + * @method set + * @param {String/object} name Name of the property to set or a object of items to set. + * @param {Object} value Value to set for the property. + * @return {tinymce.data.ObservableObject} Observable object instance. + */ + set: function(name, value) { + var key, args, oldValue = this.data[name]; + + if (value instanceof Binding) { + value = value.create(this, name); + } + + if (typeof name === "object") { + for (key in name) { + this.set(key, name[key]); + } + + return this; + } + + if (!isEqual(oldValue, value)) { + this.data[name] = value; + + args = { + target: this, + name: name, + value: value, + oldValue: oldValue + }; + + this.fire('change:' + name, args); + this.fire('change', args); + } + + return this; + }, + + /** + * Gets a property by name. + * + * @method get + * @param {String} name Name of the property to get. + * @return {Object} Object value of propery. + */ + get: function(name) { + return this.data[name]; + }, + + /** + * Returns true/false if the specified property exists. + * + * @method has + * @param {String} name Name of the property to check for. + * @return {Boolean} true/false if the item exists. + */ + has: function(name) { + return name in this.data; + }, + + /** + * Returns a dynamic property binding for the specified property name. This makes + * it possible to sync the state of two properties in two ObservableObject instances. + * + * @method bind + * @param {String} name Name of the property to sync with the property it's inserted to. + * @return {tinymce.data.Binding} Data binding instance. + */ + bind: function(name) { + return Binding.create(this, name); + }, + + /** + * Destroys the observable object and fires the "destroy" + * event and clean up any internal resources. + * + * @method destroy + */ + destroy: function() { + this.fire('destroy'); + } + }); +}); + +// Included from: js/tinymce/classes/ui/Selector.js + +/** + * Selector.js + * + * Released under LGPL License. + * Copyright (c) 1999-2015 Ephox Corp. All rights reserved + * + * License: http://www.tinymce.com/license + * Contributing: http://www.tinymce.com/contributing + */ + +/*eslint no-nested-ternary:0 */ + +/** + * Selector engine, enables you to select controls by using CSS like expressions. + * We currently only support basic CSS expressions to reduce the size of the core + * and the ones we support should be enough for most cases. + * + * @example + * Supported expressions: + * element + * element#name + * element.class + * element[attr] + * element[attr*=value] + * element[attr~=value] + * element[attr!=value] + * element[attr^=value] + * element[attr$=value] + * element:bug on IE 8 #6178 + DOMUtils.DOM.setHTML(elm, html); + } + }; + + return funcs; +}); + +// Included from: js/tinymce/classes/ui/BoxUtils.js + +/** + * BoxUtils.js + * + * Released under LGPL License. + * Copyright (c) 1999-2015 Ephox Corp. All rights reserved + * + * License: http://www.tinymce.com/license + * Contributing: http://www.tinymce.com/contributing + */ + +/** + * Utility class for box parsing and measuring. + * + * @private + * @class tinymce.ui.BoxUtils + */ +define("tinymce/ui/BoxUtils", [ +], function() { + "use strict"; + + return { + /** + * Parses the specified box value. A box value contains 1-4 properties in clockwise order. + * + * @method parseBox + * @param {String/Number} value Box value "0 1 2 3" or "0" etc. + * @return {Object} Object with top/right/bottom/left properties. + * @private + */ + parseBox: function(value) { + var len, radix = 10; + + if (!value) { + return; + } + + if (typeof value === "number") { + value = value || 0; + + return { + top: value, + left: value, + bottom: value, + right: value + }; + } + + value = value.split(' '); + len = value.length; + + if (len === 1) { + value[1] = value[2] = value[3] = value[0]; + } else if (len === 2) { + value[2] = value[0]; + value[3] = value[1]; + } else if (len === 3) { + value[3] = value[1]; + } + + return { + top: parseInt(value[0], radix) || 0, + right: parseInt(value[1], radix) || 0, + bottom: parseInt(value[2], radix) || 0, + left: parseInt(value[3], radix) || 0 + }; + }, + + measureBox: function(elm, prefix) { + function getStyle(name) { + var defaultView = document.defaultView; + + if (defaultView) { + // Remove camelcase + name = name.replace(/[A-Z]/g, function(a) { + return '-' + a; + }); + + return defaultView.getComputedStyle(elm, null).getPropertyValue(name); + } + + return elm.currentStyle[name]; + } + + function getSide(name) { + var val = parseFloat(getStyle(name), 10); + + return isNaN(val) ? 0 : val; + } + + return { + top: getSide(prefix + "TopWidth"), + right: getSide(prefix + "RightWidth"), + bottom: getSide(prefix + "BottomWidth"), + left: getSide(prefix + "LeftWidth") + }; + } + }; +}); + +// Included from: js/tinymce/classes/ui/ClassList.js + +/** + * ClassList.js + * + * Released under LGPL License. + * Copyright (c) 1999-2015 Ephox Corp. All rights reserved + * + * License: http://www.tinymce.com/license + * Contributing: http://www.tinymce.com/contributing + */ + +/** + * Handles adding and removal of classes. + * + * @private + * @class tinymce.ui.ClassList + */ +define("tinymce/ui/ClassList", [ + "tinymce/util/Tools" +], function(Tools) { + "use strict"; + + function noop() { + } + + /** + * Constructs a new class list the specified onchange + * callback will be executed when the class list gets modifed. + * + * @constructor ClassList + * @param {function} onchange Onchange callback to be executed. + */ + function ClassList(onchange) { + this.cls = []; + this.cls._map = {}; + this.onchange = onchange || noop; + this.prefix = ''; + } + + Tools.extend(ClassList.prototype, { + /** + * Adds a new class to the class list. + * + * @method add + * @param {String} cls Class to be added. + * @return {tinymce.ui.ClassList} Current class list instance. + */ + add: function(cls) { + if (cls && !this.contains(cls)) { + this.cls._map[cls] = true; + this.cls.push(cls); + this._change(); + } + + return this; + }, + + /** + * Removes the specified class from the class list. + * + * @method remove + * @param {String} cls Class to be removed. + * @return {tinymce.ui.ClassList} Current class list instance. + */ + remove: function(cls) { + if (this.contains(cls)) { + for (var i = 0; i < this.cls.length; i++) { + if (this.cls[i] === cls) { + break; + } + } + + this.cls.splice(i, 1); + delete this.cls._map[cls]; + this._change(); + } + + return this; + }, + + /** + * Toggles a class in the class list. + * + * @method toggle + * @param {String} cls Class to be added/removed. + * @param {Boolean} state Optional state if it should be added/removed. + * @return {tinymce.ui.ClassList} Current class list instance. + */ + toggle: function(cls, state) { + var curState = this.contains(cls); + + if (curState !== state) { + if (curState) { + this.remove(cls); + } else { + this.add(cls); + } + + this._change(); + } + + return this; + }, + + /** + * Returns true if the class list has the specified class. + * + * @method contains + * @param {String} cls Class to look for. + * @return {Boolean} true/false if the class exists or not. + */ + contains: function(cls) { + return !!this.cls._map[cls]; + }, + + /** + * Returns a space separated list of classes. + * + * @method toString + * @return {String} Space separated list of classes. + */ + + _change: function() { + delete this.clsValue; + this.onchange.call(this); + } + }); + + // IE 8 compatibility + ClassList.prototype.toString = function() { + var value; + + if (this.clsValue) { + return this.clsValue; + } + + value = ''; + for (var i = 0; i < this.cls.length; i++) { + if (i > 0) { + value += ' '; + } + + value += this.prefix + this.cls[i]; + } + + return value; + }; + + return ClassList; +}); + +// Included from: js/tinymce/classes/ui/ReflowQueue.js + +/** + * ReflowQueue.js + * + * Released under LGPL License. + * Copyright (c) 1999-2015 Ephox Corp. All rights reserved + * + * License: http://www.tinymce.com/license + * Contributing: http://www.tinymce.com/contributing + */ + +/** + * This class will automatically reflow controls on the next animation frame within a few milliseconds on older browsers. + * If the user manually reflows then the automatic reflow will be cancelled. This class is used internally when various control states + * changes that triggers a reflow. + * + * @class tinymce.ui.ReflowQueue + * @static + */ +define("tinymce/ui/ReflowQueue", [ + "tinymce/util/Delay" +], function(Delay) { + var dirtyCtrls = {}, animationFrameRequested; + + return { + /** + * Adds a control to the next automatic reflow call. This is the control that had a state + * change for example if the control was hidden/shown. + * + * @method add + * @param {tinymce.ui.Control} ctrl Control to add to queue. + */ + add: function(ctrl) { + var parent = ctrl.parent(); + + if (parent) { + if (!parent._layout || parent._layout.isNative()) { + return; + } + + if (!dirtyCtrls[parent._id]) { + dirtyCtrls[parent._id] = parent; + } + + if (!animationFrameRequested) { + animationFrameRequested = true; + + Delay.requestAnimationFrame(function() { + var id, ctrl; + + animationFrameRequested = false; + + for (id in dirtyCtrls) { + ctrl = dirtyCtrls[id]; + + if (ctrl.state.get('rendered')) { + ctrl.reflow(); + } + } + + dirtyCtrls = {}; + }, document.body); + } + } + }, + + /** + * Removes the specified control from the automatic reflow. This will happen when for example the user + * manually triggers a reflow. + * + * @method remove + * @param {tinymce.ui.Control} ctrl Control to remove from queue. + */ + remove: function(ctrl) { + if (dirtyCtrls[ctrl._id]) { + delete dirtyCtrls[ctrl._id]; + } + } + }; +}); + +// Included from: js/tinymce/classes/ui/Control.js + +/** + * Control.js + * + * Released under LGPL License. + * Copyright (c) 1999-2015 Ephox Corp. All rights reserved + * + * License: http://www.tinymce.com/license + * Contributing: http://www.tinymce.com/contributing + */ + +/*eslint consistent-this:0 */ + +/** + * This is the base class for all controls and containers. All UI control instances inherit + * from this one as it has the base logic needed by all of them. + * + * @class tinymce.ui.Control + */ +define("tinymce/ui/Control", [ + "tinymce/util/Class", + "tinymce/util/Tools", + "tinymce/util/EventDispatcher", + "tinymce/data/ObservableObject", + "tinymce/ui/Collection", + "tinymce/ui/DomUtils", + "tinymce/dom/DomQuery", + "tinymce/ui/BoxUtils", + "tinymce/ui/ClassList", + "tinymce/ui/ReflowQueue" +], function(Class, Tools, EventDispatcher, ObservableObject, Collection, DomUtils, $, BoxUtils, ClassList, ReflowQueue) { + "use strict"; + + var hasMouseWheelEventSupport = "onmousewheel" in document; + var hasWheelEventSupport = false; + var classPrefix = "mce-"; + var Control, idCounter = 0; + + var proto = { + Statics: { + classPrefix: classPrefix + }, + + isRtl: function() { + return Control.rtl; + }, + + /** + * Class/id prefix to use for all controls. + * + * @final + * @field {String} classPrefix + */ + classPrefix: classPrefix, + + /** + * Constructs a new control instance with the specified settings. + * + * @constructor + * @param {Object} settings Name/value object with settings. + * @setting {String} style Style CSS properties to add. + * @setting {String} border Border box values example: 1 1 1 1 + * @setting {String} padding Padding box values example: 1 1 1 1 + * @setting {String} margin Margin box values example: 1 1 1 1 + * @setting {Number} minWidth Minimal width for the control. + * @setting {Number} minHeight Minimal height for the control. + * @setting {String} classes Space separated list of classes to add. + * @setting {String} role WAI-ARIA role to use for control. + * @setting {Boolean} hidden Is the control hidden by default. + * @setting {Boolean} disabled Is the control disabled by default. + * @setting {String} name Name of the control instance. + */ + init: function(settings) { + var self = this, classes, defaultClasses; + + function applyClasses(classes) { + var i; + + classes = classes.split(' '); + for (i = 0; i < classes.length; i++) { + self.classes.add(classes[i]); + } + } + + self.settings = settings = Tools.extend({}, self.Defaults, settings); + + // Initial states + self._id = settings.id || ('mceu_' + (idCounter++)); + self._aria = {role: settings.role}; + self._elmCache = {}; + self.$ = $; + + self.state = new ObservableObject({ + visible: true, + active: false, + disabled: false, + value: '' + }); + + self.data = new ObservableObject(settings.data); + + self.classes = new ClassList(function() { + if (self.state.get('rendered')) { + self.getEl().className = this.toString(); + } + }); + self.classes.prefix = self.classPrefix; + + // Setup classes + classes = settings.classes; + if (classes) { + if (self.Defaults) { + defaultClasses = self.Defaults.classes; + + if (defaultClasses && classes != defaultClasses) { + applyClasses(defaultClasses); + } + } + + applyClasses(classes); + } + + Tools.each('title text name visible disabled active value'.split(' '), function(name) { + if (name in settings) { + self[name](settings[name]); + } + }); + + self.on('click', function() { + if (self.disabled()) { + return false; + } + }); + + /** + * Name/value object with settings for the current control. + * + * @field {Object} settings + */ + self.settings = settings; + + self.borderBox = BoxUtils.parseBox(settings.border); + self.paddingBox = BoxUtils.parseBox(settings.padding); + self.marginBox = BoxUtils.parseBox(settings.margin); + + if (settings.hidden) { + self.hide(); + } + }, + + // Will generate getter/setter methods for these properties + Properties: 'parent,name', + + /** + * Returns the root element to render controls into. + * + * @method getContainerElm + * @return {Element} HTML DOM element to render into. + */ + getContainerElm: function() { + return DomUtils.getContainer(); + }, + + /** + * Returns a control instance for the current DOM element. + * + * @method getParentCtrl + * @param {Element} elm HTML dom element to get parent control from. + * @return {tinymce.ui.Control} Control instance or undefined. + */ + getParentCtrl: function(elm) { + var ctrl, lookup = this.getRoot().controlIdLookup; + + while (elm && lookup) { + ctrl = lookup[elm.id]; + if (ctrl) { + break; + } + + elm = elm.parentNode; + } + + return ctrl; + }, + + /** + * Initializes the current controls layout rect. + * This will be executed by the layout managers to determine the + * default minWidth/minHeight etc. + * + * @method initLayoutRect + * @return {Object} Layout rect instance. + */ + initLayoutRect: function() { + var self = this, settings = self.settings, borderBox, layoutRect; + var elm = self.getEl(), width, height, minWidth, minHeight, autoResize; + var startMinWidth, startMinHeight, initialSize; + + // Measure the current element + borderBox = self.borderBox = self.borderBox || BoxUtils.measureBox(elm, 'border'); + self.paddingBox = self.paddingBox || BoxUtils.measureBox(elm, 'padding'); + self.marginBox = self.marginBox || BoxUtils.measureBox(elm, 'margin'); + initialSize = DomUtils.getSize(elm); + + // Setup minWidth/minHeight and width/height + startMinWidth = settings.minWidth; + startMinHeight = settings.minHeight; + minWidth = startMinWidth || initialSize.width; + minHeight = startMinHeight || initialSize.height; + width = settings.width; + height = settings.height; + autoResize = settings.autoResize; + autoResize = typeof autoResize != "undefined" ? autoResize : !width && !height; + + width = width || minWidth; + height = height || minHeight; + + var deltaW = borderBox.left + borderBox.right; + var deltaH = borderBox.top + borderBox.bottom; + + var maxW = settings.maxWidth || 0xFFFF; + var maxH = settings.maxHeight || 0xFFFF; + + // Setup initial layout rect + self._layoutRect = layoutRect = { + x: settings.x || 0, + y: settings.y || 0, + w: width, + h: height, + deltaW: deltaW, + deltaH: deltaH, + contentW: width - deltaW, + contentH: height - deltaH, + innerW: width - deltaW, + innerH: height - deltaH, + startMinWidth: startMinWidth || 0, + startMinHeight: startMinHeight || 0, + minW: Math.min(minWidth, maxW), + minH: Math.min(minHeight, maxH), + maxW: maxW, + maxH: maxH, + autoResize: autoResize, + scrollW: 0 + }; + + self._lastLayoutRect = {}; + + return layoutRect; + }, + + /** + * Getter/setter for the current layout rect. + * + * @method layoutRect + * @param {Object} [newRect] Optional new layout rect. + * @return {tinymce.ui.Control/Object} Current control or rect object. + */ + layoutRect: function(newRect) { + var self = this, curRect = self._layoutRect, lastLayoutRect, size, deltaWidth, deltaHeight, undef, repaintControls; + + // Initialize default layout rect + if (!curRect) { + curRect = self.initLayoutRect(); + } + + // Set new rect values + if (newRect) { + // Calc deltas between inner and outer sizes + deltaWidth = curRect.deltaW; + deltaHeight = curRect.deltaH; + + // Set x position + if (newRect.x !== undef) { + curRect.x = newRect.x; + } + + // Set y position + if (newRect.y !== undef) { + curRect.y = newRect.y; + } + + // Set minW + if (newRect.minW !== undef) { + curRect.minW = newRect.minW; + } + + // Set minH + if (newRect.minH !== undef) { + curRect.minH = newRect.minH; + } + + // Set new width and calculate inner width + size = newRect.w; + if (size !== undef) { + size = size < curRect.minW ? curRect.minW : size; + size = size > curRect.maxW ? curRect.maxW : size; + curRect.w = size; + curRect.innerW = size - deltaWidth; + } + + // Set new height and calculate inner height + size = newRect.h; + if (size !== undef) { + size = size < curRect.minH ? curRect.minH : size; + size = size > curRect.maxH ? curRect.maxH : size; + curRect.h = size; + curRect.innerH = size - deltaHeight; + } + + // Set new inner width and calculate width + size = newRect.innerW; + if (size !== undef) { + size = size < curRect.minW - deltaWidth ? curRect.minW - deltaWidth : size; + size = size > curRect.maxW - deltaWidth ? curRect.maxW - deltaWidth : size; + curRect.innerW = size; + curRect.w = size + deltaWidth; + } + + // Set new height and calculate inner height + size = newRect.innerH; + if (size !== undef) { + size = size < curRect.minH - deltaHeight ? curRect.minH - deltaHeight : size; + size = size > curRect.maxH - deltaHeight ? curRect.maxH - deltaHeight : size; + curRect.innerH = size; + curRect.h = size + deltaHeight; + } + + // Set new contentW + if (newRect.contentW !== undef) { + curRect.contentW = newRect.contentW; + } + + // Set new contentH + if (newRect.contentH !== undef) { + curRect.contentH = newRect.contentH; + } + + // Compare last layout rect with the current one to see if we need to repaint or not + lastLayoutRect = self._lastLayoutRect; + if (lastLayoutRect.x !== curRect.x || lastLayoutRect.y !== curRect.y || + lastLayoutRect.w !== curRect.w || lastLayoutRect.h !== curRect.h) { + repaintControls = Control.repaintControls; + + if (repaintControls) { + if (repaintControls.map && !repaintControls.map[self._id]) { + repaintControls.push(self); + repaintControls.map[self._id] = true; + } + } + + lastLayoutRect.x = curRect.x; + lastLayoutRect.y = curRect.y; + lastLayoutRect.w = curRect.w; + lastLayoutRect.h = curRect.h; + } + + return self; + } + + return curRect; + }, + + /** + * Repaints the control after a layout operation. + * + * @method repaint + */ + repaint: function() { + var self = this, style, bodyStyle, bodyElm, rect, borderBox; + var borderW, borderH, lastRepaintRect, round, value; + + // Use Math.round on all values on IE < 9 + round = !document.createRange ? Math.round : function(value) { + return value; + }; + + style = self.getEl().style; + rect = self._layoutRect; + lastRepaintRect = self._lastRepaintRect || {}; + + borderBox = self.borderBox; + borderW = borderBox.left + borderBox.right; + borderH = borderBox.top + borderBox.bottom; + + if (rect.x !== lastRepaintRect.x) { + style.left = round(rect.x) + 'px'; + lastRepaintRect.x = rect.x; + } + + if (rect.y !== lastRepaintRect.y) { + style.top = round(rect.y) + 'px'; + lastRepaintRect.y = rect.y; + } + + if (rect.w !== lastRepaintRect.w) { + value = round(rect.w - borderW); + style.width = (value >= 0 ? value : 0) + 'px'; + lastRepaintRect.w = rect.w; + } + + if (rect.h !== lastRepaintRect.h) { + value = round(rect.h - borderH); + style.height = (value >= 0 ? value : 0) + 'px'; + lastRepaintRect.h = rect.h; + } + + // Update body if needed + if (self._hasBody && rect.innerW !== lastRepaintRect.innerW) { + value = round(rect.innerW); + + bodyElm = self.getEl('body'); + if (bodyElm) { + bodyStyle = bodyElm.style; + bodyStyle.width = (value >= 0 ? value : 0) + 'px'; + } + + lastRepaintRect.innerW = rect.innerW; + } + + if (self._hasBody && rect.innerH !== lastRepaintRect.innerH) { + value = round(rect.innerH); + + bodyElm = bodyElm || self.getEl('body'); + if (bodyElm) { + bodyStyle = bodyStyle || bodyElm.style; + bodyStyle.height = (value >= 0 ? value : 0) + 'px'; + } + + lastRepaintRect.innerH = rect.innerH; + } + + self._lastRepaintRect = lastRepaintRect; + self.fire('repaint', {}, false); + }, + + /** + * Updates the controls layout rect by re-measuing it. + */ + updateLayoutRect: function() { + var self = this; + + self.parent()._lastRect = null; + + DomUtils.css(self.getEl(), {width: '', height: ''}); + + self._layoutRect = self._lastRepaintRect = self._lastLayoutRect = null; + self.initLayoutRect(); + }, + + /** + * Binds a callback to the specified event. This event can both be + * native browser events like "click" or custom ones like PostRender. + * + * The callback function will be passed a DOM event like object that enables yout do stop propagation. + * + * @method on + * @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; + + function resolveCallbackName(name) { + var callback, scope; + + if (typeof name != 'string') { + return name; + } + + return function(e) { + if (!callback) { + self.parentsAndSelf().each(function(ctrl) { + var callbacks = ctrl.settings.callbacks; + + if (callbacks && (callback = callbacks[name])) { + scope = ctrl; + return false; + } + }); + } + + if (!callback) { + e.action = name; + this.fire('execute', e); + return; + } + + return callback.call(scope, e); + }; + } + + getEventDispatcher(self).on(name, resolveCallbackName(callback)); + + return self; + }, + + /** + * Unbinds the specified event and optionally a specific callback. If you omit the name + * parameter all event handlers will be removed. If you omit the callback all event handles + * by the specified name will be removed. + * + * @method off + * @param {String} [name] Name for the event to unbind. + * @param {function} [callback] Callback function to unbind. + * @return {tinymce.ui.Control} Current control object. + */ + off: function(name, callback) { + 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. + * + * @method fire + * @param {String} name Name of the event to fire. + * @param {Object} [args] Arguments to pass to the event. + * @param {Boolean} [bubble] Value to control bubbling. Defaults to true. + * @return {Object} Current arguments object. + */ + fire: function(name, args, bubble) { + var self = this; + + args = args || {}; + + if (!args.control) { + args.control = self; + } + + args = getEventDispatcher(self).fire(name, args); + + // 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(); + } + } + + return args; + }, + + /** + * Returns true/false if the specified event has any listeners. + * + * @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 getEventDispatcher(this).has(name); + }, + + /** + * Returns a control collection with all parent controls. + * + * @method parents + * @param {String} selector Optional selector expression to find parents. + * @return {tinymce.ui.Collection} Collection with all parent controls. + */ + parents: function(selector) { + var self = this, ctrl, parents = new Collection(); + + // Add each parent to collection + for (ctrl = self.parent(); ctrl; ctrl = ctrl.parent()) { + parents.add(ctrl); + } + + // Filter away everything that doesn't match the selector + if (selector) { + parents = parents.filter(selector); + } + + 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. + */ + next: function() { + var parentControls = this.parent().items(); + + return parentControls[parentControls.indexOf(this) + 1]; + }, + + /** + * Returns the control previous to the current control. + * + * @method prev + * @return {tinymce.ui.Control} Previous control instance. + */ + prev: function() { + var parentControls = this.parent().items(); + + return parentControls[parentControls.indexOf(this) - 1]; + }, + + /** + * Sets the inner HTML of the control element. + * + * @method innerHtml + * @param {String} html Html string to set as inner html. + * @return {tinymce.ui.Control} Current control object. + */ + innerHtml: function(html) { + this.$el.html(html); + return this; + }, + + /** + * Returns the control DOM element or sub element. + * + * @method getEl + * @param {String} [suffix] Suffix to get element by. + * @return {Element} HTML DOM element for the current control or it's children. + */ + getEl: function(suffix) { + var id = suffix ? this._id + '-' + suffix : this._id; + + if (!this._elmCache[id]) { + this._elmCache[id] = $('#' + id)[0]; + } + + return this._elmCache[id]; + }, + + /** + * Sets the visible state to true. + * + * @method show + * @return {tinymce.ui.Control} Current control instance. + */ + show: function() { + return this.visible(true); + }, + + /** + * Sets the visible state to false. + * + * @method hide + * @return {tinymce.ui.Control} Current control instance. + */ + hide: function() { + return this.visible(false); + }, + + /** + * Focuses the current control. + * + * @method focus + * @return {tinymce.ui.Control} Current control instance. + */ + focus: function() { + try { + this.getEl().focus(); + } catch (ex) { + // Ignore IE error + } + + return this; + }, + + /** + * Blurs the current control. + * + * @method blur + * @return {tinymce.ui.Control} Current control instance. + */ + blur: function() { + this.getEl().blur(); + + return this; + }, + + /** + * Sets the specified aria property. + * + * @method aria + * @param {String} name Name of the aria property to set. + * @param {String} value Value of the aria property. + * @return {tinymce.ui.Control} Current control instance. + */ + aria: function(name, value) { + var self = this, elm = self.getEl(self.ariaTarget); + + if (typeof value === "undefined") { + return self._aria[name]; + } + + self._aria[name] = value; + + if (self.state.get('rendered')) { + elm.setAttribute(name == 'role' ? name : 'aria-' + name, value); + } + + return self; + }, + + /** + * Encodes the specified string with HTML entities. It will also + * translate the string to different languages. + * + * @method encode + * @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) { + 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. + */ + before: function(items) { + var self = this, parent = self.parent(); + + if (parent) { + parent.insert(items, parent.items().indexOf(self), true); + } + + return self; + }, + + /** + * Adds items after the current control. + * + * @method after + * @param {Array/tinymce.ui.Collection} items Array of items to append after this control. + * @return {tinymce.ui.Control} Current control instance. + */ + after: function(items) { + var self = this, parent = self.parent(); + + if (parent) { + parent.insert(items, parent.items().indexOf(self)); + } + + return self; + }, + + /** + * Removes the current control from DOM and from UI collections. + * + * @method remove + * @return {tinymce.ui.Control} Current control instance. + */ + remove: function() { + var self = this, elm = self.getEl(), parent = self.parent(), newItems, i; + + if (self.items) { + var controls = self.items().toArray(); + i = controls.length; + while (i--) { + controls[i].remove(); + } + } + + if (parent && parent.items) { + newItems = []; + + parent.items().each(function(item) { + if (item !== self) { + newItems.push(item); + } + }); + + parent.items().set(newItems); + parent._lastRect = null; + } + + if (self._eventsRoot && self._eventsRoot == self) { + $(elm).off(); + } + + var lookup = self.getRoot().controlIdLookup; + if (lookup) { + delete lookup[self._id]; + } + + if (elm && elm.parentNode) { + elm.parentNode.removeChild(elm); + } + + self.state.set('rendered', false); + self.state.destroy(); + + self.fire('remove'); + + return self; + }, + + /** + * Renders the control before the specified element. + * + * @method renderBefore + * @param {Element} elm Element to render before. + * @return {tinymce.ui.Control} Current control instance. + */ + renderBefore: function(elm) { + $(elm).before(this.renderHtml()); + this.postRender(); + return this; + }, + + /** + * Renders the control to the specified element. + * + * @method renderBefore + * @param {Element} elm Element to render to. + * @return {tinymce.ui.Control} Current control instance. + */ + renderTo: function(elm) { + $(elm || this.getContainerElm()).append(this.renderHtml()); + this.postRender(); + return this; + }, + + preRender: function() { + }, + + render: function() { + }, + + renderHtml: function() { + return '
'; + }, + + /** + * Post render method. Called after the control has been rendered to the target. + * + * @method postRender + * @return {tinymce.ui.Control} Current control instance. + */ + postRender: function() { + var self = this, settings = self.settings, elm, box, parent, name, parentEventsRoot; + + self.$el = $(self.getEl()); + self.state.set('rendered', true); + + // Bind on|b
+ * + * Will produce this on backspace: + *a|
would become|
in WebKit. + * With this patch:|
|
+ * + * Or: + *|
+ if (!isDefaultPrevented(e) && (keyCode == DELETE || keyCode == BACKSPACE)) { + isCollapsed = editor.selection.isCollapsed(); + body = editor.getBody(); + + // Selection is collapsed but the editor isn't empty + if (isCollapsed && !dom.isEmpty(body)) { + return; + } + + // Selection isn't collapsed but not all the contents is selected + if (!isCollapsed && !allContentsSelected(editor.selection.getRng())) { + return; + } + + // Manually empty the editor + e.preventDefault(); + editor.setContent(''); + + if (body.firstChild && dom.isBlock(body.firstChild)) { + editor.selection.setCursorLocation(body.firstChild, 0); + } else { + editor.selection.setCursorLocation(body, 0); + } + + editor.nodeChanged(); + } + }); + } + + /** + * WebKit doesn't select all the nodes in the body when you press Ctrl+A. + * IE selects more than the contents [a
] instead of[a]
see bug #6438 + * This selects the whole body so that backspace/delete logic will delete everything + */ + function selectAll() { + editor.shortcuts.add('meta+a', null, 'SelectAll'); + } + + /** + * WebKit has a weird issue where it some times fails to properly convert keypresses to input method keystrokes. + * The IME on Mac doesn't initialize when it doesn't fire a proper focus event. + * + * This seems to happen when the user manages to click the documentElement element then the window doesn't get proper focus until + * you enter a character into the editor. + * + * It also happens when the first focus in made to the body. + * + * See: https://bugs.webkit.org/show_bug.cgi?id=83566 + */ + function inputMethodFocus() { + if (!editor.settings.content_editable) { + // Case 1 IME doesn't initialize if you focus the document + // Disabled since it was interferring with the cE=false logic + // Also coultn't reproduce the issue on Safari 9 + /*dom.bind(editor.getDoc(), 'focusin', function() { + selection.setRng(selection.getRng()); + });*/ + + // Case 2 IME doesn't initialize if you click the documentElement it also doesn't properly fire the focusin event + // Needs to be both down/up due to weird rendering bug on Chrome Windows + dom.bind(editor.getDoc(), 'mousedown mouseup', function(e) { + var rng; + + if (e.target == editor.getDoc().documentElement) { + rng = selection.getRng(); + editor.getBody().focus(); + + if (e.type == 'mousedown') { + if (CaretContainer.isCaretContainer(rng.startContainer)) { + return; + } + + // Edge case for mousedown, drag select and mousedown again within selection on Chrome Windows to render caret + selection.placeCaretAt(e.clientX, e.clientY); + } else { + selection.setRng(rng); + } + } + }); + } + } + + /** + * Backspacing in FireFox/IE from a paragraph into a horizontal rule results in a floating text node because the + * browser just deletes the paragraph - the browser fails to merge the text node with a horizontal rule so it is + * left there. TinyMCE sees a floating text node and wraps it in a paragraph on the key up event (ForceBlocks.js + * addRootBlocks), meaning the action does nothing. With this code, FireFox/IE matche the behaviour of other + * browsers. + * + * It also fixes a bug on Firefox where it's impossible to delete HR elements. + */ + function removeHrOnBackspace() { + editor.on('keydown', function(e) { + if (!isDefaultPrevented(e) && e.keyCode === BACKSPACE) { + // Check if there is any HR elements this is faster since getRng on IE 7 & 8 is slow + if (!editor.getBody().getElementsByTagName('hr').length) { + return; + } + + if (selection.isCollapsed() && selection.getRng(true).startOffset === 0) { + var node = selection.getNode(); + var previousSibling = node.previousSibling; + + if (node.nodeName == 'HR') { + dom.remove(node); + e.preventDefault(); + return; + } + + if (previousSibling && previousSibling.nodeName && previousSibling.nodeName.toLowerCase() === "hr") { + dom.remove(previousSibling); + e.preventDefault(); + } + } + } + }); + } + + /** + * Firefox 3.x has an issue where the body element won't get proper focus if you click out + * side it's rectangle. + */ + function focusBody() { + // Fix for a focus bug in FF 3.x where the body element + // wouldn't get proper focus if the user clicked on the HTML element + if (!window.Range.prototype.getClientRects) { // Detect getClientRects got introduced in FF 4 + editor.on('mousedown', function(e) { + if (!isDefaultPrevented(e) && e.target.nodeName === "HTML") { + var body = editor.getBody(); + + // Blur the body it's focused but not correctly focused + body.blur(); + + // Refocus the body after a little while + Delay.setEditorTimeout(editor, function() { + body.focus(); + }); + } + }); + } + } + + /** + * WebKit has a bug where it isn't possible to select image, hr or anchor elements + * by clicking on them so we need to fake that. + */ + function selectControlElements() { + editor.on('click', function(e) { + var target = e.target; + + // Workaround for bug, http://bugs.webkit.org/show_bug.cgi?id=12250 + // WebKit can't even do simple things like selecting an image + // Needs to be the setBaseAndExtend or it will fail to select floated images + if (/^(IMG|HR)$/.test(target.nodeName) && dom.getContentEditableParent(target) !== "false") { + e.preventDefault(); + selection.getSel().setBaseAndExtent(target, 0, target, 1); + editor.nodeChanged(); + } + + if (target.nodeName == 'A' && dom.hasClass(target, 'mce-item-anchor')) { + e.preventDefault(); + selection.select(target); + } + }); + } + + /** + * Fixes a Gecko bug where the style attribute gets added to the wrong element when deleting between two block elements. + * + * Fixes do backspace/delete on this: + *bla[ck
r]ed
+ * + * Would become: + *bla|ed
+ * + * Instead of: + *bla|ed
+ */ + function removeStylesWhenDeletingAcrossBlockElements() { + function getAttributeApplyFunction() { + var template = dom.getAttribs(selection.getStart().cloneNode(false)); + + return function() { + var target = selection.getStart(); + + if (target !== editor.getBody()) { + dom.setAttrib(target, "style", null); + + each(template, function(attr) { + target.setAttributeNode(attr.cloneNode(true)); + }); + } + }; + } + + function isSelectionAcrossElements() { + return !selection.isCollapsed() && + dom.getParent(selection.getStart(), dom.isBlock) != dom.getParent(selection.getEnd(), dom.isBlock); + } + + editor.on('keypress', function(e) { + var applyAttributes; + + if (!isDefaultPrevented(e) && (e.keyCode == 8 || e.keyCode == 46) && isSelectionAcrossElements()) { + applyAttributes = getAttributeApplyFunction(); + editor.getDoc().execCommand('delete', false, null); + applyAttributes(); + e.preventDefault(); + return false; + } + }); + + dom.bind(editor.getDoc(), 'cut', function(e) { + var applyAttributes; + + if (!isDefaultPrevented(e) && isSelectionAcrossElements()) { + applyAttributes = getAttributeApplyFunction(); + + Delay.setEditorTimeout(editor, function() { + applyAttributes(); + }); + } + }); + } + + /** + * Screen readers on IE needs to have the role application set on the body. + */ + function ensureBodyHasRoleApplication() { + document.body.setAttribute("role", "application"); + } + + /** + * Backspacing into a table behaves differently depending upon browser type. + * Therefore, disable Backspace when cursor immediately follows a table. + */ + function disableBackspaceIntoATable() { + editor.on('keydown', function(e) { + if (!isDefaultPrevented(e) && e.keyCode === BACKSPACE) { + if (selection.isCollapsed() && selection.getRng(true).startOffset === 0) { + var previousSibling = selection.getNode().previousSibling; + if (previousSibling && previousSibling.nodeName && previousSibling.nodeName.toLowerCase() === "table") { + e.preventDefault(); + return false; + } + } + } + }); + } + + /** + * Old IE versions can't properly render BR elements in PRE tags white in contentEditable mode. So this + * logic adds a \n before the BR so that it will get rendered. + */ + function addNewLinesBeforeBrInPre() { + // IE8+ rendering mode does the right thing with BR in PRE + if (getDocumentMode() > 7) { + return; + } + + // Enable display: none in area and add a specific class that hides all BR elements in PRE to + // avoid the caret from getting stuck at the BR elements while pressing the right arrow key + setEditorCommandState('RespectVisibilityInDesign', true); + editor.contentStyles.push('.mceHideBrInPre pre br {display: none}'); + dom.addClass(editor.getBody(), 'mceHideBrInPre'); + + // Adds a \n before all BR elements in PRE to get them visual + parser.addNodeFilter('pre', function(nodes) { + var i = nodes.length, brNodes, j, brElm, sibling; + + while (i--) { + brNodes = nodes[i].getAll('br'); + j = brNodes.length; + while (j--) { + brElm = brNodes[j]; + + // Add \n before BR in PRE elements on older IE:s so the new lines get rendered + sibling = brElm.prev; + if (sibling && sibling.type === 3 && sibling.value.charAt(sibling.value - 1) != '\n') { + sibling.value += '\n'; + } else { + brElm.parent.insert(new Node('#text', 3), brElm, true).value = '\n'; + } + } + } + }); + + // Removes any \n before BR elements in PRE since other browsers and in contentEditable=false mode they will be visible + serializer.addNodeFilter('pre', function(nodes) { + var i = nodes.length, brNodes, j, brElm, sibling; + + while (i--) { + brNodes = nodes[i].getAll('br'); + j = brNodes.length; + while (j--) { + brElm = brNodes[j]; + sibling = brElm.prev; + if (sibling && sibling.type == 3) { + sibling.value = sibling.value.replace(/\r?\n$/, ''); + } + } + } + }); + } + + /** + * Moves style width/height to attribute width/height when the user resizes an image on IE. + */ + function removePreSerializedStylesWhenSelectingControls() { + dom.bind(editor.getBody(), 'mouseup', function() { + var value, node = selection.getNode(); + + // Moved styles to attributes on IMG eements + if (node.nodeName == 'IMG') { + // Convert style width to width attribute + if ((value = dom.getStyle(node, 'width'))) { + dom.setAttrib(node, 'width', value.replace(/[^0-9%]+/g, '')); + dom.setStyle(node, 'width', ''); + } + + // Convert style height to height attribute + if ((value = dom.getStyle(node, 'height'))) { + dom.setAttrib(node, 'height', value.replace(/[^0-9%]+/g, '')); + dom.setStyle(node, 'height', ''); + } + } + }); + } + + /** + * Removes a blockquote when backspace is pressed at the beginning of it. + * + * For example: + *+ * + * Becomes: + *|x
|x
+ */ + function removeBlockQuoteOnBackSpace() { + // Add block quote deletion handler + editor.on('keydown', function(e) { + var rng, container, offset, root, parent; + + if (isDefaultPrevented(e) || e.keyCode != VK.BACKSPACE) { + return; + } + + rng = selection.getRng(); + container = rng.startContainer; + offset = rng.startOffset; + root = dom.getRoot(); + parent = container; + + if (!rng.collapsed || offset !== 0) { + return; + } + + while (parent && parent.parentNode && parent.parentNode.firstChild == parent && parent.parentNode != root) { + parent = parent.parentNode; + } + + // Is the cursor at the beginning of a blockquote? + if (parent.tagName === 'BLOCKQUOTE') { + // Remove the blockquote + editor.formatter.toggle('blockquote', null, parent); + + // Move the caret to the beginning of container + rng = dom.createRng(); + rng.setStart(container, 0); + rng.setEnd(container, 0); + selection.setRng(rng); + } + }); + } + + /** + * Sets various Gecko editing options on mouse down and before a execCommand to disable inline table editing that is broken etc. + */ + function setGeckoEditingOptions() { + function setOpts() { + refreshContentEditable(); + + setEditorCommandState("StyleWithCSS", false); + setEditorCommandState("enableInlineTableEditing", false); + + if (!settings.object_resizing) { + setEditorCommandState("enableObjectResizing", false); + } + } + + if (!settings.readonly) { + editor.on('BeforeExecCommand MouseDown', setOpts); + } + } + + /** + * Fixes a gecko link bug, when a link is placed at the end of block elements there is + * no way to move the caret behind the link. This fix adds a bogus br element after the link. + * + * For example this: + * + * + * Becomes this: + * + */ + function addBrAfterLastLinks() { + function fixLinks() { + each(dom.select('a'), function(node) { + var parentNode = node.parentNode, root = dom.getRoot(); + + if (parentNode.lastChild === node) { + while (parentNode && !dom.isBlock(parentNode)) { + if (parentNode.parentNode.lastChild !== parentNode || parentNode === root) { + return; + } + + parentNode = parentNode.parentNode; + } + + dom.add(parentNode, 'br', {'data-mce-bogus': 1}); + } + }); + } + + editor.on('SetContent ExecCommand', function(e) { + if (e.type == "setcontent" || e.command === 'mceInsertLink') { + fixLinks(); + } + }); + } + + /** + * WebKit will produce DIV elements here and there by default. But since TinyMCE uses paragraphs by + * default we want to change that behavior. + */ + function setDefaultBlockType() { + if (settings.forced_root_block) { + editor.on('init', function() { + setEditorCommandState('DefaultParagraphSeparator', settings.forced_root_block); + }); + } + } + + /** + * Deletes the selected image on IE instead of navigating to previous page. + */ + function deleteControlItemOnBackSpace() { + editor.on('keydown', function(e) { + var rng; + + if (!isDefaultPrevented(e) && e.keyCode == BACKSPACE) { + rng = editor.getDoc().selection.createRange(); + if (rng && rng.item) { + e.preventDefault(); + editor.undoManager.beforeChange(); + dom.remove(rng.item(0)); + editor.undoManager.add(); + } + } + }); + } + + /** + * IE10 doesn't properly render block elements with the right height until you add contents to them. + * This fixes that by adding a padding-right to all empty text block elements. + * See: https://connect.microsoft.com/IE/feedback/details/743881 + */ + function renderEmptyBlocksFix() { + var emptyBlocksCSS; + + // IE10+ + if (getDocumentMode() >= 10) { + emptyBlocksCSS = ''; + each('p div h1 h2 h3 h4 h5 h6'.split(' '), function(name, i) { + emptyBlocksCSS += (i > 0 ? ',' : '') + name + ':empty'; + }); + + editor.contentStyles.push(emptyBlocksCSS + '{padding-right: 1px !important}'); + } + } + + /** + * Old IE versions can't retain contents within noscript elements so this logic will store the contents + * as a attribute and the insert that value as it's raw text when the DOM is serialized. + */ + function keepNoScriptContents() { + if (getDocumentMode() < 9) { + parser.addNodeFilter('noscript', function(nodes) { + var i = nodes.length, node, textNode; + + while (i--) { + node = nodes[i]; + textNode = node.firstChild; + + if (textNode) { + node.attr('data-mce-innertext', textNode.value); + } + } + }); + + serializer.addNodeFilter('noscript', function(nodes) { + var i = nodes.length, node, textNode, value; + + while (i--) { + node = nodes[i]; + textNode = nodes[i].firstChild; + + if (textNode) { + textNode.value = Entities.decode(textNode.value); + } else { + // Old IE can't retain noscript value so an attribute is used to store it + value = node.attributes.map['data-mce-innertext']; + if (value) { + node.attr('data-mce-innertext', null); + textNode = new Node('#text', 3); + textNode.value = value; + textNode.raw = true; + node.append(textNode); + } + } + } + }); + } + } + + /** + * IE has an issue where you can't select/move the caret by clicking outside the body if the document is in standards mode. + */ + function fixCaretSelectionOfDocumentElementOnIe() { + var doc = dom.doc, body = doc.body, started, startRng, htmlElm; + + // Return range from point or null if it failed + function rngFromPoint(x, y) { + var rng = body.createTextRange(); + + try { + rng.moveToPoint(x, y); + } catch (ex) { + // IE sometimes throws and exception, so lets just ignore it + rng = null; + } + + return rng; + } + + // Fires while the selection is changing + function selectionChange(e) { + var pointRng; + + // Check if the button is down or not + if (e.button) { + // Create range from mouse position + pointRng = rngFromPoint(e.x, e.y); + + if (pointRng) { + // Check if pointRange is before/after selection then change the endPoint + if (pointRng.compareEndPoints('StartToStart', startRng) > 0) { + pointRng.setEndPoint('StartToStart', startRng); + } else { + pointRng.setEndPoint('EndToEnd', startRng); + } + + pointRng.select(); + } + } else { + endSelection(); + } + } + + // Removes listeners + function endSelection() { + var rng = doc.selection.createRange(); + + // If the range is collapsed then use the last start range + if (startRng && !rng.item && rng.compareEndPoints('StartToEnd', rng) === 0) { + startRng.select(); + } + + dom.unbind(doc, 'mouseup', endSelection); + dom.unbind(doc, 'mousemove', selectionChange); + startRng = started = 0; + } + + // Make HTML element unselectable since we are going to handle selection by hand + doc.documentElement.unselectable = true; + + // Detect when user selects outside BODY + dom.bind(doc, 'mousedown contextmenu', function(e) { + if (e.target.nodeName === 'HTML') { + if (started) { + endSelection(); + } + + // Detect vertical scrollbar, since IE will fire a mousedown on the scrollbar and have target set as HTML + htmlElm = doc.documentElement; + if (htmlElm.scrollHeight > htmlElm.clientHeight) { + return; + } + + started = 1; + // Setup start position + startRng = rngFromPoint(e.x, e.y); + if (startRng) { + // Listen for selection change events + dom.bind(doc, 'mouseup', endSelection); + dom.bind(doc, 'mousemove', selectionChange); + + dom.getRoot().focus(); + startRng.select(); + } + } + }); + } + + /** + * Fixes selection issues where the caret can be placed between two inline elements like a|b + * this fix will lean the caret right into the closest inline element. + */ + function normalizeSelection() { + // Normalize selection for example a|a becomes a|a except for Ctrl+A since it selects everything + 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. + */ + function showBrokenImageIcon() { + editor.contentStyles.push( + 'img:-moz-broken {' + + '-moz-force-broken-image-icon:1;' + + 'min-width:24px;' + + 'min-height:24px' + + '}' + ); + } + + /** + * iOS has a bug where it's impossible to type if the document has a touchstart event + * bound and the user touches the document while having the on screen keyboard visible. + * + * The touch event moves the focus to the parent document while having the caret inside the iframe + * this fix moves the focus back into the iframe document. + */ + function restoreFocusOnKeyDown() { + if (!editor.inline) { + editor.on('keydown', function() { + if (document.activeElement == document.body) { + editor.getWin().focus(); + } + }); + } + } + + /** + * IE 11 has an annoying issue where you can't move focus into the editor + * by clicking on the white area HTML element. We used to be able to to fix this with + * the fixCaretSelectionOfDocumentElementOnIe fix. But since M$ removed the selection + * object it's not possible anymore. So we need to hack in a ungly CSS to force the + * body to be at least 150px. If the user clicks the HTML element out side this 150px region + * we simply move the focus into the first paragraph. Not ideal since you loose the + * positioning of the caret but goot enough for most cases. + */ + function bodyHeight() { + if (!editor.inline) { + editor.contentStyles.push('body {min-height: 150px}'); + editor.on('click', function(e) { + var rng; + + if (e.target.nodeName == 'HTML') { + // Edge seems to only need focus if we set the range + // the caret will become invisible and moved out of the iframe!! + if (Env.ie > 11) { + editor.getBody().focus(); + return; + } + + // Need to store away non collapsed ranges since the focus call will mess that up see #7382 + rng = editor.selection.getRng(); + editor.getBody().focus(); + editor.selection.setRng(rng); + editor.selection.normalize(); + editor.nodeChanged(); + } + }); + } + } + + /** + * Firefox on Mac OS will move the browser back to the previous page if you press CMD+Left arrow. + * You might then loose all your work so we need to block that behavior and replace it with our own. + */ + function blockCmdArrowNavigation() { + if (Env.mac) { + editor.on('keydown', function(e) { + if (VK.metaKeyPressed(e) && !e.shiftKey && (e.keyCode == 37 || e.keyCode == 39)) { + e.preventDefault(); + editor.selection.getSel().modify('move', e.keyCode == 37 ? 'backward' : 'forward', 'lineboundary'); + } + }); + } + } + + /** + * Disables the autolinking in IE 9+ this is then re-enabled by the autolink plugin. + */ + function disableAutoUrlDetect() { + setEditorCommandState("AutoUrlDetect", false); + } + + /** + * 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}'); + } + + /** + * iOS Safari and possible other browsers have a bug where it won't fire + * a click event when a contentEditable is focused. This function fakes click events + * by using touchstart/touchend and measuring the time and distance travelled. + */ + /* + function touchClickEvent() { + editor.on('touchstart', function(e) { + var elm, time, startTouch, changedTouches; + + elm = e.target; + time = new Date().getTime(); + changedTouches = e.changedTouches; + + if (!changedTouches || changedTouches.length > 1) { + return; + } + + startTouch = changedTouches[0]; + + editor.once('touchend', function(e) { + var endTouch = e.changedTouches[0], args; + + if (new Date().getTime() - time > 500) { + return; + } + + if (Math.abs(startTouch.clientX - endTouch.clientX) > 5) { + return; + } + + if (Math.abs(startTouch.clientY - endTouch.clientY) > 5) { + return; + } + + args = { + target: elm + }; + + each('pageX pageY clientX clientY screenX screenY'.split(' '), function(key) { + args[key] = endTouch[key]; + }); + + args = editor.fire('click', args); + + if (!args.isDefaultPrevented()) { + // iOS WebKit can't place the caret properly once + // you bind touch events so we need to do this manually + // TODO: Expand to the closest word? Touble tap still works. + editor.selection.placeCaretAt(endTouch.clientX, endTouch.clientY); + editor.nodeChanged(); + } + }); + }); + } + */ + + /** + * WebKit has a bug where it will allow forms to be submitted if they are inside a contentEditable element. + * For example this: + */ + function blockFormSubmitInsideEditor() { + editor.on('init', function() { + editor.dom.bind(editor.getBody(), 'submit', function(e) { + e.preventDefault(); + }); + }); + } + + /** + * Sometimes WebKit/Blink generates BR elements with the Apple-interchange-newline class. + * + * Scenario: + * 1) Create a table 2x2. + * 2) Select and copy cells A2-B2. + * 3) Paste and it will add BR element to table cell. + */ + function removeAppleInterchangeBrs() { + parser.addNodeFilter('br', function(nodes) { + var i = nodes.length; + + while (i--) { + if (nodes[i].attr('class') == 'Apple-interchange-newline') { + nodes[i].remove(); + } + } + }); + } + + /** + * IE cannot set custom contentType's on drag events, and also does not properly drag/drop between + * editors. This uses a special data:text/mce-internal URL to pass data when drag/drop between editors. + */ + function ieInternalDragAndDrop() { + editor.on('dragstart', function(e) { + setMceInternalContent(e); + }); + + editor.on('drop', function(e) { + if (!isDefaultPrevented(e)) { + var internalContent = getMceInternalContent(e); + + if (internalContent && internalContent.id != editor.id) { + e.preventDefault(); + + var rng = RangeUtils.getCaretRangeFromPoint(e.x, e.y, editor.getDoc()); + selection.setRng(rng); + insertClipboardContents(internalContent.html); + } + } + }); + } + + function refreshContentEditable() { + // No-op since Mozilla seems to have fixed the caret repaint issues + } + + function isHidden() { + var sel; + + if (!isGecko) { + return 0; + } + + // Weird, wheres that cursor selection? + sel = editor.selection.getSel(); + return (!sel || !sel.rangeCount || sel.rangeCount === 0); + } + + /** + * Properly empties the editor if all contents is selected and deleted this to + * prevent empty paragraphs from being produced at beginning/end of contents. + */ + function emptyEditorOnDeleteEverything() { + function isEverythingSelected(editor) { + var caretWalker = new CaretWalker(editor.getBody()); + var rng = editor.selection.getRng(); + var startCaretPos = CaretPosition.fromRangeStart(rng); + var endCaretPos = CaretPosition.fromRangeEnd(rng); + var prev = caretWalker.prev(startCaretPos); + var next = caretWalker.next(endCaretPos); + + return !editor.selection.isCollapsed() && + (!prev || prev.isAtStart()) && + (!next || (next.isAtEnd() && startCaretPos.getNode() !== next.getNode())); + } + + // Type over case delete and insert this won't cover typeover with a IME but at least it covers the common case + editor.on('keypress', function (e) { + if (!isDefaultPrevented(e) && !selection.isCollapsed() && e.charCode > 31 && !VK.metaKeyPressed(e)) { + if (isEverythingSelected(editor)) { + e.preventDefault(); + editor.setContent(String.fromCharCode(e.charCode)); + editor.selection.select(editor.getBody(), true); + editor.selection.collapse(false); + editor.nodeChanged(); + } + } + }); + + editor.on('keydown', function (e) { + var keyCode = e.keyCode; + + if (!isDefaultPrevented(e) && (keyCode == DELETE || keyCode == BACKSPACE)) { + if (isEverythingSelected(editor)) { + e.preventDefault(); + editor.setContent(''); + editor.nodeChanged(); + } + } + }); + } + + // All browsers + removeBlockQuoteOnBackSpace(); + emptyEditorWhenDeleting(); + + // Windows phone will return a range like [body, 0] on mousedown so + // it will always normalize to the wrong location + if (!Env.windowsPhone) { + normalizeSelection(); + } + + // WebKit + if (isWebKit) { + emptyEditorOnDeleteEverything(); + cleanupStylesWhenDeleting(); + inputMethodFocus(); + selectControlElements(); + setDefaultBlockType(); + blockFormSubmitInsideEditor(); + disableBackspaceIntoATable(); + removeAppleInterchangeBrs(); + + //touchClickEvent(); + + // iOS + if (Env.iOS) { + restoreFocusOnKeyDown(); + bodyHeight(); + tapLinksAndImages(); + } else { + selectAll(); + } + } + + // IE + if (isIE && Env.ie < 11) { + removeHrOnBackspace(); + ensureBodyHasRoleApplication(); + addNewLinesBeforeBrInPre(); + removePreSerializedStylesWhenSelectingControls(); + deleteControlItemOnBackSpace(); + renderEmptyBlocksFix(); + keepNoScriptContents(); + fixCaretSelectionOfDocumentElementOnIe(); + } + + if (Env.ie >= 11) { + bodyHeight(); + disableBackspaceIntoATable(); + } + + if (Env.ie) { + selectAll(); + disableAutoUrlDetect(); + ieInternalDragAndDrop(); + } + + // Gecko + if (isGecko) { + emptyEditorOnDeleteEverything(); + removeHrOnBackspace(); + focusBody(); + removeStylesWhenDeletingAcrossBlockElements(); + setGeckoEditingOptions(); + addBrAfterLastLinks(); + showBrokenImageIcon(); + blockCmdArrowNavigation(); + disableBackspaceIntoATable(); + } + + return { + refreshContentEditable: refreshContentEditable, + isHidden: isHidden + }; + }; +}); + +// Included from: js/tinymce/classes/EditorObservable.js + +/** + * EditorObservable.js + * + * Released under LGPL License. + * Copyright (c) 1999-2015 Ephox Corp. All rights reserved + * + * License: http://www.tinymce.com/license + * Contributing: http://www.tinymce.com/contributing + */ + +/** + * 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, customEventRootDelegates; + + /** + * Returns the event target so for the specified event. Some events fire + * only on document, some fire on documentElement etc. This also handles the + * custom event root setting where it returns that element instead of the body. + * + * @private + * @param {tinymce.Editor} editor Editor instance to get event target from. + * @param {String} eventName Name of the event for example "click". + * @return {Element/Document} HTML Element or document target to bind on. + */ + 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|touch|click|contextmenu|drop|dragover|dragend/.test(eventName)) { + return editor.getDoc().documentElement; + } + + // Bind to event root instead of body if it's defined + if (editor.settings.event_root) { + if (!editor.eventRoot) { + editor.eventRoot = DOM.select(editor.settings.event_root)[0]; + } + + return editor.eventRoot; + } + + return editor.getBody(); + } + + /** + * Binds a event delegate for the specified name this delegate will fire + * the event to the editor dispatcher. + * + * @private + * @param {tinymce.Editor} editor Editor instance to get event target from. + * @param {String} eventName Name of the event for example "click". + */ + function bindEventDelegate(editor, eventName) { + var eventRootElm = getEventTarget(editor, eventName), delegate; + + function isListening(editor) { + return !editor.hidden && !editor.readonly; + } + + if (!editor.delegates) { + editor.delegates = {}; + } + + if (editor.delegates[eventName]) { + return; + } + + if (editor.settings.event_root) { + if (!customEventRootDelegates) { + customEventRootDelegates = {}; + editor.editorManager.on('removeEditor', function() { + var name; + + if (!editor.editorManager.activeEditor) { + if (customEventRootDelegates) { + for (name in customEventRootDelegates) { + editor.dom.unbind(getEventTarget(editor, name)); + } + + customEventRootDelegates = null; + } + } + }); + } + + if (customEventRootDelegates[eventName]) { + return; + } + + delegate = function(e) { + var target = e.target, editors = editor.editorManager.editors, i = editors.length; + + while (i--) { + var body = editors[i].getBody(); + + if (body === target || DOM.isChildOf(target, body)) { + if (isListening(editors[i])) { + editors[i].fire(eventName, e); + } + } + } + }; + + customEventRootDelegates[eventName] = delegate; + DOM.bind(eventRootElm, eventName, delegate); + } else { + delegate = function(e) { + if (isListening(editor)) { + editor.fire(eventName, e); + } + }; + + DOM.bind(eventRootElm, eventName, delegate); + editor.delegates[eventName] = delegate; + } + } + + var EditorObservable = { + /** + * Bind any pending event delegates. This gets executed after the target body/document is created. + * + * @private + */ + bindPendingEventDelegates: function() { + var self = this; + + Tools.each(self._pendingNativeEvents, function(name) { + bindEventDelegate(self, name); + }); + }, + + /** + * Toggles a native event on/off this is called by the EventDispatcher when + * the first native event handler is added and when the last native event handler is removed. + * + * @private + */ + toggleNativeEvent: function(name, state) { + var self = this; + + // 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, self.delegates[name]); + delete self.delegates[name]; + } + }, + + /** + * Unbinds all native event handlers that means delegates, custom events bound using the Events API etc. + * + * @private + */ + unbindAllNativeEvents: function() { + var self = this, name; + + if (self.delegates) { + for (name in self.delegates) { + self.dom.unbind(getEventTarget(self, name), name, self.delegates[name]); + } + + delete self.delegates; + } + + if (!self.inline) { + self.getBody().onload = null; + self.dom.unbind(self.getWin()); + self.dom.unbind(self.getDoc()); + } + + self.dom.unbind(self.getBody()); + self.dom.unbind(self.getContainer()); + } + }; + + EditorObservable = Tools.extend({}, Observable, EditorObservable); + + return EditorObservable; +}); + +// Included from: js/tinymce/classes/Mode.js + +/** + * Mode.js + * + * Released under LGPL License. + * Copyright (c) 1999-2015 Ephox Corp. All rights reserved + * + * License: http://www.tinymce.com/license + * Contributing: http://www.tinymce.com/contributing + */ + +/** + * Mode switcher logic. + * + * @private + * @class tinymce.Mode + */ +define("tinymce/Mode", [], function() { + function setEditorCommandState(editor, cmd, state) { + try { + editor.getDoc().execCommand(cmd, false, state); + } catch (ex) { + // Ignore + } + } + + function clickBlocker(editor) { + var target, handler; + + target = editor.getBody(); + + handler = function(e) { + if (editor.dom.getParents(e.target, 'a').length > 0) { + e.preventDefault(); + } + }; + + editor.dom.bind(target, 'click', handler); + + return { + unbind: function() { + editor.dom.unbind(target, 'click', handler); + } + }; + } + + function toggleReadOnly(editor, state) { + if (editor._clickBlocker) { + editor._clickBlocker.unbind(); + editor._clickBlocker = null; + } + + if (state) { + editor._clickBlocker = clickBlocker(editor); + editor.selection.controlSelection.hideResizeRect(); + editor.readonly = true; + editor.getBody().contentEditable = false; + } else { + editor.readonly = false; + editor.getBody().contentEditable = true; + setEditorCommandState(editor, "StyleWithCSS", false); + setEditorCommandState(editor, "enableInlineTableEditing", false); + setEditorCommandState(editor, "enableObjectResizing", false); + editor.focus(); + editor.nodeChanged(); + } + } + + function setMode(editor, mode) { + var currentMode = editor.readonly ? 'readonly' : 'design'; + + if (mode == currentMode) { + return; + } + + if (editor.initialized) { + toggleReadOnly(editor, mode == 'readonly'); + } else { + editor.on('init', function() { + toggleReadOnly(editor, mode == 'readonly'); + }); + } + + // Event is NOT preventable + editor.fire('SwitchMode', {mode: mode}); + } + + return { + setMode: setMode + }; +}); + +// Included from: js/tinymce/classes/Shortcuts.js + +/** + * Shortcuts.js + * + * Released under LGPL License. + * Copyright (c) 1999-2015 Ephox Corp. All rights reserved + * + * License: http://www.tinymce.com/license + * Contributing: http://www.tinymce.com/contributing + */ + +/** + * Contains all logic for handling of keyboard shortcuts. + * + * @class tinymce.Shortcuts + * @example + * editor.shortcuts.add('ctrl+a', function() {}); + * editor.shortcuts.add('meta+a', function() {}); // "meta" maps to Command on Mac and Ctrl on PC + * editor.shortcuts.add('ctrl+alt+a', function() {}); + * editor.shortcuts.add('access+a', function() {}); // "access" maps to ctrl+alt on Mac and shift+alt on PC + */ +define("tinymce/Shortcuts", [ + "tinymce/util/Tools", + "tinymce/Env" +], function(Tools, Env) { + var each = Tools.each, explode = Tools.explode; + + var keyCodeLookup = { + "f9": 120, + "f10": 121, + "f11": 122 + }; + + var modifierNames = Tools.makeMap('alt,ctrl,shift,meta,access'); + + return function(editor) { + var self = this, shortcuts = {}, pendingPatterns = []; + + function parseShortcut(pattern) { + var id, key, shortcut = {}; + + // Parse modifiers and keys ctrl+alt+b for example + each(explode(pattern, '+'), function(value) { + if (value in modifierNames) { + shortcut[value] = true; + } else { + // Allow numeric keycodes like ctrl+219 for ctrl+[ + if (/^[0-9]{2,}$/.test(value)) { + shortcut.keyCode = parseInt(value, 10); + } else { + shortcut.charCode = value.charCodeAt(0); + shortcut.keyCode = keyCodeLookup[value] || value.toUpperCase().charCodeAt(0); + } + } + }); + + // Generate unique id for modifier combination and set default state for unused modifiers + id = [shortcut.keyCode]; + for (key in modifierNames) { + if (shortcut[key]) { + id.push(key); + } else { + shortcut[key] = false; + } + } + shortcut.id = id.join(','); + + // Handle special access modifier differently depending on Mac/Win + if (shortcut.access) { + shortcut.alt = true; + + if (Env.mac) { + shortcut.ctrl = true; + } else { + shortcut.shift = true; + } + } + + // Handle special meta modifier differently depending on Mac/Win + if (shortcut.meta) { + if (Env.mac) { + shortcut.meta = true; + } else { + shortcut.ctrl = true; + shortcut.meta = false; + } + } + + return shortcut; + } + + function createShortcut(pattern, desc, cmdFunc, scope) { + var shortcuts; + + shortcuts = Tools.map(explode(pattern, '>'), parseShortcut); + shortcuts[shortcuts.length - 1] = Tools.extend(shortcuts[shortcuts.length - 1], { + func: cmdFunc, + scope: scope || editor + }); + + return Tools.extend(shortcuts[0], { + desc: editor.translate(desc), + subpatterns: shortcuts.slice(1) + }); + } + + function hasModifier(e) { + return e.altKey || e.ctrlKey || e.metaKey; + } + + function isFunctionKey(e) { + return e.type === "keydown" && e.keyCode >= 112 && e.keyCode <= 123; + } + + function matchShortcut(e, shortcut) { + if (!shortcut) { + return false; + } + + if (shortcut.ctrl != e.ctrlKey || shortcut.meta != e.metaKey) { + return false; + } + + if (shortcut.alt != e.altKey || shortcut.shift != e.shiftKey) { + return false; + } + + if (e.keyCode == shortcut.keyCode || (e.charCode && e.charCode == shortcut.charCode)) { + e.preventDefault(); + return true; + } + + return false; + } + + function executeShortcutAction(shortcut) { + return shortcut.func ? shortcut.func.call(shortcut.scope) : null; + } + + editor.on('keyup keypress keydown', function(e) { + if ((hasModifier(e) || isFunctionKey(e)) && !e.isDefaultPrevented()) { + each(shortcuts, function(shortcut) { + if (matchShortcut(e, shortcut)) { + pendingPatterns = shortcut.subpatterns.slice(0); + + if (e.type == "keydown") { + executeShortcutAction(shortcut); + } + + return true; + } + }); + + if (matchShortcut(e, pendingPatterns[0])) { + if (pendingPatterns.length === 1) { + if (e.type == "keydown") { + executeShortcutAction(pendingPatterns[0]); + } + } + + pendingPatterns.shift(); + } + } + }); + + /** + * Adds a keyboard shortcut for some command or function. + * + * @method add + * @param {String} pattern Shortcut pattern. Like for example: ctrl+alt+o. + * @param {String} desc Text description for the command. + * @param {String/Function} cmdFunc Command name string or function to execute when the key is pressed. + * @param {Object} scope Optional scope to execute the function in. + * @return {Boolean} true/false state if the shortcut was added or not. + */ + self.add = function(pattern, desc, cmdFunc, scope) { + var cmd; + + cmd = cmdFunc; + + if (typeof cmdFunc === 'string') { + cmdFunc = function() { + editor.execCommand(cmd, false, null); + }; + } else if (Tools.isArray(cmd)) { + cmdFunc = function() { + editor.execCommand(cmd[0], cmd[1], cmd[2]); + }; + } + + each(explode(Tools.trim(pattern.toLowerCase())), function(pattern) { + var shortcut = createShortcut(pattern, desc, cmdFunc, scope); + shortcuts[shortcut.id] = shortcut; + }); + + return true; + }; + + /** + * Remove a keyboard shortcut by pattern. + * + * @method remove + * @param {String} pattern Shortcut pattern. Like for example: ctrl+alt+o. + * @return {Boolean} true/false state if the shortcut was removed or not. + */ + self.remove = function(pattern) { + var shortcut = createShortcut(pattern); + + if (shortcuts[shortcut.id]) { + delete shortcuts[shortcut.id]; + return true; + } + + return false; + }; + }; +}); + +// Included from: js/tinymce/classes/file/Uploader.js + +/** + * Uploader.js + * + * Released under LGPL License. + * Copyright (c) 1999-2015 Ephox Corp. All rights reserved + * + * License: http://www.tinymce.com/license + * Contributing: http://www.tinymce.com/contributing + */ + +/** + * Upload blobs or blob infos to the specified URL or handler. + * + * @private + * @class tinymce.file.Uploader + * @example + * var uploader = new Uploader({ + * url: '/upload.php', + * basePath: '/base/path', + * credentials: true, + * handler: function(data, success, failure) { + * ... + * } + * }); + * + * uploader.upload(blobInfos).then(function(result) { + * ... + * }); + */ +define("tinymce/file/Uploader", [ + "tinymce/util/Promise", + "tinymce/util/Tools", + "tinymce/util/Fun" +], function(Promise, Tools, Fun) { + return function(uploadStatus, settings) { + var pendingPromises = {}; + + function filename(blobInfo) { + var ext, extensions; + + extensions = { + 'image/jpeg': 'jpg', + 'image/jpg': 'jpg', + 'image/gif': 'gif', + 'image/png': 'png' + }; + + ext = extensions[blobInfo.blob().type.toLowerCase()] || 'dat'; + + return blobInfo.filename() + '.' + ext; + } + + function pathJoin(path1, path2) { + if (path1) { + return path1.replace(/\/$/, '') + '/' + path2.replace(/^\//, ''); + } + + return path2; + } + + function blobInfoToData(blobInfo) { + return { + id: blobInfo.id, + blob: blobInfo.blob, + base64: blobInfo.base64, + filename: Fun.constant(filename(blobInfo)) + }; + } + + function defaultHandler(blobInfo, success, failure, progress) { + var xhr, formData; + + xhr = new XMLHttpRequest(); + xhr.open('POST', settings.url); + xhr.withCredentials = settings.credentials; + + xhr.upload.onprogress = function(e) { + progress(e.loaded / e.total * 100); + }; + + xhr.onerror = function() { + failure("Image upload failed due to a XHR Transport error. Code: " + xhr.status); + }; + + xhr.onload = function() { + var json; + + if (xhr.status != 200) { + failure("HTTP Error: " + xhr.status); + return; + } + + json = JSON.parse(xhr.responseText); + + if (!json || typeof json.location != "string") { + failure("Invalid JSON: " + xhr.responseText); + return; + } + + success(pathJoin(settings.basePath, json.location)); + }; + + formData = new FormData(); + formData.append('file', blobInfo.blob(), blobInfo.filename()); + + xhr.send(formData); + } + + function noUpload() { + return new Promise(function(resolve) { + resolve([]); + }); + } + + function handlerSuccess(blobInfo, url) { + return { + url: url, + blobInfo: blobInfo, + status: true + }; + } + + function handlerFailure(blobInfo, error) { + return { + url: '', + blobInfo: blobInfo, + status: false, + error: error + }; + } + + function resolvePending(blobUri, result) { + Tools.each(pendingPromises[blobUri], function(resolve) { + resolve(result); + }); + + delete pendingPromises[blobUri]; + } + + function uploadBlobInfo(blobInfo, handler, openNotification) { + uploadStatus.markPending(blobInfo.blobUri()); + + return new Promise(function(resolve) { + var notification, progress; + + var noop = function() { + }; + + try { + var closeNotification = function() { + if (notification) { + notification.close(); + progress = noop; // Once it's closed it's closed + } + }; + + var success = function(url) { + closeNotification(); + uploadStatus.markUploaded(blobInfo.blobUri(), url); + resolvePending(blobInfo.blobUri(), handlerSuccess(blobInfo, url)); + resolve(handlerSuccess(blobInfo, url)); + }; + + var failure = function(error) { + closeNotification(); + uploadStatus.removeFailed(blobInfo.blobUri()); + resolvePending(blobInfo.blobUri(), handlerFailure(blobInfo, error)); + resolve(handlerFailure(blobInfo, error)); + }; + + progress = function(percent) { + if (percent < 0 || percent > 100) { + return; + } + + if (!notification) { + notification = openNotification(); + } + + notification.progressBar.value(percent); + }; + + handler(blobInfoToData(blobInfo), success, failure, progress); + } catch (ex) { + resolve(handlerFailure(blobInfo, ex.message)); + } + }); + } + + function isDefaultHandler(handler) { + return handler === defaultHandler; + } + + function pendingUploadBlobInfo(blobInfo) { + var blobUri = blobInfo.blobUri(); + + return new Promise(function(resolve) { + pendingPromises[blobUri] = pendingPromises[blobUri] || []; + pendingPromises[blobUri].push(resolve); + }); + } + + function uploadBlobs(blobInfos, openNotification) { + blobInfos = Tools.grep(blobInfos, function(blobInfo) { + return !uploadStatus.isUploaded(blobInfo.blobUri()); + }); + + return Promise.all(Tools.map(blobInfos, function(blobInfo) { + return uploadStatus.isPending(blobInfo.blobUri()) ? + pendingUploadBlobInfo(blobInfo) : uploadBlobInfo(blobInfo, settings.handler, openNotification); + })); + } + + function upload(blobInfos, openNotification) { + return (!settings.url && isDefaultHandler(settings.handler)) ? noUpload() : uploadBlobs(blobInfos, openNotification); + } + + settings = Tools.extend({ + credentials: false, + // We are adding a notify argument to this (at the moment, until it doesn't work) + handler: defaultHandler + }, settings); + + return { + upload: upload + }; + }; +}); + +// Included from: js/tinymce/classes/file/Conversions.js + +/** + * Conversions.js + * + * Released under LGPL License. + * Copyright (c) 1999-2015 Ephox Corp. All rights reserved + * + * License: http://www.tinymce.com/license + * Contributing: http://www.tinymce.com/contributing + */ + +/** + * Converts blob/uris back and forth. + * + * @private + * @class tinymce.file.Conversions + */ +define("tinymce/file/Conversions", [ + "tinymce/util/Promise" +], function(Promise) { + function blobUriToBlob(url) { + return new Promise(function(resolve) { + var xhr = new XMLHttpRequest(); + + xhr.open('GET', url, true); + xhr.responseType = 'blob'; + + xhr.onload = function() { + if (this.status == 200) { + resolve(this.response); + } + }; + + xhr.send(); + }); + } + + function parseDataUri(uri) { + var type, matches; + + uri = decodeURIComponent(uri).split(','); + + matches = /data:([^;]+)/.exec(uri[0]); + if (matches) { + type = matches[1]; + } + + return { + type: type, + data: uri[1] + }; + } + + function dataUriToBlob(uri) { + return new Promise(function(resolve) { + var str, arr, i; + + uri = parseDataUri(uri); + + // Might throw error if data isn't proper base64 + try { + str = atob(uri.data); + } catch (e) { + resolve(new Blob([])); + return; + } + + arr = new Uint8Array(str.length); + + for (i = 0; i < arr.length; i++) { + arr[i] = str.charCodeAt(i); + } + + resolve(new Blob([arr], {type: uri.type})); + }); + } + + function uriToBlob(url) { + if (url.indexOf('blob:') === 0) { + return blobUriToBlob(url); + } + + if (url.indexOf('data:') === 0) { + return dataUriToBlob(url); + } + + return null; + } + + function blobToDataUri(blob) { + return new Promise(function(resolve) { + var reader = new FileReader(); + + reader.onloadend = function() { + resolve(reader.result); + }; + + reader.readAsDataURL(blob); + }); + } + + return { + uriToBlob: uriToBlob, + blobToDataUri: blobToDataUri, + parseDataUri: parseDataUri + }; +}); + +// Included from: js/tinymce/classes/file/ImageScanner.js + +/** + * ImageScanner.js + * + * Released under LGPL License. + * Copyright (c) 1999-2015 Ephox Corp. All rights reserved + * + * License: http://www.tinymce.com/license + * Contributing: http://www.tinymce.com/contributing + */ + +/** + * Finds images with data uris or blob uris. If data uris are found it will convert them into blob uris. + * + * @private + * @class tinymce.file.ImageScanner + */ +define("tinymce/file/ImageScanner", [ + "tinymce/util/Promise", + "tinymce/util/Arr", + "tinymce/util/Fun", + "tinymce/file/Conversions", + "tinymce/Env" +], function(Promise, Arr, Fun, Conversions, Env) { + var count = 0; + + var uniqueId = function(prefix) { + return (prefix || 'blobid') + (count++); + }; + + return function(uploadStatus, blobCache) { + var cachedPromises = {}; + + function findAll(elm, predicate) { + var images, promises; + + function imageToBlobInfo(img, resolve) { + var base64, blobInfo; + + if (img.src.indexOf('blob:') === 0) { + blobInfo = blobCache.getByUri(img.src); + + if (blobInfo) { + resolve({ + image: img, + blobInfo: blobInfo + }); + } else { + Conversions.uriToBlob(img.src).then(function (blob) { + Conversions.blobToDataUri(blob).then(function (dataUri) { + base64 = Conversions.parseDataUri(dataUri).data; + blobInfo = blobCache.create(uniqueId(), blob, base64); + blobCache.add(blobInfo); + + resolve({ + image: img, + blobInfo: blobInfo + }); + }); + }); + } + + return; + } + + base64 = Conversions.parseDataUri(img.src).data; + blobInfo = blobCache.findFirst(function(cachedBlobInfo) { + return cachedBlobInfo.base64() === base64; + }); + + if (blobInfo) { + resolve({ + image: img, + blobInfo: blobInfo + }); + } else { + Conversions.uriToBlob(img.src).then(function(blob) { + blobInfo = blobCache.create(uniqueId(), blob, base64); + blobCache.add(blobInfo); + + resolve({ + image: img, + blobInfo: blobInfo + }); + }); + } + } + + if (!predicate) { + predicate = Fun.constant(true); + } + + images = Arr.filter(elm.getElementsByTagName('img'), function(img) { + var src = img.src; + + if (!Env.fileApi) { + return false; + } + + if (img.hasAttribute('data-mce-bogus')) { + return false; + } + + if (img.hasAttribute('data-mce-placeholder')) { + return false; + } + + if (!src || src == Env.transparentSrc) { + return false; + } + + if (src.indexOf('blob:') === 0) { + return !uploadStatus.isUploaded(src); + } + + if (src.indexOf('data:') === 0) { + return predicate(img); + } + + return false; + }); + + promises = Arr.map(images, function(img) { + var newPromise; + + if (cachedPromises[img.src]) { + // Since the cached promise will return the cached image + // We need to wrap it and resolve with the actual image + return new Promise(function(resolve) { + cachedPromises[img.src].then(function(imageInfo) { + resolve({ + image: img, + blobInfo: imageInfo.blobInfo + }); + }); + }); + } + + newPromise = new Promise(function(resolve) { + imageToBlobInfo(img, resolve); + }).then(function(result) { + delete cachedPromises[result.image.src]; + return result; + })['catch'](function(error) { + delete cachedPromises[img.src]; + return error; + }); + + cachedPromises[img.src] = newPromise; + + return newPromise; + }); + + return Promise.all(promises); + } + + return { + findAll: findAll + }; + }; +}); + +// Included from: js/tinymce/classes/file/BlobCache.js + +/** + * BlobCache.js + * + * Released under LGPL License. + * Copyright (c) 1999-2015 Ephox Corp. All rights reserved + * + * License: http://www.tinymce.com/license + * Contributing: http://www.tinymce.com/contributing + */ + +/** + * Hold blob info objects where a blob has extra internal information. + * + * @private + * @class tinymce.file.BlobCache + */ +define("tinymce/file/BlobCache", [ + "tinymce/util/Arr", + "tinymce/util/Fun" +], function(Arr, Fun) { + return function() { + var cache = [], constant = Fun.constant; + + function create(id, blob, base64, filename) { + return { + id: constant(id), + filename: constant(filename || id), + blob: constant(blob), + base64: constant(base64), + blobUri: constant(URL.createObjectURL(blob)) + }; + } + + function add(blobInfo) { + if (!get(blobInfo.id())) { + cache.push(blobInfo); + } + } + + function get(id) { + return findFirst(function(cachedBlobInfo) { + return cachedBlobInfo.id() === id; + }); + } + + function findFirst(predicate) { + return Arr.filter(cache, predicate)[0]; + } + + function getByUri(blobUri) { + return findFirst(function(blobInfo) { + return blobInfo.blobUri() == blobUri; + }); + } + + function removeByUri(blobUri) { + cache = Arr.filter(cache, function(blobInfo) { + if (blobInfo.blobUri() === blobUri) { + URL.revokeObjectURL(blobInfo.blobUri()); + return false; + } + + return true; + }); + } + + function destroy() { + Arr.each(cache, function(cachedBlobInfo) { + URL.revokeObjectURL(cachedBlobInfo.blobUri()); + }); + + cache = []; + } + + return { + create: create, + add: add, + get: get, + getByUri: getByUri, + findFirst: findFirst, + removeByUri: removeByUri, + destroy: destroy + }; + }; +}); + +// Included from: js/tinymce/classes/file/UploadStatus.js + +/** + * UploadStatus.js + * + * Released under LGPL License. + * Copyright (c) 1999-2016 Ephox Corp. All rights reserved + * + * License: http://www.tinymce.com/license + * Contributing: http://www.tinymce.com/contributing + */ + +/** + * Holds the current status of a blob uri, if it's pending or uploaded and what the result urls was. + * + * @private + * @class tinymce.file.UploadStatus + */ +define("tinymce/file/UploadStatus", [ +], function() { + return function() { + var PENDING = 1, UPLOADED = 2; + var blobUriStatuses = {}; + + function createStatus(status, resultUri) { + return { + status: status, + resultUri: resultUri + }; + } + + function hasBlobUri(blobUri) { + return blobUri in blobUriStatuses; + } + + function getResultUri(blobUri) { + var result = blobUriStatuses[blobUri]; + + return result ? result.resultUri : null; + } + + function isPending(blobUri) { + return hasBlobUri(blobUri) ? blobUriStatuses[blobUri].status === PENDING : false; + } + + function isUploaded(blobUri) { + return hasBlobUri(blobUri) ? blobUriStatuses[blobUri].status === UPLOADED : false; + } + + function markPending(blobUri) { + blobUriStatuses[blobUri] = createStatus(PENDING, null); + } + + function markUploaded(blobUri, resultUri) { + blobUriStatuses[blobUri] = createStatus(UPLOADED, resultUri); + } + + function removeFailed(blobUri) { + delete blobUriStatuses[blobUri]; + } + + function destroy() { + blobUriStatuses = {}; + } + + return { + hasBlobUri: hasBlobUri, + getResultUri: getResultUri, + isPending: isPending, + isUploaded: isUploaded, + markPending: markPending, + markUploaded: markUploaded, + removeFailed: removeFailed, + destroy: destroy + }; + }; +}); + +// Included from: js/tinymce/classes/ErrorReporter.js + +/** + * ErrorReporter.js + * + * Released under LGPL License. + * Copyright (c) 1999-2016 Ephox Corp. All rights reserved + * + * License: http://www.tinymce.com/license + * Contributing: http://www.tinymce.com/contributing + */ + +/** + * Various error reporting helper functions. + * + * @class tinymce.ErrorReporter + * @private + */ +define("tinymce/ErrorReporter", [ + "tinymce/AddOnManager" +], function (AddOnManager) { + var PluginManager = AddOnManager.PluginManager; + + var resolvePluginName = function (targetUrl, suffix) { + for (var name in PluginManager.urls) { + var matchUrl = PluginManager.urls[name] + '/plugin' + suffix + '.js'; + if (matchUrl === targetUrl) { + return name; + } + } + + return null; + }; + + var pluginUrlToMessage = function (editor, url) { + var plugin = resolvePluginName(url, editor.suffix); + return plugin ? + 'Failed to load plugin: ' + plugin + ' from url ' + url : + 'Failed to load plugin url: ' + url; + }; + + var displayNotification = function (editor, message) { + editor.notificationManager.open({ + type: 'error', + text: message + }); + }; + + var displayError = function (editor, message) { + if (editor._skinLoaded) { + displayNotification(editor, message); + } else { + editor.on('SkinLoaded', function () { + displayNotification(editor, message); + }); + } + }; + + var uploadError = function (editor, message) { + displayError(editor, 'Failed to upload image: ' + message); + }; + + var pluginLoadError = function (editor, url) { + displayError(editor, pluginUrlToMessage(editor, url)); + }; + + return { + pluginLoadError: pluginLoadError, + uploadError: uploadError + }; +}); + +// Included from: js/tinymce/classes/EditorUpload.js + +/** + * EditorUpload.js + * + * Released under LGPL License. + * Copyright (c) 1999-2015 Ephox Corp. All rights reserved + * + * License: http://www.tinymce.com/license + * Contributing: http://www.tinymce.com/contributing + */ + +/** + * Handles image uploads, updates undo stack and patches over various internal functions. + * + * @private + * @class tinymce.EditorUpload + */ +define("tinymce/EditorUpload", [ + "tinymce/util/Arr", + "tinymce/file/Uploader", + "tinymce/file/ImageScanner", + "tinymce/file/BlobCache", + "tinymce/file/UploadStatus", + "tinymce/ErrorReporter" +], function(Arr, Uploader, ImageScanner, BlobCache, UploadStatus, ErrorReporter) { + return function(editor) { + var blobCache = new BlobCache(), uploader, imageScanner, settings = editor.settings; + var uploadStatus = new UploadStatus(); + + function aliveGuard(callback) { + return function(result) { + if (editor.selection) { + return callback(result); + } + + return []; + }; + } + + function cacheInvalidator() { + return '?' + (new Date()).getTime(); + } + + // Replaces strings without regexps to avoid FF regexp to big issue + function replaceString(content, search, replace) { + var index = 0; + + do { + index = content.indexOf(search, index); + + if (index !== -1) { + content = content.substring(0, index) + replace + content.substr(index + search.length); + index += replace.length - search.length + 1; + } + } while (index !== -1); + + return content; + } + + function replaceImageUrl(content, targetUrl, replacementUrl) { + content = replaceString(content, 'src="' + targetUrl + '"', 'src="' + replacementUrl + '"'); + content = replaceString(content, 'data-mce-src="' + targetUrl + '"', 'data-mce-src="' + replacementUrl + '"'); + + return content; + } + + function replaceUrlInUndoStack(targetUrl, replacementUrl) { + Arr.each(editor.undoManager.data, function(level) { + if (level.type === 'fragmented') { + level.fragments = Arr.map(level.fragments, function (fragment) { + return replaceImageUrl(fragment, targetUrl, replacementUrl); + }); + } else { + level.content = replaceImageUrl(level.content, targetUrl, replacementUrl); + } + }); + } + + function openNotification() { + return editor.notificationManager.open({ + text: editor.translate('Image uploading...'), + type: 'info', + timeout: -1, + progressBar: true + }); + } + + function replaceImageUri(image, resultUri) { + blobCache.removeByUri(image.src); + replaceUrlInUndoStack(image.src, resultUri); + + editor.$(image).attr({ + src: settings.images_reuse_filename ? resultUri + cacheInvalidator() : resultUri, + 'data-mce-src': editor.convertURL(resultUri, 'src') + }); + } + + function uploadImages(callback) { + if (!uploader) { + uploader = new Uploader(uploadStatus, { + url: settings.images_upload_url, + basePath: settings.images_upload_base_path, + credentials: settings.images_upload_credentials, + handler: settings.images_upload_handler + }); + } + + return scanForImages().then(aliveGuard(function(imageInfos) { + var blobInfos; + + blobInfos = Arr.map(imageInfos, function(imageInfo) { + return imageInfo.blobInfo; + }); + + return uploader.upload(blobInfos, openNotification).then(aliveGuard(function(result) { + result = Arr.map(result, function(uploadInfo, index) { + var image = imageInfos[index].image; + + if (uploadInfo.status && editor.settings.images_replace_blob_uris !== false) { + replaceImageUri(image, uploadInfo.url); + } else if (uploadInfo.error) { + ErrorReporter.uploadError(editor, uploadInfo.error); + } + + return { + element: image, + status: uploadInfo.status + }; + }); + + if (callback) { + callback(result); + } + + return result; + })); + })); + } + + function uploadImagesAuto(callback) { + if (settings.automatic_uploads !== false) { + return uploadImages(callback); + } + } + + function isValidDataUriImage(imgElm) { + return settings.images_dataimg_filter ? settings.images_dataimg_filter(imgElm) : true; + } + + function scanForImages() { + if (!imageScanner) { + imageScanner = new ImageScanner(uploadStatus, blobCache); + } + + return imageScanner.findAll(editor.getBody(), isValidDataUriImage).then(aliveGuard(function(result) { + Arr.each(result, function(resultItem) { + replaceUrlInUndoStack(resultItem.image.src, resultItem.blobInfo.blobUri()); + resultItem.image.src = resultItem.blobInfo.blobUri(); + resultItem.image.removeAttribute('data-mce-src'); + }); + + return result; + })); + } + + function destroy() { + blobCache.destroy(); + uploadStatus.destroy(); + imageScanner = uploader = null; + } + + function replaceBlobUris(content) { + return content.replace(/src="(blob:[^"]+)"/g, function(match, blobUri) { + var resultUri = uploadStatus.getResultUri(blobUri); + + if (resultUri) { + return 'src="' + resultUri + '"'; + } + + var blobInfo = blobCache.getByUri(blobUri); + + if (!blobInfo) { + blobInfo = Arr.reduce(editor.editorManager.editors, function(result, editor) { + return result || editor.editorUpload.blobCache.getByUri(blobUri); + }, null); + } + + if (blobInfo) { + return 'src="data:' + blobInfo.blob().type + ';base64,' + blobInfo.base64() + '"'; + } + + return match; + }); + } + + editor.on('setContent', function() { + if (editor.settings.automatic_uploads !== false) { + uploadImagesAuto(); + } else { + scanForImages(); + } + }); + + editor.on('RawSaveContent', function(e) { + e.content = replaceBlobUris(e.content); + }); + + editor.on('getContent', function(e) { + if (e.source_view || e.format == 'raw') { + return; + } + + e.content = replaceBlobUris(e.content); + }); + + editor.on('PostRender', function() { + editor.parser.addNodeFilter('img', function(images) { + Arr.each(images, function(img) { + var src = img.attr('src'); + + if (blobCache.getByUri(src)) { + return; + } + + var resultUri = uploadStatus.getResultUri(src); + if (resultUri) { + img.attr('src', resultUri); + } + }); + }); + }); + + return { + blobCache: blobCache, + uploadImages: uploadImages, + uploadImagesAuto: uploadImagesAuto, + scanForImages: scanForImages, + destroy: destroy + }; + }; +}); + +// Included from: js/tinymce/classes/caret/FakeCaret.js + +/** + * FakeCaret.js + * + * Released under LGPL License. + * Copyright (c) 1999-2015 Ephox Corp. All rights reserved + * + * License: http://www.tinymce.com/license + * Contributing: http://www.tinymce.com/contributing + */ + +/** + * This module contains logic for rendering a fake visual caret. + * + * @private + * @class tinymce.caret.FakeCaret + */ +define("tinymce/caret/FakeCaret", [ + "tinymce/caret/CaretContainer", + "tinymce/caret/CaretPosition", + "tinymce/dom/NodeType", + "tinymce/dom/RangeUtils", + "tinymce/dom/DomQuery", + "tinymce/geom/ClientRect", + "tinymce/util/Delay" +], function(CaretContainer, CaretPosition, NodeType, RangeUtils, $, ClientRect, Delay) { + var isContentEditableFalse = NodeType.isContentEditableFalse; + + return function(rootNode, isBlock) { + var cursorInterval, $lastVisualCaret, caretContainerNode; + + function getAbsoluteClientRect(node, before) { + var clientRect = ClientRect.collapse(node.getBoundingClientRect(), before), + docElm, scrollX, scrollY, margin, rootRect; + + if (rootNode.tagName == 'BODY') { + docElm = rootNode.ownerDocument.documentElement; + scrollX = rootNode.scrollLeft || docElm.scrollLeft; + scrollY = rootNode.scrollTop || docElm.scrollTop; + } else { + rootRect = rootNode.getBoundingClientRect(); + scrollX = rootNode.scrollLeft - rootRect.left; + scrollY = rootNode.scrollTop - rootRect.top; + } + + clientRect.left += scrollX; + clientRect.right += scrollX; + clientRect.top += scrollY; + clientRect.bottom += scrollY; + clientRect.width = 1; + + margin = node.offsetWidth - node.clientWidth; + + if (margin > 0) { + if (before) { + margin *= -1; + } + + clientRect.left += margin; + clientRect.right += margin; + } + + return clientRect; + } + + function trimInlineCaretContainers() { + var contentEditableFalseNodes, node, sibling, i, data; + + contentEditableFalseNodes = $('*[contentEditable=false]', rootNode); + for (i = 0; i < contentEditableFalseNodes.length; i++) { + node = contentEditableFalseNodes[i]; + + sibling = node.previousSibling; + if (CaretContainer.endsWithCaretContainer(sibling)) { + data = sibling.data; + + if (data.length == 1) { + sibling.parentNode.removeChild(sibling); + } else { + sibling.deleteData(data.length - 1, 1); + } + } + + sibling = node.nextSibling; + if (CaretContainer.startsWithCaretContainer(sibling)) { + data = sibling.data; + + if (data.length == 1) { + sibling.parentNode.removeChild(sibling); + } else { + sibling.deleteData(0, 1); + } + } + } + + return null; + } + + function show(before, node) { + var clientRect, rng; + + hide(); + + if (isBlock(node)) { + caretContainerNode = CaretContainer.insertBlock('p', node, before); + clientRect = getAbsoluteClientRect(node, before); + $(caretContainerNode).css('top', clientRect.top); + + $lastVisualCaret = $('').css(clientRect).appendTo(rootNode); + + if (before) { + $lastVisualCaret.addClass('mce-visual-caret-before'); + } + + startBlink(); + + rng = node.ownerDocument.createRange(); + rng.setStart(caretContainerNode, 0); + rng.setEnd(caretContainerNode, 0); + } else { + caretContainerNode = CaretContainer.insertInline(node, before); + rng = node.ownerDocument.createRange(); + + if (isContentEditableFalse(caretContainerNode.nextSibling)) { + rng.setStart(caretContainerNode, 0); + rng.setEnd(caretContainerNode, 0); + } else { + rng.setStart(caretContainerNode, 1); + rng.setEnd(caretContainerNode, 1); + } + + return rng; + } + + return rng; + } + + function hide() { + trimInlineCaretContainers(); + + if (caretContainerNode) { + CaretContainer.remove(caretContainerNode); + caretContainerNode = null; + } + + if ($lastVisualCaret) { + $lastVisualCaret.remove(); + $lastVisualCaret = null; + } + + clearInterval(cursorInterval); + } + + function startBlink() { + cursorInterval = Delay.setInterval(function() { + $('div.mce-visual-caret', rootNode).toggleClass('mce-visual-caret-hidden'); + }, 500); + } + + function destroy() { + Delay.clearInterval(cursorInterval); + } + + function getCss() { + return ( + '.mce-visual-caret {' + + 'position: absolute;' + + 'background-color: black;' + + 'background-color: currentcolor;' + + '}' + + '.mce-visual-caret-hidden {' + + 'display: none;' + + '}' + + '*[data-mce-caret] {' + + 'position: absolute;' + + 'left: -1000px;' + + 'right: auto;' + + 'top: 0;' + + 'margin: 0;' + + 'padding: 0;' + + '}' + ); + } + + return { + show: show, + hide: hide, + getCss: getCss, + destroy: destroy + }; + }; +}); + +// Included from: js/tinymce/classes/dom/Dimensions.js + +/** + * Dimensions.js + * + * Released under LGPL License. + * Copyright (c) 1999-2015 Ephox Corp. All rights reserved + * + * License: http://www.tinymce.com/license + * Contributing: http://www.tinymce.com/contributing + */ + +/** + * This module measures nodes and returns client rects. The client rects has an + * extra node property. + * + * @private + * @class tinymce.dom.Dimensions + */ +define("tinymce/dom/Dimensions", [ + "tinymce/util/Arr", + "tinymce/dom/NodeType", + "tinymce/geom/ClientRect" +], function(Arr, NodeType, ClientRect) { + + function getClientRects(node) { + function toArrayWithNode(clientRects) { + return Arr.map(clientRects, function(clientRect) { + clientRect = ClientRect.clone(clientRect); + clientRect.node = node; + + return clientRect; + }); + } + + if (Arr.isArray(node)) { + return Arr.reduce(node, function(result, node) { + return result.concat(getClientRects(node)); + }, []); + } + + if (NodeType.isElement(node)) { + return toArrayWithNode(node.getClientRects()); + } + + if (NodeType.isText(node)) { + var rng = node.ownerDocument.createRange(); + + rng.setStart(node, 0); + rng.setEnd(node, node.data.length); + + return toArrayWithNode(rng.getClientRects()); + } + } + + return { + /** + * Returns the client rects for a specific node. + * + * @method getClientRects + * @param {Array/DOMNode} node Node or array of nodes to get client rects on. + * @param {Array} Array of client rects with a extra node property. + */ + getClientRects: getClientRects + }; +}); + +// Included from: js/tinymce/classes/caret/LineWalker.js + +/** + * LineWalker.js + * + * Released under LGPL License. + * Copyright (c) 1999-2015 Ephox Corp. All rights reserved + * + * License: http://www.tinymce.com/license + * Contributing: http://www.tinymce.com/contributing + */ + +/** + * This module lets you walk the document line by line + * returing nodes and client rects for each line. + * + * @private + * @class tinymce.caret.LineWalker + */ +define("tinymce/caret/LineWalker", [ + "tinymce/util/Fun", + "tinymce/util/Arr", + "tinymce/dom/Dimensions", + "tinymce/caret/CaretCandidate", + "tinymce/caret/CaretUtils", + "tinymce/caret/CaretWalker", + "tinymce/caret/CaretPosition", + "tinymce/geom/ClientRect" +], function(Fun, Arr, Dimensions, CaretCandidate, CaretUtils, CaretWalker, CaretPosition, ClientRect) { + var curry = Fun.curry; + + function findUntil(direction, rootNode, predicateFn, node) { + while ((node = CaretUtils.findNode(node, direction, CaretCandidate.isEditableCaretCandidate, rootNode))) { + if (predicateFn(node)) { + return; + } + } + } + + function walkUntil(direction, isAboveFn, isBeflowFn, rootNode, predicateFn, caretPosition) { + var line = 0, node, result = [], targetClientRect; + + function add(node) { + var i, clientRect, clientRects; + + clientRects = Dimensions.getClientRects(node); + if (direction == -1) { + clientRects = clientRects.reverse(); + } + + for (i = 0; i < clientRects.length; i++) { + clientRect = clientRects[i]; + if (isBeflowFn(clientRect, targetClientRect)) { + continue; + } + + if (result.length > 0 && isAboveFn(clientRect, Arr.last(result))) { + line++; + } + + clientRect.line = line; + + if (predicateFn(clientRect)) { + return true; + } + + result.push(clientRect); + } + } + + targetClientRect = Arr.last(caretPosition.getClientRects()); + if (!targetClientRect) { + return result; + } + + node = caretPosition.getNode(); + add(node); + findUntil(direction, rootNode, add, node); + + return result; + } + + function aboveLineNumber(lineNumber, clientRect) { + return clientRect.line > lineNumber; + } + + function isLine(lineNumber, clientRect) { + return clientRect.line === lineNumber; + } + + var upUntil = curry(walkUntil, -1, ClientRect.isAbove, ClientRect.isBelow); + var downUntil = curry(walkUntil, 1, ClientRect.isBelow, ClientRect.isAbove); + + function positionsUntil(direction, rootNode, predicateFn, node) { + var caretWalker = new CaretWalker(rootNode), walkFn, isBelowFn, isAboveFn, + caretPosition, result = [], line = 0, clientRect, targetClientRect; + + function getClientRect(caretPosition) { + if (direction == 1) { + return Arr.last(caretPosition.getClientRects()); + } + + return Arr.last(caretPosition.getClientRects()); + } + + if (direction == 1) { + walkFn = caretWalker.next; + isBelowFn = ClientRect.isBelow; + isAboveFn = ClientRect.isAbove; + caretPosition = CaretPosition.after(node); + } else { + walkFn = caretWalker.prev; + isBelowFn = ClientRect.isAbove; + isAboveFn = ClientRect.isBelow; + caretPosition = CaretPosition.before(node); + } + + targetClientRect = getClientRect(caretPosition); + + do { + if (!caretPosition.isVisible()) { + continue; + } + + clientRect = getClientRect(caretPosition); + + if (isAboveFn(clientRect, targetClientRect)) { + continue; + } + + if (result.length > 0 && isBelowFn(clientRect, Arr.last(result))) { + line++; + } + + clientRect = ClientRect.clone(clientRect); + clientRect.position = caretPosition; + clientRect.line = line; + + if (predicateFn(clientRect)) { + return result; + } + + result.push(clientRect); + } while ((caretPosition = walkFn(caretPosition))); + + return result; + } + + return { + upUntil: upUntil, + downUntil: downUntil, + + /** + * Find client rects with line and caret position until the predicate returns true. + * + * @method positionsUntil + * @param {Number} direction Direction forward/backward 1/-1. + * @param {DOMNode} rootNode Root node to walk within. + * @param {function} predicateFn Gets the client rect as it's input. + * @param {DOMNode} node Node to start walking from. + * @return {Array} Array of client rects with line and position properties. + */ + positionsUntil: positionsUntil, + + isAboveLine: curry(aboveLineNumber), + isLine: curry(isLine) + }; +}); + +// Included from: js/tinymce/classes/caret/LineUtils.js + +/** + * LineUtils.js + * + * Released under LGPL License. + * Copyright (c) 1999-2015 Ephox Corp. All rights reserved + * + * License: http://www.tinymce.com/license + * Contributing: http://www.tinymce.com/contributing + */ + +/** + * Utility functions for working with lines. + * + * @private + * @class tinymce.caret.LineUtils + */ +define("tinymce/caret/LineUtils", [ + "tinymce/util/Fun", + "tinymce/util/Arr", + "tinymce/dom/NodeType", + "tinymce/dom/Dimensions", + "tinymce/geom/ClientRect", + "tinymce/caret/CaretUtils", + "tinymce/caret/CaretCandidate" +], function(Fun, Arr, NodeType, Dimensions, ClientRect, CaretUtils, CaretCandidate) { + var isContentEditableFalse = NodeType.isContentEditableFalse, + findNode = CaretUtils.findNode, + curry = Fun.curry; + + function distanceToRectLeft(clientRect, clientX) { + return Math.abs(clientRect.left - clientX); + } + + function distanceToRectRight(clientRect, clientX) { + return Math.abs(clientRect.right - clientX); + } + + function findClosestClientRect(clientRects, clientX) { + function isInside(clientX, clientRect) { + return clientX >= clientRect.left && clientX <= clientRect.right; + } + + return Arr.reduce(clientRects, function(oldClientRect, clientRect) { + var oldDistance, newDistance; + + oldDistance = Math.min(distanceToRectLeft(oldClientRect, clientX), distanceToRectRight(oldClientRect, clientX)); + newDistance = Math.min(distanceToRectLeft(clientRect, clientX), distanceToRectRight(clientRect, clientX)); + + if (isInside(clientX, clientRect)) { + return clientRect; + } + + if (isInside(clientX, oldClientRect)) { + return oldClientRect; + } + + // cE=false has higher priority + if (newDistance == oldDistance && isContentEditableFalse(clientRect.node)) { + return clientRect; + } + + if (newDistance < oldDistance) { + return clientRect; + } + + return oldClientRect; + }); + } + + function walkUntil(direction, rootNode, predicateFn, node) { + while ((node = findNode(node, direction, CaretCandidate.isEditableCaretCandidate, rootNode))) { + if (predicateFn(node)) { + return; + } + } + } + + function findLineNodeRects(rootNode, targetNodeRect) { + var clientRects = []; + + function collect(checkPosFn, node) { + var lineRects; + + lineRects = Arr.filter(Dimensions.getClientRects(node), function(clientRect) { + return !checkPosFn(clientRect, targetNodeRect); + }); + + clientRects = clientRects.concat(lineRects); + + return lineRects.length === 0; + } + + clientRects.push(targetNodeRect); + walkUntil(-1, rootNode, curry(collect, ClientRect.isAbove), targetNodeRect.node); + walkUntil(1, rootNode, curry(collect, ClientRect.isBelow), targetNodeRect.node); + + return clientRects; + } + + function getContentEditableFalseChildren(rootNode) { + return Arr.filter(Arr.toArray(rootNode.getElementsByTagName('*')), isContentEditableFalse); + } + + function caretInfo(clientRect, clientX) { + return { + node: clientRect.node, + before: distanceToRectLeft(clientRect, clientX) < distanceToRectRight(clientRect, clientX) + }; + } + + function closestCaret(rootNode, clientX, clientY) { + var contentEditableFalseNodeRects, closestNodeRect; + + contentEditableFalseNodeRects = Dimensions.getClientRects(getContentEditableFalseChildren(rootNode)); + contentEditableFalseNodeRects = Arr.filter(contentEditableFalseNodeRects, function(clientRect) { + return clientY >= clientRect.top && clientY <= clientRect.bottom; + }); + + closestNodeRect = findClosestClientRect(contentEditableFalseNodeRects, clientX); + if (closestNodeRect) { + closestNodeRect = findClosestClientRect(findLineNodeRects(rootNode, closestNodeRect), clientX); + if (closestNodeRect && isContentEditableFalse(closestNodeRect.node)) { + return caretInfo(closestNodeRect, clientX); + } + } + + return null; + } + + return { + findClosestClientRect: findClosestClientRect, + findLineNodeRects: findLineNodeRects, + closestCaret: closestCaret + }; +}); + +// Included from: js/tinymce/classes/dom/MousePosition.js + +/** + * MousePosition.js + * + * Released under LGPL License. + * Copyright (c) 1999-2016 Ephox Corp. All rights reserved + * + * License: http://www.tinymce.com/license + * Contributing: http://www.tinymce.com/contributing + */ + +/** + * This module calculates an absolute coordinate inside the editor body for both local and global mouse events. + * + * @private + * @class tinymce.dom.MousePosition + */ +define("tinymce/dom/MousePosition", [ +], function() { + var getAbsolutePosition = function (elm) { + var doc, docElem, win, clientRect; + + clientRect = elm.getBoundingClientRect(); + doc = elm.ownerDocument; + docElem = doc.documentElement; + win = doc.defaultView; + + return { + top: clientRect.top + win.pageYOffset - docElem.clientTop, + left: clientRect.left + win.pageXOffset - docElem.clientLeft + }; + }; + + var getBodyPosition = function (editor) { + return editor.inline ? getAbsolutePosition(editor.getBody()) : {left: 0, top: 0}; + }; + + var getScrollPosition = function (editor) { + var body = editor.getBody(); + return editor.inline ? {left: body.scrollLeft, top: body.scrollTop} : {left: 0, top: 0}; + }; + + var getBodyScroll = function (editor) { + var body = editor.getBody(), docElm = editor.getDoc().documentElement; + var inlineScroll = {left: body.scrollLeft, top: body.scrollTop}; + var iframeScroll = {left: body.scrollLeft || docElm.scrollLeft, top: body.scrollTop || docElm.scrollTop}; + + return editor.inline ? inlineScroll : iframeScroll; + }; + + var getMousePosition = function (editor, event) { + if (event.target.ownerDocument !== editor.getDoc()) { + var iframePosition = getAbsolutePosition(editor.getContentAreaContainer()); + var scrollPosition = getBodyScroll(editor); + + return { + left: event.pageX - iframePosition.left + scrollPosition.left, + top: event.pageY - iframePosition.top + scrollPosition.top + }; + } + + return { + left: event.pageX, + top: event.pageY + }; + }; + + var calculatePosition = function (bodyPosition, scrollPosition, mousePosition) { + return { + pageX: (mousePosition.left - bodyPosition.left) + scrollPosition.left, + pageY: (mousePosition.top - bodyPosition.top) + scrollPosition.top + }; + }; + + var calc = function (editor, event) { + return calculatePosition(getBodyPosition(editor), getScrollPosition(editor), getMousePosition(editor, event)); + }; + + return { + calc: calc + }; +}); + +// Included from: js/tinymce/classes/DragDropOverrides.js + +/** + * DragDropOverrides.js + * + * Released under LGPL License. + * Copyright (c) 1999-2015 Ephox Corp. All rights reserved + * + * License: http://www.tinymce.com/license + * Contributing: http://www.tinymce.com/contributing + */ + +/** + * This module contains logic overriding the drag/drop logic of the editor. + * + * @private + * @class tinymce.DragDropOverrides + */ +define("tinymce/DragDropOverrides", [ + "tinymce/dom/NodeType", + "tinymce/util/Arr", + "tinymce/util/Fun", + "tinymce/util/Delay", + "tinymce/dom/DOMUtils", + "tinymce/dom/MousePosition" +], function( + NodeType, Arr, Fun, Delay, DOMUtils, MousePosition +) { + var isContentEditableFalse = NodeType.isContentEditableFalse, + isContentEditableTrue = NodeType.isContentEditableTrue; + + var isDraggable = function (elm) { + return isContentEditableFalse(elm); + }; + + var isValidDropTarget = function (editor, targetElement, dragElement) { + if (targetElement === dragElement || editor.dom.isChildOf(targetElement, dragElement)) { + return false; + } + + if (isContentEditableFalse(targetElement)) { + return false; + } + + return true; + }; + + var cloneElement = function (elm) { + var cloneElm = elm.cloneNode(true); + cloneElm.removeAttribute('data-mce-selected'); + return cloneElm; + }; + + var createGhost = function (editor, elm, width, height) { + var clonedElm = elm.cloneNode(true); + + editor.dom.setStyles(clonedElm, {width: width, height: height}); + editor.dom.setAttrib(clonedElm, 'data-mce-selected', null); + + var ghostElm = editor.dom.create('div', { + 'class': 'mce-drag-container', + 'data-mce-bogus': 'all', + unselectable: 'on', + contenteditable: 'false' + }); + + editor.dom.setStyles(ghostElm, { + position: 'absolute', + opacity: 0.5, + overflow: 'hidden', + border: 0, + padding: 0, + margin: 0, + width: width, + height: height + }); + + editor.dom.setStyles(clonedElm, { + margin: 0, + boxSizing: 'border-box' + }); + + ghostElm.appendChild(clonedElm); + + return ghostElm; + }; + + var appendGhostToBody = function (ghostElm, bodyElm) { + if (ghostElm.parentNode !== bodyElm) { + bodyElm.appendChild(ghostElm); + } + }; + + var moveGhost = function (ghostElm, position, width, height, maxX, maxY) { + var overflowX = 0, overflowY = 0; + + ghostElm.style.left = position.pageX + 'px'; + ghostElm.style.top = position.pageY + 'px'; + + if (position.pageX + width > maxX) { + overflowX = (position.pageX + width) - maxX; + } + + if (position.pageY + height > maxY) { + overflowY = (position.pageY + height) - maxY; + } + + ghostElm.style.width = (width - overflowX) + 'px'; + ghostElm.style.height = (height - overflowY) + 'px'; + }; + + var removeElement = function (elm) { + if (elm && elm.parentNode) { + elm.parentNode.removeChild(elm); + } + }; + + var isLeftMouseButtonPressed = function (e) { + return e.button === 0; + }; + + var hasDraggableElement = function (state) { + return state.element; + }; + + var applyRelPos = function (state, position) { + return { + pageX: position.pageX - state.relX, + pageY: position.pageY + 5 + }; + }; + + var start = function (state, editor) { + return function (e) { + if (isLeftMouseButtonPressed(e)) { + var ceElm = Arr.find(editor.dom.getParents(e.target), Fun.or(isContentEditableFalse, isContentEditableTrue)); + + if (isDraggable(ceElm)) { + var elmPos = editor.dom.getPos(ceElm); + var bodyElm = editor.getBody(); + var docElm = editor.getDoc().documentElement; + + state.element = ceElm; + state.screenX = e.screenX; + state.screenY = e.screenY; + state.maxX = (editor.inline ? bodyElm.scrollWidth : docElm.offsetWidth) - 2; + state.maxY = (editor.inline ? bodyElm.scrollHeight : docElm.offsetHeight) - 2; + state.relX = e.pageX - elmPos.x; + state.relY = e.pageY - elmPos.y; + state.width = ceElm.offsetWidth; + state.height = ceElm.offsetHeight; + state.ghost = createGhost(editor, ceElm, state.width, state.height); + } + } + }; + }; + + var move = function (state, editor) { + // Reduces laggy drag behavior on Gecko + var throttledPlaceCaretAt = Delay.throttle(function (clientX, clientY) { + editor._selectionOverrides.hideFakeCaret(); + editor.selection.placeCaretAt(clientX, clientY); + }, 0); + + return function (e) { + var movement = Math.max(Math.abs(e.screenX - state.screenX), Math.abs(e.screenY - state.screenY)); + + if (hasDraggableElement(state) && !state.dragging && movement > 10) { + var args = editor.fire('dragstart', {target: state.element}); + if (args.isDefaultPrevented()) { + return; + } + + state.dragging = true; + editor.focus(); + } + + if (state.dragging) { + var targetPos = applyRelPos(state, MousePosition.calc(editor, e)); + + appendGhostToBody(state.ghost, editor.getBody()); + moveGhost(state.ghost, targetPos, state.width, state.height, state.maxX, state.maxY); + + throttledPlaceCaretAt(e.clientX, e.clientY); + } + }; + }; + + // Returns the raw element instead of the fake cE=false element + var getRawTarget = function (selection) { + var rng = selection.getSel().getRangeAt(0); + var startContainer = rng.startContainer; + return startContainer.nodeType === 3 ? startContainer.parentNode : startContainer; + }; + + var drop = function (state, editor) { + return function (e) { + if (state.dragging) { + if (isValidDropTarget(editor, getRawTarget(editor.selection), state.element)) { + var targetClone = cloneElement(state.element); + + var args = editor.fire('drop', { + targetClone: targetClone, + clientX: e.clientX, + clientY: e.clientY + }); + + if (!args.isDefaultPrevented()) { + targetClone = args.targetClone; + + editor.undoManager.transact(function() { + removeElement(state.element); + editor.insertContent(editor.dom.getOuterHTML(targetClone)); + editor._selectionOverrides.hideFakeCaret(); + }); + } + } + } + + removeDragState(state); + }; + }; + + var stop = function (state, editor) { + return function () { + removeDragState(state); + if (state.dragging) { + editor.fire('dragend'); + } + }; + }; + + var removeDragState = function (state) { + state.dragging = false; + state.element = null; + removeElement(state.ghost); + }; + + var bindFakeDragEvents = function (editor) { + var state = {}, pageDom, dragStartHandler, dragHandler, dropHandler, dragEndHandler, rootDocument; + + pageDom = DOMUtils.DOM; + rootDocument = document; + dragStartHandler = start(state, editor); + dragHandler = move(state, editor); + dropHandler = drop(state, editor); + dragEndHandler = stop(state, editor); + + editor.on('mousedown', dragStartHandler); + editor.on('mousemove', dragHandler); + editor.on('mouseup', dropHandler); + + pageDom.bind(rootDocument, 'mousemove', dragHandler); + pageDom.bind(rootDocument, 'mouseup', dragEndHandler); + + editor.on('remove', function () { + pageDom.unbind(rootDocument, 'mousemove', dragHandler); + pageDom.unbind(rootDocument, 'mouseup', dragEndHandler); + }); + }; + + var blockIeDrop = function (editor) { + editor.on('drop', function(e) { + // FF doesn't pass out clientX/clientY for drop since this is for IE we just use null instead + var realTarget = typeof e.clientX !== 'undefined' ? editor.getDoc().elementFromPoint(e.clientX, e.clientY) : null; + + if (isContentEditableFalse(realTarget) || isContentEditableFalse(editor.dom.getContentEditableParent(realTarget))) { + e.preventDefault(); + } + }); + }; + + var init = function (editor) { + bindFakeDragEvents(editor); + blockIeDrop(editor); + }; + + return { + init: init + }; +}); + +// Included from: js/tinymce/classes/SelectionOverrides.js + +/** + * SelectionOverrides.js + * + * Released under LGPL License. + * Copyright (c) 1999-2015 Ephox Corp. All rights reserved + * + * License: http://www.tinymce.com/license + * Contributing: http://www.tinymce.com/contributing + */ + +/** + * This module contains logic overriding the selection with keyboard/mouse + * around contentEditable=false regions. + * + * @example + * // Disable the default cE=false selection + * tinymce.activeEditor.on('ShowCaret BeforeObjectSelected', function(e) { + * e.preventDefault(); + * }); + * + * @private + * @class tinymce.SelectionOverrides + */ +define("tinymce/SelectionOverrides", [ + "tinymce/Env", + "tinymce/caret/CaretWalker", + "tinymce/caret/CaretPosition", + "tinymce/caret/CaretContainer", + "tinymce/caret/CaretUtils", + "tinymce/caret/FakeCaret", + "tinymce/caret/LineWalker", + "tinymce/caret/LineUtils", + "tinymce/dom/NodeType", + "tinymce/dom/RangeUtils", + "tinymce/geom/ClientRect", + "tinymce/util/VK", + "tinymce/util/Fun", + "tinymce/util/Arr", + "tinymce/util/Delay", + "tinymce/DragDropOverrides" +], function( + Env, CaretWalker, CaretPosition, CaretContainer, CaretUtils, FakeCaret, LineWalker, + LineUtils, NodeType, RangeUtils, ClientRect, VK, Fun, Arr, Delay, DragDropOverrides +) { + var curry = Fun.curry, + isContentEditableTrue = NodeType.isContentEditableTrue, + isContentEditableFalse = NodeType.isContentEditableFalse, + isElement = NodeType.isElement, + isAfterContentEditableFalse = CaretUtils.isAfterContentEditableFalse, + isBeforeContentEditableFalse = CaretUtils.isBeforeContentEditableFalse, + getSelectedNode = RangeUtils.getSelectedNode; + + function getVisualCaretPosition(walkFn, caretPosition) { + while ((caretPosition = walkFn(caretPosition))) { + if (caretPosition.isVisible()) { + return caretPosition; + } + } + + return caretPosition; + } + + function SelectionOverrides(editor) { + var rootNode = editor.getBody(), caretWalker = new CaretWalker(rootNode); + var getNextVisualCaretPosition = curry(getVisualCaretPosition, caretWalker.next); + var getPrevVisualCaretPosition = curry(getVisualCaretPosition, caretWalker.prev), + fakeCaret = new FakeCaret(editor.getBody(), isBlock), + realSelectionId = 'sel-' + editor.dom.uniqueId(), + selectedContentEditableNode, $ = editor.$; + + function isFakeSelectionElement(elm) { + return editor.dom.hasClass(elm, 'mce-offscreen-selection'); + } + + function getRealSelectionElement() { + var container = editor.dom.get(realSelectionId); + return container ? container.getElementsByTagName('*')[0] : container; + } + + function isBlock(node) { + return editor.dom.isBlock(node); + } + + function setRange(range) { + //console.log('setRange', range); + if (range) { + editor.selection.setRng(range); + } + } + + function getRange() { + return editor.selection.getRng(); + } + + function scrollIntoView(node, alignToTop) { + editor.selection.scrollIntoView(node, alignToTop); + } + + function showCaret(direction, node, before) { + var e; + + e = editor.fire('ShowCaret', { + target: node, + direction: direction, + before: before + }); + + if (e.isDefaultPrevented()) { + return null; + } + + scrollIntoView(node, direction === -1); + + return fakeCaret.show(before, node); + } + + function selectNode(node) { + var e; + + e = editor.fire('BeforeObjectSelected', {target: node}); + if (e.isDefaultPrevented()) { + return null; + } + + return getNodeRange(node); + } + + function getNodeRange(node) { + var rng = node.ownerDocument.createRange(); + + rng.selectNode(node); + + return rng; + } + + function isMoveInsideSameBlock(fromCaretPosition, toCaretPosition) { + var inSameBlock = CaretUtils.isInSameBlock(fromCaretPosition, toCaretPosition); + + // Handle bogus BRabc|
\u00a0
').append(targetClone); + range.setStartAfter($realSelectionContainer[0].firstChild.firstChild); + range.setEndAfter(targetClone); + } else { + $realSelectionContainer.empty().append('\u00a0').append(targetClone).append('\u00a0'); + range.setStart($realSelectionContainer[0].firstChild, 1); + range.setEnd($realSelectionContainer[0].lastChild, 0); + } + + $realSelectionContainer.css({ + top: dom.getPos(node, editor.getBody()).y + }); + + $realSelectionContainer[0].focus(); + sel = editor.selection.getSel(); + sel.removeAllRanges(); + sel.addRange(range); + + editor.$('*[data-mce-selected]').removeAttr('data-mce-selected'); + node.setAttribute('data-mce-selected', 1); + selectedContentEditableNode = node; + hideFakeCaret(); + + return range; + } + + function removeContentEditableSelection() { + if (selectedContentEditableNode) { + selectedContentEditableNode.removeAttribute('data-mce-selected'); + editor.$('#' + realSelectionId).remove(); + selectedContentEditableNode = null; + } + } + + function destroy() { + fakeCaret.destroy(); + selectedContentEditableNode = null; + } + + function hideFakeCaret() { + fakeCaret.hide(); + } + + if (Env.ceFalse) { + registerEvents(); + addCss(); + } + + return { + showBlockCaretContainer: showBlockCaretContainer, + hideFakeCaret: hideFakeCaret, + destroy: destroy + }; + } + + return SelectionOverrides; +}); + +// Included from: js/tinymce/classes/util/Uuid.js + +/** + * Uuid.js + * + * Released under LGPL License. + * Copyright (c) 1999-2016 Ephox Corp. All rights reserved + * + * License: http://www.tinymce.com/license + * Contributing: http://www.tinymce.com/contributing + */ + +/** + * Generates unique ids. + * + * @class tinymce.util.Uuid + * @private + */ +define("tinymce/util/Uuid", [ +], function() { + var count = 0; + + var seed = function () { + var rnd = function () { + return Math.round(Math.random() * 0xFFFFFFFF).toString(36); + }; + + var now = new Date().getTime(); + return 's' + now.toString(36) + rnd() + rnd() + rnd(); + }; + + var uuid = function (prefix) { + return prefix + (count++) + seed(); + }; + + return { + uuid: uuid + }; +}); + +// Included from: js/tinymce/classes/ui/Sidebar.js + +/** + * Sidebar.js + * + * Released under LGPL License. + * Copyright (c) 1999-2015 Ephox Corp. All rights reserved + * + * License: http://www.tinymce.com/license + * Contributing: http://www.tinymce.com/contributing + */ + +/** + * This module handle sidebar instances for the editor. + * + * @class tinymce.ui.Sidebar + * @private + */ +define("tinymce/ui/Sidebar", [ +], function( +) { + var add = function (editor, name, settings) { + var sidebars = editor.sidebars ? editor.sidebars : []; + sidebars.push({name: name, settings: settings}); + editor.sidebars = sidebars; + }; + + return { + add: add + }; +}); + +// Included from: js/tinymce/classes/Editor.js + +/** + * Editor.js + * + * Released under LGPL License. + * Copyright (c) 1999-2015 Ephox Corp. All rights reserved + * + * License: http://www.tinymce.com/license + * Contributing: http://www.tinymce.com/contributing + */ + +/*jshint scripturl:true */ + +/** + * Include the base event class documentation. + * + * @include ../../../tools/docs/tinymce.Event.js + */ + +/** + * This class contains the core logic for a TinyMCE editor. + * + * @class tinymce.Editor + * @mixes tinymce.util.Observable + * @example + * // Add a class to all paragraphs in the editor. + * tinymce.activeEditor.dom.addClass(tinymce.activeEditor.dom.select('p'), 'someclass'); + * + * // Gets the current editors selection as text + * tinymce.activeEditor.selection.getContent({format: 'text'}); + * + * // Creates a new editor instance + * var ed = new tinymce.Editor('textareaid', { + * some_setting: 1 + * }, tinymce.EditorManager); + * + * // Select each item the user clicks on + * ed.on('click', function(e) { + * ed.selection.select(e.target); + * }); + * + * ed.render(); + */ +define("tinymce/Editor", [ + "tinymce/dom/DOMUtils", + "tinymce/dom/DomQuery", + "tinymce/AddOnManager", + "tinymce/NodeChange", + "tinymce/html/Node", + "tinymce/dom/Serializer", + "tinymce/html/Serializer", + "tinymce/dom/Selection", + "tinymce/Formatter", + "tinymce/UndoManager", + "tinymce/EnterKey", + "tinymce/ForceBlocks", + "tinymce/EditorCommands", + "tinymce/util/URI", + "tinymce/dom/ScriptLoader", + "tinymce/dom/EventUtils", + "tinymce/WindowManager", + "tinymce/NotificationManager", + "tinymce/html/Schema", + "tinymce/html/DomParser", + "tinymce/util/Quirks", + "tinymce/Env", + "tinymce/util/Tools", + "tinymce/util/Delay", + "tinymce/EditorObservable", + "tinymce/Mode", + "tinymce/Shortcuts", + "tinymce/EditorUpload", + "tinymce/SelectionOverrides", + "tinymce/util/Uuid", + "tinymce/ui/Sidebar", + "tinymce/ErrorReporter" +], function( + DOMUtils, DomQuery, AddOnManager, NodeChange, Node, DomSerializer, Serializer, + Selection, Formatter, UndoManager, EnterKey, ForceBlocks, EditorCommands, + URI, ScriptLoader, EventUtils, WindowManager, NotificationManager, + Schema, DomParser, Quirks, Env, Tools, Delay, EditorObservable, Mode, Shortcuts, EditorUpload, + SelectionOverrides, Uuid, Sidebar, ErrorReporter +) { + // 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; + + /** + * Include documentation for all the events. + * + * @include ../../../tools/docs/tinymce.Editor.js + */ + + /** + * Constructs a editor instance by id. + * + * @constructor + * @method Editor + * @param {String} id Unique id for the editor. + * @param {Object} settings Settings for the editor. + * @param {tinymce.EditorManager} editorManager EditorManager instance. + */ + function Editor(id, settings, editorManager) { + var self = this, documentBaseUrl, baseUri, defaultSettings; + + documentBaseUrl = self.documentBaseUrl = editorManager.documentBaseURL; + baseUri = editorManager.baseURI; + defaultSettings = editorManager.defaultSettings; + + /** + * Name/value collection with editor settings. + * + * @property settings + * @type Object + * @example + * // Get the value of the theme setting + * tinymce.activeEditor.windowManager.alert("You are using the " + tinymce.activeEditor.settings.theme + " theme"); + */ + settings = extend({ + id: id, + theme: 'modern', + delta_width: 0, + delta_height: 0, + popup_css: '', + plugins: '', + document_base_url: documentBaseUrl, + add_form_submit_trigger: true, + submit_patch: true, + add_unload_trigger: true, + convert_urls: true, + relative_urls: true, + remove_script_host: true, + object_resizing: true, + doctype: '', + visual: true, + font_size_style_values: 'xx-small,x-small,small,medium,large,x-large,xx-large', + + // See: http://www.w3.org/TR/CSS2/fonts.html#propdef-font-size + font_size_legacy_values: 'xx-small,small,medium,large,x-large,xx-large,300%', + forced_root_block: 'p', + hidden_input: true, + padd_empty_editor: true, + render_ui: true, + indentation: '30px', + inline_styles: true, + convert_fonts_to_spans: true, + indent: 'simple', + indent_before: 'p,h1,h2,h3,h4,h5,h6,blockquote,div,title,style,pre,script,td,th,ul,ol,li,dl,dt,dd,area,table,thead,' + + 'tfoot,tbody,tr,section,article,hgroup,aside,figure,figcaption,option,optgroup,datalist', + indent_after: 'p,h1,h2,h3,h4,h5,h6,blockquote,div,title,style,pre,script,td,th,ul,ol,li,dl,dt,dd,area,table,thead,' + + 'tfoot,tbody,tr,section,article,hgroup,aside,figure,figcaption,option,optgroup,datalist', + validate: true, + entity_encoding: 'named', + url_converter: self.convertURL, + url_converter_scope: self, + ie7_compat: true + }, defaultSettings, settings); + + // Merge external_plugins + if (defaultSettings && defaultSettings.external_plugins && settings.external_plugins) { + settings.external_plugins = extend({}, defaultSettings.external_plugins, settings.external_plugins); + } + + self.settings = settings; + AddOnManager.language = settings.language || 'en'; + AddOnManager.languageLoad = settings.language_load; + AddOnManager.baseURL = editorManager.baseURL; + + /** + * Editor instance id, normally the same as the div/textarea that was replaced. + * + * @property id + * @type String + */ + self.id = settings.id = id; + + /** + * State to force the editor to return false on a isDirty call. + * + * @property isNotDirty + * @type Boolean + * @deprecated Use editor.setDirty instead. + */ + self.setDirty(false); + + /** + * Name/Value object containing plugin instances. + * + * @property plugins + * @type Object + * @example + * // Execute a method inside a plugin directly + * tinymce.activeEditor.plugins.someplugin.someMethod(); + */ + self.plugins = {}; + + /** + * URI object to document configured for the TinyMCE instance. + * + * @property documentBaseURI + * @type tinymce.util.URI + * @example + * // Get relative URL from the location of document_base_url + * tinymce.activeEditor.documentBaseURI.toRelative('/somedir/somefile.htm'); + * + * // Get absolute URL from the location of document_base_url + * tinymce.activeEditor.documentBaseURI.toAbsolute('somefile.htm'); + */ + self.documentBaseURI = new URI(settings.document_base_url || documentBaseUrl, { + base_uri: baseUri + }); + + /** + * URI object to current document that holds the TinyMCE editor instance. + * + * @property baseURI + * @type tinymce.util.URI + * @example + * // Get relative URL from the location of the API + * tinymce.activeEditor.baseURI.toRelative('/somedir/somefile.htm'); + * + * // Get absolute URL from the location of the API + * tinymce.activeEditor.baseURI.toAbsolute('somefile.htm'); + */ + self.baseURI = baseUri; + + /** + * Array with CSS files to load into the iframe. + * + * @property contentCSS + * @type Array + */ + self.contentCSS = []; + + /** + * Array of CSS styles to add to head of document when the editor loads. + * + * @property contentStyles + * @type Array + */ + self.contentStyles = []; + + // Creates all events like onClick, onSetContent etc see Editor.Events.js for the actual logic + self.shortcuts = new Shortcuts(self); + self.loadedCSS = {}; + self.editorCommands = new EditorCommands(self); + self.suffix = editorManager.suffix; + self.editorManager = editorManager; + self.inline = settings.inline; + self.settings.content_editable = self.inline; + + if (settings.cache_suffix) { + Env.cacheSuffix = settings.cache_suffix.replace(/^[\?\&]+/, ''); + } + + if (settings.override_viewport === false) { + Env.overrideViewPort = false; + } + + // Call setup + editorManager.fire('SetupEditor', self); + self.execCallback('setup', self); + + /** + * Dom query instance with default scope to the editor document and default element is the body of the editor. + * + * @property $ + * @type tinymce.dom.DomQuery + * @example + * tinymce.activeEditor.$('p').css('color', 'red'); + * tinymce.activeEditor.$().append('new
'); + */ + self.$ = DomQuery.overrideDefaults(function() { + return { + context: self.inline ? self.getBody() : self.getDoc(), + element: self.getBody() + }; + }); + } + + Editor.prototype = { + /** + * Renders the editor/adds it to the page. + * + * @method render + */ + render: function() { + var self = this, settings = self.settings, id = self.id, suffix = self.suffix; + + function readyHandler() { + DOM.unbind(window, 'ready', readyHandler); + self.render(); + } + + // Page is not loaded yet, wait for it + if (!Event.domLoaded) { + DOM.bind(window, 'ready', readyHandler); + return; + } + + // Element not found, then skip initialization + if (!self.getElement()) { + return; + } + + // No editable support old iOS versions etc + if (!Env.contentEditable) { + return; + } + + // Hide target element early to prevent content flashing + if (!settings.inline) { + self.orgVisibility = self.getElement().style.visibility; + self.getElement().style.visibility = 'hidden'; + } else { + self.inline = true; + } + + var form = self.getElement().form || DOM.getParent(id, 'form'); + if (form) { + self.formElement = form; + + // Add hidden input for non input elements inside form elements + if (settings.hidden_input && !/TEXTAREA|INPUT/i.test(self.getElement().nodeName)) { + DOM.insertAfter(DOM.create('input', {type: 'hidden', name: id}), id); + self.hasHiddenInput = true; + } + + // Pass submit/reset from form to editor instance + self.formEventDelegate = function(e) { + self.fire(e.type, e); + }; + + DOM.bind(form, 'submit reset', self.formEventDelegate); + + // Reset contents in editor when the form is reset + self.on('reset', function() { + self.setContent(self.startContent, {format: 'raw'}); + }); + + // Check page uses id="submit" or name="submit" for it's submit button + if (settings.submit_patch && !form.submit.nodeType && !form.submit.length && !form._mceOldSubmit) { + form._mceOldSubmit = form.submit; + form.submit = function() { + self.editorManager.triggerSave(); + self.setDirty(false); + + return form._mceOldSubmit(form); + }; + } + } + + /** + * Window manager reference, use this to open new windows and dialogs. + * + * @property windowManager + * @type tinymce.WindowManager + * @example + * // Shows an alert message + * tinymce.activeEditor.windowManager.alert('Hello world!'); + * + * // Opens a new dialog with the file.htm file and the size 320x240 + * // It also adds a custom parameter this can be retrieved by using tinyMCEPopup.getWindowArg inside the dialog. + * tinymce.activeEditor.windowManager.open({ + * url: 'file.htm', + * width: 320, + * height: 240 + * }, { + * custom_param: 1 + * }); + */ + self.windowManager = new WindowManager(self); + + /** + * Notification manager reference, use this to open new windows and dialogs. + * + * @property notificationManager + * @type tinymce.NotificationManager + * @example + * // Shows a notification info message. + * tinymce.activeEditor.notificationManager.open({text: 'Hello world!', type: 'info'}); + */ + self.notificationManager = new NotificationManager(self); + + if (settings.encoding == 'xml') { + self.on('GetContent', function(e) { + if (e.save) { + e.content = DOM.encode(e.content); + } + }); + } + + if (settings.add_form_submit_trigger) { + self.on('submit', function() { + if (self.initialized) { + self.save(); + } + }); + } + + if (settings.add_unload_trigger) { + self._beforeUnload = function() { + if (self.initialized && !self.destroyed && !self.isHidden()) { + self.save({format: 'raw', no_events: true, set_dirty: false}); + } + }; + + self.editorManager.on('BeforeUnload', self._beforeUnload); + } + + // Load scripts + function loadScripts() { + var scriptLoader = ScriptLoader.ScriptLoader; + + if (settings.language && settings.language != 'en' && !settings.language_url) { + settings.language_url = self.editorManager.baseURL + '/langs/' + settings.language + '.js'; + } + + if (settings.language_url) { + scriptLoader.add(settings.language_url); + } + + if (settings.theme && typeof settings.theme != "function" && + settings.theme.charAt(0) != '-' && !ThemeManager.urls[settings.theme]) { + var themeUrl = settings.theme_url; + + if (themeUrl) { + themeUrl = self.documentBaseURI.toAbsolute(themeUrl); + } else { + themeUrl = 'themes/' + settings.theme + '/theme' + suffix + '.js'; + } + + ThemeManager.load(settings.theme, themeUrl); + } + + if (Tools.isArray(settings.plugins)) { + settings.plugins = settings.plugins.join(' '); + } + + each(settings.external_plugins, function(url, name) { + PluginManager.load(name, url); + settings.plugins += ' ' + name; + }); + + each(settings.plugins.split(/[ ,]/), function(plugin) { + plugin = trim(plugin); + + if (plugin && !PluginManager.urls[plugin]) { + if (plugin.charAt(0) == '-') { + plugin = plugin.substr(1, plugin.length); + + var dependencies = PluginManager.dependencies(plugin); + + each(dependencies, function(dep) { + var defaultSettings = { + prefix: 'plugins/', + resource: dep, + suffix: '/plugin' + suffix + '.js' + }; + + dep = PluginManager.createUrl(defaultSettings, dep); + PluginManager.load(dep.resource, dep); + }); + } else { + PluginManager.load(plugin, { + prefix: 'plugins/', + resource: plugin, + suffix: '/plugin' + suffix + '.js' + }); + } + } + }); + + scriptLoader.loadQueue(function() { + if (!self.removed) { + self.init(); + } + }, self, function (urls) { + ErrorReporter.pluginLoadError(self, urls[0]); + + if (!self.removed) { + self.init(); + } + }); + } + + self.editorManager.add(self); + loadScripts(); + }, + + /** + * Initializes the editor this will be called automatically when + * all plugins/themes and language packs are loaded by the rendered method. + * This method will setup the iframe and create the theme and plugin instances. + * + * @method init + */ + init: function() { + var self = this, settings = self.settings, elm = self.getElement(); + var w, h, minHeight, n, o, Theme, url, bodyId, bodyClass, re, i, initializedPlugins = []; + + self.rtl = settings.rtl_ui || self.editorManager.i18n.rtl; + self.editorManager.i18n.setCode(settings.language); + settings.aria_label = settings.aria_label || DOM.getAttrib(elm, 'aria-label', self.getLang('aria.rich_text_area')); + + self.fire('ScriptsLoaded'); + + /** + * Reference to the theme instance that was used to generate the UI. + * + * @property theme + * @type tinymce.Theme + * @example + * // Executes a method on the theme directly + * tinymce.activeEditor.theme.someMethod(); + */ + if (settings.theme) { + if (typeof settings.theme != "function") { + settings.theme = settings.theme.replace(/-/, ''); + Theme = ThemeManager.get(settings.theme); + self.theme = new Theme(self, ThemeManager.urls[settings.theme]); + + if (self.theme.init) { + self.theme.init(self, ThemeManager.urls[settings.theme] || self.documentBaseUrl.replace(/\/$/, ''), self.$); + } + } else { + self.theme = settings.theme; + } + } + + function initPlugin(plugin) { + var Plugin = PluginManager.get(plugin), pluginUrl, pluginInstance; + + pluginUrl = PluginManager.urls[plugin] || self.documentBaseUrl.replace(/\/$/, ''); + plugin = trim(plugin); + if (Plugin && inArray(initializedPlugins, plugin) === -1) { + each(PluginManager.dependencies(plugin), function(dep) { + initPlugin(dep); + }); + + if (self.plugins[plugin]) { + return; + } + + pluginInstance = new Plugin(self, pluginUrl, self.$); + + self.plugins[plugin] = pluginInstance; + + if (pluginInstance.init) { + pluginInstance.init(self, pluginUrl); + initializedPlugins.push(plugin); + } + } + } + + // Create all plugins + each(settings.plugins.replace(/\-/g, '').split(/[ ,]/), initPlugin); + + // Measure box + if (settings.render_ui && self.theme) { + self.orgDisplay = elm.style.display; + + if (typeof settings.theme != "function") { + w = settings.width || elm.style.width || elm.offsetWidth; + h = settings.height || elm.style.height || elm.offsetHeight; + minHeight = settings.min_height || 100; + re = /^[0-9\.]+(|px)$/i; + + if (re.test('' + w)) { + w = Math.max(parseInt(w, 10), 100); + } + + if (re.test('' + h)) { + h = Math.max(parseInt(h, 10), minHeight); + } + + // Render UI + o = self.theme.renderUI({ + targetNode: elm, + width: w, + height: h, + deltaWidth: settings.delta_width, + deltaHeight: settings.delta_height + }); + + // Resize editor + if (!settings.content_editable) { + h = (o.iframeHeight || h) + (typeof h == 'number' ? (o.deltaHeight || 0) : ''); + if (h < minHeight) { + h = minHeight; + } + } + } else { + o = settings.theme(self, elm); + + if (o.editorContainer.nodeType) { + o.editorContainer.id = o.editorContainer.id || self.id + "_parent"; + } + + if (o.iframeContainer.nodeType) { + o.iframeContainer.id = o.iframeContainer.id || self.id + "_iframecontainer"; + } + + // Use specified iframe height or the targets offsetHeight + h = o.iframeHeight || elm.offsetHeight; + } + + self.editorContainer = o.editorContainer; + } + + // Load specified content CSS last + if (settings.content_css) { + each(explode(settings.content_css), function(u) { + self.contentCSS.push(self.documentBaseURI.toAbsolute(u)); + }); + } + + // Load specified content CSS last + if (settings.content_style) { + self.contentStyles.push(settings.content_style); + } + + // Content editable mode ends here + if (settings.content_editable) { + elm = n = o = null; // Fix IE leak + return self.initContentBody(); + } + + self.iframeHTML = settings.doctype + ''; + + // We only need to override paths if we have to + // IE has a bug where it remove site absolute urls to relative ones if this is specified + if (settings.document_base_url != self.documentBaseUrl) { + self.iframeHTML += ']*>( | |\s|\u00a0|)<\/p>[\r\n]*|
[\r\n]*)$/, '');
+ });
+ }
+
+ self.load({initial: true, format: 'html'});
+ self.startContent = self.getContent({format: 'raw'});
+
+ /**
+ * Is set to true after the editor instance has been initialized
+ *
+ * @property initialized
+ * @type Boolean
+ * @example
+ * function isEditorInitialized(editor) {
+ * return editor && editor.initialized;
+ * }
+ */
+ self.initialized = true;
+ self.bindPendingEventDelegates();
+
+ self.fire('init');
+ self.focus(true);
+ self.nodeChanged({initial: true});
+ self.execCallback('init_instance_callback', self);
+
+ self.on('compositionstart compositionend', function(e) {
+ self.composing = e.type === 'compositionstart';
+ });
+
+ // Add editor specific CSS styles
+ if (self.contentStyles.length > 0) {
+ contentCssText = '';
+
+ each(self.contentStyles, function(style) {
+ contentCssText += style + "\r\n";
+ });
+
+ self.dom.addStyle(contentCssText);
+ }
+
+ // Load specified content CSS last
+ each(self.contentCSS, function(cssUrl) {
+ if (!self.loadedCSS[cssUrl]) {
+ self.dom.loadCSS(cssUrl);
+ self.loadedCSS[cssUrl] = true;
+ }
+ });
+
+ // Handle auto focus
+ if (settings.auto_focus) {
+ Delay.setEditorTimeout(self, function() {
+ var editor;
+
+ if (settings.auto_focus === true) {
+ editor = self;
+ } else {
+ editor = self.editorManager.get(settings.auto_focus);
+ }
+
+ if (!editor.destroyed) {
+ editor.focus();
+ }
+ }, 100);
+ }
+
+ // Clean up references for IE
+ targetElm = doc = body = null;
+ },
+
+ /**
+ * Focuses/activates the editor. This will set this editor as the activeEditor in the tinymce collection
+ * it will also place DOM focus inside the editor.
+ *
+ * @method focus
+ * @param {Boolean} skipFocus Skip DOM focus. Just set is as the active editor.
+ */
+ focus: function(skipFocus) {
+ var self = this, selection = self.selection, contentEditable = self.settings.content_editable, rng;
+ var controlElm, doc = self.getDoc(), body = self.getBody(), contentEditableHost;
+
+ function getContentEditableHost(node) {
+ return self.dom.getParent(node, function(node) {
+ return self.dom.getContentEditable(node) === "true";
+ });
+ }
+
+ if (!skipFocus) {
+ // Get selected control element
+ rng = selection.getRng();
+ if (rng.item) {
+ controlElm = rng.item(0);
+ }
+
+ self.quirks.refreshContentEditable();
+
+ // Move focus to contentEditable=true child if needed
+ contentEditableHost = getContentEditableHost(selection.getNode());
+ if (self.$.contains(body, contentEditableHost)) {
+ contentEditableHost.focus();
+ selection.normalize();
+ self.editorManager.setActive(self);
+ return;
+ }
+
+ // Focus the window iframe
+ if (!contentEditable) {
+ // WebKit needs this call to fire focusin event properly see #5948
+ // But Opera pre Blink engine will produce an empty selection so skip Opera
+ if (!Env.opera) {
+ self.getBody().focus();
+ }
+
+ self.getWin().focus();
+ }
+
+ // Focus the body as well since it's contentEditable
+ if (isGecko || contentEditable) {
+ // Check for setActive since it doesn't scroll to the element
+ 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) {
+ selection.normalize();
+ }
+ }
+
+ // Restore selected control element
+ // This is needed when for example an image is selected within a
+ // layer a call to focus will then remove the control selection
+ if (controlElm && controlElm.ownerDocument == doc) {
+ rng = doc.body.createControlRange();
+ rng.addElement(controlElm);
+ rng.select();
+ }
+ }
+
+ self.editorManager.setActive(self);
+ },
+
+ /**
+ * Executes a legacy callback. This method is useful to call old 2.x option callbacks.
+ * There new event model is a better way to add callback so this method might be removed in the future.
+ *
+ * @method execCallback
+ * @param {String} name Name of the callback to execute.
+ * @return {Object} Return value passed from callback function.
+ */
+ execCallback: function(name) {
+ var self = this, callback = self.settings[name], scope;
+
+ if (!callback) {
+ return;
+ }
+
+ // Look through lookup
+ if (self.callbackLookup && (scope = self.callbackLookup[name])) {
+ callback = scope.func;
+ scope = scope.scope;
+ }
+
+ if (typeof callback === 'string') {
+ scope = callback.replace(/\.\w+$/, '');
+ scope = scope ? resolve(scope) : 0;
+ callback = resolve(callback);
+ self.callbackLookup = self.callbackLookup || {};
+ self.callbackLookup[name] = {func: callback, scope: scope};
+ }
+
+ return callback.apply(scope || self, Array.prototype.slice.call(arguments, 1));
+ },
+
+ /**
+ * Translates the specified string by replacing variables with language pack items it will also check if there is
+ * a key matching the input.
+ *
+ * @method translate
+ * @param {String} text String to translate by the language pack data.
+ * @return {String} Translated string.
+ */
+ translate: function(text) {
+ var lang = this.settings.language || 'en', i18n = this.editorManager.i18n;
+
+ if (!text) {
+ return '';
+ }
+
+ text = i18n.data[lang + '.' + text] || text.replace(/\{\#([^\}]+)\}/g, function(a, b) {
+ return i18n.data[lang + '.' + b] || '{#' + b + '}';
+ });
+
+ return this.editorManager.translate(text);
+ },
+
+ /**
+ * Returns a language pack item by name/key.
+ *
+ * @method getLang
+ * @param {String} name Name/key to get from the language pack.
+ * @param {String} defaultVal Optional default value to retrieve.
+ */
+ getLang: function(name, defaultVal) {
+ return (
+ this.editorManager.i18n.data[(this.settings.language || 'en') + '.' + name] ||
+ (defaultVal !== undefined ? defaultVal : '{#' + name + '}')
+ );
+ },
+
+ /**
+ * Returns a configuration parameter by name.
+ *
+ * @method getParam
+ * @param {String} name Configruation parameter to retrieve.
+ * @param {String} defaultVal Optional default value to return.
+ * @param {String} type Optional type parameter.
+ * @return {String} Configuration parameter value or default value.
+ * @example
+ * // Returns a specific config value from the currently active editor
+ * var someval = tinymce.activeEditor.getParam('myvalue');
+ *
+ * // Returns a specific config value from a specific editor instance by id
+ * var someval2 = tinymce.get('my_editor').getParam('myvalue');
+ */
+ getParam: function(name, defaultVal, type) {
+ var value = name in this.settings ? this.settings[name] : defaultVal, output;
+
+ if (type === 'hash') {
+ output = {};
+
+ if (typeof value === 'string') {
+ each(value.indexOf('=') > 0 ? value.split(/[;,](?![^=;,]*(?:[;,]|$))/) : value.split(','), function(value) {
+ value = value.split('=');
+
+ if (value.length > 1) {
+ output[trim(value[0])] = trim(value[1]);
+ } else {
+ output[trim(value[0])] = trim(value);
+ }
+ });
+ } else {
+ output = value;
+ }
+
+ return output;
+ }
+
+ return value;
+ },
+
+ /**
+ * Dispatches out a onNodeChange event to all observers. This method should be called when you
+ * need to update the UI states or element path etc.
+ *
+ * @method nodeChanged
+ * @param {Object} args Optional args to pass to NodeChange event handlers.
+ */
+ nodeChanged: function(args) {
+ this._nodeChangeDispatcher.nodeChanged(args);
+ },
+
+ /**
+ * 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
+ * // Adds a custom button to the editor that inserts contents when clicked
+ * tinymce.init({
+ * ...
+ *
+ * toolbar: 'example'
+ *
+ * setup: function(ed) {
+ * ed.addButton('example', {
+ * title: 'My title',
+ * image: '../js/tinymce/plugins/example/img/example.gif',
+ * onclick: function() {
+ * ed.insertContent('Hello world!!');
+ * }
+ * });
+ * }
+ * });
+ */
+ addButton: function(name, settings) {
+ var self = this;
+
+ if (settings.cmd) {
+ settings.onclick = function() {
+ self.execCommand(settings.cmd);
+ };
+ }
+
+ if (!settings.text && !settings.icon) {
+ settings.icon = name;
+ }
+
+ self.buttons = self.buttons || {};
+ settings.tooltip = settings.tooltip || settings.title;
+ self.buttons[name] = settings;
+ },
+
+ /**
+ * Adds a sidebar for the editor instance.
+ *
+ * @method addSidebar
+ * @param {String} name Sidebar name to add.
+ * @param {Object} settings Settings object with icon, onshow etc.
+ * @example
+ * // Adds a custom sidebar that when clicked logs the panel element
+ * tinymce.init({
+ * ...
+ * setup: function(ed) {
+ * ed.addSidebar('example', {
+ * tooltip: 'My sidebar',
+ * icon: 'my-side-bar',
+ * onshow: function(api) {
+ * console.log(api.element());
+ * }
+ * });
+ * }
+ * });
+ */
+ addSidebar: function (name, settings) {
+ return Sidebar.add(this, name, settings);
+ },
+
+ /**
+ * 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
+ * // Adds a custom menu item to the editor that inserts contents when clicked
+ * // The context option allows you to add the menu item to an existing default menu
+ * tinymce.init({
+ * ...
+ *
+ * setup: function(ed) {
+ * ed.addMenuItem('example', {
+ * text: 'My menu item',
+ * context: 'tools',
+ * onclick: function() {
+ * ed.insertContent('Hello world!!');
+ * }
+ * });
+ * }
+ * });
+ */
+ addMenuItem: function(name, settings) {
+ var self = this;
+
+ if (settings.cmd) {
+ settings.onclick = function() {
+ self.execCommand(settings.cmd);
+ };
+ }
+
+ self.menuItems = self.menuItems || {};
+ self.menuItems[name] = settings;
+ },
+
+ /**
+ * Adds a contextual toolbar to be rendered when the selector matches.
+ *
+ * @method addContextToolbar
+ * @param {function/string} predicate Predicate that needs to return true if provided strings get converted into CSS predicates.
+ * @param {String/Array} items String or array with items to add to the context toolbar.
+ */
+ addContextToolbar: function(predicate, items) {
+ var self = this, selector;
+
+ self.contextToolbars = self.contextToolbars || [];
+
+ // Convert selector to predicate
+ if (typeof predicate == "string") {
+ selector = predicate;
+ predicate = function(elm) {
+ return self.dom.is(elm, selector);
+ };
+ }
+
+ self.contextToolbars.push({
+ id: Uuid.uuid('mcet'),
+ predicate: predicate,
+ items: items
+ });
+ },
+
+ /**
+ * Adds a custom command to the editor, you can also override existing commands with this method.
+ * The command that you add can be executed with execCommand.
+ *
+ * @method addCommand
+ * @param {String} name Command name to add/override.
+ * @param {addCommandCallback} callback Function to execute when the command occurs.
+ * @param {Object} scope Optional scope to execute the function in.
+ * @example
+ * // Adds a custom command that later can be executed using execCommand
+ * tinymce.init({
+ * ...
+ *
+ * setup: function(ed) {
+ * // Register example command
+ * ed.addCommand('mycommand', function(ui, v) {
+ * ed.windowManager.alert('Hello world!! Selection: ' + ed.selection.getContent({format: 'text'}));
+ * });
+ * }
+ * });
+ */
+ addCommand: function(name, callback, scope) {
+ /**
+ * Callback function that gets called when a command is executed.
+ *
+ * @callback addCommandCallback
+ * @param {Boolean} ui Display UI state true/false.
+ * @param {Object} value Optional value for command.
+ * @return {Boolean} True/false state if the command was handled or not.
+ */
+ this.editorCommands.addCommand(name, callback, scope);
+ },
+
+ /**
+ * Adds a custom query state command to the editor, you can also override existing commands with this method.
+ * The command that you add can be executed with queryCommandState function.
+ *
+ * @method addQueryStateHandler
+ * @param {String} name Command name to add/override.
+ * @param {addQueryStateHandlerCallback} callback Function to execute when the command state retrieval occurs.
+ * @param {Object} scope Optional scope to execute the function in.
+ */
+ addQueryStateHandler: function(name, callback, scope) {
+ /**
+ * Callback function that gets called when a queryCommandState is executed.
+ *
+ * @callback addQueryStateHandlerCallback
+ * @return {Boolean} True/false state if the command is enabled or not like is it bold.
+ */
+ this.editorCommands.addQueryStateHandler(name, callback, scope);
+ },
+
+ /**
+ * Adds a custom query value command to the editor, you can also override existing commands with this method.
+ * The command that you add can be executed with queryCommandValue function.
+ *
+ * @method addQueryValueHandler
+ * @param {String} name Command name to add/override.
+ * @param {addQueryValueHandlerCallback} callback Function to execute when the command value retrieval occurs.
+ * @param {Object} scope Optional scope to execute the function in.
+ */
+ addQueryValueHandler: function(name, callback, scope) {
+ /**
+ * Callback function that gets called when a queryCommandValue is executed.
+ *
+ * @callback addQueryValueHandlerCallback
+ * @return {Object} Value of the command or undefined.
+ */
+ this.editorCommands.addQueryValueHandler(name, callback, scope);
+ },
+
+ /**
+ * Adds a keyboard shortcut for some command or function.
+ *
+ * @method addShortcut
+ * @param {String} pattern Shortcut pattern. Like for example: ctrl+alt+o.
+ * @param {String} desc Text description for the command.
+ * @param {String/Function} cmdFunc Command name string or function to execute when the key is pressed.
+ * @param {Object} sc Optional scope to execute the function in.
+ * @return {Boolean} true/false state if the shortcut was added or not.
+ */
+ addShortcut: function(pattern, desc, cmdFunc, scope) {
+ this.shortcuts.add(pattern, desc, cmdFunc, scope);
+ },
+
+ /**
+ * Executes a command on the current instance. These commands can be TinyMCE internal commands prefixed with "mce" or
+ * they can be build in browser commands such as "Bold". A compleate list of browser commands is available on MSDN or Mozilla.org.
+ * This function will dispatch the execCommand function on each plugin, theme or the execcommand_callback option if none of these
+ * return true it will handle the command as a internal browser command.
+ *
+ * @method execCommand
+ * @param {String} cmd Command name to execute, for example mceLink or Bold.
+ * @param {Boolean} ui True/false state if a UI (dialog) should be presented or not.
+ * @param {mixed} value Optional command value, this can be anything.
+ * @param {Object} args Optional arguments object.
+ */
+ execCommand: function(cmd, ui, value, args) {
+ return this.editorCommands.execCommand(cmd, ui, value, args);
+ },
+
+ /**
+ * Returns a command specific state, for example if bold is enabled or not.
+ *
+ * @method queryCommandState
+ * @param {string} cmd Command to query state from.
+ * @return {Boolean} Command specific state, for example if bold is enabled or not.
+ */
+ queryCommandState: function(cmd) {
+ return this.editorCommands.queryCommandState(cmd);
+ },
+
+ /**
+ * Returns a command specific value, for example the current font size.
+ *
+ * @method queryCommandValue
+ * @param {string} cmd Command to query value from.
+ * @return {Object} Command specific value, for example the current font size.
+ */
+ queryCommandValue: function(cmd) {
+ return this.editorCommands.queryCommandValue(cmd);
+ },
+
+ /**
+ * Returns true/false if the command is supported or not.
+ *
+ * @method queryCommandSupported
+ * @param {String} cmd Command that we check support for.
+ * @return {Boolean} true/false if the command is supported or not.
+ */
+ queryCommandSupported: function(cmd) {
+ return this.editorCommands.queryCommandSupported(cmd);
+ },
+
+ /**
+ * Shows the editor and hides any textarea/div that the editor is supposed to replace.
+ *
+ * @method show
+ */
+ show: function() {
+ var self = this;
+
+ 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();
+
+ if (!self.hidden) {
+ // Fixed bug where IE has a blinking cursor left from the editor
+ if (ie && doc && !self.inline) {
+ doc.execCommand('SelectAll');
+ }
+
+ // 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.hidden = true;
+ 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 !!this.hidden;
+ },
+
+ /**
+ * Sets the progress state, this will display a throbber/progess for the editor.
+ * This is ideal for asynchronous operations like an AJAX save call.
+ *
+ * @method setProgressState
+ * @param {Boolean} state Boolean state if the progress should be shown or hidden.
+ * @param {Number} time Optional time to wait before the progress gets shown.
+ * @return {Boolean} Same as the input state.
+ * @example
+ * // Show progress for the active editor
+ * tinymce.activeEditor.setProgressState(true);
+ *
+ * // Hide progress for the active editor
+ * tinymce.activeEditor.setProgressState(false);
+ *
+ * // Show progress after 3 seconds
+ * tinymce.activeEditor.setProgressState(true, 3000);
+ */
+ setProgressState: function(state, time) {
+ this.fire('ProgressState', {state: state, time: time});
+ },
+
+ /**
+ * Loads contents from the textarea or div element that got converted into an editor instance.
+ * This method will move the contents from that textarea or div into the editor by using setContent
+ * so all events etc that method has will get dispatched as well.
+ *
+ * @method load
+ * @param {Object} args Optional content object, this gets passed around through the whole load process.
+ * @return {String} HTML string that got set into the editor.
+ */
+ load: function(args) {
+ var self = this, elm = self.getElement(), html;
+
+ if (elm) {
+ args = args || {};
+ args.load = true;
+
+ html = self.setContent(elm.value !== undefined ? elm.value : elm.innerHTML, args);
+ args.element = elm;
+
+ if (!args.no_events) {
+ self.fire('LoadContent', args);
+ }
+
+ args.element = elm = null;
+
+ return html;
+ }
+ },
+
+ /**
+ * Saves the contents from a editor out to the textarea or div element that got converted into an editor instance.
+ * This method will move the HTML contents from the editor into that textarea or div by getContent
+ * so all events etc that method has will get dispatched as well.
+ *
+ * @method save
+ * @param {Object} args Optional content object, this gets passed around through the whole save process.
+ * @return {String} HTML string that got set into the textarea/div.
+ */
+ save: function(args) {
+ var self = this, elm = self.getElement(), html, form;
+
+ if (!elm || !self.initialized) {
+ return;
+ }
+
+ args = args || {};
+ args.save = true;
+
+ args.element = elm;
+ html = args.content = self.getContent(args);
+
+ if (!args.no_events) {
+ self.fire('SaveContent', args);
+ }
+
+ // Always run this internal event
+ if (args.format == 'raw') {
+ self.fire('RawSaveContent', args);
+ }
+
+ html = args.content;
+
+ if (!/TEXTAREA|INPUT/i.test(elm.nodeName)) {
+ // Update DIV element when not in inline mode
+ if (!self.inline) {
+ elm.innerHTML = html;
+ }
+
+ // Update hidden form element
+ if ((form = DOM.getParent(self.id, 'form'))) {
+ each(form.elements, function(elm) {
+ if (elm.name == self.id) {
+ elm.value = html;
+ return false;
+ }
+ });
+ }
+ } else {
+ elm.value = html;
+ }
+
+ args.element = elm = null;
+
+ if (args.set_dirty !== false) {
+ self.setDirty(false);
+ }
+
+ return html;
+ },
+
+ /**
+ * Sets the specified content to the editor instance, this will cleanup the content before it gets set using
+ * the different cleanup rules options.
+ *
+ * @method setContent
+ * @param {String} content Content to set to editor, normally HTML contents but can be other formats as well.
+ * @param {Object} args Optional content object, this gets passed around through the whole set process.
+ * @return {String} HTML string that got set into the editor.
+ * @example
+ * // Sets the HTML contents of the activeEditor editor
+ * tinymce.activeEditor.setContent('some html');
+ *
+ * // Sets the raw contents of the activeEditor editor
+ * tinymce.activeEditor.setContent('some html', {format: 'raw'});
+ *
+ * // Sets the content of a specific editor (my_editor in this example)
+ * tinymce.get('my_editor').setContent(data);
+ *
+ * // Sets the bbcode contents of the activeEditor editor if the bbcode plugin was added
+ * tinymce.activeEditor.setContent('[b]some[/b] html', {format: 'bbcode'});
+ */
+ setContent: function(content, args) {
+ var self = this, body = self.getBody(), forcedRootBlockName, padd;
+
+ // Setup args object
+ args = args || {};
+ args.format = args.format || 'html';
+ args.set = true;
+ args.content = content;
+
+ // Do preprocessing
+ if (!args.no_events) {
+ self.fire('BeforeSetContent', args);
+ }
+
+ content = args.content;
+
+ // Padd empty content in Gecko and Safari. Commands will otherwise fail on the content
+ // It will also be impossible to place the caret in the editor unless there is a BR element present
+ if (content.length === 0 || /^\s+$/.test(content)) {
+ padd = ie && ie < 11 ? '' : '
';
+
+ // Todo: There is a lot more root elements that need special padding
+ // so separate this and add all of them at some point.
+ if (body.nodeName == 'TABLE') {
+ content = '
]+data-mce-bogus[^>]+>[\u200b\ufeff]+<\\/span>","\\s?("+x.join("|")+')="[^"]+"'].join("|"),"gi");return e=c.trim(e.replace(t,""))}function m(e){var t=e,r=/<(\w+) [^>]*data-mce-bogus="all"[^>]*>/g,i,a,s,l,c,u=o.schema;for(t=p(t),c=u.getShortEndedElements();l=r.exec(t);)a=r.lastIndex,s=l[0].length,i=c[l[1]]?a:n.findEndTag(u,t,a),t=t.substring(0,a-s)+t.substring(i),r.lastIndex=a-s;return f(t)}function g(){return m(o.getBody().innerHTML)}function v(e){l.inArray(x,e)===-1&&(C.addAttributeFilter(e,function(e,t){for(var n=e.length;n--;)e[n].attr(t,null)}),x.push(e))}var y,b,C,x=["data-mce-selected"];return o&&(y=o.dom,b=o.schema),y=y||h,b=b||new a(e),e.entity_encoding=e.entity_encoding||"named",e.remove_trailing_brs=!("remove_trailing_brs"in e)||e.remove_trailing_brs,C=new t(e,b),C.addAttributeFilter("data-mce-tabindex",function(e,t){for(var n=e.length,r;n--;)r=e[n],r.attr("tabindex",r.attributes.map["data-mce-tabindex"]),r.attr(t,null)}),C.addAttributeFilter("src,href,style",function(t,n){for(var r=t.length,i,o,a="data-mce-"+n,s=e.url_converter,l=e.url_converter_scope,c;r--;)i=t[r],o=i.attributes.map[a],o!==c?(i.attr(n,o.length>0?o:null),i.attr(a,null)):(o=i.attributes.map[n],"style"===n?o=y.serializeStyle(y.parseStyle(o),i.name):s&&(o=s.call(l,o,n,i.name)),i.attr(n,o.length>0?o:null))}),C.addAttributeFilter("class",function(e){for(var t=e.length,n,r;t--;)n=e[t],r=n.attr("class"),r&&(r=n.attr("class").replace(/(?:^|\s)mce-item-\w+(?!\S)/g,""),n.attr("class",r.length>0?r:null))}),C.addAttributeFilter("data-mce-type",function(e,t,n){for(var r=e.length,i;r--;)i=e[r],"bookmark"!==i.attributes.map["data-mce-type"]||n.cleanup||i.remove()}),C.addNodeFilter("noscript",function(e){for(var t=e.length,n;t--;)n=e[t].firstChild,n&&(n.value=r.decode(n.value))}),C.addNodeFilter("script,style",function(e,t){function n(e){return e.replace(/()/g,"\n").replace(/^[\r\n]*|[\r\n]*$/g,"").replace(/^\s*(()?|\s*\/\/\s*\]\]>(-->)?|\/\/\s*(-->)?|\]\]>|\/\*\s*-->\s*\*\/|\s*-->\s*)\s*$/g,"")}for(var r=e.length,i,o,a;r--;)i=e[r],o=i.firstChild?i.firstChild.value:"","script"===t?(a=i.attr("type"),a&&i.attr("type","mce-no/type"==a?null:a.replace(/^mce\-/,"")),o.length>0&&(i.firstChild.value="// ")):o.length>0&&(i.firstChild.value="")}),C.addNodeFilter("#comment",function(e){for(var t=e.length,n;t--;)n=e[t],0===n.value.indexOf("[CDATA[")?(n.name="#cdata",n.type=4,n.value=n.value.replace(/^\[CDATA\[|\]\]$/g,"")):0===n.value.indexOf("mce:protected ")&&(n.name="#text",n.type=3,n.raw=!0,n.value=unescape(n.value).substr(14))}),C.addNodeFilter("xml:namespace,input",function(e,t){for(var n=e.length,r;n--;)r=e[n],7===r.type?r.remove():1===r.type&&("input"!==t||"type"in r.attributes.map||r.attr("type","text"))}),e.fix_list_elements&&C.addNodeFilter("ul,ol",function(e){for(var t=e.length,n,r;t--;)n=e[t],r=n.parent,"ul"!==r.name&&"ol"!==r.name||n.prev&&"li"===n.prev.name&&n.prev.append(n)}),C.addAttributeFilter("data-mce-src,data-mce-href,data-mce-style,data-mce-selected,data-mce-expando,data-mce-type,data-mce-resize",function(e,t){for(var n=e.length;n--;)e[n].attr(t,null)}),{schema:b,addNodeFilter:C.addNodeFilter,addAttributeFilter:C.addAttributeFilter,serialize:function(t,n){var r=this,o,a,l,h,p,m;return s.ie&&y.select("script,style,select,map").length>0?(p=t.innerHTML,t=t.cloneNode(!1),y.setHTML(t,p)):t=t.cloneNode(!0),o=document.implementation,o.createHTMLDocument&&(a=o.createHTMLDocument(""),d("BODY"==t.nodeName?t.childNodes:[t],function(e){a.body.appendChild(a.importNode(e,!0))}),t="BODY"!=t.nodeName?a.body.firstChild:a.body,l=y.doc,y.doc=a),n=n||{},n.format=n.format||"html",n.selection&&(n.forced_root_block=""),n.no_events||(n.node=t,r.onPreProcess(n)),m=C.parse(f(n.getInner?t.innerHTML:y.getOuterHTML(t)),n),u(m),h=new i(e,b),n.content=h.serialize(m),n.cleanup||(n.content=c.trim(n.content),n.content=n.content.replace(/\uFEFF/g,"")),n.no_events||r.onPostProcess(n),l&&(y.doc=l),n.node=null,n.content},addRules:function(e){b.addValidElements(e)},setRules:function(e){b.setValidElements(e)},onPreProcess:function(e){o&&o.fire("PreProcess",e)},onPostProcess:function(e){o&&o.fire("PostProcess",e)},addTempAttr:v,trimHtml:p,getTrimmedContent:g,trimContent:m}}}),r(H,[],function(){function e(e){function t(t,n){var r,i=0,o,a,s,l,c,u,d=-1,f;if(r=t.duplicate(),r.collapse(n),f=r.parentElement(),f.ownerDocument===e.dom.doc){for(;"false"===f.contentEditable;)f=f.parentNode;if(!f.hasChildNodes())return{node:f,inside:1};for(s=f.children,o=s.length-1;i<=o;)if(u=Math.floor((i+o)/2),l=s[u],r.moveToElementText(l),d=r.compareEndPoints(n?"StartToStart":"EndToEnd",t),d>0)o=u-1;else{if(!(d<0))return{node:l};i=u+1}if(d<0)for(l?r.collapse(!1):(r.moveToElementText(f),r.collapse(!0),l=f,a=!0),c=0;0!==r.compareEndPoints(n?"StartToStart":"StartToEnd",t)&&0!==r.move("character",1)&&f==r.parentElement();)c++;else for(r.collapse(!0),c=0;0!==r.compareEndPoints(n?"StartToStart":"StartToEnd",t)&&0!==r.move("character",-1)&&f==r.parentElement();)c++;return{node:l,position:d,offset:c,inside:a}}}function n(){function n(e){var n=t(o,e),r,i,s=0,l,c,u;if(r=n.node,i=n.offset,n.inside&&!r.hasChildNodes())return void a[e?"setStart":"setEnd"](r,0);if(i===c)return void a[e?"setStartBefore":"setEndAfter"](r);if(n.position<0){if(l=n.inside?r.firstChild:r.nextSibling,!l)return void a[e?"setStartAfter":"setEndAfter"](r);if(!i)return void(3==l.nodeType?a[e?"setStart":"setEnd"](l,0):a[e?"setStartBefore":"setEndBefore"](l));for(;l;){if(3==l.nodeType&&(u=l.nodeValue,s+=u.length,s>=i)){r=l,s-=i,s=u.length-s;break}l=l.nextSibling}}else{if(l=r.previousSibling,!l)return a[e?"setStartBefore":"setEndBefore"](r);if(!i)return void(3==r.nodeType?a[e?"setStart":"setEnd"](l,r.nodeValue.length):a[e?"setStartAfter":"setEndAfter"](l));for(;l;){if(3==l.nodeType&&(s+=l.nodeValue.length,s>=i)){r=l,s-=i;break}l=l.previousSibling}}a[e?"setStart":"setEnd"](r,s)}var o=e.getRng(),a=i.createRng(),s,l,c,u,d;if(s=o.item?o.item(0):o.parentElement(),s.ownerDocument!=i.doc)return a;if(l=e.isCollapsed(),o.item)return a.setStart(s.parentNode,i.nodeIndex(s)),a.setEnd(a.startContainer,a.startOffset+1),a;try{n(!0),l||n()}catch(f){if(f.number!=-2147024809)throw f;d=r.getBookmark(2),c=o.duplicate(),c.collapse(!0),s=c.parentElement(),l||(c=o.duplicate(),c.collapse(!1),u=c.parentElement(),u.innerHTML=u.innerHTML),s.innerHTML=s.innerHTML,r.moveToBookmark(d),o=e.getRng(),n(!0),l||n()}return a}var r=this,i=e.dom,o=!1;this.getBookmark=function(n){function r(e){var t,n,r,o,a=[];for(t=e.parentNode,n=i.getRoot().parentNode;t!=n&&9!==t.nodeType;){for(r=t.children,o=r.length;o--;)if(e===r[o]){a.push(o);break}e=t,t=t.parentNode}return a}function o(e){var n;if(n=t(a,e))return{position:n.position,offset:n.offset,indexes:r(n.node),inside:n.inside}}var a=e.getRng(),s={};return 2===n&&(a.item?s.start={ctrl:!0,indexes:r(a.item(0))}:(s.start=o(!0),e.isCollapsed()||(s.end=o()))),s},this.moveToBookmark=function(e){function t(e){var t,n,r,o;for(t=i.getRoot(),n=e.length-1;n>=0;n--)o=t.children,r=e[n],r<=o.length-1&&(t=o[r]);return t}function n(n){var i=e[n?"start":"end"],a,s,l,c;i&&(a=i.position>0,s=o.createTextRange(),s.moveToElementText(t(i.indexes)),c=i.offset,c!==l?(s.collapse(i.inside||a),s.moveStart("character",a?-c:c)):s.collapse(n),r.setEndPoint(n?"StartToStart":"EndToStart",s),n&&r.collapse(!0))}var r,o=i.doc.body;e.start&&(e.start.ctrl?(r=o.createControlRange(),r.addElement(t(e.start.indexes)),r.select()):(r=o.createTextRange(),n(!0),n(),r.select()))},this.addRange=function(t){function n(e){var t,n,a,d,p;a=i.create("a"),t=e?s:c,n=e?l:u,d=r.duplicate(),t!=f&&t!=f.documentElement||(t=h,n=0),3==t.nodeType?(t.parentNode.insertBefore(a,t),d.moveToElementText(a),d.moveStart("character",n),i.remove(a),r.setEndPoint(e?"StartToStart":"EndToEnd",d)):(p=t.childNodes,p.length?(n>=p.length?i.insertAfter(a,p[p.length-1]):t.insertBefore(a,p[n]),d.moveToElementText(a)):t.canHaveHTML&&(t.innerHTML="",a=t.firstChild,d.moveToElementText(a),d.collapse(o)),r.setEndPoint(e?"StartToStart":"EndToEnd",d),i.remove(a))}var r,a,s,l,c,u,d,f=e.dom.doc,h=f.body,p,m;if(s=t.startContainer,l=t.startOffset,c=t.endContainer,u=t.endOffset,r=h.createTextRange(),s==c&&1==s.nodeType){if(l==u&&!s.hasChildNodes()){if(s.canHaveHTML)return d=s.previousSibling,d&&!d.hasChildNodes()&&i.isBlock(d)?d.innerHTML="":d=null,s.innerHTML="",r.moveToElementText(s.lastChild),r.select(),i.doc.selection.clear(),s.innerHTML="",void(d&&(d.innerHTML=""));l=i.nodeIndex(s),s=s.parentNode}if(l==u-1)try{if(m=s.childNodes[l],a=h.createControlRange(),a.addElement(m),a.select(),p=e.getRng(),p.item&&m===p.item(0))return}catch(g){}}n(!0),n(),r.select()},this.getRangeAt=n}return e}),r(I,[d],function(e){return{BACKSPACE:8,DELETE:46,DOWN:40,ENTER:13,LEFT:37,RIGHT:39,SPACEBAR:32,TAB:9,UP:38,modifierPressed:function(e){return e.shiftKey||e.ctrlKey||e.altKey||this.metaKeyPressed(e)},metaKeyPressed:function(t){return e.mac?t.metaKey:t.ctrlKey&&!t.altKey}}}),r(F,[I,m,u,d,_],function(e,t,n,r,i){function o(e,t){for(;t&&t!=e;){if(s(t)||a(t))return t;t=t.parentNode}return null}var a=i.isContentEditableFalse,s=i.isContentEditableTrue;return function(i,s){function l(e){var t=s.settings.object_resizing;return t!==!1&&!r.iOS&&("string"!=typeof t&&(t="table,img,div"),"false"!==e.getAttribute("data-mce-resize")&&(e!=s.getBody()&&s.dom.is(e,t)))}function c(t){var n,r,i,o,a;n=t.screenX-L,r=t.screenY-M,U=n*B[2]+H,W=r*B[3]+I,U=U<5?5:U,W=W<5?5:W,i="IMG"==k.nodeName&&s.settings.resize_img_proportional!==!1?!e.modifierPressed(t):e.modifierPressed(t)||"IMG"==k.nodeName&&B[2]*B[3]!==0,i&&(j(n)>j(r)?(W=Y(U*F),U=Y(W/F)):(U=Y(W/F),W=Y(U*F))),_.setStyles(T,{width:U,height:W}),o=B.startPos.x+n,a=B.startPos.y+r,o=o>0?o:0,a=a>0?a:0,_.setStyles(R,{left:o,top:a,display:"block"}),R.innerHTML=U+" × "+W,B[2]<0&&T.clientWidth<=U&&_.setStyle(T,"left",P+(H-U)),B[3]<0&&T.clientHeight<=W&&_.setStyle(T,"top",O+(I-W)),n=X.scrollWidth-K,r=X.scrollHeight-G,n+r!==0&&_.setStyles(R,{left:o-n,top:a-r}),z||(s.fire("ObjectResizeStart",{target:k,width:H,height:I}),z=!0)}function u(){function e(e,t){t&&(k.style[e]||!s.schema.isValid(k.nodeName.toLowerCase(),e)?_.setStyle(k,e,t):_.setAttrib(k,e,t))}z=!1,e("width",U),e("height",W),_.unbind(V,"mousemove",c),_.unbind(V,"mouseup",u),$!=V&&(_.unbind($,"mousemove",c),_.unbind($,"mouseup",u)),_.remove(T),_.remove(R),q&&"TABLE"!=k.nodeName||d(k),s.fire("ObjectResized",{target:k,width:U,height:W}),_.setAttrib(k,"style",_.getAttrib(k,"style")),s.nodeChanged()}function d(e,t,n){var i,o,a,d,h;f(),x(),i=_.getPos(e,X),P=i.x,O=i.y,h=e.getBoundingClientRect(),o=h.width||h.right-h.left,a=h.height||h.bottom-h.top,k!=e&&(C(),k=e,U=W=0),d=s.fire("ObjectSelected",{target:e}),l(e)&&!d.isDefaultPrevented()?S(A,function(e,i){function s(t){L=t.screenX,M=t.screenY,H=k.clientWidth,I=k.clientHeight,F=I/H,B=e,e.startPos={x:o*e[0]+P,y:a*e[1]+O},K=X.scrollWidth,G=X.scrollHeight,T=k.cloneNode(!0),_.addClass(T,"mce-clonedresizable"),_.setAttrib(T,"data-mce-bogus","all"),T.contentEditable=!1,T.unSelectabe=!0,_.setStyles(T,{left:P,top:O,margin:0}),T.removeAttribute("data-mce-selected"),X.appendChild(T),_.bind(V,"mousemove",c),_.bind(V,"mouseup",u),$!=V&&(_.bind($,"mousemove",c),_.bind($,"mouseup",u)),R=_.add(X,"div",{"class":"mce-resize-helper","data-mce-bogus":"all"},H+" × "+I)}var l;return t?void(i==t&&s(n)):(l=_.get("mceResizeHandle"+i),l&&_.remove(l),l=_.add(X,"div",{id:"mceResizeHandle"+i,"data-mce-bogus":"all","class":"mce-resizehandle",unselectable:!0,style:"cursor:"+i+"-resize; margin:0; padding:0"}),r.ie&&(l.contentEditable=!1),_.bind(l,"mousedown",function(e){e.stopImmediatePropagation(),e.preventDefault(),s(e)}),e.elm=l,void _.setStyles(l,{left:o*e[0]+P-l.offsetWidth/2,top:a*e[1]+O-l.offsetHeight/2}))}):f(),k.setAttribute("data-mce-selected","1")}function f(){var e,t;x(),k&&k.removeAttribute("data-mce-selected");for(e in A)t=_.get("mceResizeHandle"+e),t&&(_.unbind(t),_.remove(t))}function h(e){function t(e,t){if(e)do if(e===t)return!0;while(e=e.parentNode)}var n,r;if(!z&&!s.removed)return S(_.select("img[data-mce-selected],hr[data-mce-selected]"),function(e){e.removeAttribute("data-mce-selected")}),r="mousedown"==e.type?e.target:i.getNode(),r=_.$(r).closest(q?"table":"table,img,hr")[0],t(r,X)&&(w(),n=i.getStart(!0),t(n,r)&&t(i.getEnd(!0),r)&&(!q||r!=n&&"IMG"!==n.nodeName))?void d(r):void f()}function p(e,t,n){e&&e.attachEvent&&e.attachEvent("on"+t,n)}function m(e,t,n){e&&e.detachEvent&&e.detachEvent("on"+t,n)}function g(e){var t=e.srcElement,n,r,i,o,a,l,c;n=t.getBoundingClientRect(),l=D.clientX-n.left,c=D.clientY-n.top;for(r in A)if(i=A[r],o=t.offsetWidth*i[0],a=t.offsetHeight*i[1],j(o-l)<8&&j(a-c)<8){B=i;break}z=!0,s.fire("ObjectResizeStart",{target:k,width:k.clientWidth,height:k.clientHeight}),s.getDoc().selection.empty(),d(t,r,D)}function v(e){e.preventDefault?e.preventDefault():e.returnValue=!1}function y(e){return a(o(s.getBody(),e))}function b(e){var t=e.srcElement;if(y(t))return void v(e);if(t!=k){if(s.fire("ObjectSelected",{target:t}),C(),0===t.id.indexOf("mceResizeHandle"))return void(e.returnValue=!1);"IMG"!=t.nodeName&&"TABLE"!=t.nodeName||(f(),k=t,p(t,"resizestart",g))}}function C(){m(k,"resizestart",g)}function x(){for(var e in A){var t=A[e];t.elm&&(_.unbind(t.elm),delete t.elm)}}function w(){try{s.getDoc().execCommand("enableObjectResizing",!1,!1)}catch(e){}}function E(e){var t;if(q){t=V.body.createControlRange();try{return t.addElement(e),t.select(),!0}catch(n){}}}function N(){k=T=null,q&&(C(),m(X,"controlselect",b))}var _=s.dom,S=t.each,k,T,R,A,B,D,L,M,P,O,H,I,F,z,U,W,V=s.getDoc(),$=document,q=r.ie&&r.ie<11,j=Math.abs,Y=Math.round,X=s.getBody(),K,G;A={nw:[0,0,-1,-1],ne:[1,0,1,-1],se:[1,1,1,1],sw:[0,1,-1,1]};var J=".mce-content-body";return s.contentStyles.push(J+" div.mce-resizehandle {position: absolute;border: 1px solid black;box-sizing: box-sizing;background: #FFF;width: 7px;height: 7px;z-index: 10000}"+J+" .mce-resizehandle:hover {background: #000}"+J+" img[data-mce-selected],"+J+" hr[data-mce-selected] {outline: 1px solid black;resize: none}"+J+" .mce-clonedresizable {position: absolute;"+(r.gecko?"":"outline: 1px dashed black;")+"opacity: .5;filter: alpha(opacity=50);z-index: 10000}"+J+" .mce-resize-helper {background: #555;background: rgba(0,0,0,0.75);border-radius: 3px;border: 1px;color: white;display: none;font-family: sans-serif;font-size: 12px;white-space: nowrap;line-height: 14px;margin: 5px 10px;padding: 5px;position: absolute;z-index: 10001}"),
-s.on("init",function(){q?(s.on("ObjectResized",function(e){"TABLE"!=e.target.nodeName&&(f(),E(e.target))}),p(X,"controlselect",b),s.on("mousedown",function(e){D=e})):(w(),r.ie>=11&&(s.on("mousedown click",function(e){var t=e.target,n=t.nodeName;z||!/^(TABLE|IMG|HR)$/.test(n)||y(t)||(s.selection.select(t,"TABLE"==n),"mousedown"==e.type&&s.nodeChanged())}),s.dom.bind(X,"mscontrolselect",function(e){function t(e){n.setEditorTimeout(s,function(){s.selection.select(e)})}return y(e.target)?(e.preventDefault(),void t(e.target)):void(/^(TABLE|IMG|HR)$/.test(e.target.nodeName)&&(e.preventDefault(),"IMG"==e.target.tagName&&t(e.target)))})));var e=n.throttle(function(e){s.composing||h(e)});s.on("nodechange ResizeEditor ResizeWindow drop",e),s.on("keyup compositionend",function(t){k&&"TABLE"==k.nodeName&&e(t)}),s.on("hide blur",f)}),s.on("remove",x),{isResizable:l,showResizeRect:d,hideResizeRect:f,updateResizeRect:h,controlSelect:E,destroy:N}}}),r(z,[],function(){function e(e){return function(){return e}}function t(e){return function(t){return!e(t)}}function n(e,t){return function(n){return e(t(n))}}function r(){var e=s.call(arguments);return function(t){for(var n=0;n
'),t}function l(){var e,t;return e=c.createRng(),t=r.resolve(c.getRoot(),n.start),e.setStart(t.container(),t.offset()),t=r.resolve(c.getRoot(),n.end),e.setEnd(t.container(),t.offset()),e}var u,d,f,h,p,m;if(n)if(t.isArray(n.start)){if(u=c.createRng(),d=c.getRoot(),s.tridentSel)return s.tridentSel.moveToBookmark(n);i(!0)&&i()&&s.setRng(u)}else"string"==typeof n.start?s.setRng(l(n)):n.id?(o("start"),o("end"),f&&(u=c.createRng(),u.setStart(a(f),p),u.setEnd(a(h),m),s.setRng(u))):n.name?s.select(c.select(n.name)[n.index]):n.rng&&s.setRng(n.rng)}}var l=o.isContentEditableFalse;return s.isBookmarkNode=function(e){return e&&"SPAN"===e.tagName&&"bookmark"===e.getAttribute("data-mce-type")},s}),r(Y,[y,H,F,T,j,_,d,m,$],function(e,n,r,i,o,a,s,l,c){function u(e,t,i,a){var s=this;s.dom=e,s.win=t,s.serializer=i,s.editor=a,s.bookmarkManager=new o(s),s.controlSelection=new r(s,a),s.win.getSelection||(s.tridentSel=new n(s))}var d=l.each,f=l.trim,h=s.ie;return u.prototype={setCursorLocation:function(e,t){var n=this,r=n.dom.createRng();e?(r.setStart(e,t),r.setEnd(e,t),n.setRng(r),n.collapse(!1)):(n._moveEndPoint(r,n.editor.getBody(),!0),n.setRng(r))},getContent:function(e){var n=this,r=n.getRng(),i=n.dom.create("body"),o=n.getSel(),a,s,l;return e=e||{},a=s="",e.get=!0,e.format=e.format||"html",e.selection=!0,n.editor.fire("BeforeGetContent",e),"text"==e.format?n.isCollapsed()?"":r.text||(o.toString?o.toString():""):(r.cloneContents?(l=r.cloneContents(),l&&i.appendChild(l)):r.item!==t||r.htmlText!==t?(i.innerHTML="
"+(r.item?r.item(0).outerHTML:r.htmlText),i.removeChild(i.firstChild)):i.innerHTML=r.toString(),/^\s/.test(i.innerHTML)&&(a=" "),/\s+$/.test(i.innerHTML)&&(s=" "),e.getInner=!0,e.content=n.isCollapsed()?"":a+n.serializer.serialize(i,e)+s,n.editor.fire("GetContent",e),e.content)},setContent:function(e,t){var n=this,r=n.getRng(),i,o=n.win.document,a,s;if(t=t||{format:"html"},t.set=!0,t.selection=!0,t.content=e,t.no_events||n.editor.fire("BeforeSetContent",t),e=t.content,r.insertNode){e+='_',r.startContainer==o&&r.endContainer==o?o.body.innerHTML=e:(r.deleteContents(),0===o.body.childNodes.length?o.body.innerHTML=e:r.createContextualFragment?r.insertNode(r.createContextualFragment(e)):(a=o.createDocumentFragment(),s=o.createElement("div"),a.appendChild(s),s.outerHTML=e,r.insertNode(a))),i=n.dom.get("__caret"),r=o.createRange(),r.setStartBefore(i),r.setEndBefore(i),n.setRng(r),n.dom.remove("__caret");try{n.setRng(r)}catch(l){}}else r.item&&(o.execCommand("Delete",!1,null),r=n.getRng()),/^\s+/.test(e)?(r.pasteHTML('_'+e),n.dom.remove("__mce_tmp")):r.pasteHTML(e);t.no_events||n.editor.fire("SetContent",t)},getStart:function(e){var t=this,n=t.getRng(),r,i,o,a;if(n.duplicate||n.item){if(n.item)return n.item(0);for(o=n.duplicate(),o.collapse(1),r=o.parentElement(),r.ownerDocument!==t.dom.doc&&(r=t.dom.getRoot()),i=a=n.parentElement();a=a.parentNode;)if(a==r){r=i;break}return r}return r=n.startContainer,1==r.nodeType&&r.hasChildNodes()&&(e&&n.collapsed||(r=r.childNodes[Math.min(r.childNodes.length-1,n.startOffset)])),r&&3==r.nodeType?r.parentNode:r},getEnd:function(e){var t=this,n=t.getRng(),r,i;return n.duplicate||n.item?n.item?n.item(0):(n=n.duplicate(),n.collapse(0),r=n.parentElement(),r.ownerDocument!==t.dom.doc&&(r=t.dom.getRoot()),r&&"BODY"==r.nodeName?r.lastChild||r:r):(r=n.endContainer,i=n.endOffset,1==r.nodeType&&r.hasChildNodes()&&(e&&n.collapsed||(r=r.childNodes[i>0?i-1:i])),r&&3==r.nodeType?r.parentNode:r)},getBookmark:function(e,t){return this.bookmarkManager.getBookmark(e,t)},moveToBookmark:function(e){return this.bookmarkManager.moveToBookmark(e)},select:function(e,t){var n=this,r=n.dom,i=r.createRng(),o;if(n.lastFocusBookmark=null,e){if(!t&&n.controlSelection.controlSelect(e))return;o=r.nodeIndex(e),i.setStart(e.parentNode,o),i.setEnd(e.parentNode,o+1),t&&(n._moveEndPoint(i,e,!0),n._moveEndPoint(i,e)),n.setRng(i)}return e},isCollapsed:function(){var e=this,t=e.getRng(),n=e.getSel();return!(!t||t.item)&&(t.compareEndPoints?0===t.compareEndPoints("StartToEnd",t):!n||t.collapsed)},collapse:function(e){var t=this,n=t.getRng(),r;n.item&&(r=n.item(0),n=t.win.document.body.createTextRange(),n.moveToElementText(r)),n.collapse(!!e),t.setRng(n)},getSel:function(){var e=this.win;return e.getSelection?e.getSelection():e.document.selection},getRng:function(e){function t(e,t,n){try{return t.compareBoundaryPoints(e,n)}catch(r){return-1}}var n=this,r,i,o,a,s,l;if(!n.win)return null;if(a=n.win.document,"undefined"==typeof a||null===a)return null;if(!e&&n.lastFocusBookmark){var c=n.lastFocusBookmark;return c.startContainer?(i=a.createRange(),i.setStart(c.startContainer,c.startOffset),i.setEnd(c.endContainer,c.endOffset)):i=c,i}if(e&&n.tridentSel)return n.tridentSel.getRangeAt(0);try{(r=n.getSel())&&(i=r.rangeCount>0?r.getRangeAt(0):r.createRange?r.createRange():a.createRange())}catch(u){}if(l=n.editor.fire("GetSelectionRange",{range:i}),l.range!==i)return l.range;if(h&&i&&i.setStart&&a.selection){try{s=a.selection.createRange()}catch(u){}s&&s.item&&(o=s.item(0),i=a.createRange(),i.setStartBefore(o),i.setEndAfter(o))}return i||(i=a.createRange?a.createRange():a.body.createTextRange()),i.setStart&&9===i.startContainer.nodeType&&i.collapsed&&(o=n.dom.getRoot(),i.setStart(o,0),i.setEnd(o,0)),n.selectedRange&&n.explicitRange&&(0===t(i.START_TO_START,i,n.selectedRange)&&0===t(i.END_TO_END,i,n.selectedRange)?i=n.explicitRange:(n.selectedRange=null,n.explicitRange=null)),i},setRng:function(e,t){var n=this,r,i,o;if(e)if(e.select){n.explicitRange=null;try{e.select()}catch(a){}}else if(n.tridentSel){if(e.cloneRange)try{n.tridentSel.addRange(e)}catch(a){}}else{if(r=n.getSel(),o=n.editor.fire("SetSelectionRange",{range:e}),e=o.range,r){n.explicitRange=e;try{r.removeAllRanges(),r.addRange(e)}catch(a){}t===!1&&r.extend&&(r.collapse(e.endContainer,e.endOffset),r.extend(e.startContainer,e.startOffset)),n.selectedRange=r.rangeCount>0?r.getRangeAt(0):null}e.collapsed||e.startContainer!=e.endContainer||!r.setBaseAndExtent||s.ie||e.endOffset-e.startOffset<2&&e.startContainer.hasChildNodes()&&(i=e.startContainer.childNodes[e.startOffset],i&&"IMG"==i.tagName&&n.getSel().setBaseAndExtent(i,0,i,1)),n.editor.fire("AfterSetSelectionRange",{range:e})}},setNode:function(e){var t=this;return t.setContent(t.dom.getOuterHTML(e)),e},getNode:function(){function e(e,t){for(var n=e;e&&3===e.nodeType&&0===e.length;)e=t?e.nextSibling:e.previousSibling;return e||n}var t=this,n=t.getRng(),r,i,o,a,s,l=t.dom.getRoot();return n?(i=n.startContainer,o=n.endContainer,a=n.startOffset,s=n.endOffset,n.setStart?(r=n.commonAncestorContainer,!n.collapsed&&(i==o&&s-a<2&&i.hasChildNodes()&&(r=i.childNodes[a]),3===i.nodeType&&3===o.nodeType&&(i=i.length===a?e(i.nextSibling,!0):i.parentNode,o=0===s?e(o.previousSibling,!1):o.parentNode,i&&i===o))?i:r&&3==r.nodeType?r.parentNode:r):(r=n.item?n.item(0):n.parentElement(),r.ownerDocument!==t.win.document&&(r=l),r)):l},getSelectedBlocks:function(t,n){var r=this,i=r.dom,o,a,s=[];if(a=i.getRoot(),t=i.getParent(t||r.getStart(),i.isBlock),n=i.getParent(n||r.getEnd(),i.isBlock),t&&t!=a&&s.push(t),t&&n&&t!=n){o=t;for(var l=new e(t,a);(o=l.next())&&o!=n;)i.isBlock(o)&&s.push(o)}return n&&t!=n&&n!=a&&s.push(n),s},isForward:function(){var e=this.dom,t=this.getSel(),n,r;return!(t&&t.anchorNode&&t.focusNode)||(n=e.createRng(),n.setStart(t.anchorNode,t.anchorOffset),n.collapse(!0),r=e.createRng(),r.setStart(t.focusNode,t.focusOffset),r.collapse(!0),n.compareBoundaryPoints(n.START_TO_START,r)<=0)},normalize:function(){var e=this,t=e.getRng();return s.range&&new i(e.dom).normalize(t)&&e.setRng(t,e.isForward()),t},selectorChanged:function(e,t){var n=this,r;return n.selectorChangedData||(n.selectorChangedData={},r={},n.editor.on("NodeChange",function(e){var t=e.element,i=n.dom,o=i.getParents(t,null,i.getRoot()),a={};d(n.selectorChangedData,function(e,t){d(o,function(n){if(i.is(n,t))return r[t]||(d(e,function(e){e(!0,{node:n,selector:t,parents:o})}),r[t]=e),a[t]=e,!1})}),d(r,function(e,n){a[n]||(delete r[n],d(e,function(e){e(!1,{node:t,selector:n,parents:o})}))})})),n.selectorChangedData[e]||(n.selectorChangedData[e]=[]),n.selectorChangedData[e].push(t),n},getScrollContainer:function(){for(var e,t=this.dom.getRoot();t&&"BODY"!=t.nodeName;){if(t.scrollHeight>t.clientHeight){e=t;break}t=t.parentNode}return e},scrollIntoView:function(e,t){function n(e){for(var t=0,n=0,r=e;r&&r.nodeType;)t+=r.offsetLeft||0,n+=r.offsetTop||0,r=r.offsetParent;return{x:t,y:n}}var r,i,o=this,s=o.dom,l=s.getRoot(),c,u,d=0;if(a.isElement(e)){if(t===!1&&(d=e.offsetHeight),"BODY"!=l.nodeName){var f=o.getScrollContainer();if(f)return r=n(e).y-n(f).y+d,u=f.clientHeight,c=f.scrollTop,void((r
").append(t.childNodes)}var l=r.selection.getRng(),c,u;c=t.matchNodeNames("pre"),l.collapsed||(u=r.selection.getSelectedBlocks(),s(a(a(u,c),i),function(e){o(e.previousSibling,e)}))}),{postProcess:i}}),r(J,[y,T,j,X,m,K,G],function(e,t,n,r,i,o,a){return function(s){function l(e){return e.nodeType&&(e=e.nodeName),!!s.schema.getTextBlockElements()[e.toLowerCase()]}function c(e){return/^(TH|TD)$/.test(e.nodeName)}function u(e){return e&&/^(IMG)$/.test(e.nodeName)}function d(e,t){return Y.getParents(e,t,Y.getRoot())}function f(e){return 1===e.nodeType&&"_mce_caret"===e.id}function h(){g({valigntop:[{selector:"td,th",styles:{verticalAlign:"top"}}],valignmiddle:[{selector:"td,th",styles:{verticalAlign:"middle"}}],valignbottom:[{selector:"td,th",styles:{verticalAlign:"bottom"}}],alignleft:[{selector:"figure.image",collapsed:!1,classes:"align-left",ceFalseOverride:!0,preview:"font-family font-size"},{selector:"figure,p,h1,h2,h3,h4,h5,h6,td,th,tr,div,ul,ol,li",styles:{textAlign:"left"},inherit:!1,preview:!1,defaultBlock:"div"},{selector:"img,table",collapsed:!1,styles:{"float":"left"},preview:"font-family font-size"}],aligncenter:[{selector:"figure,p,h1,h2,h3,h4,h5,h6,td,th,tr,div,ul,ol,li",styles:{textAlign:"center"},inherit:!1,preview:!1,defaultBlock:"div"},{selector:"figure.image",collapsed:!1,classes:"align-center",ceFalseOverride:!0,preview:"font-family font-size"},{selector:"img",collapsed:!1,styles:{display:"block",marginLeft:"auto",marginRight:"auto"},preview:!1},{selector:"table",collapsed:!1,styles:{marginLeft:"auto",marginRight:"auto"},preview:"font-family font-size"}],alignright:[{selector:"figure.image",collapsed:!1,classes:"align-right",ceFalseOverride:!0,preview:"font-family font-size"},{selector:"figure,p,h1,h2,h3,h4,h5,h6,td,th,tr,div,ul,ol,li",styles:{textAlign:"right"},inherit:!1,preview:"font-family font-size",defaultBlock:"div"},{selector:"img,table",collapsed:!1,styles:{"float":"right"},preview:"font-family font-size"}],alignjustify:[{selector:"figure,p,h1,h2,h3,h4,h5,h6,td,th,tr,div,ul,ol,li",styles:{textAlign:"justify"},inherit:!1,defaultBlock:"div",preview:"font-family font-size"}],bold:[{inline:"strong",remove:"all"},{inline:"span",styles:{fontWeight:"bold"}},{inline:"b",remove:"all"}],italic:[{inline:"em",remove:"all"},{inline:"span",styles:{fontStyle:"italic"}},{inline:"i",remove:"all"}],underline:[{inline:"span",styles:{textDecoration:"underline"},exact:!0},{inline:"u",remove:"all"}],strikethrough:[{inline:"span",styles:{textDecoration:"line-through"},exact:!0},{inline:"strike",remove:"all"}],forecolor:{inline:"span",styles:{color:"%value"},links:!0,remove_similar:!0},hilitecolor:{inline:"span",styles:{backgroundColor:"%value"},links:!0,remove_similar:!0},fontname:{inline:"span",styles:{fontFamily:"%value"}},fontsize:{inline:"span",styles:{fontSize:"%value"}},fontsize_class:{inline:"span",attributes:{"class":"%value"}},blockquote:{block:"blockquote",wrapper:1,remove:"all"},subscript:{inline:"sub"},superscript:{inline:"sup"},code:{inline:"code"},link:{inline:"a",selector:"a",remove:"all",split:!0,deep:!0,onmatch:function(){return!0},onformat:function(e,t,n){ue(n,function(t,n){Y.setAttrib(e,n,t)})}},removeformat:[{selector:"b,strong,em,i,font,u,strike,sub,sup,dfn,code,samp,kbd,var,cite,mark,q,del,ins",remove:"all",split:!0,expand:!1,block_expand:!0,deep:!0},{selector:"span",attributes:["style","class"],remove:"empty",split:!0,expand:!1,deep:!0},{selector:"*",attributes:["style","class"],split:!1,expand:!1,deep:!0}]}),ue("p h1 h2 h3 h4 h5 h6 div address pre div dt dd samp".split(/\s/),function(e){g(e,{block:e,remove:"all"})}),g(s.settings.formats)}function p(){s.addShortcut("meta+b","bold_desc","Bold"),s.addShortcut("meta+i","italic_desc","Italic"),s.addShortcut("meta+u","underline_desc","Underline");for(var e=1;e<=6;e++)s.addShortcut("access+"+e,"",["FormatBlock",!1,"h"+e]);s.addShortcut("access+7","",["FormatBlock",!1,"p"]),s.addShortcut("access+8","",["FormatBlock",!1,"div"]),s.addShortcut("access+9","",["FormatBlock",!1,"address"])}function m(e){return e?j[e]:j}function g(e,t){e&&("string"!=typeof e?ue(e,function(e,t){g(t,e)}):(t=t.length?t:[t],ue(t,function(e){e.deep===oe&&(e.deep=!e.selector),e.split===oe&&(e.split=!e.selector||e.inline),e.remove===oe&&e.selector&&!e.inline&&(e.remove="none"),e.selector&&e.inline&&(e.mixed=!0,e.block_expand=!0),"string"==typeof e.classes&&(e.classes=e.classes.split(/\s+/))}),j[e]=t))}function v(e){return e&&j[e]&&delete j[e],j}function y(e,t){var n=m(t);if(n)for(var r=0;r
'),n}function w(t){var n,r,i;if(3==L.nodeType&&(t?M>0:M
|)$/," "))),e}function f(){var e,t,n;e=D.getRng(!0),t=e.startContainer,n=e.startOffset,3==t.nodeType&&e.collapsed&&("\xa0"===t.data[n]?(t.deleteData(n,1),/[\u00a0| ]$/.test(c)||(c+=" ")):"\xa0"===t.data[n-1]&&(t.deleteData(n-1,1),/[\u00a0| ]$/.test(c)||(c=" "+c)))}function h(){if(A){var e=a.getBody(),n=new o(L);t.each(L.select("*[data-mce-fragment]"),function(t){for(var r=t.parentNode;r&&r!=e;r=r.parentNode)B[t.nodeName.toLowerCase()]&&n.compare(r,t)&&L.remove(t,!0)})}}function p(e){for(var t=e;t=t.walk();)1===t.type&&t.attr("data-mce-fragment","1")}function m(e){t.each(e.getElementsByTagName("*"),function(e){e.removeAttribute("data-mce-fragment")})}function g(e){return!!e.getAttribute("data-mce-fragment")}function v(e){return e&&!a.schema.getShortEndedElements()[e.nodeName]}function y(t){function n(e){for(var t=a.getBody();e&&e!==t;e=e.parentNode)if("false"===a.dom.getContentEditable(e))return e;return null}function o(e){var t=i.fromRangeStart(e),n=new r(a.getBody());if(t=n.next(t))return t.toRange()}var s,c,u;if(t){if(D.scrollIntoView(t),s=n(t))return L.remove(t),void D.select(s);S=L.createRng(),k=t.previousSibling,k&&3==k.nodeType?(S.setStart(k,k.nodeValue.length),e.ie||(T=t.nextSibling,T&&3==T.nodeType&&(k.appendData(T.data),T.parentNode.removeChild(T)))):(S.setStartBefore(t),S.setEndBefore(t)),c=L.getParent(t,L.isBlock),L.remove(t),c&&L.isEmpty(c)&&(a.$(c).empty(),S.setStart(c,0),S.setEnd(c,0),l(c)||g(c)||!(u=o(S))?L.add(c,L.create("br",{"data-mce-bogus":"1"})):(S=u,L.remove(c))),D.setRng(S)}}var b,C,x,w,E,N,_,S,k,T,R,A,B=a.schema.getTextInlineElements(),D=a.selection,L=a.dom;/^ | $/.test(c)&&(c=d(c)),b=a.parser,A=u.merge,C=new n({validate:a.settings.validate},a.schema),R='',N={content:c,format:"html",selection:!0},a.fire("BeforeSetContent",N),c=N.content,c.indexOf("{$caret}")==-1&&(c+="{$caret}"),c=c.replace(/\{\$caret\}/,R),S=D.getRng();var M=S.startContainer||(S.parentElement?S.parentElement():null),P=a.getBody();M===P&&D.isCollapsed()&&L.isBlock(P.firstChild)&&v(P.firstChild)&&L.isEmpty(P.firstChild)&&(S=L.createRng(),S.setStart(P.firstChild,0),S.setEnd(P.firstChild,0),D.setRng(S)),D.isCollapsed()||(a.selection.setRng(a.selection.getRng()),a.getDoc().execCommand("Delete",!1,null),f()),x=D.getNode();var O={context:x.nodeName.toLowerCase(),data:u.data};if(E=b.parse(c,O),u.paste===!0&&s.isListFragment(E)&&s.isParentBlockLi(L,x))return S=s.insertAtCaret(C,L,a.selection.getRng(!0),E),a.selection.setRng(S),void a.fire("SetContent",N);if(p(E),k=E.lastChild,"mce_marker"==k.attr("id"))for(_=k,k=k.prev;k;k=k.walk(!0))if(3==k.type||!L.isBlock(k.name)){a.schema.isValidChild(k.parent.name,"span")&&k.parent.insert(_,k,"br"===k.name);break}if(a._selectionOverrides.showBlockCaretContainer(x),O.invalid){for(D.setContent(R),x=D.getNode(),w=a.getBody(),9==x.nodeType?x=k=w:k=x;k!==w;)x=k,k=k.parentNode;c=x==w?w.innerHTML:L.getOuterHTML(x),c=C.serialize(b.parse(c.replace(//i,function(){return C.serialize(E)}))),x==w?L.setHTML(w,c):L.setOuterHTML(x,c)}else c=C.serialize(E),k=x.firstChild,T=x.lastChild,!k||k===T&&"BR"===k.nodeName?L.setHTML(x,c):D.setContent(c);h(),y(L.get("mce_marker")),m(a.getBody()),a.fire("SetContent",N),a.addVisual()},u=function(e){var n;return"string"!=typeof e?(n=t.extend({paste:e.paste,data:{paste:e.paste}},e),{content:e.content,details:n}):{content:e,details:{}}},d=function(e,t){var n=u(t);c(e,n.content,n.details)};return{insertAtCaret:d}}),r(le,[d,m,T,y,se],function(e,n,r,i,o){var a=n.each,s=n.extend,l=n.map,c=n.inArray,u=n.explode,d=e.ie&&e.ie<11,f=!0,h=!1;return function(n){function p(e,t,r,i){var o,s,l=0;if(/^(mceAddUndoLevel|mceEndUndoLevel|mceBeginUndoLevel|mceRepaint)$/.test(e)||i&&i.skip_focus||n.focus(),i=n.fire("BeforeExecCommand",{command:e,ui:t,value:r}),i.isDefaultPrevented())return!1;if(s=e.toLowerCase(),o=B.exec[s])return o(s,t,r),n.fire("ExecCommand",{command:e,ui:t,value:r}),!0;if(a(n.plugins,function(i){if(i.execCommand&&i.execCommand(e,t,r))return n.fire("ExecCommand",{command:e,ui:t,value:r}),l=!0,!1}),l)return l;if(n.theme&&n.theme.execCommand&&n.theme.execCommand(e,t,r))return n.fire("ExecCommand",{command:e,ui:t,value:r}),!0;try{l=n.getDoc().execCommand(e,t,r)}catch(c){}return!!l&&(n.fire("ExecCommand",{command:e,ui:t,value:r}),!0)}function m(e){var t;if(!n.quirks.isHidden()){if(e=e.toLowerCase(),t=B.state[e])return t(e);try{return n.getDoc().queryCommandState(e)}catch(r){}return!1}}function g(e){var t;if(!n.quirks.isHidden()){if(e=e.toLowerCase(),t=B.value[e])return t(e);try{return n.getDoc().queryCommandValue(e)}catch(r){}}}function v(e,t){t=t||"exec",a(e,function(e,n){a(n.toLowerCase().split(","),function(n){B[t][n]=e})})}function y(e,t,r){e=e.toLowerCase(),B.exec[e]=function(e,i,o,a){return t.call(r||n,i,o,a)}}function b(e){if(e=e.toLowerCase(),B.exec[e])return!0;try{return n.getDoc().queryCommandSupported(e)}catch(t){}return!1}function C(e,t,r){e=e.toLowerCase(),B.state[e]=function(){return t.call(r||n)}}function x(e,t,r){e=e.toLowerCase(),B.value[e]=function(){return t.call(r||n)}}function w(e){return e=e.toLowerCase(),!!B.exec[e]}function E(e,r,i){return r===t&&(r=h),i===t&&(i=null),n.getDoc().execCommand(e,r,i)}function N(e){return A.match(e)}function _(e,r){A.toggle(e,r?{value:r}:t),n.nodeChanged()}function S(e){L=R.getBookmark(e)}function k(){R.moveToBookmark(L)}var T,R,A,B={state:{},exec:{},value:{}},D=n.settings,L;n.on("PreInit",function(){T=n.dom,R=n.selection,D=n.settings,A=n.formatter}),s(this,{execCommand:p,queryCommandState:m,queryCommandValue:g,queryCommandSupported:b,addCommands:v,addCommand:y,addQueryStateHandler:C,addQueryValueHandler:x,hasCustomCommand:w}),v({"mceResetDesignMode,mceBeginUndoLevel":function(){},"mceEndUndoLevel,mceAddUndoLevel":function(){n.undoManager.add()},"Cut,Copy,Paste":function(t){var r=n.getDoc(),i;try{E(t)}catch(o){i=f}if("paste"!==t||r.queryCommandEnabled(t)||(i=!0),i||!r.queryCommandSupported(t)){var a=n.translate("Your browser doesn't support direct access to the clipboard. Please use the Ctrl+X/C/V keyboard shortcuts instead.");e.mac&&(a=a.replace(/Ctrl\+/g,"\u2318+")),n.notificationManager.open({text:a,type:"error"})}},unlink:function(){if(R.isCollapsed()){var e=n.dom.getParent(n.selection.getStart(),"a");return void(e&&n.dom.remove(e,!0))}A.remove("link")},"JustifyLeft,JustifyCenter,JustifyRight,JustifyFull,JustifyNone":function(e){var t=e.substring(7);"full"==t&&(t="justify"),a("left,center,right,justify".split(","),function(e){t!=e&&A.remove("align"+e)}),"none"!=t&&_("align"+t)},"InsertUnorderedList,InsertOrderedList":function(e){var t,n;E(e),t=T.getParent(R.getNode(),"ol,ul"),t&&(n=t.parentNode,/^(H[1-6]|P|ADDRESS|PRE)$/.test(n.nodeName)&&(S(),T.split(n,t),k()))},"Bold,Italic,Underline,Strikethrough,Superscript,Subscript":function(e){_(e)},"ForeColor,HiliteColor,FontName":function(e,t,n){_(e,n)},FontSize:function(e,t,n){var r,i;n>=1&&n<=7&&(i=u(D.font_size_style_values),r=u(D.font_size_classes),n=r?r[n-1]||n:i[n-1]||n),_(e,n)},RemoveFormat:function(e){A.remove(e)},mceBlockQuote:function(){_("blockquote")},FormatBlock:function(e,t,n){return _(n||"p")},mceCleanup:function(){var e=R.getBookmark();n.setContent(n.getContent({cleanup:f}),{cleanup:f}),R.moveToBookmark(e)},mceRemoveNode:function(e,t,r){var i=r||R.getNode();i!=n.getBody()&&(S(),n.dom.remove(i,f),k())},mceSelectNodeDepth:function(e,t,r){var i=0;T.getParent(R.getNode(),function(e){if(1==e.nodeType&&i++==r)return R.select(e),h},n.getBody())},mceSelectNode:function(e,t,n){R.select(n)},mceInsertContent:function(e,t,r){o.insertAtCaret(n,r)},mceInsertRawHTML:function(e,t,r){R.setContent("tiny_mce_marker"),n.setContent(n.getContent().replace(/tiny_mce_marker/g,function(){return r}))},mceToggleFormat:function(e,t,n){_(n)},mceSetContent:function(e,t,r){n.setContent(r)},"Indent,Outdent":function(e){var t,r,i;t=D.indentation,r=/[a-z%]+$/i.exec(t),t=parseInt(t,10),m("InsertUnorderedList")||m("InsertOrderedList")?E(e):(D.forced_root_block||T.getParent(R.getNode(),T.isBlock)||A.apply("div"),a(R.getSelectedBlocks(),function(o){if("false"!==T.getContentEditable(o)&&"LI"!==o.nodeName){var a=n.getParam("indent_use_margin",!1)?"margin":"padding";a="TABLE"===o.nodeName?"margin":a,a+="rtl"==T.getStyle(o,"direction",!0)?"Right":"Left","outdent"==e?(i=Math.max(0,parseInt(o.style[a]||0,10)-t),T.setStyle(o,a,i?i+r:"")):(i=parseInt(o.style[a]||0,10)+t+r,T.setStyle(o,a,i))}}))},mceRepaint:function(){},InsertHorizontalRule:function(){n.execCommand("mceInsertContent",!1,"
")},mceToggleVisualAid:function(){n.hasVisual=!n.hasVisual,n.addVisual()},mceReplaceContent:function(e,t,r){n.execCommand("mceInsertContent",!1,r.replace(/\{\$selection\}/g,R.getContent({format:"text"})))},mceInsertLink:function(e,t,n){var r;"string"==typeof n&&(n={href:n}),r=T.getParent(R.getNode(),"a"),n.href=n.href.replace(" ","%20"),r&&n.href||A.remove("link"),n.href&&A.apply("link",n,r)},selectAll:function(){var e=T.getRoot(),t;R.getRng().setStart?(t=T.createRng(),t.setStart(e,0),t.setEnd(e,e.childNodes.length),R.setRng(t)):(t=R.getRng(),t.item||(t.moveToElementText(e),t.select()))},"delete":function(){E("Delete");var e=n.getBody();T.isEmpty(e)&&(n.setContent(""),e.firstChild&&T.isBlock(e.firstChild)?n.selection.setCursorLocation(e.firstChild,0):n.selection.setCursorLocation(e,0))},mceNewDocument:function(){n.setContent("")},InsertLineBreak:function(e,t,o){function a(){for(var e=new i(m,v),t,r=n.schema.getNonEmptyElements();t=e.next();)if(r[t.nodeName.toLowerCase()]||t.length>0)return!0}var s=o,l,c,u,h=R.getRng(!0);new r(T).normalize(h);var p=h.startOffset,m=h.startContainer;if(1==m.nodeType&&m.hasChildNodes()){var g=p>m.childNodes.length-1;m=m.childNodes[Math.min(p,m.childNodes.length-1)]||m,p=g&&3==m.nodeType?m.nodeValue.length:0}var v=T.getParent(m,T.isBlock),y=v?v.nodeName.toUpperCase():"",b=v?T.getParent(v.parentNode,T.isBlock):null,C=b?b.nodeName.toUpperCase():"",x=s&&s.ctrlKey;"LI"!=C||x||(v=b,y=C),m&&3==m.nodeType&&p>=m.nodeValue.length&&(d||a()||(l=T.create("br"),h.insertNode(l),h.setStartAfter(l),h.setEndAfter(l),c=!0)),l=T.create("br"),h.insertNode(l);var w=T.doc.documentMode;return d&&"PRE"==y&&(!w||w<8)&&l.parentNode.insertBefore(T.doc.createTextNode("\r"),l),u=T.create("span",{}," "),l.parentNode.insertBefore(u,l),R.scrollIntoView(u),T.remove(u),c?(h.setStartBefore(l),h.setEndBefore(l)):(h.setStartAfter(l),h.setEndAfter(l)),R.setRng(h),n.undoManager.add(),f}}),v({"JustifyLeft,JustifyCenter,JustifyRight,JustifyFull":function(e){var t="align"+e.substring(7),n=R.isCollapsed()?[T.getParent(R.getNode(),T.isBlock)]:R.getSelectedBlocks(),r=l(n,function(e){return!!A.matchNode(e,t)});return c(r,f)!==-1},"Bold,Italic,Underline,Strikethrough,Superscript,Subscript":function(e){return N(e)},mceBlockQuote:function(){return N("blockquote")},Outdent:function(){var e;if(D.inline_styles){if((e=T.getParent(R.getStart(),T.isBlock))&&parseInt(e.style.paddingLeft,10)>0)return f;if((e=T.getParent(R.getEnd(),T.isBlock))&&parseInt(e.style.paddingLeft,10)>0)return f}return m("InsertUnorderedList")||m("InsertOrderedList")||!D.inline_styles&&!!T.getParent(R.getNode(),"BLOCKQUOTE")},"InsertUnorderedList,InsertOrderedList":function(e){var t=T.getParent(R.getNode(),"ul,ol");return t&&("insertunorderedlist"===e&&"UL"===t.tagName||"insertorderedlist"===e&&"OL"===t.tagName)}},"state"),v({"FontSize,FontName":function(e){var t=0,n;return(n=T.getParent(R.getNode(),"span"))&&(t="fontsize"==e?n.style.fontSize:n.style.fontFamily.replace(/, /g,",").replace(/[\'\"]/g,"").toLowerCase()),t}},"value"),v({Undo:function(){n.undoManager.undo()},Redo:function(){n.undoManager.redo()}})}}),r(ce,[m],function(e){function t(e,o){var a=this,s,l;if(e=r(e),o=a.settings=o||{},s=o.base_uri,/^([\w\-]+):([^\/]{2})/i.test(e)||/^\s*#/.test(e))return void(a.source=e);var c=0===e.indexOf("//");0!==e.indexOf("/")||c||(e=(s?s.protocol||"http":"http")+"://mce_host"+e),/^[\w\-]*:?\/\//.test(e)||(l=o.base_uri?o.base_uri.path:new t(location.href).directory,""===o.base_uri.protocol?e="//mce_host"+a.toAbsPath(l,e):(e=/([^#?]*)([#?]?.*)/.exec(e),e=(s&&s.protocol||"http")+"://mce_host"+a.toAbsPath(l,e[1])+e[2])),e=e.replace(/@@/g,"(mce_at)"),e=/^(?:(?![^:@]+:[^:@\/]*@)([^:\/?#.]+):)?(?:\/\/)?((?:(([^:@\/]*):?([^:@\/]*))?@)?([^:\/?#]*)(?::(\d*))?)(((\/(?:[^?#](?![^?#\/]*\.[^?#\/.]+(?:[?#]|$)))*\/?)?([^?#\/]*))(?:\?([^#]*))?(?:#(.*))?)/.exec(e),n(i,function(t,n){var r=e[n];r&&(r=r.replace(/\(mce_at\)/g,"@@")),a[t]=r}),s&&(a.protocol||(a.protocol=s.protocol),a.userInfo||(a.userInfo=s.userInfo),a.port||"mce_host"!==a.host||(a.port=s.port),a.host&&"mce_host"!==a.host||(a.host=s.host),a.source=""),c&&(a.protocol="")}var n=e.each,r=e.trim,i="source protocol authority userInfo user password host port relative path directory file query anchor".split(" "),o={ftp:21,http:80,https:443,mailto:25};return t.prototype={setPath:function(e){var t=this;e=/^(.*?)\/?(\w+)?$/.exec(e),t.path=e[0],t.directory=e[1],t.file=e[2],t.source="",t.getURI()},toRelative:function(e){var n=this,r;if("./"===e)return e;if(e=new t(e,{base_uri:n}),"mce_host"!=e.host&&n.host!=e.host&&e.host||n.port!=e.port||n.protocol!=e.protocol&&""!==e.protocol)return e.getURI();var i=n.getURI(),o=e.getURI();return i==o||"/"==i.charAt(i.length-1)&&i.substr(0,i.length-1)==o?i:(r=n.toRelPath(n.path,e.path),e.query&&(r+="?"+e.query),e.anchor&&(r+="#"+e.anchor),r)},toAbsolute:function(e,n){return e=new t(e,{base_uri:this}),e.getURI(n&&this.isSameOrigin(e))},isSameOrigin:function(e){if(this.host==e.host&&this.protocol==e.protocol){if(this.port==e.port)return!0;var t=o[this.protocol];if(t&&(this.port||t)==(e.port||t))return!0}return!1},toRelPath:function(e,t){var n,r=0,i="",o,a;if(e=e.substring(0,e.lastIndexOf("/")),e=e.split("/"),n=t.split("/"),e.length>=n.length)for(o=0,a=e.length;o=n.length||e[o]!=n[o]){r=o+1;break}if(e.length
\xa0
').append(h),t.setStartAfter(o[0].firstChild.firstChild),t.setEndAfter(h)):(o.empty().append("\xa0").append(h).append("\xa0"),t.setStart(o[0].firstChild,1),t.setEnd(o[0].lastChild,0)),o.css({top:i.getPos(n,c.getBody()).y}),o[0].focus(),a=c.selection.getSel(),a.removeAllRanges(),a.addRange(t),c.$("*[data-mce-selected]").removeAttr("data-mce-selected"),n.setAttribute("data-mce-selected",1),ce=n,t)):null)}function ee(){ce&&(ce.removeAttribute("data-mce-selected"),c.$("#"+le).remove(),ce=null)}function te(){se.destroy(),ce=null}function ne(){se.hide()}var re=c.getBody(),ie=new t(re),oe=y(g,ie.next),ae=y(g,ie.prev),se=new o(c.getBody(),S),le="sel-"+c.dom.uniqueId(),ce,ue=c.$;return e.ceFalse&&(G(),J()),{showBlockCaretContainer:W,hideFakeCaret:ne,destroy:te}}var y=f.curry,b=l.isContentEditableTrue,C=l.isContentEditableFalse,x=l.isElement,w=i.isAfterContentEditableFalse,E=i.isBeforeContentEditableFalse,N=c.getSelectedNode;return v}),r(rt,[],function(){var e=0,t=function(){var e=function(){return Math.round(4294967295*Math.random()).toString(36)},t=(new Date).getTime();return"s"+t.toString(36)+e()+e()+e()},n=function(n){return n+e++ +t()};return{uuid:n}}),r(it,[],function(){var e=function(e,t,n){var r=e.sidebars?e.sidebars:[];r.push({name:t,settings:n}),e.sidebars=r};return{add:e}}),r(ot,[w,g,N,R,A,O,P,Y,J,te,ne,re,le,ce,E,f,Le,Ie,B,L,ze,d,m,u,Ue,We,Ve,Ke,nt,rt,it],function(e,n,r,i,o,a,s,l,c,u,d,f,h,p,m,g,v,y,b,C,x,w,E,N,_,S,k,T,R,A,B){function D(e,t,i){var o=this,a,s,l;a=o.documentBaseUrl=i.documentBaseURL,s=i.baseURI,l=i.defaultSettings,t=O({id:e,theme:"modern",delta_width:0,delta_height:0,popup_css:"",plugins:"",document_base_url:a,add_form_submit_trigger:!0,submit_patch:!0,add_unload_trigger:!0,convert_urls:!0,relative_urls:!0,remove_script_host:!0,object_resizing:!0,doctype:"",visual:!0,font_size_style_values:"xx-small,x-small,small,medium,large,x-large,xx-large",font_size_legacy_values:"xx-small,small,medium,large,x-large,xx-large,300%",forced_root_block:"p",hidden_input:!0,padd_empty_editor:!0,render_ui:!0,indentation:"30px",inline_styles:!0,convert_fonts_to_spans:!0,indent:"simple",indent_before:"p,h1,h2,h3,h4,h5,h6,blockquote,div,title,style,pre,script,td,th,ul,ol,li,dl,dt,dd,area,table,thead,tfoot,tbody,tr,section,article,hgroup,aside,figure,figcaption,option,optgroup,datalist",indent_after:"p,h1,h2,h3,h4,h5,h6,blockquote,div,title,style,pre,script,td,th,ul,ol,li,dl,dt,dd,area,table,thead,tfoot,tbody,tr,section,article,hgroup,aside,figure,figcaption,option,optgroup,datalist",validate:!0,entity_encoding:"named",url_converter:o.convertURL,url_converter_scope:o,ie7_compat:!0},l,t),l&&l.external_plugins&&t.external_plugins&&(t.external_plugins=O({},l.external_plugins,t.external_plugins)),o.settings=t,r.language=t.language||"en",r.languageLoad=t.language_load,r.baseURL=i.baseURL,o.id=t.id=e,o.setDirty(!1),o.plugins={},o.documentBaseURI=new p(t.document_base_url||a,{base_uri:s}),o.baseURI=s,o.contentCSS=[],o.contentStyles=[],o.shortcuts=new k(o),o.loadedCSS={},o.editorCommands=new h(o),o.suffix=i.suffix,o.editorManager=i,o.inline=t.inline,o.settings.content_editable=o.inline,t.cache_suffix&&(w.cacheSuffix=t.cache_suffix.replace(/^[\?\&]+/,"")),t.override_viewport===!1&&(w.overrideViewPort=!1),i.fire("SetupEditor",o),o.execCallback("setup",o),o.$=n.overrideDefaults(function(){return{context:o.inline?o.getBody():o.getDoc(),element:o.getBody()}})}var L=e.DOM,M=r.ThemeManager,P=r.PluginManager,O=E.extend,H=E.each,I=E.explode,F=E.inArray,z=E.trim,U=E.resolve,W=g.Event,V=w.gecko,$=w.ie;return D.prototype={render:function(){function e(){L.unbind(window,"ready",e),n.render()}function t(){var e=m.ScriptLoader;if(r.language&&"en"!=r.language&&!r.language_url&&(r.language_url=n.editorManager.baseURL+"/langs/"+r.language+".js"),r.language_url&&e.add(r.language_url),r.theme&&"function"!=typeof r.theme&&"-"!=r.theme.charAt(0)&&!M.urls[r.theme]){var t=r.theme_url;t=t?n.documentBaseURI.toAbsolute(t):"themes/"+r.theme+"/theme"+o+".js",M.load(r.theme,t)}E.isArray(r.plugins)&&(r.plugins=r.plugins.join(" ")),H(r.external_plugins,function(e,t){P.load(t,e),r.plugins+=" "+t}),H(r.plugins.split(/[ ,]/),function(e){if(e=z(e),e&&!P.urls[e])if("-"==e.charAt(0)){e=e.substr(1,e.length);var t=P.dependencies(e);H(t,function(e){var t={prefix:"plugins/",resource:e,suffix:"/plugin"+o+".js"};e=P.createUrl(t,e),P.load(e.resource,e)})}else P.load(e,{prefix:"plugins/",resource:e,suffix:"/plugin"+o+".js"})}),e.loadQueue(function(){n.removed||n.init()})}var n=this,r=n.settings,i=n.id,o=n.suffix;if(!W.domLoaded)return void L.bind(window,"ready",e);if(n.getElement()&&w.contentEditable){r.inline?n.inline=!0:(n.orgVisibility=n.getElement().style.visibility,n.getElement().style.visibility="hidden");var a=n.getElement().form||L.getParent(i,"form");a&&(n.formElement=a,r.hidden_input&&!/TEXTAREA|INPUT/i.test(n.getElement().nodeName)&&(L.insertAfter(L.create("input",{type:"hidden",name:i}),i),n.hasHiddenInput=!0),n.formEventDelegate=function(e){n.fire(e.type,e)},L.bind(a,"submit reset",n.formEventDelegate),n.on("reset",function(){n.setContent(n.startContent,{format:"raw"})}),!r.submit_patch||a.submit.nodeType||a.submit.length||a._mceOldSubmit||(a._mceOldSubmit=a.submit,a.submit=function(){return n.editorManager.triggerSave(),n.setDirty(!1),a._mceOldSubmit(a)})),n.windowManager=new v(n),n.notificationManager=new y(n),"xml"==r.encoding&&n.on("GetContent",function(e){e.save&&(e.content=L.encode(e.content))}),r.add_form_submit_trigger&&n.on("submit",function(){n.initialized&&n.save()}),r.add_unload_trigger&&(n._beforeUnload=function(){!n.initialized||n.destroyed||n.isHidden()||n.save({format:"raw",no_events:!0,set_dirty:!1})},n.editorManager.on("BeforeUnload",n._beforeUnload)),n.editorManager.add(n),t()}},init:function(){function e(n){var r=P.get(n),i,o;if(i=P.urls[n]||t.documentBaseUrl.replace(/\/$/,""),n=z(n),r&&F(m,n)===-1){if(H(P.dependencies(n),function(t){e(t)}),t.plugins[n])return;o=new r(t,i,t.$),t.plugins[n]=o,o.init&&(o.init(t,i),m.push(n))}}var t=this,n=t.settings,r=t.getElement(),i,o,a,s,l,c,u,d,f,h,p,m=[];if(t.rtl=n.rtl_ui||t.editorManager.i18n.rtl,t.editorManager.i18n.setCode(n.language),n.aria_label=n.aria_label||L.getAttrib(r,"aria-label",t.getLang("aria.rich_text_area")),t.fire("ScriptsLoaded"),n.theme&&("function"!=typeof n.theme?(n.theme=n.theme.replace(/-/,""),c=M.get(n.theme),t.theme=new c(t,M.urls[n.theme]),t.theme.init&&t.theme.init(t,M.urls[n.theme]||t.documentBaseUrl.replace(/\/$/,""),t.$)):t.theme=n.theme),H(n.plugins.replace(/\-/g,"").split(/[ ,]/),e),n.render_ui&&t.theme&&(t.orgDisplay=r.style.display,"function"!=typeof n.theme?(i=n.width||r.style.width||r.offsetWidth,o=n.height||r.style.height||r.offsetHeight,a=n.min_height||100,h=/^[0-9\.]+(|px)$/i,h.test(""+i)&&(i=Math.max(parseInt(i,10),100)),h.test(""+o)&&(o=Math.max(parseInt(o,10),a)),l=t.theme.renderUI({targetNode:r,width:i,height:o,deltaWidth:n.delta_width,deltaHeight:n.delta_height}),n.content_editable||(o=(l.iframeHeight||o)+("number"==typeof o?l.deltaHeight||0:""),o",n.document_base_url!=t.documentBaseUrl&&(t.iframeHTML+=']*>( | |\s|\u00a0|)<\/p>[\r\n]*|
[\r\n]*)$/,"")}),n.load({initial:!0,format:"html"}),n.startContent=n.getContent({format:"raw"}),n.initialized=!0,n.bindPendingEventDelegates(),n.fire("init"),n.focus(!0),n.nodeChanged({initial:!0}),n.execCallback("init_instance_callback",n),n.on("compositionstart compositionend",function(e){n.composing="compositionstart"===e.type}),n.contentStyles.length>0&&(m="",H(n.contentStyles,function(e){m+=e+"\r\n"}),n.dom.addStyle(m)),H(n.contentCSS,function(e){n.loadedCSS[e]||(n.dom.loadCSS(e),n.loadedCSS[e]=!0)}),r.auto_focus&&N.setEditorTimeout(n,function(){var e;e=r.auto_focus===!0?n:n.editorManager.get(r.auto_focus),e.destroyed||e.focus()},100),s=h=p=null},focus:function(e){function t(e){return n.dom.getParent(e,function(e){return"true"===n.dom.getContentEditable(e)})}var n=this,r=n.selection,i=n.settings.content_editable,o,a,s=n.getDoc(),l=n.getBody(),c;if(!e){if(o=r.getRng(),o.item&&(a=o.item(0)),n.quirks.refreshContentEditable(),c=t(r.getNode()),n.$.contains(l,c))return c.focus(),r.normalize(),void n.editorManager.setActive(n);if(i||(w.opera||n.getBody().focus(),n.getWin().focus()),V||i){if(l.setActive)try{l.setActive()}catch(u){l.focus()}else l.focus();i&&r.normalize()}a&&a.ownerDocument==s&&(o=s.body.createControlRange(),o.addElement(a),o.select())}n.editorManager.setActive(n)},execCallback:function(e){var t=this,n=t.settings[e],r;if(n)return t.callbackLookup&&(r=t.callbackLookup[e])&&(n=r.func,r=r.scope),"string"==typeof n&&(r=n.replace(/\.\w+$/,""),r=r?U(r):0,n=U(n),t.callbackLookup=t.callbackLookup||{},t.callbackLookup[e]={func:n,scope:r}),n.apply(r||t,Array.prototype.slice.call(arguments,1))},translate:function(e){var t=this.settings.language||"en",n=this.editorManager.i18n;return e?(e=n.data[t+"."+e]||e.replace(/\{\#([^\}]+)\}/g,function(e,r){return n.data[t+"."+r]||"{#"+r+"}"}),this.editorManager.translate(e)):""},getLang:function(e,n){return this.editorManager.i18n.data[(this.settings.language||"en")+"."+e]||(n!==t?n:"{#"+e+"}")},getParam:function(e,t,n){var r=e in this.settings?this.settings[e]:t,i;return"hash"===n?(i={},"string"==typeof r?H(r.indexOf("=")>0?r.split(/[;,](?![^=;,]*(?:[;,]|$))/):r.split(","),function(e){
-e=e.split("="),e.length>1?i[z(e[0])]=z(e[1]):i[z(e[0])]=z(e)}):i=r,i):r},nodeChanged:function(e){this._nodeChangeDispatcher.nodeChanged(e)},addButton:function(e,t){var n=this;t.cmd&&(t.onclick=function(){n.execCommand(t.cmd)}),t.text||t.icon||(t.icon=e),n.buttons=n.buttons||{},t.tooltip=t.tooltip||t.title,n.buttons[e]=t},addSidebar:function(e,t){return B.add(this,e,t)},addMenuItem:function(e,t){var n=this;t.cmd&&(t.onclick=function(){n.execCommand(t.cmd)}),n.menuItems=n.menuItems||{},n.menuItems[e]=t},addContextToolbar:function(e,t){var n=this,r;n.contextToolbars=n.contextToolbars||[],"string"==typeof e&&(r=e,e=function(e){return n.dom.is(e,r)}),n.contextToolbars.push({id:A.uuid("mcet"),predicate:e,items:t})},addCommand:function(e,t,n){this.editorCommands.addCommand(e,t,n)},addQueryStateHandler:function(e,t,n){this.editorCommands.addQueryStateHandler(e,t,n)},addQueryValueHandler:function(e,t,n){this.editorCommands.addQueryValueHandler(e,t,n)},addShortcut:function(e,t,n,r){this.shortcuts.add(e,t,n,r)},execCommand:function(e,t,n,r){return this.editorCommands.execCommand(e,t,n,r)},queryCommandState:function(e){return this.editorCommands.queryCommandState(e)},queryCommandValue:function(e){return this.editorCommands.queryCommandValue(e)},queryCommandSupported:function(e){return this.editorCommands.queryCommandSupported(e)},show:function(){var e=this;e.hidden&&(e.hidden=!1,e.inline?e.getBody().contentEditable=!0:(L.show(e.getContainer()),L.hide(e.id)),e.load(),e.fire("show"))},hide:function(){var e=this,t=e.getDoc();e.hidden||($&&t&&!e.inline&&t.execCommand("SelectAll"),e.save(),e.inline?(e.getBody().contentEditable=!1,e==e.editorManager.focusedEditor&&(e.editorManager.focusedEditor=null)):(L.hide(e.getContainer()),L.setStyle(e.id,"display",e.orgDisplay)),e.hidden=!0,e.fire("hide"))},isHidden:function(){return!!this.hidden},setProgressState:function(e,t){this.fire("ProgressState",{state:e,time:t})},load:function(e){var n=this,r=n.getElement(),i;if(r)return e=e||{},e.load=!0,i=n.setContent(r.value!==t?r.value:r.innerHTML,e),e.element=r,e.no_events||n.fire("LoadContent",e),e.element=r=null,i},save:function(e){var t=this,n=t.getElement(),r,i;if(n&&t.initialized)return e=e||{},e.save=!0,e.element=n,r=e.content=t.getContent(e),e.no_events||t.fire("SaveContent",e),"raw"==e.format&&t.fire("RawSaveContent",e),r=e.content,/TEXTAREA|INPUT/i.test(n.nodeName)?n.value=r:(t.inline||(n.innerHTML=r),(i=L.getParent(t.id,"form"))&&H(i.elements,function(e){if(e.name==t.id)return e.value=r,!1})),e.element=n=null,e.set_dirty!==!1&&t.setDirty(!1),r},setContent:function(e,t){var n=this,r=n.getBody(),i,o;return t=t||{},t.format=t.format||"html",t.set=!0,t.content=e,t.no_events||n.fire("BeforeSetContent",t),e=t.content,0===e.length||/^\s+$/.test(e)?(o=$&&$<11?"":'
',"TABLE"==r.nodeName?e="