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; function ScriptLoader() { var QUEUED = 0, LOADING = 1, LOADED = 2, 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 callback function to execute ones this script gets loaded. */ function loadScript(url, callback) { var dom = DOM, elm, id; // Execute callback when script is loaded function done() { dom.remove(id); if (elm) { elm.onreadystatechange = elm.onload = elm = null; } callback(); } function error() { /*eslint no-console:0 */ // Report the error so it's easier for people to spot loading errors if (typeof console !== "undefined" && console.log) { console.log("Failed to load: " + url); } // We can't mark it as done if there is a load error since // A) We don't want to produce 404 errors on the server and // B) the onerror event won't fire on all browsers. // done(); } id = dom.uniqueId(); // 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} callback Optional callback function to execute ones this script gets loaded. * @param {Object} scope Optional scope to execute callback in. */ this.add = this.load = function(url, callback, scope) { var state = states[url]; // Add url to load queue if (state == undef) { queue.push(url); states[url] = QUEUED; } if (callback) { // Store away callback for later execution if (!scriptLoadedCallbacks[url]) { scriptLoadedCallbacks[url] = []; } scriptLoadedCallbacks[url].push({ func: callback, scope: scope || this }); } }; this.remove = function(url) { delete states[url]; delete scriptLoadedCallbacks[url]; }; /** * Starts the loading of the queue. * * @method loadQueue * @param {function} callback Optional callback to execute when all queued items are loaded. * @param {Object} scope Optional scope to execute the callback in. */ this.loadQueue = function(callback, scope) { this.loadScripts(queue, callback, scope); }; /** * 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 ones all items are loaded. * @param {Object} scope Optional scope to execute callback in. */ this.loadScripts = function(scripts, callback, scope) { var loadScripts; function execScriptLoadedCallbacks(url) { // Execute URL callback functions each(scriptLoadedCallbacks[url], function(callback) { callback.func.call(callback.scope); }); scriptLoadedCallbacks[url] = undef; } queueLoadedCallbacks.push({ func: callback, 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) { execScriptLoadedCallbacks(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--; execScriptLoadedCallbacks(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) { callback.func.call(callback.scope); }); 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} callback Optional callback to execute ones the add-on is loaded. * @param {Object} scope Optional scope to execute the callback in. * @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, callback, scope) { 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 (callback) { if (scope) { callback.call(scope); } else { callback.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); } } }; 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 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(doc.createTextNode('\u00a0')); 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 remove(caretContainerNode) { if (isElement(caretContainerNode) && isCaretContainer(caretContainerNode)) { if (caretContainerNode.innerHTML != ' ') { 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; } return { isCaretContainer: isCaretContainer, isCaretContainerBlock: isCaretContainerBlock, isCaretContainerInline: isCaretContainerInline, insertInline: insertInline, insertBlock: insertBlock, 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, isContentEditableFalse = NodeType.isContentEditableFalse, isCaretContainer = CaretContainer.isCaretContainer; 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 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) { 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, i, attributesOrder, args = arguments; 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); } // Split string children for (i = 3; i < args.length; i++) { if (typeof args[i] === "string") { args[i] = split(args[i]); } children.push.apply(children, args[i]); } name = split(name); ni = name.length; while (ni--) { attributesOrder = [].concat(globalAttributes, split(attributes)); schema[name[ni]] = { attributes: arrayToMap(attributesOrder), attributesOrder: attributesOrder, children: arrayToMap(children, dummyObj) }; } } 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 = split("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 = split( "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 = split( "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.push.apply(globalAttributes, split("contenteditable contextmenu draggable dropzone " + "hidden spellcheck translate")); blockContent.push.apply(blockContent, split("article aside details dialog figure header footer hgroup section nav")); phrasingContent.push.apply(phrasingContent, split("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.push("xml:lang"); html4PhrasingContent = split("acronym applet basefont big font strike tt"); phrasingContent.push.apply(phrasingContent, html4PhrasingContent); each(html4PhrasingContent, function(name) { add(name, "", phrasingContent); }); html4BlockContent = split("center dir isindex noframes"); blockContent.push.apply(blockContent, html4BlockContent); // Flow content elements from the HTML5 spec (block+inline) flowContent = [].concat(blockContent, phrasingContent); each(html4BlockContent, function(name) { add(name, "", flowContent); }); } // Flow content elements from the HTML5 spec (block+inline) flowContent = flowContent || [].concat(blockContent, phrasingContent); // 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); } } } }); } // 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, tempAttrs = ["data-mce-selected"]; /** * 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); }; } return { constant: constant, negate: negate, and: and, or: or, curry: curry, compose: compose }; }); // 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" ], function(Env, Tools, CaretContainer, CaretBookmark, CaretPosition, NodeType) { 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) { var sibling; 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) || findSibling(rng.endContainer); } 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, found, hasContentEditableState, lastContentEditable; lastContentEditable = contentEditable; nodeName = node.nodeName.toLowerCase(); parentName = node.parentNode.nodeName.toLowerCase(); // Node has a contentEditable value if (node.nodeType === 1 && getContentEditable(node)) { lastContentEditable = contentEditable; contentEditable = getContentEditable(node) === "true"; hasContentEditableState = true; // We don't want to wrap the container only it's children } // Stop wrapping on br elements if (isEq(nodeName, 'br')) { 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) { // Look for matching formats each(formatList, function(format) { // Check collapsed state if it exists if ('collapsed' in format && format.collapsed !== isCollapsed) { return; } if (dom.is(node, format.selector) && !isCaretNode(node)) { setElementFormat(node, format); found = true; return false; } }); // 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) { 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/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/Env" ], function(VK, Env) { return function(editor) { var self = this, index = 0, data = [], beforeBookmark, isFirstTypedCharacter, locks = 0; function getContent() { return editor.serializer.getTrimmedContent(); } function setDirty(state) { editor.setDirty(state); } function addNonTypingUndoLevel(e) { self.typing = false; self.add({}, e); } // 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') { 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 || keyCode == 13 || 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] && getContent() != data[0].content); // 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; level = level || {}; level.content = getContent(); 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 && lastLevel.content == level.content) { return null; } // Set before bookmark on previous level if (data[index]) { data[index].beforeBookmark = beforeBookmark; } // Time to compress if (settings.custom_undo_redo_levels) { if (data.length > settings.custom_undo_redo_levels) { 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]; editor.setContent(level.content, {format: 'raw'}); editor.selection.moveToBookmark(level.beforeBookmark); 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]; editor.setContent(level.content, {format: 'raw'}); editor.selection.moveToBookmark(level.bookmark); 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] && getContent() != data[0].content); }, /** * 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 && !this.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) { 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]; editor.setContent(lastLevel.content, {format: 'raw'}); editor.selection.moveToBookmark(lastLevel.beforeBookmark); 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/Env" ], function(TreeWalker, RangeUtils, 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.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, curry = Fun.curry, isElement = NodeType.isElement, isCaretCandidate = CaretCandidate.isCaretCandidate; function isForwards(direction) { return direction > 0; } function isBackwards(direction) { return direction < 0; } function findNode(node, direction, predicateFn, rootNode, shallow) { var walker = new TreeWalker(node, rootNode); if (isBackwards(direction)) { if (isContentEditableFalse(node)) { node = walker.prev(true); if (predicateFn(node)) { return node; } } while ((node = walker.prev(shallow))) { if (predicateFn(node)) { return node; } } } if (isForwards(direction)) { if (isContentEditableFalse(node)) { node = walker.next(true); if (predicateFn(node)) { return node; } } while ((node = 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 = selection.getNode(); if (elm.tagName == 'A') { 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 += 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. * * @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. * * @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. * * @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. * * @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() { var body, parent; // Check if the editor was hidden and the re-initialize contentEditable mode by removing and adding the body again if (isHidden()) { body = editor.getBody(); parent = body.parentNode; parent.removeChild(body); parent.appendChild(body); body.focus(); } } 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); return !editor.selection.isCollapsed() && !caretWalker.prev(startCaretPos) && !caretWalker.next(endCaretPos); } // 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.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 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} 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.id() + '.' + 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(), fileName(blobInfo)); 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() { closeNotification(); uploadStatus.removeFailed(blobInfo.blobUri()); resolvePending(blobInfo.blobUri(), handlerFailure(blobInfo, failure)); resolve(handlerFailure(blobInfo, failure)); }; 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; 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 }); } 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) { var blobInfoId = 'blobid' + (count++), blobInfo = blobCache.create(blobInfoId, 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) { return { id: constant(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/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" ], function(Arr, Uploader, ImageScanner, BlobCache, UploadStatus) { 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 []; }; } // 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) { 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: 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); } 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, container; 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(); container = caretContainerNode.firstChild; rng.setStart(container, 0); rng.setEnd(container, 1); } 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); } }; }; var drop = function (state, editor) { return function (e) { if (state.dragging) { if (isValidDropTarget(editor, editor.selection.getNode(), 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", "tinymce/text/Zwsp" ], function( Env, CaretWalker, CaretPosition, CaretContainer, CaretUtils, FakeCaret, LineWalker, LineUtils, NodeType, RangeUtils, ClientRect, VK, Fun, Arr, Delay, DragDropOverrides, Zwsp ) { 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 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; fakeCaret.hide(); 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|
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.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); // Convert element type to id:s if (o.editorContainer.nodeType) { o.editorContainer = o.editorContainer.id = o.editorContainer.id || self.id + "_parent"; } // Convert element type to id:s if (o.iframeContainer.nodeType) { o.iframeContainer = 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 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 = '