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; } ); /** * ScriptLoader.js * * Released under LGPL License. * Copyright (c) 1999-2017 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.core.dom.ScriptLoader', [ "tinymce.core.dom.DOMUtils", "tinymce.core.util.Tools" ], function (DOMUtils, Tools) { var DOM = DOMUtils.DOM; var each = Tools.each, grep = Tools.grep; var isFunction = function (f) { return typeof f === 'function'; }; function ScriptLoader() { var QUEUED = 0, LOADING = 1, LOADED = 2, FAILED = 3, states = {}, queue = [], scriptLoadedCallbacks = {}, queueLoadedCallbacks = [], loading = 0, undef; /** * Loads a specific script directly without adding it to the load queue. * * @method load * @param {String} url Absolute URL to script to add. * @param {function} callback Optional success callback function when the script loaded successfully. * @param {function} callback Optional failure callback function when the script failed to load. */ function loadScript(url, success, failure) { var dom = DOM, elm, id; // Execute callback when script is loaded function done() { dom.remove(id); if (elm) { elm.onreadystatechange = elm.onload = elm = null; } success(); } function error() { /*eslint no-console:0 */ // We can't mark it as done if there is a load error since // A) We don't want to produce 404 errors on the server and // B) the onerror event won't fire on all browsers. // done(); if (isFunction(failure)) { failure(); } else { // Report the error so it's easier for people to spot loading errors if (typeof console !== "undefined" && console.log) { console.log("Failed to load script: " + url); } } } id = dom.uniqueId(); // Create new script element elm = document.createElement('script'); elm.id = id; elm.type = 'text/javascript'; elm.src = Tools._addCacheSuffix(url); // Seems that onreadystatechange works better on IE 10 onload seems to fire incorrectly if ("onreadystatechange" in elm) { elm.onreadystatechange = function () { if (/loaded|complete/.test(elm.readyState)) { done(); } }; } else { elm.onload = done; } // Add onerror event will get fired on some browsers but not all of them elm.onerror = error; // Add script to document (document.getElementsByTagName('head')[0] || document.body).appendChild(elm); } /** * Returns true/false if a script has been loaded or not. * * @method isDone * @param {String} url URL to check for. * @return {Boolean} true/false if the URL is loaded. */ this.isDone = function (url) { return states[url] == LOADED; }; /** * Marks a specific script to be loaded. This can be useful if a script got loaded outside * the script loader or to skip it from loading some script. * * @method markDone * @param {string} url Absolute URL to the script to mark as loaded. */ this.markDone = function (url) { states[url] = LOADED; }; /** * Adds a specific script to the load queue of the script loader. * * @method add * @param {String} url Absolute URL to script to add. * @param {function} success Optional success callback function to execute when the script loades successfully. * @param {Object} scope Optional scope to execute callback in. * @param {function} failure Optional failure callback function to execute when the script failed to load. */ this.add = this.load = function (url, success, scope, failure) { var state = states[url]; // Add url to load queue if (state == undef) { queue.push(url); states[url] = QUEUED; } if (success) { // Store away callback for later execution if (!scriptLoadedCallbacks[url]) { scriptLoadedCallbacks[url] = []; } scriptLoadedCallbacks[url].push({ success: success, failure: failure, scope: scope || this }); } }; this.remove = function (url) { delete states[url]; delete scriptLoadedCallbacks[url]; }; /** * Starts the loading of the queue. * * @method loadQueue * @param {function} success Optional callback to execute when all queued items are loaded. * @param {function} failure Optional callback to execute when queued items failed to load. * @param {Object} scope Optional scope to execute the callback in. */ this.loadQueue = function (success, scope, failure) { this.loadScripts(queue, success, scope, failure); }; /** * Loads the specified queue of files and executes the callback ones they are loaded. * This method is generally not used outside this class but it might be useful in some scenarios. * * @method loadScripts * @param {Array} scripts Array of queue items to load. * @param {function} callback Optional callback to execute when scripts is loaded successfully. * @param {Object} scope Optional scope to execute callback in. * @param {function} callback Optional callback to execute if scripts failed to load. */ this.loadScripts = function (scripts, success, scope, failure) { var loadScripts, failures = []; function execCallbacks(name, url) { // Execute URL callback functions each(scriptLoadedCallbacks[url], function (callback) { if (isFunction(callback[name])) { callback[name].call(callback.scope); } }); scriptLoadedCallbacks[url] = undef; } queueLoadedCallbacks.push({ success: success, failure: failure, scope: scope || this }); loadScripts = function () { var loadingScripts = grep(scripts); // Current scripts has been handled scripts.length = 0; // Load scripts that needs to be loaded each(loadingScripts, function (url) { // Script is already loaded then execute script callbacks directly if (states[url] === LOADED) { execCallbacks('success', url); return; } if (states[url] === FAILED) { execCallbacks('failure', url); return; } // Is script not loading then start loading it if (states[url] !== LOADING) { states[url] = LOADING; loading++; loadScript(url, function () { states[url] = LOADED; loading--; execCallbacks('success', url); // Load more scripts if they where added by the recently loaded script loadScripts(); }, function () { states[url] = FAILED; loading--; failures.push(url); execCallbacks('failure', url); // Load more scripts if they where added by the recently loaded script loadScripts(); }); } }); // No scripts are currently loading then execute all pending queue loaded callbacks if (!loading) { each(queueLoadedCallbacks, function (callback) { if (failures.length === 0) { if (isFunction(callback.success)) { callback.success.call(callback.scope); } } else { if (isFunction(callback.failure)) { callback.failure.call(callback.scope, failures); } } }); queueLoadedCallbacks.length = 0; } }; loadScripts(); }; } ScriptLoader.ScriptLoader = new ScriptLoader(); return ScriptLoader; } ); /** * AddOnManager.js * * Released under LGPL License. * Copyright (c) 1999-2017 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.core.AddOnManager', [ "tinymce.core.dom.ScriptLoader", "tinymce.core.util.Tools" ], function (ScriptLoader, Tools) { var each = Tools.each; function AddOnManager() { var self = this; self.items = []; self.urls = {}; self.lookup = {}; } AddOnManager.prototype = { /** * Returns the specified add on by the short name. * * @method get * @param {String} name Add-on to look for. * @return {tinymce.Theme/tinymce.Plugin} Theme or plugin add-on instance or undefined. */ get: function (name) { if (this.lookup[name]) { return this.lookup[name].instance; } return undefined; }, dependencies: function (name) { var result; if (this.lookup[name]) { result = this.lookup[name].dependencies; } return result || []; }, /** * Loads a language pack for the specified add-on. * * @method requireLangPack * @param {String} name Short name of the add-on. * @param {String} languages Optional comma or space separated list of languages to check if it matches the name. */ requireLangPack: function (name, languages) { var language = AddOnManager.language; if (language && AddOnManager.languageLoad !== false) { if (languages) { languages = ',' + languages + ','; // Load short form sv.js or long form sv_SE.js if (languages.indexOf(',' + language.substr(0, 2) + ',') != -1) { language = language.substr(0, 2); } else if (languages.indexOf(',' + language + ',') == -1) { return; } } ScriptLoader.ScriptLoader.add(this.urls[name] + '/langs/' + language + '.js'); } }, /** * Adds a instance of the add-on by it's short name. * * @method add * @param {String} id Short name/id for the add-on. * @param {tinymce.Theme/tinymce.Plugin} addOn Theme or plugin to add. * @return {tinymce.Theme/tinymce.Plugin} The same theme or plugin instance that got passed in. * @example * // Create a simple plugin * tinymce.create('tinymce.plugins.TestPlugin', { * TestPlugin: function(ed, url) { * ed.on('click', function(e) { * ed.windowManager.alert('Hello World!'); * }); * } * }); * * // Register plugin using the add method * tinymce.PluginManager.add('test', tinymce.plugins.TestPlugin); * * // Initialize TinyMCE * tinymce.init({ * ... * plugins: '-test' // Init the plugin but don't try to load it * }); */ add: function (id, addOn, dependencies) { this.items.push(addOn); this.lookup[id] = { instance: addOn, dependencies: dependencies }; return addOn; }, remove: function (name) { delete this.urls[name]; delete this.lookup[name]; }, createUrl: function (baseUrl, dep) { if (typeof dep === "object") { return dep; } return { prefix: baseUrl.prefix, resource: dep, suffix: baseUrl.suffix }; }, /** * Add a set of components that will make up the add-on. Using the url of the add-on name as the base url. * This should be used in development mode. A new compressor/javascript munger process will ensure that the * components are put together into the plugin.js file and compressed correctly. * * @method addComponents * @param {String} pluginName name of the plugin to load scripts from (will be used to get the base url for the plugins). * @param {Array} scripts Array containing the names of the scripts to load. */ addComponents: function (pluginName, scripts) { var pluginUrl = this.urls[pluginName]; each(scripts, function (script) { ScriptLoader.ScriptLoader.add(pluginUrl + "/" + script); }); }, /** * Loads an add-on from a specific url. * * @method load * @param {String} name Short name of the add-on that gets loaded. * @param {String} addOnUrl URL to the add-on that will get loaded. * @param {function} success Optional success callback to execute when an add-on is loaded. * @param {Object} scope Optional scope to execute the callback in. * @param {function} failure Optional failure callback to execute when an add-on failed to load. * @example * // Loads a plugin from an external URL * tinymce.PluginManager.load('myplugin', '/some/dir/someplugin/plugin.js'); * * // Initialize TinyMCE * tinymce.init({ * ... * plugins: '-myplugin' // Don't try to load it again * }); */ load: function (name, addOnUrl, success, scope, failure) { var self = this, url = addOnUrl; function loadDependencies() { var dependencies = self.dependencies(name); each(dependencies, function (dep) { var newUrl = self.createUrl(addOnUrl, dep); self.load(newUrl.resource, newUrl, undefined, undefined); }); if (success) { if (scope) { success.call(scope); } else { success.call(ScriptLoader); } } } if (self.urls[name]) { return; } if (typeof addOnUrl === "object") { url = addOnUrl.prefix + addOnUrl.resource + addOnUrl.suffix; } if (url.indexOf('/') !== 0 && url.indexOf('://') == -1) { url = AddOnManager.baseURL + '/' + url; } self.urls[name] = url.substring(0, url.lastIndexOf('/')); if (self.lookup[name]) { loadDependencies(); } else { ScriptLoader.ScriptLoader.add(url, loadDependencies, scope, failure); } } }; AddOnManager.PluginManager = new AddOnManager(); AddOnManager.ThemeManager = new AddOnManager(); return AddOnManager; } ); /** * TinyMCE theme class. * * @class tinymce.Theme */ /** * This method is responsible for rendering/generating the overall user interface with toolbars, buttons, iframe containers etc. * * @method renderUI * @param {Object} obj Object parameter containing the targetNode DOM node that will be replaced visually with an editor instance. * @return {Object} an object with items like iframeContainer, editorContainer, sizeContainer, deltaWidth, deltaHeight. */ /** * Plugin base class, this is a pseudo class that describes how a plugin is to be created for TinyMCE. The methods below are all optional. * * @class tinymce.Plugin * @example * tinymce.PluginManager.add('example', function(editor, url) { * // Add a button that opens a window * editor.addButton('example', { * text: 'My button', * icon: false, * onclick: function() { * // Open window * editor.windowManager.open({ * title: 'Example plugin', * body: [ * {type: 'textbox', name: 'title', label: 'Title'} * ], * onsubmit: function(e) { * // Insert content when the window form is submitted * editor.insertContent('Title: ' + e.data.title); * } * }); * } * }); * * // Adds a menu item to the tools menu * editor.addMenuItem('example', { * text: 'Example plugin', * context: 'tools', * onclick: function() { * // Open window with a specific url * editor.windowManager.open({ * title: 'TinyMCE site', * url: 'http://www.tinymce.com', * width: 800, * height: 600, * buttons: [{ * text: 'Close', * onclick: 'close' * }] * }); * } * }); * }); */ define( 'ephox.katamari.api.Cell', [ ], function () { var Cell = function (initial) { var value = initial; var get = function () { return value; }; var set = function (v) { value = v; }; var clone = function () { return Cell(get()); }; return { get: get, set: set, clone: clone }; }; return Cell; } ); /** * NodeType.js * * Released under LGPL License. * Copyright (c) 1999-2017 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.core.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 = node.ownerDocument.defaultView.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 hasAttribute(attrName, attrValue) { return function (node) { return isElement(node) && node.hasAttribute(attrName); }; } 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, hasAttribute: hasAttribute, hasAttributeValue: hasAttributeValue, matchStyleValues: matchStyleValues, isBogus: isBogus }; } ); /** * Fun.js * * Released under LGPL License. * Copyright (c) 1999-2017 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.core.util.Fun', [ ], function () { var slice = [].slice; function constant(value) { return function () { return value; }; } function negate(predicate) { return function (x) { return !predicate(x); }; } function compose(f, g) { return function (x) { return f(g(x)); }; } function or() { var args = slice.call(arguments); return function (x) { for (var i = 0; i < args.length; i++) { if (args[i](x)) { return true; } } return false; }; } function and() { var args = slice.call(arguments); return function (x) { for (var i = 0; i < args.length; i++) { if (!args[i](x)) { return false; } } return true; }; } function curry(fn) { var args = slice.call(arguments); if (args.length - 1 >= fn.length) { return fn.apply(this, args.slice(1)); } return function () { var tempArgs = args.concat([].slice.call(arguments)); return curry.apply(this, tempArgs); }; } function noop() { } return { constant: constant, negate: negate, and: and, or: or, curry: curry, compose: compose, noop: noop }; } ); /** * Zwsp.js * * Released under LGPL License. * Copyright (c) 1999-2017 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.core.text.Zwsp', [ ], function () { // This is technically not a ZWSP but a ZWNBSP or a BYTE ORDER MARK it used to be a ZWSP var ZWSP = '\uFEFF'; var isZwsp = function (chr) { return chr === ZWSP; }; var trim = function (text) { return text.replace(new RegExp(ZWSP, 'g'), ''); }; return { isZwsp: isZwsp, ZWSP: ZWSP, trim: trim }; } ); /** * CaretContainer.js * * Released under LGPL License. * Copyright (c) 1999-2017 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.core.caret.CaretContainer', [ "tinymce.core.dom.NodeType", "tinymce.core.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); } var hasContent = function (node) { return node.firstChild !== node.lastChild || !NodeType.isBr(node.firstChild); }; 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; } var prependInline = function (node) { if (NodeType.isText(node)) { var data = node.data; if (data.length > 0 && data.charAt(0) !== Zwsp.ZWSP) { node.insertData(0, Zwsp.ZWSP); } return node; } else { return null; } }; var appendInline = function (node) { if (NodeType.isText(node)) { var data = node.data; if (data.length > 0 && data.charAt(data.length - 1) !== Zwsp.ZWSP) { node.insertData(data.length, Zwsp.ZWSP); } return node; } else { return null; } }; var isBeforeInline = function (pos) { return pos && NodeType.isText(pos.container()) && pos.container().data.charAt(pos.offset()) === Zwsp.ZWSP; }; var isAfterInline = function (pos) { return pos && NodeType.isText(pos.container()) && pos.container().data.charAt(pos.offset() - 1) === Zwsp.ZWSP; }; function createBogusBr() { var br = document.createElement('br'); br.setAttribute('data-mce-bogus', '1'); return br; } function insertBlock(blockName, node, before) { var doc, blockNode, parentNode; doc = node.ownerDocument; blockNode = doc.createElement(blockName); blockNode.setAttribute('data-mce-caret', before ? 'before' : 'after'); blockNode.setAttribute('data-mce-bogus', 'all'); blockNode.appendChild(createBogusBr()); parentNode = node.parentNode; if (!before) { if (node.nextSibling) { parentNode.insertBefore(blockNode, node.nextSibling); } else { parentNode.appendChild(blockNode); } } else { parentNode.insertBefore(blockNode, node); } return blockNode; } function startsWithCaretContainer(node) { return isText(node) && node.data[0] == Zwsp.ZWSP; } function endsWithCaretContainer(node) { return isText(node) && node.data[node.data.length - 1] == Zwsp.ZWSP; } function trimBogusBr(elm) { var brs = elm.getElementsByTagName('br'); var lastBr = brs[brs.length - 1]; if (NodeType.isBogus(lastBr)) { lastBr.parentNode.removeChild(lastBr); } } function showCaretContainerBlock(caretContainer) { if (caretContainer && caretContainer.hasAttribute('data-mce-caret')) { trimBogusBr(caretContainer); caretContainer.removeAttribute('data-mce-caret'); caretContainer.removeAttribute('data-mce-bogus'); caretContainer.removeAttribute('style'); caretContainer.removeAttribute('_moz_abspos'); return caretContainer; } return null; } return { isCaretContainer: isCaretContainer, isCaretContainerBlock: isCaretContainerBlock, isCaretContainerInline: isCaretContainerInline, showCaretContainerBlock: showCaretContainerBlock, insertInline: insertInline, prependInline: prependInline, appendInline: appendInline, isBeforeInline: isBeforeInline, isAfterInline: isAfterInline, insertBlock: insertBlock, hasContent: hasContent, startsWithCaretContainer: startsWithCaretContainer, endsWithCaretContainer: endsWithCaretContainer }; } ); /** * RangeUtils.js * * Released under LGPL License. * Copyright (c) 1999-2017 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.core.dom.RangeUtils', [ "tinymce.core.util.Tools", "tinymce.core.dom.TreeWalker", "tinymce.core.dom.NodeType", "tinymce.core.dom.Range", "tinymce.core.caret.CaretContainer" ], function (Tools, TreeWalker, NodeType, Range, CaretContainer) { var each = Tools.each, isContentEditableTrue = NodeType.isContentEditableTrue, isContentEditableFalse = NodeType.isContentEditableFalse, isCaretContainer = CaretContainer.isCaretContainer; function hasCeProperty(node) { return isContentEditableTrue(node) || isContentEditableFalse(node); } function getEndChild(container, index) { var childNodes = container.childNodes; index--; if (index > childNodes.length - 1) { index = childNodes.length - 1; } else if (index < 0) { index = 0; } return childNodes[index] || container; } function findParent(node, rootNode, predicate) { while (node && node !== rootNode) { if (predicate(node)) { return node; } node = node.parentNode; } return null; } function hasParent(node, rootNode, predicate) { return findParent(node, rootNode, predicate) !== null; } function hasParentWithName(node, rootNode, name) { return hasParent(node, rootNode, function (node) { return node.nodeName === name; }); } function isFormatterCaret(node) { return node.id === '_mce_caret'; } function isCeFalseCaretContainer(node, rootNode) { return isCaretContainer(node) && hasParent(node, rootNode, isFormatterCaret) === false; } function RangeUtils(dom) { /** * Walks the specified range like object and executes the callback for each sibling collection it finds. * * @private * @method walk * @param {Object} rng Range like object. * @param {function} callback Callback function to execute for each sibling collection. */ this.walk = function (rng, callback) { var startContainer = rng.startContainer, startOffset = rng.startOffset, endContainer = rng.endContainer, endOffset = rng.endOffset, ancestor, startPoint, endPoint, node, parent, siblings, nodes; // Handle table cell selection the table plugin enables // you to fake select table cells and perform formatting actions on them nodes = dom.select('td[data-mce-selected],th[data-mce-selected]'); if (nodes.length > 0) { each(nodes, function (node) { callback([node]); }); return; } /** * Excludes start/end text node if they are out side the range * * @private * @param {Array} nodes Nodes to exclude items from. * @return {Array} Array with nodes excluding the start/end container if needed. */ function exclude(nodes) { var node; // First node is excluded node = nodes[0]; if (node.nodeType === 3 && node === startContainer && startOffset >= node.nodeValue.length) { nodes.splice(0, 1); } // Last node is excluded node = nodes[nodes.length - 1]; if (endOffset === 0 && nodes.length > 0 && node === endContainer && node.nodeType === 3) { nodes.splice(nodes.length - 1, 1); } return nodes; } /** * Collects siblings * * @private * @param {Node} node Node to collect siblings from. * @param {String} name Name of the sibling to check for. * @param {Node} endNode * @return {Array} Array of collected siblings. */ function collectSiblings(node, name, endNode) { var siblings = []; for (; node && node != endNode; 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(startNode, endNode, next) { var siblingName = next ? 'nextSibling' : 'previousSibling'; for (node = startNode, parent = node.parentNode; node && node != endNode; node = parent) { parent = node.parentNode; siblings = collectSiblings(node == startNode ? 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 = false, 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|
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.core.caret.CaretBookmark', [ 'tinymce.core.dom.NodeType', 'tinymce.core.dom.DOMUtils', 'tinymce.core.util.Fun', 'tinymce.core.util.Arr', 'tinymce.core.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 }; } ); /** * BookmarkManager.js * * Released under LGPL License. * Copyright (c) 1999-2017 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.core.dom.BookmarkManager', [ 'tinymce.core.caret.CaretBookmark', 'tinymce.core.caret.CaretContainer', 'tinymce.core.caret.CaretPosition', 'tinymce.core.dom.NodeType', 'tinymce.core.dom.RangeUtils', 'tinymce.core.Env', 'tinymce.core.text.Zwsp', 'tinymce.core.util.Tools' ], function (CaretBookmark, CaretContainer, CaretPosition, NodeType, RangeUtils, Env, Zwsp, Tools) { var isContentEditableFalse = NodeType.isContentEditableFalse; var getNormalizedTextOffset = function (container, offset) { var node, trimmedOffset; trimmedOffset = Zwsp.trim(container.data.slice(0, offset)).length; for (node = container.previousSibling; node && node.nodeType === 3; node = node.previousSibling) { trimmedOffset += Zwsp.trim(node.data).length; } return trimmedOffset; }; var trimEmptyTextNode = function (node) { if (NodeType.isText(node) && node.data.length === 0) { node.parentNode.removeChild(node); } }; /** * 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 = [], childNodes, after = 0; if (container.nodeType === 3) { point.push(normalized ? getNormalizedTextOffset(container, offset) : offset); } else { childNodes = container.childNodes; if (offset >= childNodes.length && childNodes.length) { after = 1; offset = Math.max(0, childNodes.length - 1); } point.push(dom.nodeIndex(childNodes[offset], normalized) + after); } for (; container && container != root; container = container.parentNode) { point.push(dom.nodeIndex(container, normalized)); } return point; } bookmark.start = getPoint(rng, true); if (!selection.isCollapsed()) { bookmark.end = getPoint(rng); } return bookmark; } function findAdjacentContentEditableFalseElm(rng) { function findSibling(node, offset) { var sibling; if (NodeType.isElement(node)) { node = RangeUtils.getNode(node, offset); if (isContentEditableFalse(node)) { return node; } } if (CaretContainer.isCaretContainer(node)) { if (NodeType.isText(node) && CaretContainer.isCaretContainerBlock(node)) { node = node.parentNode; } sibling = node.previousSibling; if (isContentEditableFalse(sibling)) { return sibling; } sibling = node.nextSibling; if (isContentEditableFalse(sibling)) { return sibling; } } } return findSibling(rng.startContainer, rng.startOffset) || findSibling(rng.endContainer, rng.endOffset); } if (type == 2) { element = selection.getNode(); name = element ? element.nodeName : null; rng = selection.getRng(); if (isContentEditableFalse(element) || name == 'IMG') { return { name: name, index: findIndex(name, element) }; } if (selection.tridentSel) { return selection.tridentSel.getBookmark(type); } element = findAdjacentContentEditableFalseElm(rng); if (element) { name = element.tagName; return { name: name, index: findIndex(name, element) }; } return getLocation(rng); } if (type == 3) { rng = selection.getRng(); return { start: CaretBookmark.create(dom.getRoot(), CaretPosition.fromRangeStart(rng)), end: CaretBookmark.create(dom.getRoot(), CaretPosition.fromRangeEnd(rng)) }; } // Handle simple range if (type) { return { rng: selection.getRng() }; } rng = selection.getRng(); id = dom.uniqueId(); collapsed = selection.isCollapsed(); styles = 'overflow:hidden;line-height:0px'; // Explorer method if (rng.duplicate || rng.item) { // Text selection if (!rng.item) { rng2 = rng.duplicate(); try { // Insert start marker rng.collapse(); rng.pasteHTML('' + chr + ''); // Insert end marker if (!collapsed) { rng2.collapse(false); // Detect the empty space after block elements in IE and move the // end back one character ] becomes]
rng.moveToElementText(rng2.parentElement()); if (rng.compareEndPoints('StartToEnd', rng2) === 0) { rng2.move('character', -1); } rng2.pasteHTML('' + chr + ''); } } catch (ex) { // IE might throw unspecified error so lets ignore it return null; } } else { // Control selection element = rng.item(0); name = element.nodeName; return { name: name, index: findIndex(name, element) }; } } else { element = selection.getNode(); name = element.nodeName; if (name == 'IMG') { return { name: name, index: findIndex(name, element) }; } // W3C method rng2 = normalizeTableCellSelection(rng.cloneRange()); // Insert end marker if (!collapsed) { rng2.collapse(false); var endBookmarkNode = dom.create('span', { 'data-mce-type': "bookmark", id: id + '_end', style: styles }, chr); rng2.insertNode(endBookmarkNode); trimEmptyTextNode(endBookmarkNode.nextSibling); } rng = normalizeTableCellSelection(rng); rng.collapse(true); var startBookmarkNode = dom.create('span', { 'data-mce-type': "bookmark", id: id + '_start', style: styles }, chr); rng.insertNode(startBookmarkNode); trimEmptyTextNode(startBookmarkNode.previousSibling); } 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
->|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; } if (startCaretPosition.isEqual(CaretPosition.after(rootNode)) && rootNode.lastChild) { caretPosition = CaretPosition.after(rootNode.lastChild); if (isBackwards(direction) && isCaretCandidate(rootNode.lastChild) && isElement(rootNode.lastChild)) { return isBr(rootNode.lastChild) ? CaretPosition.before(rootNode.lastChild) : caretPosition; } } else { 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); } }; }; } ); /** * CaretFinder.js * * Released under LGPL License. * Copyright (c) 1999-2017 Ephox Corp. All rights reserved * * License: http://www.tinymce.com/license * Contributing: http://www.tinymce.com/contributing */ define( 'tinymce.core.caret.CaretFinder', [ 'ephox.katamari.api.Fun', 'ephox.katamari.api.Option', 'tinymce.core.caret.CaretCandidate', 'tinymce.core.caret.CaretPosition', 'tinymce.core.caret.CaretUtils', 'tinymce.core.caret.CaretWalker', 'tinymce.core.dom.NodeType' ], function (Fun, Option, CaretCandidate, CaretPosition, CaretUtils, CaretWalker, NodeType) { var walkToPositionIn = function (forward, rootNode, startNode) { var position = forward ? CaretPosition.before(startNode) : CaretPosition.after(startNode); return fromPosition(forward, rootNode, position); }; var afterElement = function (node) { return NodeType.isBr(node) ? CaretPosition.before(node) : CaretPosition.after(node); }; var isBeforeOrStart = function (position) { if (CaretPosition.isTextPosition(position)) { return position.offset() === 0; } else { return CaretCandidate.isCaretCandidate(position.getNode()); } }; var isAfterOrEnd = function (position) { if (CaretPosition.isTextPosition(position)) { return position.offset() === position.container().data.length; } else { return CaretCandidate.isCaretCandidate(position.getNode(true)); } }; var isBeforeAfterSameElement = function (from, to) { return !CaretPosition.isTextPosition(from) && !CaretPosition.isTextPosition(to) && from.getNode() === to.getNode(true); }; var isAtBr = function (position) { return !CaretPosition.isTextPosition(position) && NodeType.isBr(position.getNode()); }; var shouldSkipPosition = function (forward, from, to) { if (forward) { return !isBeforeAfterSameElement(from, to) && !isAtBr(from) && isAfterOrEnd(from) && isBeforeOrStart(to); } else { return !isBeforeAfterSameElement(to, from) && isBeforeOrStart(from) && isAfterOrEnd(to); } }; // Finds:a|b
->a|b
var fromPosition = function (forward, rootNode, position) { var walker = new CaretWalker(rootNode); return Option.from(forward ? walker.next(position) : walker.prev(position)); }; // Finds:a|b
->ab|
var navigate = function (forward, rootNode, from) { return fromPosition(forward, rootNode, from).bind(function (to) { if (CaretUtils.isInSameBlock(from, to, rootNode) && shouldSkipPosition(forward, from, to)) { return fromPosition(forward, rootNode, to); } else { return Option.some(to); } }); }; var positionIn = function (forward, element) { var startNode = forward ? element.firstChild : element.lastChild; if (NodeType.isText(startNode)) { return Option.some(new CaretPosition(startNode, forward ? 0 : startNode.data.length)); } else if (startNode) { if (CaretCandidate.isCaretCandidate(startNode)) { return Option.some(forward ? CaretPosition.before(startNode) : afterElement(startNode)); } else { return walkToPositionIn(forward, element, startNode); } } else { return Option.none(); } }; return { fromPosition: fromPosition, nextPosition: Fun.curry(fromPosition, true), prevPosition: Fun.curry(fromPosition, false), navigate: navigate, positionIn: positionIn, firstPositionIn: Fun.curry(positionIn, true), lastPositionIn: Fun.curry(positionIn, false) }; } ); /** * RangeNormalizer.js * * Released under LGPL License. * Copyright (c) 1999-2017 Ephox Corp. All rights reserved * * License: http://www.tinymce.com/license * Contributing: http://www.tinymce.com/contributing */ define( 'tinymce.core.dom.RangeNormalizer', [ 'tinymce.core.caret.CaretFinder', 'tinymce.core.caret.CaretPosition', 'tinymce.core.caret.CaretUtils' ], function (CaretFinder, CaretPosition, CaretUtils) { var createRange = function (sc, so, ec, eo) { var rng = document.createRange(); rng.setStart(sc, so); rng.setEnd(ec, eo); return rng; }; // If you triple click a paragraph in this case: //a
b
// It would become this range in webkit: //[a
]b
// We would want it to be: //[a]
b
// Since it would otherwise produces spans out of thin air on insertContent for example. var normalizeBlockSelectionRange = function (rng) { var startPos = CaretPosition.fromRangeStart(rng); var endPos = CaretPosition.fromRangeEnd(rng); var rootNode = rng.commonAncestorContainer; return CaretFinder.fromPosition(false, rootNode, endPos) .map(function (newEndPos) { if (!CaretUtils.isInSameBlock(startPos, endPos, rootNode) && CaretUtils.isInSameBlock(startPos, newEndPos, rootNode)) { return createRange(startPos.container(), startPos.offset(), newEndPos.container(), newEndPos.offset()); } else { return rng; } }).getOr(rng); }; var normalizeBlockSelection = function (rng) { return rng.collapsed ? rng : normalizeBlockSelectionRange(rng); }; var normalize = function (rng) { return normalizeBlockSelection(rng); }; return { normalize: normalize }; } ); define("global!console", [], function () { if (typeof console === "undefined") console = { log: function () {} }; return console; }); defineGlobal("global!document", document); define( 'ephox.sugar.api.node.Element', [ 'ephox.katamari.api.Fun', 'global!Error', 'global!console', 'global!document' ], function (Fun, Error, console, document) { var fromHtml = function (html, scope) { var doc = scope || document; var div = doc.createElement('div'); div.innerHTML = html; if (!div.hasChildNodes() || div.childNodes.length > 1) { console.error('HTML does not have a single root node', html); throw 'HTML must have a single root node'; } return fromDom(div.childNodes[0]); }; var fromTag = function (tag, scope) { var doc = scope || document; var node = doc.createElement(tag); return fromDom(node); }; var fromText = function (text, scope) { var doc = scope || document; var node = doc.createTextNode(text); return fromDom(node); }; var fromDom = function (node) { if (node === null || node === undefined) throw new Error('Node cannot be null or undefined'); return { dom: Fun.constant(node) }; }; return { fromHtml: fromHtml, fromTag: fromTag, fromText: fromText, fromDom: fromDom }; } ); define( 'ephox.katamari.api.Type', [ 'global!Array', 'global!String' ], function (Array, String) { var typeOf = function(x) { if (x === null) return 'null'; var t = typeof x; if (t === 'object' && Array.prototype.isPrototypeOf(x)) return 'array'; if (t === 'object' && String.prototype.isPrototypeOf(x)) return 'string'; return t; }; var isType = function (type) { return function (value) { return typeOf(value) === type; }; }; return { isString: isType('string'), isObject: isType('object'), isArray: isType('array'), isNull: isType('null'), isBoolean: isType('boolean'), isUndefined: isType('undefined'), isFunction: isType('function'), isNumber: isType('number') }; } ); define( 'ephox.katamari.data.Immutable', [ 'ephox.katamari.api.Arr', 'ephox.katamari.api.Fun', 'global!Array', 'global!Error' ], function (Arr, Fun, Array, Error) { return function () { var fields = arguments; return function(/* values */) { // Don't use array slice(arguments), makes the whole function unoptimisable on Chrome var values = new Array(arguments.length); for (var i = 0; i < values.length; i++) values[i] = arguments[i]; if (fields.length !== values.length) throw new Error('Wrong number of arguments to struct. Expected "[' + fields.length + ']", got ' + values.length + ' arguments'); var struct = {}; Arr.each(fields, function (name, i) { struct[name] = Fun.constant(values[i]); }); return struct; }; }; } ); define( 'ephox.katamari.api.Obj', [ 'ephox.katamari.api.Option', 'global!Object' ], function (Option, Object) { // There are many variations of Object iteration that are faster than the 'for-in' style: // http://jsperf.com/object-keys-iteration/107 // // Use the native keys if it is available (IE9+), otherwise fall back to manually filtering var keys = (function () { var fastKeys = Object.keys; // This technically means that 'each' and 'find' on IE8 iterate through the object twice. // This code doesn't run on IE8 much, so it's an acceptable tradeoff. // If it becomes a problem we can always duplicate the feature detection inside each and find as well. var slowKeys = function (o) { var r = []; for (var i in o) { if (o.hasOwnProperty(i)) { r.push(i); } } return r; }; return fastKeys === undefined ? slowKeys : fastKeys; })(); var each = function (obj, f) { var props = keys(obj); for (var k = 0, len = props.length; k < len; k++) { var i = props[k]; var x = obj[i]; f(x, i, obj); } }; /** objectMap :: (JsObj(k, v), (v, k, JsObj(k, v) -> x)) -> JsObj(k, x) */ var objectMap = function (obj, f) { return tupleMap(obj, function (x, i, obj) { return { k: i, v: f(x, i, obj) }; }); }; /** tupleMap :: (JsObj(k, v), (v, k, JsObj(k, v) -> { k: x, v: y })) -> JsObj(x, y) */ var tupleMap = function (obj, f) { var r = {}; each(obj, function (x, i) { var tuple = f(x, i, obj); r[tuple.k] = tuple.v; }); return r; }; /** bifilter :: (JsObj(k, v), (v, k -> Bool)) -> { t: JsObj(k, v), f: JsObj(k, v) } */ var bifilter = function (obj, pred) { var t = {}; var f = {}; each(obj, function(x, i) { var branch = pred(x, i) ? t : f; branch[i] = x; }); return { t: t, f: f }; }; /** mapToArray :: (JsObj(k, v), (v, k -> a)) -> [a] */ var mapToArray = function (obj, f) { var r = []; each(obj, function(value, name) { r.push(f(value, name)); }); return r; }; /** find :: (JsObj(k, v), (v, k, JsObj(k, v) -> Bool)) -> Option v */ var find = function (obj, pred) { var props = keys(obj); for (var k = 0, len = props.length; k < len; k++) { var i = props[k]; var x = obj[i]; if (pred(x, i, obj)) { return Option.some(x); } } return Option.none(); }; /** values :: JsObj(k, v) -> [v] */ var values = function (obj) { return mapToArray(obj, function (v) { return v; }); }; var size = function (obj) { return values(obj).length; }; return { bifilter: bifilter, each: each, map: objectMap, mapToArray: mapToArray, tupleMap: tupleMap, find: find, keys: keys, values: values, size: size }; } ); define( 'ephox.katamari.util.BagUtils', [ 'ephox.katamari.api.Arr', 'ephox.katamari.api.Type', 'global!Error' ], function (Arr, Type, Error) { var sort = function (arr) { return arr.slice(0).sort(); }; var reqMessage = function (required, keys) { throw new Error('All required keys (' + sort(required).join(', ') + ') were not specified. Specified keys were: ' + sort(keys).join(', ') + '.'); }; var unsuppMessage = function (unsupported) { throw new Error('Unsupported keys for object: ' + sort(unsupported).join(', ')); }; var validateStrArr = function (label, array) { if (!Type.isArray(array)) throw new Error('The ' + label + ' fields must be an array. Was: ' + array + '.'); Arr.each(array, function (a) { if (!Type.isString(a)) throw new Error('The value ' + a + ' in the ' + label + ' fields was not a string.'); }); }; var invalidTypeMessage = function (incorrect, type) { throw new Error('All values need to be of type: ' + type + '. Keys (' + sort(incorrect).join(', ') + ') were not.'); }; var checkDupes = function (everything) { var sorted = sort(everything); var dupe = Arr.find(sorted, function (s, i) { return i < sorted.length -1 && s === sorted[i + 1]; }); dupe.each(function (d) { throw new Error('The field: ' + d + ' occurs more than once in the combined fields: [' + sorted.join(', ') + '].'); }); }; return { sort: sort, reqMessage: reqMessage, unsuppMessage: unsuppMessage, validateStrArr: validateStrArr, invalidTypeMessage: invalidTypeMessage, checkDupes: checkDupes }; } ); define( 'ephox.katamari.data.MixedBag', [ 'ephox.katamari.api.Arr', 'ephox.katamari.api.Fun', 'ephox.katamari.api.Obj', 'ephox.katamari.api.Option', 'ephox.katamari.util.BagUtils', 'global!Error', 'global!Object' ], function (Arr, Fun, Obj, Option, BagUtils, Error, Object) { return function (required, optional) { var everything = required.concat(optional); if (everything.length === 0) throw new Error('You must specify at least one required or optional field.'); BagUtils.validateStrArr('required', required); BagUtils.validateStrArr('optional', optional); BagUtils.checkDupes(everything); return function (obj) { var keys = Obj.keys(obj); // Ensure all required keys are present. var allReqd = Arr.forall(required, function (req) { return Arr.contains(keys, req); }); if (! allReqd) BagUtils.reqMessage(required, keys); var unsupported = Arr.filter(keys, function (key) { return !Arr.contains(everything, key); }); if (unsupported.length > 0) BagUtils.unsuppMessage(unsupported); var r = {}; Arr.each(required, function (req) { r[req] = Fun.constant(obj[req]); }); Arr.each(optional, function (opt) { r[opt] = Fun.constant(Object.prototype.hasOwnProperty.call(obj, opt) ? Option.some(obj[opt]): Option.none()); }); return r; }; }; } ); define( 'ephox.katamari.api.Struct', [ 'ephox.katamari.data.Immutable', 'ephox.katamari.data.MixedBag' ], function (Immutable, MixedBag) { return { immutable: Immutable, immutableBag: MixedBag }; } ); define( 'ephox.sugar.alien.Recurse', [ ], function () { /** * Applies f repeatedly until it completes (by returning Option.none()). * * Normally would just use recursion, but JavaScript lacks tail call optimisation. * * This is what recursion looks like when manually unravelled :) */ var toArray = function (target, f) { var r = []; var recurse = function (e) { r.push(e); return f(e); }; var cur = f(target); do { cur = cur.bind(recurse); } while (cur.isSome()); return r; }; return { toArray: toArray }; } ); define( 'ephox.katamari.api.Global', [ ], function () { // Use window object as the global if it's available since CSP will block script evals if (typeof window !== 'undefined') { return window; } else { return Function('return this;')(); } } ); define( 'ephox.katamari.api.Resolve', [ 'ephox.katamari.api.Global' ], function (Global) { /** path :: ([String], JsObj?) -> JsObj */ var path = function (parts, scope) { var o = scope !== undefined ? scope : Global; for (var i = 0; i < parts.length && o !== undefined && o !== null; ++i) o = o[parts[i]]; return o; }; /** resolve :: (String, JsObj?) -> JsObj */ var resolve = function (p, scope) { var parts = p.split('.'); return path(parts, scope); }; /** step :: (JsObj, String) -> JsObj */ var step = function (o, part) { if (o[part] === undefined || o[part] === null) o[part] = {}; return o[part]; }; /** forge :: ([String], JsObj?) -> JsObj */ var forge = function (parts, target) { var o = target !== undefined ? target : Global; for (var i = 0; i < parts.length; ++i) o = step(o, parts[i]); return o; }; /** namespace :: (String, JsObj?) -> JsObj */ var namespace = function (name, target) { var parts = name.split('.'); return forge(parts, target); }; return { path: path, resolve: resolve, forge: forge, namespace: namespace }; } ); define( 'ephox.sand.util.Global', [ 'ephox.katamari.api.Resolve' ], function (Resolve) { var unsafe = function (name, scope) { return Resolve.resolve(name, scope); }; var getOrDie = function (name, scope) { var actual = unsafe(name, scope); if (actual === undefined) throw name + ' not available on this browser'; return actual; }; return { getOrDie: getOrDie }; } ); define( 'ephox.sand.api.Node', [ 'ephox.sand.util.Global' ], function (Global) { /* * MDN says (yes) for IE, but it's undefined on IE8 */ var node = function () { var f = Global.getOrDie('Node'); return f; }; /* * Most of numerosity doesn't alter the methods on the object. * We're making an exception for Node, because bitwise and is so easy to get wrong. * * Might be nice to ADT this at some point instead of having individual methods. */ var compareDocumentPosition = function (a, b, match) { // Returns: 0 if e1 and e2 are the same node, or a bitmask comparing the positions // of nodes e1 and e2 in their documents. See the URL below for bitmask interpretation // https://developer.mozilla.org/en-US/docs/Web/API/Node/compareDocumentPosition return (a.compareDocumentPosition(b) & match) !== 0; }; var documentPositionPreceding = function (a, b) { return compareDocumentPosition(a, b, node().DOCUMENT_POSITION_PRECEDING); }; var documentPositionContainedBy = function (a, b) { return compareDocumentPosition(a, b, node().DOCUMENT_POSITION_CONTAINED_BY); }; return { documentPositionPreceding: documentPositionPreceding, documentPositionContainedBy: documentPositionContainedBy }; } ); define( 'ephox.katamari.api.Thunk', [ ], function () { var cached = function (f) { var called = false; var r; return function() { if (!called) { called = true; r = f.apply(null, arguments); } return r; }; }; return { cached: cached }; } ); defineGlobal("global!Number", Number); define( 'ephox.sand.detect.Version', [ 'ephox.katamari.api.Arr', 'global!Number', 'global!String' ], function (Arr, Number, String) { var firstMatch = function (regexes, s) { for (var i = 0; i < regexes.length; i++) { var x = regexes[i]; if (x.test(s)) return x; } return undefined; }; var find = function (regexes, agent) { var r = firstMatch(regexes, agent); if (!r) return { major : 0, minor : 0 }; var group = function(i) { return Number(agent.replace(r, '$' + i)); }; return nu(group(1), group(2)); }; var detect = function (versionRegexes, agent) { var cleanedAgent = String(agent).toLowerCase(); if (versionRegexes.length === 0) return unknown(); return find(versionRegexes, cleanedAgent); }; var unknown = function () { return nu(0, 0); }; var nu = function (major, minor) { return { major: major, minor: minor }; }; return { nu: nu, detect: detect, unknown: unknown }; } ); define( 'ephox.sand.core.Browser', [ 'ephox.katamari.api.Fun', 'ephox.sand.detect.Version' ], function (Fun, Version) { var edge = 'Edge'; var chrome = 'Chrome'; var ie = 'IE'; var opera = 'Opera'; var firefox = 'Firefox'; var safari = 'Safari'; var isBrowser = function (name, current) { return function () { return current === name; }; }; var unknown = function () { return nu({ current: undefined, version: Version.unknown() }); }; var nu = function (info) { var current = info.current; var version = info.version; return { current: current, version: version, // INVESTIGATE: Rename to Edge ? isEdge: isBrowser(edge, current), isChrome: isBrowser(chrome, current), // NOTE: isIe just looks too weird isIE: isBrowser(ie, current), isOpera: isBrowser(opera, current), isFirefox: isBrowser(firefox, current), isSafari: isBrowser(safari, current) }; }; return { unknown: unknown, nu: nu, edge: Fun.constant(edge), chrome: Fun.constant(chrome), ie: Fun.constant(ie), opera: Fun.constant(opera), firefox: Fun.constant(firefox), safari: Fun.constant(safari) }; } ); define( 'ephox.sand.core.OperatingSystem', [ 'ephox.katamari.api.Fun', 'ephox.sand.detect.Version' ], function (Fun, Version) { var windows = 'Windows'; var ios = 'iOS'; var android = 'Android'; var linux = 'Linux'; var osx = 'OSX'; var solaris = 'Solaris'; var freebsd = 'FreeBSD'; // Though there is a bit of dupe with this and Browser, trying to // reuse code makes it much harder to follow and change. var isOS = function (name, current) { return function () { return current === name; }; }; var unknown = function () { return nu({ current: undefined, version: Version.unknown() }); }; var nu = function (info) { var current = info.current; var version = info.version; return { current: current, version: version, isWindows: isOS(windows, current), // TODO: Fix capitalisation isiOS: isOS(ios, current), isAndroid: isOS(android, current), isOSX: isOS(osx, current), isLinux: isOS(linux, current), isSolaris: isOS(solaris, current), isFreeBSD: isOS(freebsd, current) }; }; return { unknown: unknown, nu: nu, windows: Fun.constant(windows), ios: Fun.constant(ios), android: Fun.constant(android), linux: Fun.constant(linux), osx: Fun.constant(osx), solaris: Fun.constant(solaris), freebsd: Fun.constant(freebsd) }; } ); define( 'ephox.sand.detect.DeviceType', [ 'ephox.katamari.api.Fun' ], function (Fun) { return function (os, browser, userAgent) { var isiPad = os.isiOS() && /ipad/i.test(userAgent) === true; var isiPhone = os.isiOS() && !isiPad; var isAndroid3 = os.isAndroid() && os.version.major === 3; var isAndroid4 = os.isAndroid() && os.version.major === 4; var isTablet = isiPad || isAndroid3 || ( isAndroid4 && /mobile/i.test(userAgent) === true ); var isTouch = os.isiOS() || os.isAndroid(); var isPhone = isTouch && !isTablet; var iOSwebview = browser.isSafari() && os.isiOS() && /safari/i.test(userAgent) === false; return { isiPad : Fun.constant(isiPad), isiPhone: Fun.constant(isiPhone), isTablet: Fun.constant(isTablet), isPhone: Fun.constant(isPhone), isTouch: Fun.constant(isTouch), isAndroid: os.isAndroid, isiOS: os.isiOS, isWebView: Fun.constant(iOSwebview) }; }; } ); define( 'ephox.sand.detect.UaString', [ 'ephox.katamari.api.Arr', 'ephox.sand.detect.Version', 'global!String' ], function (Arr, Version, String) { var detect = function (candidates, userAgent) { var agent = String(userAgent).toLowerCase(); return Arr.find(candidates, function (candidate) { return candidate.search(agent); }); }; // They (browser and os) are the same at the moment, but they might // not stay that way. var detectBrowser = function (browsers, userAgent) { return detect(browsers, userAgent).map(function (browser) { var version = Version.detect(browser.versionRegexes, userAgent); return { current: browser.name, version: version }; }); }; var detectOs = function (oses, userAgent) { return detect(oses, userAgent).map(function (os) { var version = Version.detect(os.versionRegexes, userAgent); return { current: os.name, version: version }; }); }; return { detectBrowser: detectBrowser, detectOs: detectOs }; } ); define( 'ephox.katamari.str.StrAppend', [ ], function () { var addToStart = function (str, prefix) { return prefix + str; }; var addToEnd = function (str, suffix) { return str + suffix; }; var removeFromStart = function (str, numChars) { return str.substring(numChars); }; var removeFromEnd = function (str, numChars) { return str.substring(0, str.length - numChars); }; return { addToStart: addToStart, addToEnd: addToEnd, removeFromStart: removeFromStart, removeFromEnd: removeFromEnd }; } ); define( 'ephox.katamari.str.StringParts', [ 'ephox.katamari.api.Option', 'global!Error' ], function (Option, Error) { /** Return the first 'count' letters from 'str'. - * e.g. first("abcde", 2) === "ab" - */ var first = function(str, count) { return str.substr(0, count); }; /** Return the last 'count' letters from 'str'. * e.g. last("abcde", 2) === "de" */ var last = function(str, count) { return str.substr(str.length - count, str.length); }; var head = function(str) { return str === '' ? Option.none() : Option.some(str.substr(0, 1)); }; var tail = function(str) { return str === '' ? Option.none() : Option.some(str.substring(1)); }; return { first: first, last: last, head: head, tail: tail }; } ); define( 'ephox.katamari.api.Strings', [ 'ephox.katamari.str.StrAppend', 'ephox.katamari.str.StringParts', 'global!Error' ], function (StrAppend, StringParts, Error) { var checkRange = function(str, substr, start) { if (substr === '') return true; if (str.length < substr.length) return false; var x = str.substr(start, start + substr.length); return x === substr; }; /** Given a string and object, perform template-replacements on the string, as specified by the object. * Any template fields of the form ${name} are replaced by the string or number specified as obj["name"] * Based on Douglas Crockford's 'supplant' method for template-replace of strings. Uses different template format. */ var supplant = function(str, obj) { var isStringOrNumber = function(a) { var t = typeof a; return t === 'string' || t === 'number'; }; return str.replace(/\${([^{}]*)}/g, function (a, b) { var value = obj[b]; return isStringOrNumber(value) ? value : a; } ); }; var removeLeading = function (str, prefix) { return startsWith(str, prefix) ? StrAppend.removeFromStart(str, prefix.length) : str; }; var removeTrailing = function (str, prefix) { return endsWith(str, prefix) ? StrAppend.removeFromEnd(str, prefix.length) : str; }; var ensureLeading = function (str, prefix) { return startsWith(str, prefix) ? str : StrAppend.addToStart(str, prefix); }; var ensureTrailing = function (str, prefix) { return endsWith(str, prefix) ? str : StrAppend.addToEnd(str, prefix); }; var contains = function(str, substr) { return str.indexOf(substr) !== -1; }; var capitalize = function(str) { return StringParts.head(str).bind(function (head) { return StringParts.tail(str).map(function (tail) { return head.toUpperCase() + tail; }); }).getOr(str); }; /** Does 'str' start with 'prefix'? * Note: all strings start with the empty string. * More formally, for all strings x, startsWith(x, ""). * This is so that for all strings x and y, startsWith(y + x, y) */ var startsWith = function(str, prefix) { return checkRange(str, prefix, 0); }; /** Does 'str' end with 'suffix'? * Note: all strings end with the empty string. * More formally, for all strings x, endsWith(x, ""). * This is so that for all strings x and y, endsWith(x + y, y) */ var endsWith = function(str, suffix) { return checkRange(str, suffix, str.length - suffix.length); }; /** removes all leading and trailing spaces */ var trim = function(str) { return str.replace(/^\s+|\s+$/g, ''); }; var lTrim = function(str) { return str.replace(/^\s+/g, ''); }; var rTrim = function(str) { return str.replace(/\s+$/g, ''); }; return { supplant: supplant, startsWith: startsWith, removeLeading: removeLeading, removeTrailing: removeTrailing, ensureLeading: ensureLeading, ensureTrailing: ensureTrailing, endsWith: endsWith, contains: contains, trim: trim, lTrim: lTrim, rTrim: rTrim, capitalize: capitalize }; } ); define( 'ephox.sand.info.PlatformInfo', [ 'ephox.katamari.api.Fun', 'ephox.katamari.api.Strings' ], function (Fun, Strings) { var normalVersionRegex = /.*?version\/\ ?([0-9]+)\.([0-9]+).*/; var checkContains = function (target) { return function (uastring) { return Strings.contains(uastring, target); }; }; var browsers = [ { name : 'Edge', versionRegexes: [/.*?edge\/ ?([0-9]+)\.([0-9]+)$/], search: function (uastring) { var monstrosity = Strings.contains(uastring, 'edge/') && Strings.contains(uastring, 'chrome') && Strings.contains(uastring, 'safari') && Strings.contains(uastring, 'applewebkit'); return monstrosity; } }, { name : 'Chrome', versionRegexes: [/.*?chrome\/([0-9]+)\.([0-9]+).*/, normalVersionRegex], search : function (uastring) { return Strings.contains(uastring, 'chrome') && !Strings.contains(uastring, 'chromeframe'); } }, { name : 'IE', versionRegexes: [/.*?msie\ ?([0-9]+)\.([0-9]+).*/, /.*?rv:([0-9]+)\.([0-9]+).*/], search: function (uastring) { return Strings.contains(uastring, 'msie') || Strings.contains(uastring, 'trident'); } }, // INVESTIGATE: Is this still the Opera user agent? { name : 'Opera', versionRegexes: [normalVersionRegex, /.*?opera\/([0-9]+)\.([0-9]+).*/], search : checkContains('opera') }, { name : 'Firefox', versionRegexes: [/.*?firefox\/\ ?([0-9]+)\.([0-9]+).*/], search : checkContains('firefox') }, { name : 'Safari', versionRegexes: [normalVersionRegex, /.*?cpu os ([0-9]+)_([0-9]+).*/], search : function (uastring) { return (Strings.contains(uastring, 'safari') || Strings.contains(uastring, 'mobile/')) && Strings.contains(uastring, 'applewebkit'); } } ]; var oses = [ { name : 'Windows', search : checkContains('win'), versionRegexes: [/.*?windows\ nt\ ?([0-9]+)\.([0-9]+).*/] }, { name : 'iOS', search : function (uastring) { return Strings.contains(uastring, 'iphone') || Strings.contains(uastring, 'ipad'); }, versionRegexes: [/.*?version\/\ ?([0-9]+)\.([0-9]+).*/, /.*cpu os ([0-9]+)_([0-9]+).*/, /.*cpu iphone os ([0-9]+)_([0-9]+).*/] }, { name : 'Android', search : checkContains('android'), versionRegexes: [/.*?android\ ?([0-9]+)\.([0-9]+).*/] }, { name : 'OSX', search : checkContains('os x'), versionRegexes: [/.*?os\ x\ ?([0-9]+)_([0-9]+).*/] }, { name : 'Linux', search : checkContains('linux'), versionRegexes: [ ] }, { name : 'Solaris', search : checkContains('sunos'), versionRegexes: [ ] }, { name : 'FreeBSD', search : checkContains('freebsd'), versionRegexes: [ ] } ]; return { browsers: Fun.constant(browsers), oses: Fun.constant(oses) }; } ); define( 'ephox.sand.core.PlatformDetection', [ 'ephox.sand.core.Browser', 'ephox.sand.core.OperatingSystem', 'ephox.sand.detect.DeviceType', 'ephox.sand.detect.UaString', 'ephox.sand.info.PlatformInfo' ], function (Browser, OperatingSystem, DeviceType, UaString, PlatformInfo) { var detect = function (userAgent) { var browsers = PlatformInfo.browsers(); var oses = PlatformInfo.oses(); var browser = UaString.detectBrowser(browsers, userAgent).fold( Browser.unknown, Browser.nu ); var os = UaString.detectOs(oses, userAgent).fold( OperatingSystem.unknown, OperatingSystem.nu ); var deviceType = DeviceType(os, browser, userAgent); return { browser: browser, os: os, deviceType: deviceType }; }; return { detect: detect }; } ); defineGlobal("global!navigator", navigator); define( 'ephox.sand.api.PlatformDetection', [ 'ephox.katamari.api.Thunk', 'ephox.sand.core.PlatformDetection', 'global!navigator' ], function (Thunk, PlatformDetection, navigator) { var detect = Thunk.cached(function () { var userAgent = navigator.userAgent; return PlatformDetection.detect(userAgent); }); return { detect: detect }; } ); define( 'ephox.sugar.api.node.NodeTypes', [ ], function () { return { ATTRIBUTE: 2, CDATA_SECTION: 4, COMMENT: 8, DOCUMENT: 9, DOCUMENT_TYPE: 10, DOCUMENT_FRAGMENT: 11, ELEMENT: 1, TEXT: 3, PROCESSING_INSTRUCTION: 7, ENTITY_REFERENCE: 5, ENTITY: 6, NOTATION: 12 }; } ); define( 'ephox.sugar.api.search.Selectors', [ 'ephox.katamari.api.Arr', 'ephox.katamari.api.Option', 'ephox.sugar.api.node.Element', 'ephox.sugar.api.node.NodeTypes', 'global!Error', 'global!document' ], function (Arr, Option, Element, NodeTypes, Error, document) { /* * There's a lot of code here; the aim is to allow the browser to optimise constant comparisons, * instead of doing object lookup feature detection on every call */ var STANDARD = 0; var MSSTANDARD = 1; var WEBKITSTANDARD = 2; var FIREFOXSTANDARD = 3; var selectorType = (function () { var test = document.createElement('span'); // As of Chrome 34 / Safari 7.1 / FireFox 34, everyone except IE has the unprefixed function. // Still check for the others, but do it last. return test.matches !== undefined ? STANDARD : test.msMatchesSelector !== undefined ? MSSTANDARD : test.webkitMatchesSelector !== undefined ? WEBKITSTANDARD : test.mozMatchesSelector !== undefined ? FIREFOXSTANDARD : -1; })(); var ELEMENT = NodeTypes.ELEMENT; var DOCUMENT = NodeTypes.DOCUMENT; var is = function (element, selector) { var elem = element.dom(); if (elem.nodeType !== ELEMENT) return false; // documents have querySelector but not matches // As of Chrome 34 / Safari 7.1 / FireFox 34, everyone except IE has the unprefixed function. // Still check for the others, but do it last. else if (selectorType === STANDARD) return elem.matches(selector); else if (selectorType === MSSTANDARD) return elem.msMatchesSelector(selector); else if (selectorType === WEBKITSTANDARD) return elem.webkitMatchesSelector(selector); else if (selectorType === FIREFOXSTANDARD) return elem.mozMatchesSelector(selector); else throw new Error('Browser lacks native selectors'); // unfortunately we can't throw this on startup :( }; var bypassSelector = function (dom) { // Only elements and documents support querySelector return dom.nodeType !== ELEMENT && dom.nodeType !== DOCUMENT || // IE fix for complex queries on empty nodes: http://jsfiddle.net/spyder/fv9ptr5L/ dom.childElementCount === 0; }; var all = function (selector, scope) { var base = scope === undefined ? document : scope.dom(); return bypassSelector(base) ? [] : Arr.map(base.querySelectorAll(selector), Element.fromDom); }; var one = function (selector, scope) { var base = scope === undefined ? document : scope.dom(); return bypassSelector(base) ? Option.none() : Option.from(base.querySelector(selector)).map(Element.fromDom); }; return { all: all, is: is, one: one }; } ); define( 'ephox.sugar.api.dom.Compare', [ 'ephox.katamari.api.Arr', 'ephox.katamari.api.Fun', 'ephox.sand.api.Node', 'ephox.sand.api.PlatformDetection', 'ephox.sugar.api.search.Selectors' ], function (Arr, Fun, Node, PlatformDetection, Selectors) { var eq = function (e1, e2) { return e1.dom() === e2.dom(); }; var isEqualNode = function (e1, e2) { return e1.dom().isEqualNode(e2.dom()); }; var member = function (element, elements) { return Arr.exists(elements, Fun.curry(eq, element)); }; // DOM contains() method returns true if e1===e2, we define our contains() to return false (a node does not contain itself). var regularContains = function (e1, e2) { var d1 = e1.dom(), d2 = e2.dom(); return d1 === d2 ? false : d1.contains(d2); }; var ieContains = function (e1, e2) { // IE only implements the contains() method for Element nodes. // It fails for Text nodes, so implement it using compareDocumentPosition() // https://connect.microsoft.com/IE/feedback/details/780874/node-contains-is-incorrect // Note that compareDocumentPosition returns CONTAINED_BY if 'e2 *is_contained_by* e1': // Also, compareDocumentPosition defines a node containing itself as false. return Node.documentPositionContainedBy(e1.dom(), e2.dom()); }; var browser = PlatformDetection.detect().browser; // Returns: true if node e1 contains e2, otherwise false. // (returns false if e1===e2: A node does not contain itself). var contains = browser.isIE() ? ieContains : regularContains; return { eq: eq, isEqualNode: isEqualNode, member: member, contains: contains, // Only used by DomUniverse. Remove (or should Selectors.is move here?) is: Selectors.is }; } ); define( 'ephox.sugar.api.search.Traverse', [ 'ephox.katamari.api.Type', 'ephox.katamari.api.Arr', 'ephox.katamari.api.Fun', 'ephox.katamari.api.Option', 'ephox.katamari.api.Struct', 'ephox.sugar.alien.Recurse', 'ephox.sugar.api.dom.Compare', 'ephox.sugar.api.node.Element' ], function (Type, Arr, Fun, Option, Struct, Recurse, Compare, Element) { // The document associated with the current element var owner = function (element) { return Element.fromDom(element.dom().ownerDocument); }; var documentElement = function (element) { // TODO: Avoid unnecessary wrap/unwrap here var doc = owner(element); return Element.fromDom(doc.dom().documentElement); }; // The window element associated with the element var defaultView = function (element) { var el = element.dom(); var defaultView = el.ownerDocument.defaultView; return Element.fromDom(defaultView); }; var parent = function (element) { var dom = element.dom(); return Option.from(dom.parentNode).map(Element.fromDom); }; var findIndex = function (element) { return parent(element).bind(function (p) { // TODO: Refactor out children so we can avoid the constant unwrapping var kin = children(p); return Arr.findIndex(kin, function (elem) { return Compare.eq(element, elem); }); }); }; var parents = function (element, isRoot) { var stop = Type.isFunction(isRoot) ? isRoot : Fun.constant(false); // This is used a *lot* so it needs to be performant, not recursive var dom = element.dom(); var ret = []; while (dom.parentNode !== null && dom.parentNode !== undefined) { var rawParent = dom.parentNode; var parent = Element.fromDom(rawParent); ret.push(parent); if (stop(parent) === true) break; else dom = rawParent; } return ret; }; var siblings = function (element) { // TODO: Refactor out children so we can just not add self instead of filtering afterwards var filterSelf = function (elements) { return Arr.filter(elements, function (x) { return !Compare.eq(element, x); }); }; return parent(element).map(children).map(filterSelf).getOr([]); }; var offsetParent = function (element) { var dom = element.dom(); return Option.from(dom.offsetParent).map(Element.fromDom); }; var prevSibling = function (element) { var dom = element.dom(); return Option.from(dom.previousSibling).map(Element.fromDom); }; var nextSibling = function (element) { var dom = element.dom(); return Option.from(dom.nextSibling).map(Element.fromDom); }; var prevSiblings = function (element) { // This one needs to be reversed, so they're still in DOM order return Arr.reverse(Recurse.toArray(element, prevSibling)); }; var nextSiblings = function (element) { return Recurse.toArray(element, nextSibling); }; var children = function (element) { var dom = element.dom(); return Arr.map(dom.childNodes, Element.fromDom); }; var child = function (element, index) { var children = element.dom().childNodes; return Option.from(children[index]).map(Element.fromDom); }; var firstChild = function (element) { return child(element, 0); }; var lastChild = function (element) { return child(element, element.dom().childNodes.length - 1); }; var spot = Struct.immutable('element', 'offset'); var leaf = function (element, offset) { var cs = children(element); return cs.length > 0 && offset < cs.length ? spot(cs[offset], 0) : spot(element, offset); }; return { owner: owner, defaultView: defaultView, documentElement: documentElement, parent: parent, findIndex: findIndex, parents: parents, siblings: siblings, prevSibling: prevSibling, offsetParent: offsetParent, prevSiblings: prevSiblings, nextSibling: nextSibling, nextSiblings: nextSiblings, children: children, child: child, firstChild: firstChild, lastChild: lastChild, leaf: leaf }; } ); define( 'ephox.sugar.api.dom.Insert', [ 'ephox.sugar.api.search.Traverse' ], function (Traverse) { var before = function (marker, element) { var parent = Traverse.parent(marker); parent.each(function (v) { v.dom().insertBefore(element.dom(), marker.dom()); }); }; var after = function (marker, element) { var sibling = Traverse.nextSibling(marker); sibling.fold(function () { var parent = Traverse.parent(marker); parent.each(function (v) { append(v, element); }); }, function (v) { before(v, element); }); }; var prepend = function (parent, element) { var firstChild = Traverse.firstChild(parent); firstChild.fold(function () { append(parent, element); }, function (v) { parent.dom().insertBefore(element.dom(), v.dom()); }); }; var append = function (parent, element) { parent.dom().appendChild(element.dom()); }; var appendAt = function (parent, element, index) { Traverse.child(parent, index).fold(function () { append(parent, element); }, function (v) { before(v, element); }); }; var wrap = function (element, wrapper) { before(element, wrapper); append(wrapper, element); }; return { before: before, after: after, prepend: prepend, append: append, appendAt: appendAt, wrap: wrap }; } ); define( 'ephox.sugar.api.dom.InsertAll', [ 'ephox.katamari.api.Arr', 'ephox.sugar.api.dom.Insert' ], function (Arr, Insert) { var before = function (marker, elements) { Arr.each(elements, function (x) { Insert.before(marker, x); }); }; var after = function (marker, elements) { Arr.each(elements, function (x, i) { var e = i === 0 ? marker : elements[i - 1]; Insert.after(e, x); }); }; var prepend = function (parent, elements) { Arr.each(elements.slice().reverse(), function (x) { Insert.prepend(parent, x); }); }; var append = function (parent, elements) { Arr.each(elements, function (x) { Insert.append(parent, x); }); }; return { before: before, after: after, prepend: prepend, append: append }; } ); define( 'ephox.sugar.api.dom.Remove', [ 'ephox.katamari.api.Arr', 'ephox.sugar.api.dom.InsertAll', 'ephox.sugar.api.search.Traverse' ], function (Arr, InsertAll, Traverse) { var empty = function (element) { // shortcut "empty node" trick. Requires IE 9. element.dom().textContent = ''; // If the contents was a single empty text node, the above doesn't remove it. But, it's still faster in general // than removing every child node manually. // The following is (probably) safe for performance as 99.9% of the time the trick works and // Traverse.children will return an empty array. Arr.each(Traverse.children(element), function (rogue) { remove(rogue); }); }; var remove = function (element) { var dom = element.dom(); if (dom.parentNode !== null) dom.parentNode.removeChild(dom); }; var unwrap = function (wrapper) { var children = Traverse.children(wrapper); if (children.length > 0) InsertAll.before(wrapper, children); remove(wrapper); }; return { empty: empty, remove: remove, unwrap: unwrap }; } ); define( 'ephox.sugar.api.node.Node', [ 'ephox.sugar.api.node.NodeTypes' ], function (NodeTypes) { var name = function (element) { var r = element.dom().nodeName; return r.toLowerCase(); }; var type = function (element) { return element.dom().nodeType; }; var value = function (element) { return element.dom().nodeValue; }; var isType = function (t) { return function (element) { return type(element) === t; }; }; var isComment = function (element) { return type(element) === NodeTypes.COMMENT || name(element) === '#comment'; }; var isElement = isType(NodeTypes.ELEMENT); var isText = isType(NodeTypes.TEXT); var isDocument = isType(NodeTypes.DOCUMENT); return { name: name, type: type, value: value, isElement: isElement, isText: isText, isDocument: isDocument, isComment: isComment }; } ); define( 'ephox.sugar.impl.NodeValue', [ 'ephox.sand.api.PlatformDetection', 'ephox.katamari.api.Option', 'global!Error' ], function (PlatformDetection, Option, Error) { return function (is, name) { var get = function (element) { if (!is(element)) throw new Error('Can only get ' + name + ' value of a ' + name + ' node'); return getOption(element).getOr(''); }; var getOptionIE10 = function (element) { // Prevent IE10 from throwing exception when setting parent innerHTML clobbers (TBIO-451). try { return getOptionSafe(element); } catch (e) { return Option.none(); } }; var getOptionSafe = function (element) { return is(element) ? Option.from(element.dom().nodeValue) : Option.none(); }; var browser = PlatformDetection.detect().browser; var getOption = browser.isIE() && browser.version.major === 10 ? getOptionIE10 : getOptionSafe; var set = function (element, value) { if (!is(element)) throw new Error('Can only set raw ' + name + ' value of a ' + name + ' node'); element.dom().nodeValue = value; }; return { get: get, getOption: getOption, set: set }; }; } ); define( 'ephox.sugar.api.node.Text', [ 'ephox.sugar.api.node.Node', 'ephox.sugar.impl.NodeValue' ], function (Node, NodeValue) { var api = NodeValue(Node.isText, 'text'); var get = function (element) { return api.get(element); }; var getOption = function (element) { return api.getOption(element); }; var set = function (element, value) { api.set(element, value); }; return { get: get, getOption: getOption, set: set }; } ); define( 'ephox.sugar.api.node.Body', [ 'ephox.katamari.api.Thunk', 'ephox.sugar.api.node.Element', 'ephox.sugar.api.node.Node', 'global!document' ], function (Thunk, Element, Node, document) { // Node.contains() is very, very, very good performance // http://jsperf.com/closest-vs-contains/5 var inBody = function (element) { // Technically this is only required on IE, where contains() returns false for text nodes. // But it's cheap enough to run everywhere and Sugar doesn't have platform detection (yet). var dom = Node.isText(element) ? element.dom().parentNode : element.dom(); // use ownerDocument.body to ensure this works inside iframes. // Normally contains is bad because an element "contains" itself, but here we want that. return dom !== undefined && dom !== null && dom.ownerDocument.body.contains(dom); }; var body = Thunk.cached(function() { return getBody(Element.fromDom(document)); }); var getBody = function (doc) { var body = doc.dom().body; if (body === null || body === undefined) throw 'Body is not available yet'; return Element.fromDom(body); }; return { body: body, getBody: getBody, inBody: inBody }; } ); define( 'ephox.sugar.api.search.PredicateFilter', [ 'ephox.katamari.api.Arr', 'ephox.sugar.api.node.Body', 'ephox.sugar.api.search.Traverse' ], function (Arr, Body, Traverse) { // maybe TraverseWith, similar to traverse but with a predicate? var all = function (predicate) { return descendants(Body.body(), predicate); }; var ancestors = function (scope, predicate, isRoot) { return Arr.filter(Traverse.parents(scope, isRoot), predicate); }; var siblings = function (scope, predicate) { return Arr.filter(Traverse.siblings(scope), predicate); }; var children = function (scope, predicate) { return Arr.filter(Traverse.children(scope), predicate); }; var descendants = function (scope, predicate) { var result = []; // Recurse.toArray() might help here Arr.each(Traverse.children(scope), function (x) { if (predicate(x)) { result = result.concat([ x ]); } result = result.concat(descendants(x, predicate)); }); return result; }; return { all: all, ancestors: ancestors, siblings: siblings, children: children, descendants: descendants }; } ); define( 'ephox.sugar.api.search.SelectorFilter', [ 'ephox.sugar.api.search.PredicateFilter', 'ephox.sugar.api.search.Selectors' ], function (PredicateFilter, Selectors) { var all = function (selector) { return Selectors.all(selector); }; // For all of the following: // // jQuery does siblings of firstChild. IE9+ supports scope.dom().children (similar to Traverse.children but elements only). // Traverse should also do this (but probably not by default). // var ancestors = function (scope, selector, isRoot) { // It may surprise you to learn this is exactly what JQuery does // TODO: Avoid all this wrapping and unwrapping return PredicateFilter.ancestors(scope, function (e) { return Selectors.is(e, selector); }, isRoot); }; var siblings = function (scope, selector) { // It may surprise you to learn this is exactly what JQuery does // TODO: Avoid all the wrapping and unwrapping return PredicateFilter.siblings(scope, function (e) { return Selectors.is(e, selector); }); }; var children = function (scope, selector) { // It may surprise you to learn this is exactly what JQuery does // TODO: Avoid all the wrapping and unwrapping return PredicateFilter.children(scope, function (e) { return Selectors.is(e, selector); }); }; var descendants = function (scope, selector) { return Selectors.all(selector, scope); }; return { all: all, ancestors: ancestors, siblings: siblings, children: children, descendants: descendants }; } ); /** * ElementType.js * * Released under LGPL License. * Copyright (c) 1999-2017 Ephox Corp. All rights reserved * * License: http://www.tinymce.com/license * Contributing: http://www.tinymce.com/contributing */ define( 'tinymce.core.dom.ElementType', [ 'ephox.katamari.api.Arr', 'ephox.katamari.api.Fun', 'ephox.sugar.api.node.Node' ], function (Arr, Fun, Node) { var blocks = [ 'article', 'aside', 'details', 'div', 'dt', 'figcaption', 'footer', 'form', 'fieldset', 'header', 'hgroup', 'html', 'main', 'nav', 'section', 'summary', 'body', 'p', 'dl', 'multicol', 'dd', 'figure', 'address', 'center', 'blockquote', 'h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'listing', 'xmp', 'pre', 'plaintext', 'menu', 'dir', 'ul', 'ol', 'li', 'hr', 'table', 'tbody', 'thead', 'tfoot', 'th', 'tr', 'td', 'caption' ]; var voids = [ 'area', 'base', 'basefont', 'br', 'col', 'frame', 'hr', 'img', 'input', 'isindex', 'link', 'meta', 'param', 'embed', 'source', 'wbr', 'track' ]; var tableCells = ['td', 'th']; var tableSections = ['thead', 'tbody', 'tfoot']; var textBlocks = [ 'h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'p', 'div', 'address', 'pre', 'form', 'blockquote', 'center', 'dir', 'fieldset', 'header', 'footer', 'article', 'section', 'hgroup', 'aside', 'nav', 'figure' ]; var headings = ['h1', 'h2', 'h3', 'h4', 'h5', 'h6']; var listItems = ['li', 'dd', 'dt']; var lists = ['ul', 'ol', 'dl']; var lazyLookup = function (items) { var lookup; return function (node) { lookup = lookup ? lookup : Arr.mapToObject(items, Fun.constant(true)); return lookup.hasOwnProperty(Node.name(node)); }; }; var isHeading = lazyLookup(headings); var isBlock = lazyLookup(blocks); var isInline = function (node) { return Node.isElement(node) && !isBlock(node); }; var isBr = function (node) { return Node.isElement(node) && Node.name(node) === 'br'; }; return { isBlock: isBlock, isInline: isInline, isHeading: isHeading, isTextBlock: lazyLookup(textBlocks), isList: lazyLookup(lists), isListItem: lazyLookup(listItems), isVoid: lazyLookup(voids), isTableSection: lazyLookup(tableSections), isTableCell: lazyLookup(tableCells), isBr: isBr }; } ); /** * PaddingBr.js * * Released under LGPL License. * Copyright (c) 1999-2017 Ephox Corp. All rights reserved * * License: http://www.tinymce.com/license * Contributing: http://www.tinymce.com/contributing */ define( 'tinymce.core.dom.PaddingBr', [ 'ephox.katamari.api.Arr', 'ephox.sugar.api.dom.Insert', 'ephox.sugar.api.dom.Remove', 'ephox.sugar.api.node.Element', 'ephox.sugar.api.node.Node', 'ephox.sugar.api.node.Text', 'ephox.sugar.api.search.SelectorFilter', 'ephox.sugar.api.search.Traverse', 'tinymce.core.dom.ElementType' ], function (Arr, Insert, Remove, Element, Node, Text, SelectorFilter, Traverse, ElementType) { var getLastChildren = function (elm) { var children = [], rawNode = elm.dom(); while (rawNode) { children.push(Element.fromDom(rawNode)); rawNode = rawNode.lastChild; } return children; }; var removeTrailingBr = function (elm) { var allBrs = SelectorFilter.descendants(elm, 'br'); var brs = Arr.filter(getLastChildren(elm).slice(-1), ElementType.isBr); if (allBrs.length === brs.length) { Arr.each(brs, Remove.remove); } }; var fillWithPaddingBr = function (elm) { Remove.empty(elm); Insert.append(elm, Element.fromHtml('*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 (!dom.isBlock(startContainer)) { startContainer = findParentContainer(true); } if (!dom.isBlock(endContainer)) { endContainer = findParentContainer(); } } } // Setup index for startContainer if (startContainer.nodeType === 1) { startOffset = dom.nodeIndex(startContainer); startContainer = startContainer.parentNode; } // Setup index for endContainer if (endContainer.nodeType === 1) { endOffset = dom.nodeIndex(endContainer) + 1; endContainer = endContainer.parentNode; } // Return new range like object return { startContainer: startContainer, startOffset: startOffset, endContainer: endContainer, endOffset: endOffset }; }; return { expandRng: expandRng }; } ); /** * MatchFormat.js * * Released under LGPL License. * Copyright (c) 1999-2017 Ephox Corp. All rights reserved * * License: http://www.tinymce.com/license * Contributing: http://www.tinymce.com/contributing */ define( 'tinymce.core.fmt.MatchFormat', [ 'tinymce.core.fmt.FormatUtils' ], function (FormatUtils) { var isEq = FormatUtils.isEq; var matchesUnInheritedFormatSelector = function (ed, node, name) { var formatList = ed.formatter.get(name); if (formatList) { for (var i = 0; i < formatList.length; i++) { if (formatList[i].inherit === false && ed.dom.is(node, formatList[i].selector)) { return true; } } } return false; }; var matchParents = function (editor, node, name, vars) { var root = editor.dom.getRoot(); if (node === root) { return false; } // Find first node with similar format settings node = editor.dom.getParent(node, function (node) { if (matchesUnInheritedFormatSelector(editor, node, name)) { return true; } return node.parentNode === root || !!matchNode(editor, node, name, vars, true); }); // Do an exact check on the similar format element return matchNode(editor, node, name, vars); }; var matchName = function (dom, 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); } }; var matchItems = function (dom, node, format, itemName, similar, vars) { var key, value, items = format[itemName], i; // Custom match if (format.onmatch) { return format.onmatch(node, format, itemName); } // Check all items if (items) { // Non indexed object if (typeof items.length === 'undefined') { for (key in items) { if (items.hasOwnProperty(key)) { if (itemName === 'attributes') { value = dom.getAttrib(node, key); } else { value = FormatUtils.getStyle(dom, node, key); } if (similar && !value && !format.exact) { return; } if ((!similar || format.exact) && !isEq(value, FormatUtils.normalizeStyleValue(dom, FormatUtils.replaceVars(items[key], vars), key))) { return; } } } } else { // Only one match needed for indexed arrays for (i = 0; i < items.length; i++) { if (itemName === 'attributes' ? dom.getAttrib(node, items[i]) : FormatUtils.getStyle(dom, node, items[i])) { return format; } } } } return format; }; var matchNode = function (ed, node, name, vars, similar) { var formatList = ed.formatter.get(name), format, i, x, classes, dom = ed.dom; 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(ed.dom, node, format) && matchItems(dom, node, format, 'attributes', similar, vars) && matchItems(dom, node, format, 'styles', similar, vars)) { // Match classes if ((classes = format.classes)) { for (x = 0; x < classes.length; x++) { if (!ed.dom.hasClass(node, classes[x])) { return; } } } return format; } } } }; var match = function (editor, name, vars, node) { var startNode; // Check specified node if (node) { return matchParents(editor, node, name, vars); } // Check selected node node = editor.selection.getNode(); if (matchParents(editor, node, name, vars)) { return true; } // Check start node if it's different startNode = editor.selection.getStart(); if (startNode !== node) { if (matchParents(editor, startNode, name, vars)) { return true; } } return false; }; var matchAll = function (editor, names, vars) { var startElement, matchedFormatNames = [], checkedMap = {}; // Check start of selection for formats startElement = editor.selection.getStart(); editor.dom.getParent(startElement, function (node) { var i, name; for (i = 0; i < names.length; i++) { name = names[i]; if (!checkedMap[name] && matchNode(editor, node, name, vars)) { checkedMap[name] = true; matchedFormatNames.push(name); } } }, editor.dom.getRoot()); return matchedFormatNames; }; var canApply = function (editor, name) { var formatList = editor.formatter.get(name), startNode, parents, i, x, selector, dom = editor.dom; if (formatList) { startNode = editor.selection.getStart(); parents = FormatUtils.getParents(dom, 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; }; return { matchNode: matchNode, matchName: matchName, match: match, matchAll: matchAll, canApply: canApply, matchesUnInheritedFormatSelector: matchesUnInheritedFormatSelector }; } ); /** * CaretFormat.js * * Released under LGPL License. * Copyright (c) 1999-2017 Ephox Corp. All rights reserved * * License: http://www.tinymce.com/license * Contributing: http://www.tinymce.com/contributing */ define( 'tinymce.core.fmt.CaretFormat', [ 'ephox.katamari.api.Arr', 'ephox.sugar.api.node.Element', 'tinymce.core.dom.PaddingBr', 'tinymce.core.dom.RangeUtils', 'tinymce.core.dom.TreeWalker', 'tinymce.core.fmt.ExpandRange', 'tinymce.core.fmt.FormatUtils', 'tinymce.core.fmt.MatchFormat', 'tinymce.core.text.Zwsp', 'tinymce.core.util.Fun', 'tinymce.core.util.Tools' ], function (Arr, Element, PaddingBr, RangeUtils, TreeWalker, ExpandRange, FormatUtils, MatchFormat, Zwsp, Fun, Tools) { var ZWSP = Zwsp.ZWSP, CARET_ID = '_mce_caret', DEBUG = false; var isCaretNode = function (node) { return node.nodeType === 1 && node.id === CARET_ID; }; var isCaretContainerEmpty = function (node, nodes) { while (node) { if ((node.nodeType === 3 && node.nodeValue !== ZWSP) || node.childNodes.length > 1) { return false; } // Collect nodes if (nodes && node.nodeType === 1) { nodes.push(node); } node = node.firstChild; } return true; }; var findFirstTextNode = function (node) { var walker; if (node) { walker = new TreeWalker(node, node); for (node = walker.current(); node; node = walker.next()) { if (node.nodeType === 3) { return node; } } } return null; }; var createCaretContainer = function (dom, fill) { var caretContainer = dom.create('span', { id: CARET_ID, 'data-mce-bogus': '1', style: DEBUG ? 'color:red' : '' }); if (fill) { caretContainer.appendChild(dom.doc.createTextNode(ZWSP)); } return caretContainer; }; var getParentCaretContainer = function (node) { while (node) { if (node.id === CARET_ID) { return node; } node = node.parentNode; } }; // 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 var unmarkBogusCaretParents = function (dom, selection) { var caretContainer; caretContainer = getParentCaretContainer(selection.getStart()); if (caretContainer && !dom.isEmpty(caretContainer)) { Tools.walk(caretContainer, function (node) { if (node.nodeType === 1 && node.id !== CARET_ID && !dom.isEmpty(node)) { dom.setAttrib(node, 'data-mce-bogus', null); } }, 'childNodes'); } }; var trimZwspFromCaretContainer = function (caretContainerNode) { var textNode = findFirstTextNode(caretContainerNode); if (textNode && textNode.nodeValue.charAt(0) === ZWSP) { textNode.deleteData(0, 1); } return textNode; }; var removeCaretContainerNode = function (dom, selection, node, moveCaret) { var rng, block, textNode; rng = selection.getRng(true); block = dom.getParent(node, dom.isBlock); if (isCaretContainerEmpty(node)) { if (moveCaret !== false) { rng.setStartBefore(node); rng.setEndBefore(node); } dom.remove(node); } else { textNode = trimZwspFromCaretContainer(node); if (rng.startContainer === textNode && rng.startOffset > 0) { rng.setStart(textNode, rng.startOffset - 1); } if (rng.endContainer === textNode && rng.endOffset > 0) { rng.setEnd(textNode, rng.endOffset - 1); } dom.remove(node, true); } if (block && dom.isEmpty(block)) { PaddingBr.fillWithPaddingBr(Element.fromDom(block)); } selection.setRng(rng); }; // Removes the caret container for the specified node or all on the current document var removeCaretContainer = function (dom, selection, node, moveCaret) { if (!node) { node = getParentCaretContainer(selection.getStart()); if (!node) { while ((node = dom.get(CARET_ID))) { removeCaretContainerNode(dom, selection, node, false); } } } else { removeCaretContainerNode(dom, selection, node, moveCaret); } }; var insertCaretContainerNode = function (editor, caretContainer, formatNode) { var dom = editor.dom, block = dom.getParent(formatNode, Fun.curry(FormatUtils.isTextBlock, editor)); if (block && dom.isEmpty(block)) { // Replace formatNode with caretContainer when removing format from empty block like|
formatNode.parentNode.replaceChild(caretContainer, formatNode); } else { PaddingBr.removeTrailingBr(Element.fromDom(formatNode)); if (dom.isEmpty(formatNode)) { formatNode.parentNode.replaceChild(caretContainer, formatNode); } else { dom.insertAfter(caretContainer, formatNode); } } }; var appendNode = function (parentNode, node) { parentNode.appendChild(node); return node; }; var insertFormatNodesIntoCaretContainer = function (formatNodes, caretContainer) { var innerMostFormatNode = Arr.foldr(formatNodes, function (parentNode, formatNode) { return appendNode(parentNode, formatNode.cloneNode(false)); }, caretContainer); return appendNode(innerMostFormatNode, innerMostFormatNode.ownerDocument.createTextNode(ZWSP)); }; var setupCaretEvents = function (editor) { if (!editor._hasCaretEvents) { bindEvents(editor); editor._hasCaretEvents = true; } }; var applyCaretFormat = function (editor, name, vars) { var rng, caretContainer, textNode, offset, bookmark, container, text; var dom = editor.dom, selection = editor.selection; setupCaretEvents(editor); rng = selection.getRng(true); offset = rng.startOffset; container = rng.startContainer; text = container.nodeValue; caretContainer = getParentCaretContainer(selection.getStart()); if (caretContainer) { textNode = findFirstTextNode(caretContainer); } // Expand to word if caret is in the middle of a text node and the char before/after is a alpha numeric character var wordcharRegex = /[^\s\u00a0\u00ad\u200b\ufeff]/; if (text && offset > 0 && offset < text.length && wordcharRegex.test(text.charAt(offset)) && wordcharRegex.test(text.charAt(offset - 1))) { // Get bookmark of caret position bookmark = selection.getBookmark(); // Collapse bookmark range (WebKit) rng.collapse(true); // Expand the range to the closest word and split it at those points rng = ExpandRange.expandRng(editor, rng, editor.formatter.get(name)); rng = new RangeUtils(dom).split(rng); // Apply the format to the range editor.formatter.apply(name, vars, rng); // Move selection back to caret position selection.moveToBookmark(bookmark); } else { if (!caretContainer || textNode.nodeValue !== ZWSP) { caretContainer = createCaretContainer(dom, true); textNode = caretContainer.firstChild; rng.insertNode(caretContainer); offset = 1; editor.formatter.apply(name, vars, caretContainer); } else { editor.formatter.apply(name, vars, caretContainer); } // Move selection to text node selection.setCursorLocation(textNode, offset); } }; var removeCaretFormat = function (editor, name, vars, similar) { var dom = editor.dom, selection = editor.selection; var rng = selection.getRng(true), container, offset, bookmark; var hasContentAfter, node, formatNode, parents = [], caretContainer; setupCaretEvents(editor); container = rng.startContainer; offset = rng.startOffset; node = container; if (container.nodeType === 3) { if (offset !== container.nodeValue.length) { hasContentAfter = true; } node = node.parentNode; } while (node) { if (MatchFormat.matchNode(editor, node, name, vars, similar)) { formatNode = node; break; } if (node.nextSibling) { hasContentAfter = true; } parents.push(node); node = node.parentNode; } // Node doesn't have the specified format if (!formatNode) { return; } // Is there contents after the caret then remove the format on the element if (hasContentAfter) { bookmark = selection.getBookmark(); // Collapse bookmark range (WebKit) rng.collapse(true); // Expand the range to the closest word and split it at those points rng = ExpandRange.expandRng(editor, rng, editor.formatter.get(name), true); rng = new RangeUtils(dom).split(rng); editor.formatter.remove(name, vars, rng); selection.moveToBookmark(bookmark); } else { caretContainer = getParentCaretContainer(formatNode); var newCaretContainer = createCaretContainer(dom, false); var caretNode = insertFormatNodesIntoCaretContainer(parents, newCaretContainer); if (caretContainer) { insertCaretContainerNode(editor, newCaretContainer, caretContainer); } else { insertCaretContainerNode(editor, newCaretContainer, formatNode); } removeCaretContainerNode(dom, selection, caretContainer, false); selection.setCursorLocation(caretNode, 1); if (dom.isEmpty(formatNode)) { dom.remove(formatNode); } } }; var bindEvents = function (editor) { var dom = editor.dom, selection = editor.selection; if (!editor._hasCaretEvents) { var markCaretContainersBogus, disableCaretContainer; editor.on('BeforeGetContent', function (e) { if (markCaretContainersBogus && e.format !== 'raw') { markCaretContainersBogus(); } }); editor.on('mouseup keydown', function (e) { if (disableCaretContainer) { disableCaretContainer(e); } }); // 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(dom, selection, null, false); // Remove caret container if it's empty if (keyCode === 8 && selection.isCollapsed() && selection.getStart().innerHTML === ZWSP) { removeCaretContainer(dom, selection, getParentCaretContainer(selection.getStart())); } // Remove caret container on keydown and it's left/right arrow keys if (keyCode === 37 || keyCode === 39) { removeCaretContainer(dom, selection, getParentCaretContainer(selection.getStart())); } unmarkBogusCaretParents(dom, selection); }; // Remove bogus state if they got filled by contents using editor.selection.setContent editor.on('SetContent', function (e) { if (e.selection) { unmarkBogusCaretParents(dom, selection); } }); editor._hasCaretEvents = true; } }; return { applyCaretFormat: applyCaretFormat, removeCaretFormat: removeCaretFormat, isCaretNode: isCaretNode }; } ); /** * Hooks.js * * Released under LGPL License. * Copyright (c) 1999-2017 Ephox Corp. All rights reserved * * License: http://www.tinymce.com/license * Contributing: http://www.tinymce.com/contributing */ /** * Internal class for overriding formatting. * * @private * @class tinymce.fmt.Hooks */ define( 'tinymce.core.fmt.Hooks', [ "tinymce.core.util.Arr", "tinymce.core.dom.NodeType", "tinymce.core.dom.DomQuery" ], function (Arr, NodeType, $) { var postProcessHooks = {}, filter = Arr.filter, each = Arr.each; function addPostProcessHook(name, hook) { var hooks = postProcessHooks[name]; if (!hooks) { postProcessHooks[name] = hooks = []; } postProcessHooks[name].push(hook); } function postProcess(name, editor) { each(postProcessHooks[name], function (hook) { hook(editor); }); } addPostProcessHook("pre", function (editor) { var rng = editor.selection.getRng(), isPre, blocks; function hasPreSibling(pre) { return isPre(pre.previousSibling) && Arr.indexOf(blocks, pre.previousSibling) !== -1; } function joinPre(pre1, pre2) { $(pre2).remove(); $(pre1).append(' bug on IE 8 #6178
DOMUtils.DOM.setHTML(elm, html);
}
};
return funcs;
}
);
/**
* Class.js
*
* Released under LGPL License.
* Copyright (c) 1999-2017 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.core.util.Class',
[
"tinymce.core.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;
}
);
/**
* EventDispatcher.js
*
* Released under LGPL License.
* Copyright (c) 1999-2017 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.core.util.EventDispatcher',
[
"tinymce.core.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;
}
);
/**
* Binding.js
*
* Released under LGPL License.
* Copyright (c) 1999-2017 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.core.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;
}
);
/**
* Observable.js
*
* Released under LGPL License.
* Copyright (c) 1999-2017 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.core.util.Observable',
[
"tinymce.core.util.EventDispatcher"
],
function (EventDispatcher) {
function getEventDispatcher(obj) {
if (!obj._eventDispatcher) {
obj._eventDispatcher = new EventDispatcher({
scope: obj,
toggleEvent: function (name, state) {
if (EventDispatcher.isNative(name) && obj.toggleNativeEvent) {
obj.toggleNativeEvent(name, state);
}
}
});
}
return obj._eventDispatcher;
}
return {
/**
* Fires the specified event by name. Consult the
* event reference for more details on each event.
*
* @method fire
* @param {String} name Name of the event to fire.
* @param {Object?} args Event arguments.
* @param {Boolean?} bubble True/false if the event is to be bubbled.
* @return {Object} Event args instance passed in.
* @example
* instance.fire('event', {...});
*/
fire: function (name, args, bubble) {
var self = this;
// Prevent all events except the remove event after the instance has been removed
if (self.removed && name !== "remove") {
return args;
}
args = getEventDispatcher(self).fire(name, args, bubble);
// Bubble event up to parents
if (bubble !== false && self.parent) {
var parent = self.parent();
while (parent && !args.isPropagationStopped()) {
parent.fire(name, args, false);
parent = parent.parent();
}
}
return args;
},
/**
* Binds an event listener to a specific event by name. Consult the
* event reference for more details on each event.
*
* @method on
* @param {String} name Event name or space separated list of events to bind.
* @param {callback} callback Callback to be executed when the event occurs.
* @param {Boolean} first Optional flag if the event should be prepended. Use this with care.
* @return {Object} Current class instance.
* @example
* instance.on('event', function(e) {
* // Callback logic
* });
*/
on: function (name, callback, prepend) {
return getEventDispatcher(this).on(name, callback, prepend);
},
/**
* Unbinds an event listener to a specific event by name. Consult the
* event reference for more details on each event.
*
* @method off
* @param {String?} name Name of the event to unbind.
* @param {callback?} callback Callback to unbind.
* @return {Object} Current class instance.
* @example
* // Unbind specific callback
* instance.off('event', handler);
*
* // Unbind all listeners by name
* instance.off('event');
*
* // Unbind all events
* instance.off();
*/
off: function (name, callback) {
return getEventDispatcher(this).off(name, callback);
},
/**
* Bind the event callback and once it fires the callback is removed. Consult the
* event reference for more details on each event.
*
* @method once
* @param {String} name Name of the event to bind.
* @param {callback} callback Callback to bind only once.
* @return {Object} Current class instance.
*/
once: function (name, callback) {
return getEventDispatcher(this).once(name, callback);
},
/**
* Returns true/false if the object has a event of the specified name.
*
* @method hasEventListeners
* @param {String} name Name of the event to check for.
* @return {Boolean} true/false if the event exists or not.
*/
hasEventListeners: function (name) {
return getEventDispatcher(this).has(name);
}
};
}
);
/**
* ObservableObject.js
*
* Released under LGPL License.
* Copyright (c) 1999-2017 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.core.data.ObservableObject',
[
'tinymce.core.data.Binding',
'tinymce.core.util.Class',
'tinymce.core.util.Observable',
'tinymce.core.util.Tools'
], function (Binding, Class, Observable, 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');
}
});
}
);
/**
* Selector.js
*
* Released under LGPL License.
* Copyright (c) 1999-2017 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: | | a b
' + (rng.item ? rng.item(0).outerHTML : rng.htmlText);
tmpElm.removeChild(tmpElm.firstChild);
} else {
tmpElm.innerHTML = rng.toString();
}
// Keep whitespace before and after
if (/^\s/.test(tmpElm.innerHTML)) {
whiteSpaceBefore = ' ';
}
if (/\s+$/.test(tmpElm.innerHTML)) {
whiteSpaceAfter = ' ';
}
args.getInner = true;
args.content = self.isCollapsed() ? '' : whiteSpaceBefore + self.serializer.serialize(tmpElm, args) + whiteSpaceAfter;
self.editor.fire('GetContent', args);
return args.content;
},
/**
* Sets the current selection to the specified content. If any contents is selected it will be replaced
* with the contents passed in to this function. If there is no selection the contents will be inserted
* where the caret is placed in the editor/page.
*
* @method setContent
* @param {String} content HTML contents to set could also be other formats depending on settings.
* @param {Object} args Optional settings object with for example data format.
* @example
* // Inserts some HTML contents at the current selection
* tinymce.activeEditor.selection.setContent('Some contents');
*/
setContent: function (content, args) {
var self = this, rng = self.getRng(), caretNode, doc = self.win.document, frag, temp;
args = args || { format: 'html' };
args.set = true;
args.selection = true;
args.content = content;
// Dispatch before set content event
if (!args.no_events) {
self.editor.fire('BeforeSetContent', args);
}
content = args.content;
if (rng.insertNode) {
// Make caret marker since insertNode places the caret in the beginning of text after insert
content += '_';
// Delete and insert new node
if (rng.startContainer == doc && rng.endContainer == doc) {
// WebKit will fail if the body is empty since the range is then invalid and it can't insert contents
doc.body.innerHTML = content;
} else {
rng.deleteContents();
if (doc.body.childNodes.length === 0) {
doc.body.innerHTML = content;
} else {
// createContextualFragment doesn't exists in IE 9 DOMRanges
if (rng.createContextualFragment) {
rng.insertNode(rng.createContextualFragment(content));
} else {
// Fake createContextualFragment call in IE 9
frag = doc.createDocumentFragment();
temp = doc.createElement('div');
frag.appendChild(temp);
temp.outerHTML = content;
rng.insertNode(frag);
}
}
}
// Move to caret marker
caretNode = self.dom.get('__caret');
// Make sure we wrap it compleatly, Opera fails with a simple select call
rng = doc.createRange();
rng.setStartBefore(caretNode);
rng.setEndBefore(caretNode);
self.setRng(rng);
// Remove the caret position
self.dom.remove('__caret');
try {
self.setRng(rng);
} catch (ex) {
// Might fail on Opera for some odd reason
}
} else {
if (rng.item) {
// Delete content and get caret text selection
doc.execCommand('Delete', false, null);
rng = self.getRng();
}
// Explorer removes spaces from the beginning of pasted contents
if (/^\s+/.test(content)) {
rng.pasteHTML('_' + content);
self.dom.remove('__mce_tmp');
} else {
rng.pasteHTML(content);
}
}
// Dispatch set content event
if (!args.no_events) {
self.editor.fire('SetContent', args);
}
},
/**
* Returns the start element of a selection range. If the start is in a text
* node the parent element will be returned.
*
* @method getStart
* @param {Boolean} real Optional state to get the real parent when the selection is collapsed not the closest element.
* @return {Element} Start element of selection range.
*/
getStart: function (real) {
var self = this, rng = self.getRng(), startElement, parentElement, checkRng, node;
if (rng.duplicate || rng.item) {
// Control selection, return first item
if (rng.item) {
return rng.item(0);
}
// Get start element
checkRng = rng.duplicate();
checkRng.collapse(1);
startElement = checkRng.parentElement();
if (startElement.ownerDocument !== self.dom.doc) {
startElement = self.dom.getRoot();
}
// Check if range parent is inside the start element, then return the inner parent element
// This will fix issues when a single element is selected, IE would otherwise return the wrong start element
parentElement = node = rng.parentElement();
while ((node = node.parentNode)) {
if (node == startElement) {
startElement = parentElement;
break;
}
}
return startElement;
}
startElement = rng.startContainer;
if (startElement.nodeType == 1 && startElement.hasChildNodes()) {
if (!real || !rng.collapsed) {
startElement = startElement.childNodes[Math.min(startElement.childNodes.length - 1, rng.startOffset)];
}
}
if (startElement && startElement.nodeType == 3) {
return startElement.parentNode;
}
return startElement;
},
/**
* Returns the end element of a selection range. If the end is in a text
* node the parent element will be returned.
*
* @method getEnd
* @param {Boolean} real Optional state to get the real parent when the selection is collapsed not the closest element.
* @return {Element} End element of selection range.
*/
getEnd: function (real) {
var self = this, rng = self.getRng(), endElement, endOffset;
if (rng.duplicate || rng.item) {
if (rng.item) {
return rng.item(0);
}
rng = rng.duplicate();
rng.collapse(0);
endElement = rng.parentElement();
if (endElement.ownerDocument !== self.dom.doc) {
endElement = self.dom.getRoot();
}
if (endElement && endElement.nodeName == 'BODY') {
return endElement.lastChild || endElement;
}
return endElement;
}
endElement = rng.endContainer;
endOffset = rng.endOffset;
if (endElement.nodeType == 1 && endElement.hasChildNodes()) {
if (!real || !rng.collapsed) {
endElement = endElement.childNodes[endOffset > 0 ? endOffset - 1 : endOffset];
}
}
if (endElement && endElement.nodeType == 3) {
return endElement.parentNode;
}
return endElement;
},
/**
* 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);
*/
getBookmark: function (type, normalized) {
return this.bookmarkManager.getBookmark(type, normalized);
},
/**
* 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);
*/
moveToBookmark: function (bookmark) {
return this.bookmarkManager.moveToBookmark(bookmark);
},
/**
* Selects the specified element. This will place the start and end of the selection range around the element.
*
* @method select
* @param {Element} node HTML DOM element to select.
* @param {Boolean} content Optional bool state if the contents should be selected or not on non IE browser.
* @return {Element} Selected element the same element as the one that got passed in.
* @example
* // Select the first paragraph in the active editor
* tinymce.activeEditor.selection.select(tinymce.activeEditor.dom.select('p')[0]);
*/
select: function (node, content) {
var self = this, dom = self.dom, rng = dom.createRng(), idx;
// Clear stored range set by FocusManager
self.lastFocusBookmark = null;
if (node) {
if (!content && self.controlSelection.controlSelect(node)) {
return;
}
idx = dom.nodeIndex(node);
rng.setStart(node.parentNode, idx);
rng.setEnd(node.parentNode, idx + 1);
// Find first/last text node or BR element
if (content) {
self._moveEndPoint(rng, node, true);
self._moveEndPoint(rng, node);
}
self.setRng(rng);
}
return node;
},
/**
* Returns true/false if the selection range is collapsed or not. Collapsed means if it's a caret or a larger selection.
*
* @method isCollapsed
* @return {Boolean} true/false state if the selection range is collapsed or not.
* Collapsed means if it's a caret or a larger selection.
*/
isCollapsed: function () {
var self = this, rng = self.getRng(), sel = self.getSel();
if (!rng || rng.item) {
return false;
}
if (rng.compareEndPoints) {
return rng.compareEndPoints('StartToEnd', rng) === 0;
}
return !sel || rng.collapsed;
},
/**
* Collapse the selection to start or end of range.
*
* @method collapse
* @param {Boolean} toStart Optional boolean state if to collapse to end or not. Defaults to false.
*/
collapse: function (toStart) {
var self = this, rng = self.getRng(), node;
// Control range on IE
if (rng.item) {
node = rng.item(0);
rng = self.win.document.body.createTextRange();
rng.moveToElementText(node);
}
rng.collapse(!!toStart);
self.setRng(rng);
},
/**
* Returns the browsers internal selection object.
*
* @method getSel
* @return {Selection} Internal browser selection object.
*/
getSel: function () {
var win = this.win;
return win.getSelection ? win.getSelection() : win.document.selection;
},
/**
* Returns the browsers internal range object.
*
* @method getRng
* @param {Boolean} w3c Forces a compatible W3C range on IE.
* @return {Range} Internal browser range object.
* @see http://www.quirksmode.org/dom/range_intro.html
* @see http://www.dotvoid.com/2001/03/using-the-range-object-in-mozilla/
*/
getRng: function (w3c) {
var self = this, selection, rng, elm, doc, ieRng;
function tryCompareBoundaryPoints(how, sourceRange, destinationRange) {
try {
return sourceRange.compareBoundaryPoints(how, destinationRange);
} catch (ex) {
// Gecko throws wrong document exception if the range points
// to nodes that where removed from the dom #6690
// Browsers should mutate existing DOMRange instances so that they always point
// to something in the document this is not the case in Gecko works fine in IE/WebKit/Blink
// For performance reasons just return -1
return -1;
}
}
if (!self.win) {
return null;
}
doc = self.win.document;
if (typeof doc === 'undefined' || doc === null) {
return null;
}
// Use last rng passed from FocusManager if it's available this enables
// calls to editor.selection.getStart() to work when caret focus is lost on IE
if (!w3c && self.lastFocusBookmark) {
var bookmark = self.lastFocusBookmark;
// Convert bookmark to range IE 11 fix
if (bookmark.startContainer) {
rng = doc.createRange();
rng.setStart(bookmark.startContainer, bookmark.startOffset);
rng.setEnd(bookmark.endContainer, bookmark.endOffset);
} else {
rng = bookmark;
}
return rng;
}
// Found tridentSel object then we need to use that one
if (w3c && self.tridentSel) {
return self.tridentSel.getRangeAt(0);
}
try {
if ((selection = self.getSel())) {
if (selection.rangeCount > 0) {
rng = selection.getRangeAt(0);
} else {
rng = selection.createRange ? selection.createRange() : doc.createRange();
}
}
} catch (ex) {
// IE throws unspecified error here if TinyMCE is placed in a frame/iframe
}
rng = eventProcessRanges(self.editor, [ rng ])[0];
// We have W3C ranges and it's IE then fake control selection since IE9 doesn't handle that correctly yet
// IE 11 doesn't support the selection object so we check for that as well
if (isIE && rng && rng.setStart && doc.selection) {
try {
// IE will sometimes throw an exception here
ieRng = doc.selection.createRange();
} catch (ex) {
// Ignore
}
if (ieRng && ieRng.item) {
elm = ieRng.item(0);
rng = doc.createRange();
rng.setStartBefore(elm);
rng.setEndAfter(elm);
}
}
// No range found then create an empty one
// This can occur when the editor is placed in a hidden container element on Gecko
// Or on IE when there was an exception
if (!rng) {
rng = doc.createRange ? doc.createRange() : doc.body.createTextRange();
}
// If range is at start of document then move it to start of body
if (rng.setStart && rng.startContainer.nodeType === 9 && rng.collapsed) {
elm = self.dom.getRoot();
rng.setStart(elm, 0);
rng.setEnd(elm, 0);
}
if (self.selectedRange && self.explicitRange) {
if (tryCompareBoundaryPoints(rng.START_TO_START, rng, self.selectedRange) === 0 &&
tryCompareBoundaryPoints(rng.END_TO_END, rng, self.selectedRange) === 0) {
// Safari, Opera and Chrome only ever select text which causes the range to change.
// This lets us use the originally set range if the selection hasn't been changed by the user.
rng = self.explicitRange;
} else {
self.selectedRange = null;
self.explicitRange = null;
}
}
return rng;
},
/**
* Changes the selection to the specified DOM range.
*
* @method setRng
* @param {Range} rng Range to select.
* @param {Boolean} forward Optional boolean if the selection is forwards or backwards.
*/
setRng: function (rng, forward) {
var self = this, sel, node, evt;
if (!isValidRange(rng)) {
return;
}
// Is IE specific range
if (rng.select) {
self.explicitRange = null;
try {
rng.select();
} catch (ex) {
// Needed for some odd IE bug #1843306
}
return;
}
if (!self.tridentSel) {
sel = self.getSel();
evt = self.editor.fire('SetSelectionRange', { range: rng, forward: forward });
rng = evt.range;
if (sel) {
self.explicitRange = rng;
try {
sel.removeAllRanges();
sel.addRange(rng);
} catch (ex) {
// IE might throw errors here if the editor is within a hidden container and selection is changed
}
// Forward is set to false and we have an extend function
if (forward === false && sel.extend) {
sel.collapse(rng.endContainer, rng.endOffset);
sel.extend(rng.startContainer, rng.startOffset);
}
// adding range isn't always successful so we need to check range count otherwise an exception can occur
self.selectedRange = sel.rangeCount > 0 ? sel.getRangeAt(0) : null;
}
// WebKit egde case selecting images works better using setBaseAndExtent when the image is floated
if (!rng.collapsed && rng.startContainer === rng.endContainer && sel.setBaseAndExtent && !Env.ie) {
if (rng.endOffset - rng.startOffset < 2) {
if (rng.startContainer.hasChildNodes()) {
node = rng.startContainer.childNodes[rng.startOffset];
if (node && node.tagName === 'IMG') {
sel.setBaseAndExtent(
rng.startContainer,
rng.startOffset,
rng.endContainer,
rng.endOffset
);
// Since the setBaseAndExtent is fixed in more recent Blink versions we
// need to detect if it's doing the wrong thing and falling back to the
// crazy incorrect behavior api call since that seems to be the only way
// to get it to work on Safari WebKit as of 2017-02-23
if (sel.anchorNode !== rng.startContainer || sel.focusNode !== rng.endContainer) {
sel.setBaseAndExtent(node, 0, node, 1);
}
}
}
}
}
self.editor.fire('AfterSetSelectionRange', { range: rng, forward: forward });
} else {
// Is W3C Range fake range on IE
if (rng.cloneRange) {
try {
self.tridentSel.addRange(rng);
} catch (ex) {
//IE9 throws an error here if called before selection is placed in the editor
}
}
}
},
/**
* Sets the current selection to the specified DOM element.
*
* @method setNode
* @param {Element} elm Element to set as the contents of the selection.
* @return {Element} Returns the element that got passed in.
* @example
* // Inserts a DOM node at current selection/caret location
* tinymce.activeEditor.selection.setNode(tinymce.activeEditor.dom.create('img', {src: 'some.gif', title: 'some title'}));
*/
setNode: function (elm) {
var self = this;
self.setContent(self.dom.getOuterHTML(elm));
return elm;
},
/**
* Returns the currently selected element or the common ancestor element for both start and end of the selection.
*
* @method getNode
* @return {Element} Currently selected element or common ancestor element.
* @example
* // Alerts the currently selected elements node name
* alert(tinymce.activeEditor.selection.getNode().nodeName);
*/
getNode: function () {
var self = this, rng = self.getRng(), elm;
var startContainer, endContainer, startOffset, endOffset, root = self.dom.getRoot();
function skipEmptyTextNodes(node, forwards) {
var orig = node;
while (node && node.nodeType === 3 && node.length === 0) {
node = forwards ? node.nextSibling : node.previousSibling;
}
return node || orig;
}
// Range maybe lost after the editor is made visible again
if (!rng) {
return root;
}
startContainer = rng.startContainer;
endContainer = rng.endContainer;
startOffset = rng.startOffset;
endOffset = rng.endOffset;
if (rng.setStart) {
elm = rng.commonAncestorContainer;
// Handle selection a image or other control like element such as anchors
if (!rng.collapsed) {
if (startContainer == endContainer) {
if (endOffset - startOffset < 2) {
if (startContainer.hasChildNodes()) {
elm = startContainer.childNodes[startOffset];
}
}
}
// If the anchor node is a element instead of a text node then return this element
//if (tinymce.isWebKit && sel.anchorNode && sel.anchorNode.nodeType == 1)
// return sel.anchorNode.childNodes[sel.anchorOffset];
// Handle cases where the selection is immediately wrapped around a node and return that node instead of it's parent.
// This happens when you double click an underlined word in FireFox.
if (startContainer.nodeType === 3 && endContainer.nodeType === 3) {
if (startContainer.length === startOffset) {
startContainer = skipEmptyTextNodes(startContainer.nextSibling, true);
} else {
startContainer = startContainer.parentNode;
}
if (endOffset === 0) {
endContainer = skipEmptyTextNodes(endContainer.previousSibling, false);
} else {
endContainer = endContainer.parentNode;
}
if (startContainer && startContainer === endContainer) {
return startContainer;
}
}
}
if (elm && elm.nodeType == 3) {
return elm.parentNode;
}
return elm;
}
elm = rng.item ? rng.item(0) : rng.parentElement();
// IE 7 might return elements outside the iframe
if (elm.ownerDocument !== self.win.document) {
elm = root;
}
return elm;
},
getSelectedBlocks: function (startElm, endElm) {
var self = this, dom = self.dom, node, root, selectedBlocks = [];
root = dom.getRoot();
startElm = dom.getParent(startElm || self.getStart(), dom.isBlock);
endElm = dom.getParent(endElm || self.getEnd(), dom.isBlock);
if (startElm && startElm != root) {
selectedBlocks.push(startElm);
}
if (startElm && endElm && startElm != endElm) {
node = startElm;
var walker = new TreeWalker(startElm, root);
while ((node = walker.next()) && node != endElm) {
if (dom.isBlock(node)) {
selectedBlocks.push(node);
}
}
}
if (endElm && startElm != endElm && endElm != root) {
selectedBlocks.push(endElm);
}
return selectedBlocks;
},
isForward: function () {
var dom = this.dom, sel = this.getSel(), anchorRange, focusRange;
// No support for selection direction then always return true
if (!sel || !sel.anchorNode || !sel.focusNode) {
return true;
}
anchorRange = dom.createRng();
anchorRange.setStart(sel.anchorNode, sel.anchorOffset);
anchorRange.collapse(true);
focusRange = dom.createRng();
focusRange.setStart(sel.focusNode, sel.focusOffset);
focusRange.collapse(true);
return anchorRange.compareBoundaryPoints(anchorRange.START_TO_START, focusRange) <= 0;
},
normalize: function () {
var self = this, rng = self.getRng();
if (new RangeUtils(self.dom).normalize(rng) && !MultiRange.hasMultipleRanges(self.getSel())) {
self.setRng(rng, self.isForward());
}
return rng;
},
/**
* Executes callback when the current selection starts/stops matching the specified selector. The current
* state will be passed to the callback as it's first argument.
*
* @method selectorChanged
* @param {String} selector CSS selector to check for.
* @param {function} callback Callback with state and args when the selector is matches or not.
*/
selectorChanged: function (selector, callback) {
var self = this, currentSelectors;
if (!self.selectorChangedData) {
self.selectorChangedData = {};
currentSelectors = {};
self.editor.on('NodeChange', function (e) {
var node = e.element, dom = self.dom, parents = dom.getParents(node, null, dom.getRoot()), matchedSelectors = {};
// Check for new matching selectors
each(self.selectorChangedData, function (callbacks, selector) {
each(parents, function (node) {
if (dom.is(node, selector)) {
if (!currentSelectors[selector]) {
// Execute callbacks
each(callbacks, function (callback) {
callback(true, { node: node, selector: selector, parents: parents });
});
currentSelectors[selector] = callbacks;
}
matchedSelectors[selector] = callbacks;
return false;
}
});
});
// Check if current selectors still match
each(currentSelectors, function (callbacks, selector) {
if (!matchedSelectors[selector]) {
delete currentSelectors[selector];
each(callbacks, function (callback) {
callback(false, { node: node, selector: selector, parents: parents });
});
}
});
});
}
// Add selector listeners
if (!self.selectorChangedData[selector]) {
self.selectorChangedData[selector] = [];
}
self.selectorChangedData[selector].push(callback);
return self;
},
getScrollContainer: function () {
var scrollContainer, node = this.dom.getRoot();
while (node && node.nodeName != 'BODY') {
if (node.scrollHeight > node.clientHeight) {
scrollContainer = node;
break;
}
node = node.parentNode;
}
return scrollContainer;
},
scrollIntoView: function (elm, alignToTop) {
ScrollIntoView.scrollIntoView(this.editor, elm, alignToTop);
},
placeCaretAt: function (clientX, clientY) {
this.setRng(RangeUtils.getCaretRangeFromPoint(clientX, clientY, this.editor.getDoc()));
},
_moveEndPoint: function (rng, node, start) {
var root = node, walker = new TreeWalker(node, root);
var nonEmptyElementsMap = this.dom.schema.getNonEmptyElements();
do {
// Text node
if (node.nodeType == 3 && trim(node.nodeValue).length !== 0) {
if (start) {
rng.setStart(node, 0);
} else {
rng.setEnd(node, node.nodeValue.length);
}
return;
}
// BR/IMG/INPUT elements but not table cells
if (nonEmptyElementsMap[node.nodeName] && !/^(TD|TH)$/.test(node.nodeName)) {
if (start) {
rng.setStartBefore(node);
} else {
if (node.nodeName == 'BR') {
rng.setEndBefore(node);
} else {
rng.setEndAfter(node);
}
}
return;
}
// Found empty text block old IE can place the selection inside those
if (Env.ie && Env.ie < 11 && this.dom.isBlock(node) && this.dom.isEmpty(node)) {
if (start) {
rng.setStart(node, 0);
} else {
rng.setEnd(node, 0);
}
return;
}
} while ((node = (start ? walker.next() : walker.prev())));
// Failed to find any text node or other suitable location then move to the root of body
if (root.nodeName == 'BODY') {
if (start) {
rng.setStart(root, 0);
} else {
rng.setEnd(root, root.childNodes.length);
}
}
},
getBoundingClientRect: function () {
var rng = this.getRng();
return rng.collapsed ? CaretPosition.fromRangeStart(rng).getClientRects()[0] : rng.getBoundingClientRect();
},
destroy: function () {
this.win = null;
this.controlSelection.destroy();
}
};
return Selection;
}
);
/**
* Node.js
*
* Released under LGPL License.
* Copyright (c) 1999-2017 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.core.html.Node',
[
],
function () {
var whiteSpaceRegExp = /^[ \t\r\n]*$/;
var typeLookup = {
'#text': 3,
'#comment': 8,
'#cdata': 4,
'#pi': 7,
'#doctype': 10,
'#document-fragment': 11
};
// Walks the tree left/right
function walk(node, rootNode, 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 !== rootNode) {
sibling = node[siblingName];
if (sibling) {
return sibling;
}
// Walk up the parents to look for siblings
for (parent = node.parent; parent && parent !== rootNode; 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} refNode 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, refNode, before) {
var parent;
if (node.parent) {
node.remove();
}
parent = refNode.parent || this;
if (before) {
if (refNode === parent.firstChild) {
parent.firstChild = node;
} else {
refNode.prev.next = node;
}
node.prev = refNode.prev;
node.next = refNode;
refNode.prev = node;
} else {
if (refNode === parent.lastChild) {
parent.lastChild = node;
} else {
refNode.next.prev = node;
}
node.next = refNode.next;
node.prev = refNode;
refNode.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.
* @param {Object} whitespace Name/value object with elements that are automatically treated whitespace preservables.
* @param {function} predicate Optional predicate that gets called after the other rules determine that the node is empty. Should return true if the node is a content node.
* @return {Boolean} true/false if the node is empty or not.
*/
isEmpty: function (elements, whitespace, predicate) {
var self = this, node = self.firstChild, i, name;
whitespace = whitespace || {};
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;
}
// Keep whitespace preserve elements
if (node.type === 3 && node.parent && whitespace[node.parent.name] && whiteSpaceRegExp.test(node.value)) {
return false;
}
// Predicate tells that the node is contents
if (predicate && predicate(node)) {
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;
}
);
/**
* SaxParser.js
*
* Released under LGPL License.
* Copyright (c) 1999-2017 Ephox Corp. All rights reserved
*
* License: http://www.tinymce.com/license
* Contributing: http://www.tinymce.com/contributing
*/
/*eslint max-depth:[2, 9] */
/**
* This class parses HTML code using pure JavaScript and executes various events for each item it finds. It will
* always execute the events in the right order for tag soup code like . It will also remove elements
* and attributes that doesn't fit the schema if the validate setting is enabled.
*
* @example
* var parser = new tinymce.html.SaxParser({
* validate: true,
*
* comment: function(text) {
* console.log('Comment:', text);
* },
*
* cdata: function(text) {
* console.log('CDATA:', text);
* },
*
* text: function(text, raw) {
* console.log('Text:', text, 'Raw:', raw);
* },
*
* start: function(name, attrs, empty) {
* console.log('Start:', name, attrs, empty);
* },
*
* end: function(name) {
* console.log('End:', name);
* },
*
* pi: function(name, text) {
* console.log('PI:', name, text);
* },
*
* doctype: function(text) {
* console.log('DocType:', text);
* }
* }, schema);
* @class tinymce.html.SaxParser
* @version 3.4
*/
define(
'tinymce.core.html.SaxParser',
[
"tinymce.core.html.Schema",
"tinymce.core.html.Entities",
"tinymce.core.util.Tools"
],
function (Schema, Entities, Tools) {
var each = Tools.each;
var isValidPrefixAttrName = function (name) {
return name.indexOf('data-') === 0 || name.indexOf('aria-') === 0;
};
var trimComments = function (text) {
return text.replace(//g, '');
};
/**
* Returns the index of the end tag for a specific start tag. This can be
* used to skip all children of a parent element from being processed.
*
* @private
* @method findEndTag
* @param {tinymce.html.Schema} schema Schema instance to use to match short ended elements.
* @param {String} html HTML string to find the end tag in.
* @param {Number} startIndex Indext to start searching at should be after the start tag.
* @return {Number} Index of the end tag.
*/
function findEndTag(schema, html, startIndex) {
var count = 1, index, matches, tokenRegExp, shortEndedElements;
shortEndedElements = schema.getShortEndedElements();
tokenRegExp = /<([!?\/])?([A-Za-z0-9\-_\:\.]+)((?:\s+[^"\'>]+(?:(?:"[^"]*")|(?:\'[^\']*\')|[^>]*))*|\/|\s+)>/g;
tokenRegExp.lastIndex = index = startIndex;
while ((matches = tokenRegExp.exec(html))) {
index = tokenRegExp.lastIndex;
if (matches[1] === '/') { // End element
count--;
} else if (!matches[1]) { // Start element
if (matches[2] in shortEndedElements) {
continue;
}
count++;
}
if (count === 0) {
break;
}
}
return index;
}
/**
* Constructs a new SaxParser instance.
*
* @constructor
* @method SaxParser
* @param {Object} settings Name/value collection of settings. comment, cdata, text, start and end are callbacks.
* @param {tinymce.html.Schema} schema HTML Schema class to use when parsing.
*/
function SaxParser(settings, schema) {
var self = this;
function noop() { }
settings = settings || {};
self.schema = schema = schema || new Schema();
if (settings.fix_self_closing !== false) {
settings.fix_self_closing = true;
}
// Add handler functions from settings and setup default handlers
each('comment cdata text start end pi doctype'.split(' '), function (name) {
if (name) {
self[name] = settings[name] || noop;
}
});
/**
* Parses the specified HTML string and executes the callbacks for each item it finds.
*
* @example
* new SaxParser({...}).parse('text');
* @method parse
* @param {String} html Html string to sax parse.
*/
self.parse = function (html) {
var self = this, matches, index = 0, value, endRegExp, stack = [], attrList, i, text, name;
var isInternalElement, removeInternalElements, shortEndedElements, fillAttrsMap, isShortEnded;
var validate, elementRule, isValidElement, attr, attribsValue, validAttributesMap, validAttributePatterns;
var attributesRequired, attributesDefault, attributesForced, processHtml;
var anyAttributesRequired, selfClosing, tokenRegExp, attrRegExp, specialElements, attrValue, idCount = 0;
var decode = Entities.decode, fixSelfClosing, filteredUrlAttrs = Tools.makeMap('src,href,data,background,formaction,poster');
var scriptUriRegExp = /((java|vb)script|mhtml):/i, dataUriRegExp = /^data:/i;
function processEndTag(name) {
var pos, i;
// Find position of parent of the same type
pos = stack.length;
while (pos--) {
if (stack[pos].name === name) {
break;
}
}
// Found parent
if (pos >= 0) {
// Close all the open elements
for (i = stack.length - 1; i >= pos; i--) {
name = stack[i];
if (name.valid) {
self.end(name.name);
}
}
// Remove the open elements from the stack
stack.length = pos;
}
}
function parseAttribute(match, name, value, val2, val3) {
var attrRule, i, trimRegExp = /[\s\u0000-\u001F]+/g;
name = name.toLowerCase();
value = name in fillAttrsMap ? name : decode(value || val2 || val3 || ''); // Handle boolean attribute than value attribute
// Validate name and value pass through all data- attributes
if (validate && !isInternalElement && isValidPrefixAttrName(name) === false) {
attrRule = validAttributesMap[name];
// Find rule by pattern matching
if (!attrRule && validAttributePatterns) {
i = validAttributePatterns.length;
while (i--) {
attrRule = validAttributePatterns[i];
if (attrRule.pattern.test(name)) {
break;
}
}
// No rule matched
if (i === -1) {
attrRule = null;
}
}
// No attribute rule found
if (!attrRule) {
return;
}
// Validate value
if (attrRule.validValues && !(value in attrRule.validValues)) {
return;
}
}
// Block any javascript: urls or non image data uris
if (filteredUrlAttrs[name] && !settings.allow_script_urls) {
var uri = value.replace(trimRegExp, '');
try {
// Might throw malformed URI sequence
uri = decodeURIComponent(uri);
} catch (ex) {
// Fallback to non UTF-8 decoder
uri = unescape(uri);
}
if (scriptUriRegExp.test(uri)) {
return;
}
if (!settings.allow_html_data_urls && dataUriRegExp.test(uri) && !/^data:image\//i.test(uri)) {
return;
}
}
// Block data or event attributes on elements marked as internal
if (isInternalElement && (name in filteredUrlAttrs || name.indexOf('on') === 0)) {
return;
}
// Add attribute to list and map
attrList.map[name] = value;
attrList.push({
name: name,
value: value
});
}
// Precompile RegExps and map objects
tokenRegExp = new RegExp('<(?:' +
'(?:!--([\\w\\W]*?)-->)|' + // Comment
'(?:!\\[CDATA\\[([\\w\\W]*?)\\]\\]>)|' + // CDATA
'(?:!DOCTYPE([\\w\\W]*?)>)|' + // DOCTYPE
'(?:\\?([^\\s\\/<>]+) ?([\\w\\W]*?)[?/]>)|' + // PI
'(?:\\/([A-Za-z][A-Za-z0-9\\-_\\:\\.]*)>)|' + // End element
'(?:([A-Za-z][A-Za-z0-9\\-_\\:\\.]*)((?:\\s+[^"\'>]+(?:(?:"[^"]*")|(?:\'[^\']*\')|[^>]*))*|\\/|\\s+)>)' + // Start element
')', 'g');
attrRegExp = /([\w:\-]+)(?:\s*=\s*(?:(?:\"((?:[^\"])*)\")|(?:\'((?:[^\'])*)\')|([^>\s]+)))?/g;
// Setup lookup tables for empty elements and boolean attributes
shortEndedElements = schema.getShortEndedElements();
selfClosing = settings.self_closing_elements || schema.getSelfClosingElements();
fillAttrsMap = schema.getBoolAttrs();
validate = settings.validate;
removeInternalElements = settings.remove_internals;
fixSelfClosing = settings.fix_self_closing;
specialElements = schema.getSpecialElements();
processHtml = html + '>';
while ((matches = tokenRegExp.exec(processHtml))) { // Adds and extra '>' to keep regexps from doing catastrofic backtracking on malformed html
// Text
if (index < matches.index) {
self.text(decode(html.substr(index, matches.index - index)));
}
if ((value = matches[6])) { // End element
value = value.toLowerCase();
// IE will add a ":" in front of elements it doesn't understand like custom elements or HTML5 elements
if (value.charAt(0) === ':') {
value = value.substr(1);
}
processEndTag(value);
} else if ((value = matches[7])) { // Start element
// Did we consume the extra character then treat it as text
// This handles the case with html like this: "text a html.length) {
self.text(decode(html.substr(matches.index)));
index = matches.index + matches[0].length;
continue;
}
value = value.toLowerCase();
// IE will add a ":" in front of elements it doesn't understand like custom elements or HTML5 elements
if (value.charAt(0) === ':') {
value = value.substr(1);
}
isShortEnded = value in shortEndedElements;
// Is self closing tag for example an
a
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 && settings.padd_empty_with_br !== true) { textNode = new Node('#text', 3); textNode.value = '\u00a0'; node.replace(textNode); } } } }); } self.addAttributeFilter('href', function (nodes) { var i = nodes.length, node; var appendRel = function (rel) { var parts = rel.split(' ').filter(function (p) { return p.length > 0; }); return parts.concat(['noopener']).sort().join(' '); }; var addNoOpener = function (rel) { var newRel = rel ? Tools.trim(rel) : ''; if (!/\b(noopener)\b/g.test(newRel)) { return appendRel(newRel); } else { return newRel; } }; if (!settings.allow_unsafe_link_target) { while (i--) { node = nodes[i]; if (node.name === 'a' && node.attr('target') === '_blank') { node.attr('rel', addNoOpener(node.attr('rel'))); } } } }); // 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.fix_list_elements) { self.addNodeFilter('ul,ol', function (nodes) { var i = nodes.length, node, parentNode; while (i--) { node = nodes[i]; parentNode = node.parent; if (parentNode.name === 'ul' || parentNode.name === 'ol') { if (node.prev && node.prev.name === 'li') { node.prev.append(node); } else { var li = new Node('li', 1); li.attr('style', 'list-style-type: none'); node.wrap(li); } } } }); } 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); } }); } }; } ); /** * Writer.js * * Released under LGPL License. * Copyright (c) 1999-2017 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.core.html.Serializer', [ "tinymce.core.html.Writer", "tinymce.core.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(); }; }; } ); /** * Serializer.js * * Released under LGPL License. * Copyright (c) 1999-2017 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.core.dom.Serializer', [ "tinymce.core.dom.DOMUtils", "tinymce.core.html.DomParser", "tinymce.core.html.SaxParser", "tinymce.core.html.Entities", "tinymce.core.html.Serializer", "tinymce.core.html.Node", "tinymce.core.html.Schema", "tinymce.core.Env", "tinymce.core.util.Tools", "tinymce.core.text.Zwsp" ], function (DOMUtils, DomParser, SaxParser, Entities, Serializer, Node, Schema, Env, Tools, Zwsp) { var each = Tools.each, trim = Tools.trim; var DOM = DOMUtils.DOM; /** * IE 11 has a fantastic bug where it will produce two trailing BR elements to iframe bodies when * the iframe is hidden by display: none on a parent container. The DOM is actually out of sync * with innerHTML in this case. It's like IE adds shadow DOM BR elements that appears on innerHTML * but not as the lastChild of the body. So this fix simply removes the last two * BR elements at the end of the document. * * Example of what happens: text becomes text|a
when there is only one item left except the zwsp caret container nodes
var hasOnlyTwoOrLessPositionsLeft = function (elm) {
return Options.liftN([
CaretFinder.firstPositionIn(elm),
CaretFinder.lastPositionIn(elm)
], function (firstPos, lastPos) {
var normalizedFirstPos = InlineUtils.normalizePosition(true, firstPos);
var normalizedLastPos = InlineUtils.normalizePosition(false, lastPos);
return CaretFinder.nextPosition(elm, normalizedFirstPos).map(function (pos) {
return pos.isEqual(normalizedLastPos);
}).getOr(true);
}).getOr(true);
};
var setCaretLocation = function (editor, caret) {
return function (location) {
return BoundaryCaret.renderCaret(caret, location).map(function (pos) {
BoundarySelection.setCaretPosition(editor, pos);
return true;
}).getOr(false);
};
};
var deleteFromTo = function (editor, caret, from, to) {
var rootNode = editor.getBody();
var isInlineTarget = Fun.curry(InlineUtils.isInlineTarget, editor);
editor.undoManager.ignore(function () {
editor.selection.setRng(rangeFromPositions(from, to));
editor.execCommand('Delete');
BoundaryLocation.readLocation(isInlineTarget, rootNode, CaretPosition.fromRangeStart(editor.selection.getRng()))
.map(BoundaryLocation.inside)
.map(setCaretLocation(editor, caret));
});
editor.nodeChanged();
};
var rescope = function (rootNode, node) {
var parentBlock = CaretUtils.getParentBlock(node, rootNode);
return parentBlock ? parentBlock : rootNode;
};
var backspaceDeleteCollapsed = function (editor, caret, forward, from) {
var rootNode = rescope(editor.getBody(), from.container());
var isInlineTarget = Fun.curry(InlineUtils.isInlineTarget, editor);
var fromLocation = BoundaryLocation.readLocation(isInlineTarget, rootNode, from);
return fromLocation.bind(function (location) {
if (forward) {
return location.fold(
Fun.constant(Option.some(BoundaryLocation.inside(location))), // Before
Option.none, // Start
Fun.constant(Option.some(BoundaryLocation.outside(location))), // End
Option.none // After
);
} else {
return location.fold(
Option.none, // Before
Fun.constant(Option.some(BoundaryLocation.outside(location))), // Start
Option.none, // End
Fun.constant(Option.some(BoundaryLocation.inside(location))) // After
);
}
})
.map(setCaretLocation(editor, caret))
.getOrThunk(function () {
var toPosition = CaretFinder.navigate(forward, rootNode, from);
var toLocation = toPosition.bind(function (pos) {
return BoundaryLocation.readLocation(isInlineTarget, rootNode, pos);
});
if (fromLocation.isSome() && toLocation.isSome()) {
return InlineUtils.findRootInline(isInlineTarget, rootNode, from).map(function (elm) {
if (hasOnlyTwoOrLessPositionsLeft(elm)) {
DeleteElement.deleteElement(editor, forward, Element.fromDom(elm));
return true;
} else {
return false;
}
}).getOr(false);
} else {
return toLocation.bind(function (_) {
return toPosition.map(function (to) {
if (forward) {
deleteFromTo(editor, caret, from, to);
} else {
deleteFromTo(editor, caret, to, from);
}
return true;
});
}).getOr(false);
}
});
};
var backspaceDelete = function (editor, caret, forward) {
if (editor.selection.isCollapsed() && isFeatureEnabled(editor)) {
var from = CaretPosition.fromRangeStart(editor.selection.getRng());
return backspaceDeleteCollapsed(editor, caret, forward, from);
}
return false;
};
return {
backspaceDelete: backspaceDelete
};
}
);
define(
'tinymce.core.delete.TableDeleteAction',
[
'ephox.katamari.api.Adt',
'ephox.katamari.api.Arr',
'ephox.katamari.api.Fun',
'ephox.katamari.api.Option',
'ephox.katamari.api.Options',
'ephox.katamari.api.Struct',
'ephox.sugar.api.dom.Compare',
'ephox.sugar.api.node.Element',
'ephox.sugar.api.search.SelectorFilter',
'ephox.sugar.api.search.SelectorFind'
],
function (Adt, Arr, Fun, Option, Options, Struct, Compare, Element, SelectorFilter, SelectorFind) {
var tableCellRng = Struct.immutable('start', 'end');
var tableSelection = Struct.immutable('rng', 'table', 'cells');
var deleteAction = Adt.generate([
{ removeTable: [ 'element' ] },
{ emptyCells: [ 'cells' ] }
]);
var getClosestCell = function (container, isRoot) {
return SelectorFind.closest(Element.fromDom(container), 'td,th', isRoot);
};
var getClosestTable = function (cell, isRoot) {
return SelectorFind.ancestor(cell, 'table', isRoot);
};
var isExpandedCellRng = function (cellRng) {
return Compare.eq(cellRng.start(), cellRng.end()) === false;
};
var getTableFromCellRng = function (cellRng, isRoot) {
return getClosestTable(cellRng.start(), isRoot)
.bind(function (startParentTable) {
return getClosestTable(cellRng.end(), isRoot)
.bind(function (endParentTable) {
return Compare.eq(startParentTable, endParentTable) ? Option.some(startParentTable) : Option.none();
});
});
};
var getCellRng = function (rng, isRoot) {
return Options.liftN([ // get start and end cell
getClosestCell(rng.startContainer, isRoot),
getClosestCell(rng.endContainer, isRoot)
], tableCellRng)
.filter(isExpandedCellRng);
};
var getTableSelectionFromCellRng = function (cellRng, isRoot) {
return getTableFromCellRng(cellRng, isRoot)
.bind(function (table) {
var cells = SelectorFilter.descendants(table, 'td,th');
return tableSelection(cellRng, table, cells);
});
};
var getTableSelectionFromRng = function (rootNode, rng) {
var isRoot = Fun.curry(Compare.eq, rootNode);
return getCellRng(rng, isRoot)
.map(function (cellRng) {
return getTableSelectionFromCellRng(cellRng, isRoot);
});
};
var getCellIndex = function (cellArray, cell) {
return Arr.findIndex(cellArray, function (x) {
return Compare.eq(x, cell);
});
};
var getSelectedCells = function (tableSelection) {
return Options.liftN([
getCellIndex(tableSelection.cells(), tableSelection.rng().start()),
getCellIndex(tableSelection.cells(), tableSelection.rng().end())
], function (startIndex, endIndex) {
return tableSelection.cells().slice(startIndex, endIndex + 1);
});
};
var getAction = function (tableSelection) {
return getSelectedCells(tableSelection)
.bind(function (selected) {
var cells = tableSelection.cells();
return selected.length === cells.length ? deleteAction.removeTable(tableSelection.table()) : deleteAction.emptyCells(selected);
});
};
var getActionFromCells = function (cells) {
return deleteAction.emptyCells(cells);
};
var getActionFromRange = function (rootNode, rng) {
return getTableSelectionFromRng(rootNode, rng)
.map(getAction);
};
return {
getActionFromRange: getActionFromRange,
getActionFromCells: getActionFromCells
};
}
);
/**
* TableDelete.js
*
* Released under LGPL License.
* Copyright (c) 1999-2017 Ephox Corp. All rights reserved
*
* License: http://www.tinymce.com/license
* Contributing: http://www.tinymce.com/contributing
*/
define(
'tinymce.core.delete.TableDelete',
[
'ephox.katamari.api.Arr',
'ephox.katamari.api.Fun',
'ephox.sugar.api.node.Element',
'tinymce.core.delete.DeleteElement',
'tinymce.core.delete.TableDeleteAction',
'tinymce.core.dom.PaddingBr',
'tinymce.core.selection.TableCellSelection'
],
function (Arr, Fun, Element, DeleteElement, TableDeleteAction, PaddingBr, TableCellSelection) {
var emptyCells = function (editor, cells) {
Arr.each(cells, PaddingBr.fillWithPaddingBr);
editor.selection.setCursorLocation(cells[0].dom(), 0);
return true;
};
var deleteTableElement = function (editor, table) {
DeleteElement.deleteElement(editor, false, table);
return true;
};
var handleCellRange = function (editor, rootNode, rng) {
return TableDeleteAction.getActionFromRange(rootNode, rng)
.map(function (action) {
return action.fold(
Fun.curry(deleteTableElement, editor),
Fun.curry(emptyCells, editor)
);
}).getOr(false);
};
var deleteRange = function (editor) {
var rootNode = Element.fromDom(editor.getBody());
var rng = editor.selection.getRng();
var selectedCells = TableCellSelection.getCellsFromEditor(editor);
return selectedCells.length !== 0 ? emptyCells(editor, selectedCells) : handleCellRange(editor, rootNode, rng);
};
var backspaceDelete = function (editor) {
return editor.selection.isCollapsed() ? false : deleteRange(editor);
};
return {
backspaceDelete: backspaceDelete
};
}
);
/**
* Commands.js
*
* Released under LGPL License.
* Copyright (c) 1999-2017 Ephox Corp. All rights reserved
*
* License: http://www.tinymce.com/license
* Contributing: http://www.tinymce.com/contributing
*/
define(
'tinymce.core.delete.DeleteCommands',
[
'tinymce.core.delete.BlockBoundaryDelete',
'tinymce.core.delete.BlockRangeDelete',
'tinymce.core.delete.CefDelete',
'tinymce.core.delete.DeleteUtils',
'tinymce.core.delete.InlineBoundaryDelete',
'tinymce.core.delete.TableDelete'
],
function (BlockBoundaryDelete, BlockRangeDelete, CefDelete, DeleteUtils, BoundaryDelete, TableDelete) {
var nativeCommand = function (editor, command) {
editor.getDoc().execCommand(command, false, null);
};
var deleteCommand = function (editor) {
if (CefDelete.backspaceDelete(editor, false)) {
return;
} else if (BoundaryDelete.backspaceDelete(editor, false)) {
return;
} else if (BlockBoundaryDelete.backspaceDelete(editor, false)) {
return;
} else if (TableDelete.backspaceDelete(editor)) {
return;
} else if (BlockRangeDelete.backspaceDelete(editor, false)) {
return;
} else {
nativeCommand(editor, 'Delete');
DeleteUtils.paddEmptyBody(editor);
}
};
var forwardDeleteCommand = function (editor) {
if (CefDelete.backspaceDelete(editor, true)) {
return;
} else if (BoundaryDelete.backspaceDelete(editor, true)) {
return;
} else if (BlockBoundaryDelete.backspaceDelete(editor, true)) {
return;
} else if (TableDelete.backspaceDelete(editor)) {
return;
} else if (BlockRangeDelete.backspaceDelete(editor, true)) {
return;
} else {
nativeCommand(editor, 'ForwardDelete');
}
};
return {
deleteCommand: deleteCommand,
forwardDeleteCommand: forwardDeleteCommand
};
}
);
/**
* InsertList.js
*
* Released under LGPL License.
* Copyright (c) 1999-2017 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.core.InsertList',
[
"tinymce.core.util.Tools",
"tinymce.core.caret.CaretWalker",
"tinymce.core.caret.CaretPosition"
],
function (Tools, CaretWalker, CaretPosition) {
var hasOnlyOneChild = function (node) {
return node.firstChild && node.firstChild === node.lastChild;
};
var isPaddingNode = function (node) {
return node.name === 'br' || node.value === '\u00a0';
};
var isPaddedEmptyBlock = function (schema, node) {
var blockElements = schema.getBlockElements();
return blockElements[node.name] && hasOnlyOneChild(node) && isPaddingNode(node.firstChild);
};
var isEmptyFragmentElement = function (schema, node) {
var nonEmptyElements = schema.getNonEmptyElements();
return node && (node.isEmpty(nonEmptyElements) || isPaddedEmptyBlock(schema, node));
};
var isListFragment = function (schema, 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(RangeNormalizer.normalize(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(editor.schema, 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); validInsertion(editor, value, parentNode); } 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()); trimBrsFromTableCell(editor.dom, editor.selection.getStart()); 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 }; } ); /** * EditorCommands.js * * Released under LGPL License. * Copyright (c) 1999-2017 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.core.EditorCommands', [ 'tinymce.core.delete.DeleteCommands', 'tinymce.core.dom.NodeType', 'tinymce.core.dom.RangeUtils', 'tinymce.core.dom.TreeWalker', 'tinymce.core.Env', 'tinymce.core.InsertContent', 'tinymce.core.util.Tools' ], function (DeleteCommands, NodeType, RangeUtils, TreeWalker, Env, InsertContent, Tools) { // 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 (editor.removed) { return; } 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; if (editor.quirks.isHidden() || editor.removed) { 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; if (editor.quirks.isHidden() || editor.removed) { 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} commandList 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(commandList, type) { type = type || 'exec'; each(commandList, function (callback, command) { each(command.toLowerCase().split(','), function (command) { commands[type][command] = callback; }); }); } function addCommand(command, callback, scope) { command = command.toLowerCase(); commands.exec[command] = function (command, ui, value, args) { return callback.call(scope || editor, ui, value, args); }; } /** * Returns true/false if the command is supported or not. * * @method queryCommandSupported * @param {String} command Command that we check support for. * @return {Boolean} true/false if the command is supported or not. */ function queryCommandSupported(command) { command = command.toLowerCase(); if (commands.exec[command]) { return true; } // Browser commands try { return editor.getDoc().queryCommandSupported(command); } catch (ex) { // Fails sometimes see bug: 1896577 } return false; } function addQueryStateHandler(command, callback, scope) { command = command.toLowerCase(); commands.state[command] = function () { return callback.call(scope || editor); }; } function addQueryValueHandler(command, callback, scope) { command = command.toLowerCase(); commands.value[command] = function () { return callback.call(scope || editor); }; } function hasCustomCommand(command) { command = command.toLowerCase(); return !!commands.exec[command]; } // Expose public methods extend(this, { execCommand: execCommand, queryCommandState: queryCommandState, queryCommandValue: queryCommandValue, queryCommandSupported: queryCommandSupported, addCommands: addCommands, addCommand: addCommand, addQueryStateHandler: addQueryStateHandler, addQueryValueHandler: addQueryValueHandler, hasCustomCommand: hasCustomCommand }); // Private methods function execNativeCommand(command, ui, value) { if (ui === undefined) { ui = FALSE; } if (value === undefined) { value = null; } return editor.getDoc().execCommand(command, ui, value); } function isFormatMatch(name) { return formatter.match(name); } function toggleFormat(name, value) { formatter.toggle(name, value ? { value: value } : undefined); editor.nodeChanged(); } function storeSelection(type) { bookmark = selection.getBookmark(type); } function restoreSelection() { selection.moveToBookmark(bookmark); } // Add execCommand overrides addCommands({ // Ignore these, added for compatibility 'mceResetDesignMode,mceBeginUndoLevel': function () { }, // Add undo manager logic 'mceEndUndoLevel,mceAddUndoLevel': function () { editor.undoManager.add(); }, 'Cut,Copy,Paste': function (command) { var doc = editor.getDoc(), failed; // Try executing the native command try { execNativeCommand(command); } catch (ex) { // Command failed failed = TRUE; } // Chrome reports the paste command as supported however older IE:s will return false for cut/paste if (command === 'paste' && !doc.queryCommandEnabled(command)) { failed = true; } // Present alert message about clipboard access not being available if (failed || !doc.queryCommandSupported(command)) { var msg = editor.translate( "Your browser doesn't support direct access to the clipboard. " + "Please use the Ctrl+X/C/V keyboard shortcuts instead." ); if (Env.mac) { msg = msg.replace(/Ctrl\+/g, '\u2318+'); } editor.notificationManager.open({ text: msg, type: 'error' }); } }, // Override unlink command unlink: function () { if (selection.isCollapsed()) { var elm = editor.dom.getParent(editor.selection.getStart(), 'a'); if (elm) { editor.dom.remove(elm, true); } return; } formatter.remove("link"); }, // Override justify commands to use the text formatter engine 'JustifyLeft,JustifyCenter,JustifyRight,JustifyFull,JustifyNone': function (command) { var align = command.substring(7); if (align == 'full') { align = 'justify'; } // Remove all other alignments first each('left,center,right,justify'.split(','), function (name) { if (align != name) { formatter.remove('align' + name); } }); if (align != 'none') { toggleFormat('align' + align); } }, // Override list commands to fix WebKit bug 'InsertUnorderedList,InsertOrderedList': function (command) { var listElm, listParent; execNativeCommand(command); // WebKit produces lists within block elements so we need to split them // we will replace the native list creation logic to custom logic later on // TODO: Remove this when the list creation logic is removed listElm = dom.getParent(selection.getNode(), 'ol,ul'); if (listElm) { listParent = listElm.parentNode; // If list is within a text block then split that block if (/^(H[1-6]|P|ADDRESS|PRE)$/.test(listParent.nodeName)) { storeSelection(); dom.split(listParent, listElm); restoreSelection(); } } }, // Override commands to use the text formatter engine 'Bold,Italic,Underline,Strikethrough,Superscript,Subscript': function (command) { toggleFormat(command); }, // Override commands to use the text formatter engine 'ForeColor,HiliteColor,FontName': function (command, ui, value) { toggleFormat(command, value); }, FontSize: function (command, ui, value) { var fontClasses, fontSizes; // Convert font size 1-7 to styles if (value >= 1 && value <= 7) { fontSizes = explode(settings.font_size_style_values); fontClasses = explode(settings.font_size_classes); if (fontClasses) { value = fontClasses[value - 1] || value; } else { value = fontSizes[value - 1] || value; } } toggleFormat(command, value); }, RemoveFormat: function (command) { formatter.remove(command); }, mceBlockQuote: function () { toggleFormat('blockquote'); }, FormatBlock: function (command, ui, value) { return toggleFormat(value || 'p'); }, mceCleanup: function () { var bookmark = selection.getBookmark(); editor.setContent(editor.getContent({ cleanup: TRUE }), { cleanup: TRUE }); selection.moveToBookmark(bookmark); }, mceRemoveNode: function (command, ui, value) { var node = value || selection.getNode(); // Make sure that the body node isn't removed if (node != editor.getBody()) { storeSelection(); editor.dom.remove(node, TRUE); restoreSelection(); } }, mceSelectNodeDepth: function (command, ui, value) { var counter = 0; dom.getParent(selection.getNode(), function (node) { if (node.nodeType == 1 && counter++ == value) { selection.select(node); return FALSE; } }, editor.getBody()); }, mceSelectNode: function (command, ui, value) { selection.select(value); }, mceInsertContent: function (command, ui, value) { InsertContent.insertAtCaret(editor, value); }, mceInsertRawHTML: function (command, ui, value) { selection.setContent('tiny_mce_marker'); editor.setContent( editor.getContent().replace(/tiny_mce_marker/g, function () { return value; }) ); }, mceToggleFormat: function (command, ui, value) { toggleFormat(value); }, mceSetContent: function (command, ui, value) { editor.setContent(value); }, 'Indent,Outdent': function (command) { var intentValue, indentUnit, value; // Setup indent level intentValue = settings.indentation; indentUnit = /[a-z%]+$/i.exec(intentValue); intentValue = parseInt(intentValue, 10); if (!queryCommandState('InsertUnorderedList') && !queryCommandState('InsertOrderedList')) { // If forced_root_blocks is set to false we don't have a block to indent so lets create a div if (!settings.forced_root_block && !dom.getParent(selection.getNode(), dom.isBlock)) { formatter.apply('div'); } each(selection.getSelectedBlocks(), function (element) { if (dom.getContentEditable(element) === "false") { return; } if (element.nodeName !== "LI") { var indentStyleName = editor.getParam('indent_use_margin', false) ? 'margin' : 'padding'; indentStyleName = element.nodeName === 'TABLE' ? 'margin' : indentStyleName; indentStyleName += dom.getStyle(element, 'direction', true) == 'rtl' ? 'Right' : 'Left'; if (command == 'outdent') { value = Math.max(0, parseInt(element.style[indentStyleName] || 0, 10) - intentValue); dom.setStyle(element, indentStyleName, value ? value + indentUnit : ''); } else { value = (parseInt(element.style[indentStyleName] || 0, 10) + intentValue) + indentUnit; dom.setStyle(element, indentStyleName, value); } } }); } else { execNativeCommand(command); } }, mceRepaint: function () { }, InsertHorizontalRule: function () { editor.execCommand('mceInsertContent', false, '|
rng = selection.getRng(); if (!rng.item) { rng.moveToElementText(root); rng.select(); } } }, "delete": function () { DeleteCommands.deleteCommand(editor); }, "forwardDelete": function () { DeleteCommands.forwardDeleteCommand(editor); }, 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(); } }); }; } ); /** * EditorFocus.js * * Released under LGPL License. * Copyright (c) 1999-2017 Ephox Corp. All rights reserved * * License: http://www.tinymce.com/license * Contributing: http://www.tinymce.com/contributing */ define( 'tinymce.core.EditorFocus', [ 'ephox.katamari.api.Option', 'ephox.sugar.api.dom.Compare', 'ephox.sugar.api.node.Element', 'tinymce.core.caret.CaretFinder', 'tinymce.core.dom.ElementType', 'tinymce.core.dom.RangeUtils', 'tinymce.core.Env' ], function (Option, Compare, Element, CaretFinder, ElementType, RangeUtils, Env) { var getContentEditableHost = function (editor, node) { return editor.dom.getParent(node, function (node) { return editor.dom.getContentEditable(node) === "true"; }); }; var getCollapsedNode = function (rng) { return rng.collapsed ? Option.from(RangeUtils.getNode(rng.startContainer, rng.startOffset)).map(Element.fromDom) : Option.none(); }; var getFocusInElement = function (root, rng) { return getCollapsedNode(rng).bind(function (node) { if (ElementType.isTableSection(node)) { return Option.some(node); } else if (Compare.contains(root, node) === false) { return Option.some(root); } else { return Option.none(); } }); }; var normalizeSelection = function (editor, rng) { getFocusInElement(Element.fromDom(editor.getBody()), rng).bind(function (elm) { return CaretFinder.firstPositionIn(elm.dom()); }).fold( function () { editor.selection.normalize(); }, function (caretPos) { editor.selection.setRng(caretPos.toRange()); } ); }; var focusBody = function (body) { if (body.setActive) { // IE 11 sometimes throws "Invalid function" then fallback to focus // setActive is better since it doesn't scroll to the element being focused try { body.setActive(); } catch (ex) { body.focus(); } } else { body.focus(); } }; var focusEditor = function (editor) { var selection = editor.selection, contentEditable = editor.settings.content_editable, rng; var controlElm, doc = editor.getDoc(), body = editor.getBody(), contentEditableHost; // Get selected control element rng = selection.getRng(); if (rng.item) { controlElm = rng.item(0); } editor.quirks.refreshContentEditable(); // Move focus to contentEditable=true child if needed contentEditableHost = getContentEditableHost(editor, selection.getNode()); if (editor.$.contains(body, contentEditableHost)) { focusBody(contentEditableHost); normalizeSelection(editor, rng); activateEditor(editor); 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) { focusBody(body); } editor.getWin().focus(); } // Focus the body as well since it's contentEditable if (Env.gecko || contentEditable) { // Restore previous selection before focus to prevent Chrome from // jumping to the top of the document in long inline editors if (contentEditable && document.activeElement !== body) { editor.selection.setRng(editor.lastRng); } focusBody(body); normalizeSelection(editor, rng); } // 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(); } activateEditor(editor); }; var activateEditor = function (editor) { editor.editorManager.setActive(editor); }; var focus = function (editor, skipFocus) { if (editor.removed) { return; } skipFocus ? activateEditor(editor) : focusEditor(editor); }; return { focus: focus }; } ); /** * EditorObservable.js * * Released under LGPL License. * Copyright (c) 1999-2017 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.core.EditorObservable', [ "tinymce.core.util.Observable", "tinymce.core.dom.DOMUtils", "tinymce.core.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, delegate; function isListening(editor) { return !editor.hidden && !editor.readonly; } if (!editor.delegates) { editor.delegates = {}; } if (editor.delegates[eventName] || editor.removed) { return; } eventRootElm = getEventTarget(editor, eventName); 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.get(), 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; } ); /** * ErrorReporter.js * * Released under LGPL License. * Copyright (c) 1999-2017 Ephox Corp. All rights reserved * * License: http://www.tinymce.com/license * Contributing: http://www.tinymce.com/contributing */ /** * Various error reporting helper functions. * * @class tinymce.ErrorReporter * @private */ define( 'tinymce.core.ErrorReporter', [ 'tinymce.core.AddOnManager' ], function (AddOnManager) { var PluginManager = AddOnManager.PluginManager; var resolvePluginName = function (targetUrl, suffix) { for (var name in PluginManager.urls) { var matchUrl = PluginManager.urls[name] + '/plugin' + suffix + '.js'; if (matchUrl === targetUrl) { return name; } } return null; }; var pluginUrlToMessage = function (editor, url) { var plugin = resolvePluginName(url, editor.suffix); return plugin ? 'Failed to load plugin: ' + plugin + ' from url ' + url : 'Failed to load plugin url: ' + url; }; var displayNotification = function (editor, message) { editor.notificationManager.open({ type: 'error', text: message }); }; var displayError = function (editor, message) { if (editor._skinLoaded) { displayNotification(editor, message); } else { editor.on('SkinLoaded', function () { displayNotification(editor, message); }); } }; var uploadError = function (editor, message) { displayError(editor, 'Failed to upload image: ' + message); }; var pluginLoadError = function (editor, url) { displayError(editor, pluginUrlToMessage(editor, url)); }; var initError = function (message) { var console = window.console; if (console && !window.test) { // Skip test env if (console.error) { console.error.apply(console, arguments); } else { console.log.apply(console, arguments); } } }; return { pluginLoadError: pluginLoadError, uploadError: uploadError, displayError: displayError, initError: initError }; } ); /** * CaretContainerInput.js * * Released under LGPL License. * Copyright (c) 1999-2017 Ephox Corp. All rights reserved * * License: http://www.tinymce.com/license * Contributing: http://www.tinymce.com/contributing */ /** * This module shows the invisble block that the caret is currently in when contents is added to that block. */ define( 'tinymce.core.caret.CaretContainerInput', [ 'ephox.katamari.api.Fun', 'ephox.sugar.api.node.Element', 'ephox.sugar.api.search.SelectorFind', 'tinymce.core.caret.CaretContainer' ], function (Fun, Element, SelectorFind, CaretContainer) { var findBlockCaretContainer = function (editor) { return SelectorFind.descendant(Element.fromDom(editor.getBody()), '*[data-mce-caret]').fold(Fun.constant(null), function (elm) { return elm.dom(); }); }; var removeIeControlRect = function (editor) { editor.selection.setRng(editor.selection.getRng()); }; var showBlockCaretContainer = function (editor, blockCaretContainer) { if (blockCaretContainer.hasAttribute('data-mce-caret')) { CaretContainer.showCaretContainerBlock(blockCaretContainer); removeIeControlRect(editor); editor.selection.scrollIntoView(blockCaretContainer); } }; var handleBlockContainer = function (editor, e) { var blockCaretContainer = findBlockCaretContainer(editor); if (!blockCaretContainer) { return; } if (e.type === 'compositionstart') { e.preventDefault(); e.stopPropagation(); showBlockCaretContainer(blockCaretContainer); return; } if (CaretContainer.hasContent(blockCaretContainer)) { showBlockCaretContainer(editor, blockCaretContainer); } }; var setup = function (editor) { editor.on('keyup compositionstart', Fun.curry(handleBlockContainer, editor)); }; return { setup: setup }; } ); /** * Uploader.js * * Released under LGPL License. * Copyright (c) 1999-2017 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.core.file.Uploader', [ "tinymce.core.util.Promise", "tinymce.core.util.Tools", "tinymce.core.util.Fun" ], function (Promise, Tools, Fun) { return function (uploadStatus, settings) { var pendingPromises = {}; function pathJoin(path1, path2) { if (path1) { return path1.replace(/\/$/, '') + '/' + path2.replace(/^\//, ''); } return path2; } 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 || xhr.status >= 300) { failure("HTTP Error: " + xhr.status); return; } json = JSON.parse(xhr.responseText); if (!json || typeof json.location != "string") { failure("Invalid JSON: " + xhr.responseText); return; } success(pathJoin(settings.basePath, json.location)); }; formData = new FormData(); formData.append('file', blobInfo.blob(), blobInfo.filename()); xhr.send(formData); } function noUpload() { return new Promise(function (resolve) { resolve([]); }); } function handlerSuccess(blobInfo, url) { return { url: url, blobInfo: blobInfo, status: true }; } function handlerFailure(blobInfo, error) { return { url: '', blobInfo: blobInfo, status: false, error: error }; } function resolvePending(blobUri, result) { Tools.each(pendingPromises[blobUri], function (resolve) { resolve(result); }); delete pendingPromises[blobUri]; } function uploadBlobInfo(blobInfo, handler, openNotification) { uploadStatus.markPending(blobInfo.blobUri()); return new Promise(function (resolve) { var notification, progress; var noop = function () { }; try { var closeNotification = function () { if (notification) { notification.close(); progress = noop; // Once it's closed it's closed } }; var success = function (url) { closeNotification(); uploadStatus.markUploaded(blobInfo.blobUri(), url); resolvePending(blobInfo.blobUri(), handlerSuccess(blobInfo, url)); resolve(handlerSuccess(blobInfo, url)); }; var failure = function (error) { closeNotification(); uploadStatus.removeFailed(blobInfo.blobUri()); resolvePending(blobInfo.blobUri(), handlerFailure(blobInfo, error)); resolve(handlerFailure(blobInfo, error)); }; progress = function (percent) { if (percent < 0 || percent > 100) { return; } if (!notification) { notification = openNotification(); } notification.progressBar.value(percent); }; handler(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 }; }; } ); define( 'ephox.sand.api.Window', [ 'ephox.sand.util.Global' ], function (Global) { /****************************************************************************************** * BIG BIG WARNING: Don't put anything other than top-level window functions in here. * * Objects that are technically available as window.X should be in their own module X (e.g. Blob, FileReader, URL). ****************************************************************************************** */ /* * IE10 and above per * https://developer.mozilla.org/en/docs/Web/API/window.requestAnimationFrame */ var requestAnimationFrame = function (callback) { var f = Global.getOrDie('requestAnimationFrame'); f(callback); }; /* * IE10 and above per * https://developer.mozilla.org/en-US/docs/Web/API/WindowBase64.atob */ var atob = function (base64) { var f = Global.getOrDie('atob'); return f(base64); }; return { atob: atob, requestAnimationFrame: requestAnimationFrame }; } ); /** * Conversions.js * * Released under LGPL License. * Copyright (c) 1999-2017 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.core.file.Conversions', [ 'ephox.sand.api.Window', 'tinymce.core.util.Promise' ], function (Window, Promise) { function blobUriToBlob(url) { return new Promise(function (resolve, reject) { var rejectWithError = function () { reject("Cannot convert " + url + " to Blob. Resource might not exist or is inaccessible."); }; try { var xhr = new XMLHttpRequest(); xhr.open('GET', url, true); xhr.responseType = 'blob'; xhr.onload = function () { if (this.status == 200) { resolve(this.response); } else { // IE11 makes it into onload but responds with status 500 rejectWithError(); } }; // Chrome fires an error event instead of the exception // Also there seems to be no way to intercept the message that is logged to the console xhr.onerror = rejectWithError; xhr.send(); } catch (ex) { rejectWithError(); } }); } 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 = Window.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 }; } ); /** * ImageScanner.js * * Released under LGPL License. * Copyright (c) 1999-2017 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.core.file.ImageScanner', [ "tinymce.core.util.Promise", "tinymce.core.util.Arr", "tinymce.core.util.Fun", "tinymce.core.file.Conversions", "tinymce.core.Env" ], function (Promise, Arr, Fun, Conversions, Env) { var count = 0; var uniqueId = function (prefix) { return (prefix || 'blobid') + (count++); }; var imageToBlobInfo = function (blobCache, img, resolve, reject) { var base64, blobInfo; if (img.src.indexOf('blob:') === 0) { blobInfo = blobCache.getByUri(img.src); if (blobInfo) { resolve({ image: img, blobInfo: blobInfo }); } else { Conversions.uriToBlob(img.src).then(function (blob) { Conversions.blobToDataUri(blob).then(function (dataUri) { base64 = Conversions.parseDataUri(dataUri).data; blobInfo = blobCache.create(uniqueId(), blob, base64); blobCache.add(blobInfo); resolve({ image: img, blobInfo: blobInfo }); }); }, function (err) { reject(err); }); } return; } base64 = Conversions.parseDataUri(img.src).data; blobInfo = blobCache.findFirst(function (cachedBlobInfo) { return cachedBlobInfo.base64() === base64; }); if (blobInfo) { resolve({ image: img, blobInfo: blobInfo }); } else { Conversions.uriToBlob(img.src).then(function (blob) { blobInfo = blobCache.create(uniqueId(), blob, base64); blobCache.add(blobInfo); resolve({ image: img, blobInfo: blobInfo }); }, function (err) { reject(err); }); } }; var getAllImages = function (elm) { return elm ? elm.getElementsByTagName('img') : []; }; return function (uploadStatus, blobCache) { var cachedPromises = {}; function findAll(elm, predicate) { var images, promises; if (!predicate) { predicate = Fun.constant(true); } images = Arr.filter(getAllImages(elm), 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) { if (typeof imageInfo === 'string') { // error apparently return imageInfo; } resolve({ image: img, blobInfo: imageInfo.blobInfo }); }); }); } newPromise = new Promise(function (resolve, reject) { imageToBlobInfo(blobCache, img, resolve, reject); }).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 }; }; } ); define( 'ephox.sand.api.URL', [ 'ephox.sand.util.Global' ], function (Global) { /* * IE10 and above per * https://developer.mozilla.org/en-US/docs/Web/API/URL.createObjectURL * * Also Safari 6.1+ * Safari 6.0 has 'webkitURL' instead, but doesn't support flexbox so we * aren't supporting it anyway */ var url = function () { return Global.getOrDie('URL'); }; var createObjectURL = function (blob) { return url().createObjectURL(blob); }; var revokeObjectURL = function (u) { url().revokeObjectURL(u); }; return { createObjectURL: createObjectURL, revokeObjectURL: revokeObjectURL }; } ); /** * Uuid.js * * Released under LGPL License. * Copyright (c) 1999-2017 Ephox Corp. All rights reserved * * License: http://www.tinymce.com/license * Contributing: http://www.tinymce.com/contributing */ /** * Generates unique ids. * * @class tinymce.util.Uuid * @private */ define( 'tinymce.core.util.Uuid', [ ], function () { var count = 0; var seed = function () { var rnd = function () { return Math.round(Math.random() * 0xFFFFFFFF).toString(36); }; var now = new Date().getTime(); return 's' + now.toString(36) + rnd() + rnd() + rnd(); }; var uuid = function (prefix) { return prefix + (count++) + seed(); }; return { uuid: uuid }; } ); /** * BlobCache.js * * Released under LGPL License. * Copyright (c) 1999-2017 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.core.file.BlobCache', [ 'ephox.sand.api.URL', 'tinymce.core.util.Arr', 'tinymce.core.util.Fun', 'tinymce.core.util.Uuid' ], function (URL, Arr, Fun, Uuid) { return function () { var cache = [], constant = Fun.constant; function mimeToExt(mime) { var mimes = { 'image/jpeg': 'jpg', 'image/jpg': 'jpg', 'image/gif': 'gif', 'image/png': 'png' }; return mimes[mime.toLowerCase()] || 'dat'; } function create(o, blob, base64, filename) { return typeof o === 'object' ? toBlobInfo(o) : toBlobInfo({ id: o, name: filename, blob: blob, base64: base64 }); } function toBlobInfo(o) { var id, name; if (!o.blob || !o.base64) { throw "blob and base64 representations of the image are required for BlobInfo to be created"; } id = o.id || Uuid.uuid('blobid'); name = o.name || id; return { id: constant(id), name: constant(name), filename: constant(name + '.' + mimeToExt(o.blob.type)), blob: constant(o.blob), base64: constant(o.base64), blobUri: constant(o.blobUri || URL.createObjectURL(o.blob)), uri: constant(o.uri) }; } 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 }; }; } ); /** * UploadStatus.js * * Released under LGPL License. * Copyright (c) 1999-2017 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.core.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 }; }; } ); /** * EditorUpload.js * * Released under LGPL License. * Copyright (c) 1999-2017 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.core.EditorUpload', [ "tinymce.core.util.Arr", "tinymce.core.file.Uploader", "tinymce.core.file.ImageScanner", "tinymce.core.file.BlobCache", "tinymce.core.file.UploadStatus", "tinymce.core.ErrorReporter" ], function (Arr, Uploader, ImageScanner, BlobCache, UploadStatus, ErrorReporter) { return function (editor) { var blobCache = new BlobCache(), uploader, imageScanner, settings = editor.settings; var uploadStatus = new UploadStatus(); function aliveGuard(callback) { return function (result) { if (editor.selection) { return callback(result); } return []; }; } function cacheInvalidator() { return '?' + (new Date()).getTime(); } // Replaces strings without regexps to avoid FF regexp to big issue function replaceString(content, search, replace) { var index = 0; do { index = content.indexOf(search, index); if (index !== -1) { content = content.substring(0, index) + replace + content.substr(index + search.length); index += replace.length - search.length + 1; } } while (index !== -1); return content; } function replaceImageUrl(content, targetUrl, replacementUrl) { content = replaceString(content, 'src="' + targetUrl + '"', 'src="' + replacementUrl + '"'); content = replaceString(content, 'data-mce-src="' + targetUrl + '"', 'data-mce-src="' + replacementUrl + '"'); return content; } function replaceUrlInUndoStack(targetUrl, replacementUrl) { Arr.each(editor.undoManager.data, function (level) { if (level.type === 'fragmented') { level.fragments = Arr.map(level.fragments, function (fragment) { return replaceImageUrl(fragment, targetUrl, replacementUrl); }); } else { level.content = replaceImageUrl(level.content, targetUrl, replacementUrl); } }); } function openNotification() { return editor.notificationManager.open({ text: editor.translate('Image uploading...'), type: 'info', timeout: -1, progressBar: true }); } function replaceImageUri(image, resultUri) { blobCache.removeByUri(image.src); replaceUrlInUndoStack(image.src, resultUri); editor.$(image).attr({ src: settings.images_reuse_filename ? resultUri + cacheInvalidator() : resultUri, 'data-mce-src': editor.convertURL(resultUri, 'src') }); } function uploadImages(callback) { if (!uploader) { uploader = new Uploader(uploadStatus, { url: settings.images_upload_url, basePath: settings.images_upload_base_path, credentials: settings.images_upload_credentials, handler: settings.images_upload_handler }); } return scanForImages().then(aliveGuard(function (imageInfos) { var blobInfos; blobInfos = Arr.map(imageInfos, function (imageInfo) { return imageInfo.blobInfo; }); return uploader.upload(blobInfos, openNotification).then(aliveGuard(function (result) { var filteredResult = Arr.map(result, function (uploadInfo, index) { var image = imageInfos[index].image; if (uploadInfo.status && editor.settings.images_replace_blob_uris !== false) { replaceImageUri(image, uploadInfo.url); } else if (uploadInfo.error) { ErrorReporter.uploadError(editor, uploadInfo.error); } return { element: image, status: uploadInfo.status }; }); if (callback) { callback(filteredResult); } return filteredResult; })); })); } 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) { result = Arr.filter(result, function (resultItem) { // ImageScanner internally converts images that it finds, but it may fail to do so if image source is inaccessible. // In such case resultItem will contain appropriate text error message, instead of image data. if (typeof resultItem === 'string') { ErrorReporter.displayError(editor, resultItem); return false; } return true; }); 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.get(), function (result, editor) { return result || editor.editorUpload && 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 }; }; } ); /** * ForceBlocks.js * * Released under LGPL License. * Copyright (c) 1999-2017 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.core.ForceBlocks', [ 'ephox.katamari.api.Fun' ], function (Fun) { var addRootBlocks = function (editor) { var settings = editor.settings, dom = editor.dom, selection = editor.selection; var schema = editor.schema, blockElements = schema.getBlockElements(); 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(); } }; var setup = function (editor) { if (editor.settings.forced_root_block) { editor.on('NodeChange', Fun.curry(addRootBlocks, editor)); } }; return { setup: setup }; } ); /** * Dimensions.js * * Released under LGPL License. * Copyright (c) 1999-2017 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.core.dom.Dimensions', [ "tinymce.core.util.Arr", "tinymce.core.dom.NodeType", "tinymce.core.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 }; } ); /** * LineUtils.js * * Released under LGPL License. * Copyright (c) 1999-2017 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.core.caret.LineUtils', [ "tinymce.core.util.Fun", "tinymce.core.util.Arr", "tinymce.core.dom.NodeType", "tinymce.core.dom.Dimensions", "tinymce.core.geom.ClientRect", "tinymce.core.caret.CaretUtils", "tinymce.core.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 }; } ); /** * LineWalker.js * * Released under LGPL License. * Copyright (c) 1999-2017 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.core.caret.LineWalker', [ "tinymce.core.util.Fun", "tinymce.core.util.Arr", "tinymce.core.dom.Dimensions", "tinymce.core.caret.CaretCandidate", "tinymce.core.caret.CaretUtils", "tinymce.core.caret.CaretWalker", "tinymce.core.caret.CaretPosition", "tinymce.core.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) }; } ); /** * CefUtils.js * * Released under LGPL License. * Copyright (c) 1999-2017 Ephox Corp. All rights reserved * * License: http://www.tinymce.com/license * Contributing: http://www.tinymce.com/contributing */ define( 'tinymce.core.keyboard.CefUtils', [ 'tinymce.core.caret.CaretPosition', 'tinymce.core.caret.CaretUtils', 'tinymce.core.dom.NodeType', 'tinymce.core.util.Fun' ], function (CaretPosition, CaretUtils, NodeType, Fun) { var isContentEditableTrue = NodeType.isContentEditableTrue; var isContentEditableFalse = NodeType.isContentEditableFalse; var showCaret = function (direction, editor, node, before) { // TODO: Figure out a better way to handle this dependency return editor._selectionOverrides.showCaret(direction, node, before); }; var getNodeRange = function (node) { var rng = node.ownerDocument.createRange(); rng.selectNode(node); return rng; }; var selectNode = function (editor, node) { var e; e = editor.fire('BeforeObjectSelected', { target: node }); if (e.isDefaultPrevented()) { return null; } return getNodeRange(node); }; var renderCaretAtRange = function (editor, range) { var caretPosition, ceRoot; range = CaretUtils.normalizeRange(1, editor.getBody(), range); caretPosition = CaretPosition.fromRangeStart(range); if (isContentEditableFalse(caretPosition.getNode())) { return showCaret(1, editor, caretPosition.getNode(), !caretPosition.isAtEnd()); } if (isContentEditableFalse(caretPosition.getNode(true))) { return showCaret(1, editor, caretPosition.getNode(true), false); } // TODO: Should render caret before/after depending on where you click on the page forces after now ceRoot = editor.dom.getParent(caretPosition.getNode(), Fun.or(isContentEditableFalse, isContentEditableTrue)); if (isContentEditableFalse(ceRoot)) { return showCaret(1, editor, ceRoot, false); } return null; }; var renderRangeCaret = function (editor, range) { var caretRange; if (!range || !range.collapsed) { return range; } caretRange = renderCaretAtRange(editor, range); if (caretRange) { return caretRange; } return range; }; return { showCaret: showCaret, selectNode: selectNode, renderCaretAtRange: renderCaretAtRange, renderRangeCaret: renderRangeCaret }; } ); /** * CefNavigation.js * * Released under LGPL License. * Copyright (c) 1999-2017 Ephox Corp. All rights reserved * * License: http://www.tinymce.com/license * Contributing: http://www.tinymce.com/contributing */ define( 'tinymce.core.keyboard.CefNavigation', [ 'tinymce.core.caret.CaretContainer', 'tinymce.core.caret.CaretPosition', 'tinymce.core.caret.CaretUtils', 'tinymce.core.caret.CaretWalker', 'tinymce.core.caret.LineUtils', 'tinymce.core.caret.LineWalker', 'tinymce.core.dom.NodeType', 'tinymce.core.dom.RangeUtils', 'tinymce.core.Env', 'tinymce.core.keyboard.CefUtils', 'tinymce.core.util.Arr', 'tinymce.core.util.Fun' ], function (CaretContainer, CaretPosition, CaretUtils, CaretWalker, LineUtils, LineWalker, NodeType, RangeUtils, Env, CefUtils, Arr, Fun) { var isContentEditableFalse = NodeType.isContentEditableFalse; var getSelectedNode = RangeUtils.getSelectedNode; var isAfterContentEditableFalse = CaretUtils.isAfterContentEditableFalse; var isBeforeContentEditableFalse = CaretUtils.isBeforeContentEditableFalse; var getVisualCaretPosition = function (walkFn, caretPosition) { while ((caretPosition = walkFn(caretPosition))) { if (caretPosition.isVisible()) { return caretPosition; } } return caretPosition; }; var isMoveInsideSameBlock = function (fromCaretPosition, toCaretPosition) { var inSameBlock = CaretUtils.isInSameBlock(fromCaretPosition, toCaretPosition); // Handle bogus BRabc|
x
becomes this:x
var trimInlineElementsOnLeftSideOfBlock = function (dom, nonEmptyElementsMap, 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 { if (isEmptyAnchor(node)) { dom.remove(node); } } } }; var normalizeZwspOffset = function (start, container, offset) { if (NodeType.isText(container) === false) { return offset; } if (start) { return offset === 1 && container.data.charAt(offset - 1) === Zwsp.ZWSP ? 0 : offset; } else { return offset === container.data.length - 1 && container.data.charAt(offset) === Zwsp.ZWSP ? container.data.length : offset; } }; var includeZwspInRange = function (rng) { var newRng = rng.cloneRange(); newRng.setStart(rng.startContainer, normalizeZwspOffset(true, rng.startContainer, rng.startOffset)); newRng.setEnd(rng.endContainer, normalizeZwspOffset(false, rng.endContainer, rng.endOffset)); return newRng; }; var firstNonWhiteSpaceNodeSibling = function (node) { while (node) { if (node.nodeType === 1 || (node.nodeType === 3 && node.data && /[\r\n\s]/.test(node.data))) { return node; } node = node.nextSibling; } }; // Inserts a BR element if the forced_root_block option is set to false or empty string var insertBr = function (editor, evt) { editor.execCommand("InsertLineBreak", false, evt); }; // Trims any linebreaks at the beginning of node user for example when pressing enter in a PRE element var trimLeadingLineBreaks = function (node) { do { if (node.nodeType === 3) { node.nodeValue = node.nodeValue.replace(/^[\r\n]+/, ''); } node = node.firstChild; } while (node); }; var getEditableRoot = function (dom, node) { var root = dom.getRoot(), parent, editableRoot; // Get all parents until we hit a non editable parent or the root parent = node; while (parent !== root && dom.getContentEditable(parent) !== "false") { if (dom.getContentEditable(parent) === "true") { editableRoot = parent; } parent = parent.parentNode; } return parent !== root ? editableRoot : root; }; var setForcedBlockAttrs = function (editor, node) { var forcedRootBlockName = editor.settings.forced_root_block; if (forcedRootBlockName && forcedRootBlockName.toLowerCase() === node.tagName.toLowerCase()) { editor.dom.setAttribs(node, editor.settings.forced_root_block_attrs); } }; // Wraps any text nodes or inline elements in the specified forced root block name var wrapSelfAndSiblingsInDefaultBlock = function (editor, newBlockName, rng, container, offset) { var newBlock, parentBlock, startNode, node, next, rootBlockName, blockName = newBlockName || 'P'; var dom = editor.dom, editableRoot = getEditableRoot(dom, container); // Not in a block element or in a table cell or caption parentBlock = dom.getParent(container, dom.isBlock); if (!parentBlock || !canSplitBlock(dom, parentBlock)) { parentBlock = parentBlock || editableRoot; if (parentBlock == editor.getBody() || isTableCell(parentBlock)) { rootBlockName = parentBlock.nodeName.toLowerCase(); } else { rootBlockName = parentBlock.parentNode.nodeName.toLowerCase(); } if (!parentBlock.hasChildNodes()) { newBlock = dom.create(blockName); setForcedBlockAttrs(editor, newBlock); parentBlock.appendChild(newBlock); rng.setStart(newBlock, 0); rng.setEnd(newBlock, 0); return newBlock; } // Find parent that is the first child of parentBlock node = container; while (node.parentNode != parentBlock) { node = node.parentNode; } // Loop left to find start node start wrapping at while (node && !dom.isBlock(node)) { startNode = node; node = node.previousSibling; } if (startNode && editor.schema.isValidChild(rootBlockName, blockName.toLowerCase())) { newBlock = dom.create(blockName); setForcedBlockAttrs(editor, newBlock); startNode.parentNode.insertBefore(newBlock, startNode); // Start wrapping until we hit a block node = startNode; while (node && !dom.isBlock(node)) { next = node.nextSibling; newBlock.appendChild(node); node = next; } // Restore range to it's past location rng.setStart(container, offset); rng.setEnd(container, offset); } } return container; }; // Adds a BR at the end of blocks that only contains an IMG or INPUT since // these might be floated and then they won't expand the block var addBrToBlockIfNeeded = function (dom, block) { var lastChild; // IE will render the blocks correctly other browsers needs a BR block.normalize(); // Remove empty text nodes that got left behind by the extract // Check if the block is empty or contains a floated last child lastChild = block.lastChild; if (!lastChild || (/^(left|right)$/gi.test(dom.getStyle(lastChild, 'float', true)))) { dom.add(block, 'br'); } }; var getContainerBlock = function (containerBlock) { var containerBlockParent = containerBlock.parentNode; if (/^(LI|DT|DD)$/.test(containerBlockParent.nodeName)) { return containerBlockParent; } return containerBlock; }; var isFirstOrLastLi = function (containerBlock, parentBlock, first) { var node = containerBlock[first ? 'firstChild' : 'lastChild']; // Find first/last element since there might be whitespace there while (node) { if (node.nodeType == 1) { break; } node = node[first ? 'nextSibling' : 'previousSibling']; } return node === parentBlock; }; var insert = function (editor, evt) { var tmpRng, editableRoot, container, offset, parentBlock, shiftKey; var newBlock, fragment, containerBlock, parentBlockName, containerBlockName, newBlockName, isAfterLastNodeInContainer; var dom = editor.dom, selection = editor.selection, settings = editor.settings; var schema = editor.schema, nonEmptyElementsMap = schema.getNonEmptyElements(); var rng = editor.selection.getRng(); // 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; var moveCaretBeforeOnEnterElementsMap = schema.getMoveCaretBeforeOnEnterElements(); if (!root) { return; } if (/^(LI|DT|DD)$/.test(root.nodeName)) { var firstChild = firstNonWhiteSpaceNodeSibling(root.firstChild); if (firstChild && /^(UL|OL|DL)$/.test(firstChild.nodeName)) { root.insertBefore(dom.doc.createTextNode('\u00a0'), root.firstChild); } } rng = dom.createRng(); root.normalize(); if (root.hasChildNodes()) { walker = new TreeWalker(root, root); while ((node = walker.current())) { if (node.nodeType == 3) { rng.setStart(node, 0); rng.setEnd(node, 0); break; } if (moveCaretBeforeOnEnterElementsMap[node.nodeName.toLowerCase()]) { rng.setStartBefore(node); rng.setEndBefore(node); break; } lastNode = node; node = walker.next(); } if (!node) { rng.setStart(lastNode, 0); rng.setEnd(lastNode, 0); } } else { if (root.nodeName == 'BR') { if (root.nextSibling && dom.isBlock(root.nextSibling)) { rng.setStartBefore(root); rng.setEndBefore(root); } else { rng.setStartAfter(root); rng.setEndAfter(root); } } else { rng.setStart(root, 0); rng.setEnd(root, 0); } } selection.setRng(rng); // Remove tempElm created for old IE:s dom.remove(tempElm); selection.scrollIntoView(root); } // Creates a new block element by cloning the current one or creating a new one if the name is specified // This function will also copy any text formatting from the parent block and add it to the new one function createNewBlock(name) { var node = container, block, clonedNode, caretNode, textInlineElements = schema.getTextInlineElements(); if (name || parentBlockName == "TABLE" || parentBlockName == "HR") { block = dom.create(name || newBlockName); setForcedBlockAttrs(editor, block); } else { block = parentBlock.cloneNode(false); } caretNode = block; if (settings.keep_styles === false) { dom.setAttrib(block, 'style', null); // wipe out any styles that came over with the block dom.setAttrib(block, 'class', null); } else { // Clone any parent styles do { if (textInlineElements[node.nodeName]) { // Never clone a caret containers if (node.id == '_mce_caret') { continue; } clonedNode = node.cloneNode(false); dom.setAttrib(clonedNode, 'id', ''); // Remove ID since it needs to be document unique if (block.hasChildNodes()) { clonedNode.appendChild(block.firstChild); block.appendChild(clonedNode); } else { caretNode = clonedNode; block.appendChild(clonedNode); } } } while ((node = node.parentNode) && node != editableRoot); } emptyBlock(caretNode); return block; } // Returns true/false if the caret is at the start/end of the parent block element function isCaretAtStartOrEndOfBlock(start) { var walker, node, name, normalizedOffset; normalizedOffset = normalizeZwspOffset(start, container, offset); // Caret is in the middle of a text node like "a|b" if (container.nodeType == 3 && (start ? normalizedOffset > 0 : normalizedOffset < container.nodeValue.length)) { return false; } // If after the last element in block node edge case for #5091 if (container.parentNode == parentBlock && isAfterLastNodeInContainer && !start) { return true; } // If the caret if before the first element in parentBlock if (start && container.nodeType == 1 && container == parentBlock.firstChild) { return true; } // Caret can be before/after a table or a hr if (containerAndSiblingName(container, 'TABLE') || containerAndSiblingName(container, 'HR')) { return (isAfterLastNodeInContainer && !start) || (!isAfterLastNodeInContainer && start); } // Walk the DOM and look for text nodes or non empty elements walker = new TreeWalker(container, parentBlock); // If caret is in beginning or end of a text block then jump to the next/previous node if (container.nodeType == 3) { if (start && normalizedOffset === 0) { walker.prev(); } else if (!start && normalizedOffset == container.nodeValue.length) { walker.next(); } } while ((node = walker.current())) { if (node.nodeType === 1) { // Ignore bogus elements if (!node.getAttribute('data-mce-bogus')) { // Keep empty elements like but not trailing br:s liketext|
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 }); }; return { insert: insert }; } ); /** * EnterKey.js * * Released under LGPL License. * Copyright (c) 1999-2017 Ephox Corp. All rights reserved * * License: http://www.tinymce.com/license * Contributing: http://www.tinymce.com/contributing */ define( 'tinymce.core.keyboard.EnterKey', [ 'tinymce.core.keyboard.InsertNewLine', 'tinymce.core.util.VK' ], function (InsertNewLine, VK) { var endTypingLevel = function (undoManager) { if (undoManager.typing) { undoManager.typing = false; undoManager.add(); } }; var handleEnterKeyEvent = function (editor, event) { if (event.isDefaultPrevented()) { return; } event.preventDefault(); endTypingLevel(editor.undoManager); editor.undoManager.transact(function () { if (editor.selection.isCollapsed() === false) { editor.execCommand('Delete'); } InsertNewLine.insert(editor, event); }); }; var setup = function (editor) { editor.on('keydown', function (event) { if (event.keyCode === VK.ENTER) { handleEnterKeyEvent(editor, event); } }); }; return { setup: setup }; } ); /** * InsertSpace.js * * Released under LGPL License. * Copyright (c) 1999-2017 Ephox Corp. All rights reserved * * License: http://www.tinymce.com/license * Contributing: http://www.tinymce.com/contributing */ define( 'tinymce.core.keyboard.InsertSpace', [ 'ephox.katamari.api.Fun', 'tinymce.core.caret.CaretPosition', 'tinymce.core.dom.NodeType', 'tinymce.core.keyboard.BoundaryLocation', 'tinymce.core.keyboard.InlineUtils' ], function (Fun, CaretPosition, NodeType, BoundaryLocation, InlineUtils) { var isValidInsertPoint = function (location, caretPosition) { return isAtStartOrEnd(location) && NodeType.isText(caretPosition.container()); }; var insertNbspAtPosition = function (editor, caretPosition) { var container = caretPosition.container(); var offset = caretPosition.offset(); container.insertData(offset, '\u00a0'); editor.selection.setCursorLocation(container, offset + 1); }; var insertAtLocation = function (editor, caretPosition, location) { if (isValidInsertPoint(location, caretPosition)) { insertNbspAtPosition(editor, caretPosition); return true; } else { return false; } }; var insertAtCaret = function (editor) { var isInlineTarget = Fun.curry(InlineUtils.isInlineTarget, editor); var caretPosition = CaretPosition.fromRangeStart(editor.selection.getRng()); var boundaryLocation = BoundaryLocation.readLocation(isInlineTarget, editor.getBody(), caretPosition); return boundaryLocation.map(Fun.curry(insertAtLocation, editor, caretPosition)).getOr(false); }; var isAtStartOrEnd = function (location) { return location.fold( Fun.constant(false), // Before Fun.constant(true), // Start Fun.constant(true), // End Fun.constant(false) // After ); }; var insertAtSelection = function (editor) { return editor.selection.isCollapsed() ? insertAtCaret(editor) : false; }; return { insertAtSelection: insertAtSelection }; } ); /** * SpaceKey.js * * Released under LGPL License. * Copyright (c) 1999-2017 Ephox Corp. All rights reserved * * License: http://www.tinymce.com/license * Contributing: http://www.tinymce.com/contributing */ define( 'tinymce.core.keyboard.SpaceKey', [ 'tinymce.core.keyboard.InsertSpace', 'tinymce.core.keyboard.MatchKeys', 'tinymce.core.util.VK' ], function (InsertSpace, MatchKeys, VK) { var executeKeydownOverride = function (editor, evt) { MatchKeys.execute([ { keyCode: VK.SPACEBAR, action: MatchKeys.action(InsertSpace.insertAtSelection, editor) } ], evt).each(function (_) { evt.preventDefault(); }); }; var setup = function (editor) { editor.on('keydown', function (evt) { if (evt.isDefaultPrevented() === false) { executeKeydownOverride(editor, evt); } }); }; return { setup: setup }; } ); /** * KeyboardOverrides.js * * Released under LGPL License. * Copyright (c) 1999-2017 Ephox Corp. All rights reserved * * License: http://www.tinymce.com/license * Contributing: http://www.tinymce.com/contributing */ define( 'tinymce.core.keyboard.KeyboardOverrides', [ 'tinymce.core.keyboard.ArrowKeys', 'tinymce.core.keyboard.BoundarySelection', 'tinymce.core.keyboard.DeleteBackspaceKeys', 'tinymce.core.keyboard.EnterKey', 'tinymce.core.keyboard.SpaceKey' ], function (ArrowKeys, BoundarySelection, DeleteBackspaceKeys, EnterKey, SpaceKey) { var setup = function (editor) { var caret = BoundarySelection.setupSelectedState(editor); ArrowKeys.setup(editor, caret); DeleteBackspaceKeys.setup(editor, caret); EnterKey.setup(editor); SpaceKey.setup(editor); }; return { setup: setup }; } ); /** * NodeChange.js * * Released under LGPL License. * Copyright (c) 1999-2017 Ephox Corp. All rights reserved * * License: http://www.tinymce.com/license * Contributing: http://www.tinymce.com/contributing */ /** * This class handles the nodechange event dispatching both manual and through selection change events. * * @class tinymce.NodeChange * @private */ define( 'tinymce.core.NodeChange', [ "tinymce.core.dom.RangeUtils", "tinymce.core.Env", "tinymce.core.util.Delay" ], function (RangeUtils, Env, Delay) { return function (editor) { var lastRng, lastPath = []; /** * Returns true/false if the current element path has been changed or not. * * @private * @return {Boolean} True if the element path is the same false if it's not. */ function isSameElementPath(startElm) { var i, currentPath; currentPath = editor.$(startElm).parentsUntil(editor.getBody()).add(startElm); if (currentPath.length === lastPath.length) { for (i = currentPath.length; i >= 0; i--) { if (currentPath[i] !== lastPath[i]) { break; } } if (i === -1) { lastPath = currentPath; return true; } } lastPath = currentPath; return false; } // Gecko doesn't support the "selectionchange" event if (!('onselectionchange' in editor.getDoc())) { editor.on('NodeChange Click MouseUp KeyUp Focus', function (e) { var nativeRng, fakeRng; // Since DOM Ranges mutate on modification // of the DOM we need to clone it's contents nativeRng = editor.selection.getRng(); fakeRng = { startContainer: nativeRng.startContainer, startOffset: nativeRng.startOffset, endContainer: nativeRng.endContainer, endOffset: nativeRng.endOffset }; // Always treat nodechange as a selectionchange since applying // formatting to the current range wouldn't update the range but it's parent if (e.type == 'nodechange' || !RangeUtils.compareRanges(fakeRng, lastRng)) { editor.fire('SelectionChange'); } lastRng = fakeRng; }); } // IE has a bug where it fires a selectionchange on right click that has a range at the start of the body // When the contextmenu event fires the selection is located at the right location editor.on('contextmenu', function () { editor.fire('SelectionChange'); }); // Selection change is delayed ~200ms on IE when you click inside the current range editor.on('SelectionChange', function () { var startElm = editor.selection.getStart(true); // When focusout from after cef element to other input element the startelm can be undefined. // IE 8 will fire a selectionchange event with an incorrect selection // when focusing out of table cells. Click inside cell -> toolbar = Invalid SelectionChange event if (!startElm || (!Env.range && editor.selection.isCollapsed())) { return; } if (!isSameElementPath(startElm) && editor.dom.isChildOf(startElm, editor.getBody())) { editor.nodeChanged({ selectionChange: true }); } }); // Fire an extra nodeChange on mouseup for compatibility reasons editor.on('MouseUp', function (e) { if (!e.isDefaultPrevented()) { // Delay nodeChanged call for WebKit edge case issue where the range // isn't updated until after you click outside a selected image if (editor.selection.getNode().nodeName == 'IMG') { Delay.setEditorTimeout(editor, function () { editor.nodeChanged(); }); } else { editor.nodeChanged(); } } }); /** * 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. */ this.nodeChanged = function (args) { var selection = editor.selection, node, parents, root; // Fix for bug #1896577 it seems that this can not be fired while the editor is loading if (editor.initialized && selection && !editor.settings.disable_nodechange && !editor.readonly) { // Get start node root = editor.getBody(); node = selection.getStart(true) || root; // Make sure the node is within the editor root or is the editor root if (node.ownerDocument != editor.getDoc() || !editor.dom.isChildOf(node, root)) { node = root; } // 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); } }; }; } ); /** * MousePosition.js * * Released under LGPL License. * Copyright (c) 1999-2017 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.core.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 }; } ); /** * DragDropOverrides.js * * Released under LGPL License. * Copyright (c) 1999-2017 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.core.DragDropOverrides', [ "tinymce.core.dom.NodeType", "tinymce.core.util.Arr", "tinymce.core.util.Fun", "tinymce.core.util.Delay", "tinymce.core.dom.DOMUtils", "tinymce.core.dom.MousePosition" ], function ( NodeType, Arr, Fun, Delay, DOMUtils, MousePosition ) { var isContentEditableFalse = NodeType.isContentEditableFalse, isContentEditableTrue = NodeType.isContentEditableTrue; var isDraggable = function (rootElm, elm) { return isContentEditableFalse(elm) && elm !== rootElm; }; 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(editor.getBody(), ceElm)) { var elmPos = editor.dom.getPos(ceElm); var bodyElm = editor.getBody(); var docElm = editor.getDoc().documentElement; state.element = ceElm; state.screenX = e.screenX; state.screenY = e.screenY; state.maxX = (editor.inline ? bodyElm.scrollWidth : docElm.offsetWidth) - 2; state.maxY = (editor.inline ? bodyElm.scrollHeight : docElm.offsetHeight) - 2; state.relX = e.pageX - elmPos.x; state.relY = e.pageY - elmPos.y; state.width = ceElm.offsetWidth; state.height = ceElm.offsetHeight; state.ghost = createGhost(editor, ceElm, state.width, state.height); } } }; }; var move = function (state, editor) { // Reduces laggy drag behavior on Gecko var throttledPlaceCaretAt = Delay.throttle(function (clientX, clientY) { editor._selectionOverrides.hideFakeCaret(); editor.selection.placeCaretAt(clientX, clientY); }, 0); return function (e) { var movement = Math.max(Math.abs(e.screenX - state.screenX), Math.abs(e.screenY - state.screenY)); if (hasDraggableElement(state) && !state.dragging && movement > 10) { var args = editor.fire('dragstart', { target: state.element }); if (args.isDefaultPrevented()) { return; } state.dragging = true; editor.focus(); } if (state.dragging) { var targetPos = applyRelPos(state, MousePosition.calc(editor, e)); appendGhostToBody(state.ghost, editor.getBody()); moveGhost(state.ghost, targetPos, state.width, state.height, state.maxX, state.maxY); throttledPlaceCaretAt(e.clientX, e.clientY); } }; }; // Returns the raw element instead of the fake cE=false element var getRawTarget = function (selection) { var rng = selection.getSel().getRangeAt(0); var startContainer = rng.startContainer; return startContainer.nodeType === 3 ? startContainer.parentNode : startContainer; }; var drop = function (state, editor) { return function (e) { if (state.dragging) { if (isValidDropTarget(editor, getRawTarget(editor.selection), state.element)) { var targetClone = cloneElement(state.element); var args = editor.fire('drop', { targetClone: targetClone, clientX: e.clientX, clientY: e.clientY }); if (!args.isDefaultPrevented()) { targetClone = args.targetClone; editor.undoManager.transact(function () { removeElement(state.element); editor.insertContent(editor.dom.getOuterHTML(targetClone)); editor._selectionOverrides.hideFakeCaret(); }); } } } removeDragState(state); }; }; var stop = function (state, editor) { return function () { removeDragState(state); if (state.dragging) { editor.fire('dragend'); } }; }; var removeDragState = function (state) { state.dragging = false; state.element = null; removeElement(state.ghost); }; var bindFakeDragEvents = function (editor) { var state = {}, pageDom, dragStartHandler, dragHandler, dropHandler, dragEndHandler, rootDocument; pageDom = DOMUtils.DOM; rootDocument = document; dragStartHandler = start(state, editor); dragHandler = move(state, editor); dropHandler = drop(state, editor); dragEndHandler = stop(state, editor); editor.on('mousedown', dragStartHandler); editor.on('mousemove', dragHandler); editor.on('mouseup', dropHandler); pageDom.bind(rootDocument, 'mousemove', dragHandler); pageDom.bind(rootDocument, 'mouseup', dragEndHandler); editor.on('remove', function () { pageDom.unbind(rootDocument, 'mousemove', dragHandler); pageDom.unbind(rootDocument, 'mouseup', dragEndHandler); }); }; var blockIeDrop = function (editor) { editor.on('drop', function (e) { // FF doesn't pass out clientX/clientY for drop since this is for IE we just use null instead var realTarget = typeof e.clientX !== 'undefined' ? editor.getDoc().elementFromPoint(e.clientX, e.clientY) : null; if (isContentEditableFalse(realTarget) || isContentEditableFalse(editor.dom.getContentEditableParent(realTarget))) { e.preventDefault(); } }); }; var init = function (editor) { bindFakeDragEvents(editor); blockIeDrop(editor); }; return { init: init }; } ); /** * FakeCaret.js * * Released under LGPL License. * Copyright (c) 1999-2017 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.core.caret.FakeCaret', [ 'tinymce.core.caret.CaretContainer', 'tinymce.core.caret.CaretContainerRemove', 'tinymce.core.caret.CaretPosition', 'tinymce.core.dom.DomQuery', 'tinymce.core.dom.NodeType', 'tinymce.core.dom.RangeUtils', 'tinymce.core.geom.ClientRect', 'tinymce.core.util.Delay' ], function (CaretContainer, CaretContainerRemove, CaretPosition, DomQuery, NodeType, RangeUtils, ClientRect, Delay) { var isContentEditableFalse = NodeType.isContentEditableFalse; var isTableCell = function (node) { return node && /^(TD|TH)$/i.test(node.nodeName); }; 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 = DomQuery('*[contentEditable=false]', rootNode); for (i = 0; i < contentEditableFalseNodes.length; i++) { node = contentEditableFalseNodes[i]; sibling = node.previousSibling; if (CaretContainer.endsWithCaretContainer(sibling)) { data = sibling.data; if (data.length == 1) { sibling.parentNode.removeChild(sibling); } else { sibling.deleteData(data.length - 1, 1); } } sibling = node.nextSibling; if (CaretContainer.startsWithCaretContainer(sibling)) { data = sibling.data; if (data.length == 1) { sibling.parentNode.removeChild(sibling); } else { sibling.deleteData(0, 1); } } } return null; } function show(before, node) { var clientRect, rng; hide(); if (isTableCell(node)) { return null; } if (isBlock(node)) { caretContainerNode = CaretContainer.insertBlock('p', node, before); clientRect = getAbsoluteClientRect(node, before); DomQuery(caretContainerNode).css('top', clientRect.top); $lastVisualCaret = DomQuery('').css(clientRect).appendTo(rootNode); if (before) { $lastVisualCaret.addClass('mce-visual-caret-before'); } startBlink(); rng = node.ownerDocument.createRange(); rng.setStart(caretContainerNode, 0); rng.setEnd(caretContainerNode, 0); } else { caretContainerNode = CaretContainer.insertInline(node, before); rng = node.ownerDocument.createRange(); if (isContentEditableFalse(caretContainerNode.nextSibling)) { rng.setStart(caretContainerNode, 0); rng.setEnd(caretContainerNode, 0); } else { rng.setStart(caretContainerNode, 1); rng.setEnd(caretContainerNode, 1); } return rng; } return rng; } function hide() { trimInlineCaretContainers(); if (caretContainerNode) { CaretContainerRemove.remove(caretContainerNode); caretContainerNode = null; } if ($lastVisualCaret) { $lastVisualCaret.remove(); $lastVisualCaret = null; } clearInterval(cursorInterval); } function startBlink() { cursorInterval = Delay.setInterval(function () { DomQuery('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 }; }; } ); /** * SelectionOverrides.js * * Released under LGPL License. * Copyright (c) 1999-2017 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.core.SelectionOverrides', [ 'ephox.katamari.api.Arr', 'ephox.sugar.api.dom.Remove', 'ephox.sugar.api.node.Element', 'ephox.sugar.api.properties.Attr', 'ephox.sugar.api.search.SelectorFilter', 'ephox.sugar.api.search.SelectorFind', 'tinymce.core.DragDropOverrides', 'tinymce.core.EditorView', 'tinymce.core.Env', 'tinymce.core.caret.CaretContainer', 'tinymce.core.caret.CaretPosition', 'tinymce.core.caret.CaretUtils', 'tinymce.core.caret.CaretWalker', 'tinymce.core.caret.FakeCaret', 'tinymce.core.caret.LineUtils', 'tinymce.core.dom.ElementType', 'tinymce.core.dom.NodeType', 'tinymce.core.dom.RangePoint', 'tinymce.core.keyboard.CefUtils', 'tinymce.core.util.Delay', 'tinymce.core.util.VK' ], function ( Arr, Remove, Element, Attr, SelectorFilter, SelectorFind, DragDropOverrides, EditorView, Env, CaretContainer, CaretPosition, CaretUtils, CaretWalker, FakeCaret, LineUtils, ElementType, NodeType, RangePoint, CefUtils, Delay, VK ) { var isContentEditableTrue = NodeType.isContentEditableTrue, isContentEditableFalse = NodeType.isContentEditableFalse, isAfterContentEditableFalse = CaretUtils.isAfterContentEditableFalse, isBeforeContentEditableFalse = CaretUtils.isBeforeContentEditableFalse; function SelectionOverrides(editor) { var rootNode = editor.getBody(); var fakeCaret = new FakeCaret(editor.getBody(), isBlock), realSelectionId = 'sel-' + editor.dom.uniqueId(), selectedContentEditableNode; function isFakeSelectionElement(elm) { return editor.dom.hasClass(elm, 'mce-offscreen-selection'); } function getRealSelectionElement() { var container = editor.dom.get(realSelectionId); return container ? container.getElementsByTagName('*')[0] : container; } function isBlock(node) { return editor.dom.isBlock(node); } function setRange(range) { //console.log('setRange', range); if (range) { editor.selection.setRng(range); } } function getRange() { return editor.selection.getRng(); } function scrollIntoView(node, alignToTop) { editor.selection.scrollIntoView(node, alignToTop); } function showCaret(direction, node, before) { var e; e = editor.fire('ShowCaret', { target: node, direction: direction, before: before }); if (e.isDefaultPrevented()) { return null; } scrollIntoView(node, direction === -1); return fakeCaret.show(before, node); } function getNormalizedRangeEndPoint(direction, range) { range = CaretUtils.normalizeRange(direction, rootNode, range); if (direction == -1) { return CaretPosition.fromRangeStart(range); } return CaretPosition.fromRangeEnd(range); } function showBlockCaretContainer(blockCaretContainer) { if (blockCaretContainer.hasAttribute('data-mce-caret')) { CaretContainer.showCaretContainerBlock(blockCaretContainer); setRange(getRange()); // Removes control rect on IE scrollIntoView(blockCaretContainer[0]); } } function registerEvents() { function getContentEditableRoot(node) { var root = editor.getBody(); while (node && node != root) { if (isContentEditableTrue(node) || isContentEditableFalse(node)) { return node; } node = node.parentNode; } return null; } // Some browsers (Chrome) lets you place the caret after a cE=false // Make sure we render the caret container in this case editor.on('mouseup', function (e) { var range = getRange(); if (range.collapsed && EditorView.isXYInContentArea(editor, e.clientX, e.clientY)) { setRange(CefUtils.renderCaretAtRange(editor, range)); } }); editor.on('click', function (e) { var contentEditableRoot; contentEditableRoot = getContentEditableRoot(e.target); if (contentEditableRoot) { // Prevent clicks on links in a cE=false element if (isContentEditableFalse(contentEditableRoot)) { e.preventDefault(); editor.focus(); } // Removes fake selection if a cE=true is clicked within a cE=false like the toc title if (isContentEditableTrue(contentEditableRoot)) { if (editor.dom.isChildOf(contentEditableRoot, editor.selection.getNode())) { removeContentEditableSelection(); } } } }); editor.on('blur NewBlock', function () { removeContentEditableSelection(); hideFakeCaret(); }); function handleTouchSelect(editor) { var moved = false; editor.on('touchstart', function () { moved = false; }); editor.on('touchmove', function () { moved = true; }); editor.on('touchend', function (e) { var contentEditableRoot = getContentEditableRoot(e.target); if (isContentEditableFalse(contentEditableRoot)) { if (!moved) { e.preventDefault(); setContentEditableSelection(CefUtils.selectNode(editor, contentEditableRoot)); } } }); } var hasNormalCaretPosition = function (elm) { var caretWalker = new CaretWalker(elm); if (!elm.firstChild) { return false; } var startPos = CaretPosition.before(elm.firstChild); var newPos = caretWalker.next(startPos); return newPos && !isBeforeContentEditableFalse(newPos) && !isAfterContentEditableFalse(newPos); }; var isInSameBlock = function (node1, node2) { var block1 = editor.dom.getParent(node1, editor.dom.isBlock); var block2 = editor.dom.getParent(node2, editor.dom.isBlock); return block1 === block2; }; // Checks if the target node is in a block and if that block has a caret position better than the // suggested caretNode this is to prevent the caret from being sucked in towards a cE=false block if // they are adjacent on the vertical axis var hasBetterMouseTarget = function (targetNode, caretNode) { var targetBlock = editor.dom.getParent(targetNode, editor.dom.isBlock); var caretBlock = editor.dom.getParent(caretNode, editor.dom.isBlock); return targetBlock && !isInSameBlock(targetBlock, caretBlock) && hasNormalCaretPosition(targetBlock); }; handleTouchSelect(editor); editor.on('mousedown', function (e) { var contentEditableRoot; if (EditorView.isXYInContentArea(editor, e.clientX, e.clientY) === false) { return; } contentEditableRoot = getContentEditableRoot(e.target); if (contentEditableRoot) { if (isContentEditableFalse(contentEditableRoot)) { e.preventDefault(); setContentEditableSelection(CefUtils.selectNode(editor, contentEditableRoot)); } else { removeContentEditableSelection(); // Check that we're not attempting a shift + click select within a contenteditable='true' element if (!(isContentEditableTrue(contentEditableRoot) && e.shiftKey) && !RangePoint.isXYWithinRange(e.clientX, e.clientY, editor.selection.getRng())) { ElementType.isVoid(Element.fromDom(e.target)) ? editor.selection.select(e.target) : editor.selection.placeCaretAt(e.clientX, e.clientY); } } } else { // Remove needs to be called here since the mousedown might alter the selection without calling selection.setRng // and therefore not fire the AfterSetSelectionRange event. removeContentEditableSelection(); hideFakeCaret(); var caretInfo = LineUtils.closestCaret(rootNode, e.clientX, e.clientY); if (caretInfo) { if (!hasBetterMouseTarget(e.target, caretInfo.node)) { e.preventDefault(); editor.getBody().focus(); setRange(showCaret(1, caretInfo.node, caretInfo.before)); } } } }); editor.on('keypress', function (e) { if (VK.modifierPressed(e)) { return; } switch (e.keyCode) { default: if (isContentEditableFalse(editor.selection.getNode())) { e.preventDefault(); } break; } }); editor.on('getSelectionRange', function (e) { var rng = e.range; if (selectedContentEditableNode) { if (!selectedContentEditableNode.parentNode) { selectedContentEditableNode = null; return; } rng = rng.cloneRange(); rng.selectNode(selectedContentEditableNode); e.range = rng; } }); editor.on('setSelectionRange', function (e) { var rng; rng = setContentEditableSelection(e.range, e.forward); if (rng) { e.range = rng; } }); editor.on('AfterSetSelectionRange', function (e) { var rng = e.range; if (!isRangeInCaretContainer(rng)) { hideFakeCaret(); } if (!isFakeSelectionElement(rng.startContainer.parentNode)) { removeContentEditableSelection(); } }); editor.on('focus', function () { // Make sure we have a proper fake caret on focus Delay.setEditorTimeout(editor, function () { editor.selection.setRng(CefUtils.renderRangeCaret(editor, editor.selection.getRng())); }, 0); }); editor.on('copy', function (e) { var clipboardData = e.clipboardData; // Make sure we get proper html/text for the fake cE=false selection // Doesn't work at all on Edge since it doesn't have proper clipboardData support if (!e.isDefaultPrevented() && e.clipboardData && !Env.ie) { var realSelectionElement = getRealSelectionElement(); if (realSelectionElement) { e.preventDefault(); clipboardData.clearData(); clipboardData.setData('text/html', realSelectionElement.outerHTML); clipboardData.setData('text/plain', realSelectionElement.outerText); } } }); DragDropOverrides.init(editor); } function addCss() { var styles = editor.contentStyles, rootClass = '.mce-content-body'; styles.push(fakeCaret.getCss()); styles.push( rootClass + ' .mce-offscreen-selection {' + 'position: absolute;' + 'left: -9999999999px;' + 'max-width: 1000000px;' + '}' + rootClass + ' *[contentEditable=false] {' + 'cursor: default;' + '}' + rootClass + ' *[contentEditable=true] {' + 'cursor: text;' + '}' ); } function isWithinCaretContainer(node) { return ( CaretContainer.isCaretContainer(node) || CaretContainer.startsWithCaretContainer(node) || CaretContainer.endsWithCaretContainer(node) ); } function isRangeInCaretContainer(rng) { return isWithinCaretContainer(rng.startContainer) || isWithinCaretContainer(rng.endContainer); } function setContentEditableSelection(range, forward) { var node, $ = editor.$, dom = editor.dom, $realSelectionContainer, sel, startContainer, startOffset, endOffset, e, caretPosition, targetClone, origTargetClone; if (!range) { return null; } if (range.collapsed) { if (!isRangeInCaretContainer(range)) { if (forward === false) { caretPosition = getNormalizedRangeEndPoint(-1, range); if (isContentEditableFalse(caretPosition.getNode(true))) { return showCaret(-1, caretPosition.getNode(true), false); } if (isContentEditableFalse(caretPosition.getNode())) { return showCaret(-1, caretPosition.getNode(), !caretPosition.isAtEnd()); } } else { caretPosition = getNormalizedRangeEndPoint(1, range); if (isContentEditableFalse(caretPosition.getNode())) { return showCaret(1, caretPosition.getNode(), !caretPosition.isAtEnd()); } if (isContentEditableFalse(caretPosition.getNode(true))) { return showCaret(1, caretPosition.getNode(true), false); } } } return null; } startContainer = range.startContainer; startOffset = range.startOffset; endOffset = range.endOffset; // Normalizes [] to [] if (startContainer.nodeType == 3 && startOffset == 0 && isContentEditableFalse(startContainer.parentNode)) { startContainer = startContainer.parentNode; startOffset = dom.nodeIndex(startContainer); startContainer = startContainer.parentNode; } if (startContainer.nodeType != 1) { return null; } if (endOffset == startOffset + 1) { node = startContainer.childNodes[startOffset]; } if (!isContentEditableFalse(node)) { return null; } targetClone = origTargetClone = node.cloneNode(true); e = editor.fire('ObjectSelected', { target: node, targetClone: targetClone }); if (e.isDefaultPrevented()) { return null; } $realSelectionContainer = SelectorFind.descendant(Element.fromDom(editor.getBody()), '#' + realSelectionId).fold( function () { return $([]); }, function (elm) { return $([elm.dom()]); } ); targetClone = e.targetClone; if ($realSelectionContainer.length === 0) { $realSelectionContainer = $( '' ).attr('id', realSelectionId); $realSelectionContainer.appendTo(editor.getBody()); } range = editor.dom.createRng(); // WHY is IE making things so hard! Copy on x produces: x // This is a ridiculous hack where we place the selection from a block over the inline element // so that just the inline element is copied as is and not converted. if (targetClone === origTargetClone && Env.ie) { $realSelectionContainer.empty().append('\u00a0
').append(targetClone); range.setStartAfter($realSelectionContainer[0].firstChild.firstChild); range.setEndAfter(targetClone); } else { $realSelectionContainer.empty().append('\u00a0').append(targetClone).append('\u00a0'); range.setStart($realSelectionContainer[0].firstChild, 1); range.setEnd($realSelectionContainer[0].lastChild, 0); } $realSelectionContainer.css({ top: dom.getPos(node, editor.getBody()).y }); $realSelectionContainer[0].focus(); sel = editor.selection.getSel(); sel.removeAllRanges(); sel.addRange(range); Arr.each(SelectorFilter.descendants(Element.fromDom(editor.getBody()), '*[data-mce-selected]'), function (elm) { Attr.remove(elm, 'data-mce-selected'); }); node.setAttribute('data-mce-selected', 1); selectedContentEditableNode = node; hideFakeCaret(); return range; } function removeContentEditableSelection() { if (selectedContentEditableNode) { selectedContentEditableNode.removeAttribute('data-mce-selected'); SelectorFind.descendant(Element.fromDom(editor.getBody()), '#' + realSelectionId).each(Remove.remove); selectedContentEditableNode = null; } } function destroy() { fakeCaret.destroy(); selectedContentEditableNode = null; } function hideFakeCaret() { fakeCaret.hide(); } if (Env.ceFalse) { registerEvents(); addCss(); } return { showCaret: showCaret, showBlockCaretContainer: showBlockCaretContainer, hideFakeCaret: hideFakeCaret, destroy: destroy }; } return SelectionOverrides; } ); /** * Diff.js * * Released under LGPL License. * Copyright (c) 1999-2017 Ephox Corp. All rights reserved * * License: http://www.tinymce.com/license * Contributing: http://www.tinymce.com/contributing */ /** * JS Implementation of the O(ND) Difference Algorithm by Eugene W. Myers. * * @class tinymce.undo.Diff * @private */ define( 'tinymce.core.undo.Diff', [ ], function () { var KEEP = 0, INSERT = 1, DELETE = 2; var diff = function (left, right) { var size = left.length + right.length + 2; var vDown = new Array(size); var vUp = new Array(size); var snake = function (start, end, diag) { return { start: start, end: end, diag: diag }; }; var buildScript = function (start1, end1, start2, end2, script) { var middle = getMiddleSnake(start1, end1, start2, end2); if (middle === null || middle.start === end1 && middle.diag === end1 - end2 || middle.end === start1 && middle.diag === start1 - start2) { var i = start1; var j = start2; while (i < end1 || j < end2) { if (i < end1 && j < end2 && left[i] === right[j]) { script.push([KEEP, left[i]]); ++i; ++j; } else { if (end1 - start1 > end2 - start2) { script.push([DELETE, left[i]]); ++i; } else { script.push([INSERT, right[j]]); ++j; } } } } else { buildScript(start1, middle.start, start2, middle.start - middle.diag, script); for (var i2 = middle.start; i2 < middle.end; ++i2) { script.push([KEEP, left[i2]]); } buildScript(middle.end, end1, middle.end - middle.diag, end2, script); } }; var buildSnake = function (start, diag, end1, end2) { var end = start; while (end - diag < end2 && end < end1 && left[end] === right[end - diag]) { ++end; } return snake(start, end, diag); }; var getMiddleSnake = function (start1, end1, start2, end2) { // Myers Algorithm // Initialisations var m = end1 - start1; var n = end2 - start2; if (m === 0 || n === 0) { return null; } var delta = m - n; var sum = n + m; var offset = (sum % 2 === 0 ? sum : sum + 1) / 2; vDown[1 + offset] = start1; vUp[1 + offset] = end1 + 1; for (var d = 0; d <= offset; ++d) { // Down for (var k = -d; k <= d; k += 2) { // First step var i = k + offset; if (k === -d || k != d && vDown[i - 1] < vDown[i + 1]) { vDown[i] = vDown[i + 1]; } else { vDown[i] = vDown[i - 1] + 1; } var x = vDown[i]; var y = x - start1 + start2 - k; while (x < end1 && y < end2 && left[x] === right[y]) { vDown[i] = ++x; ++y; } // Second step if (delta % 2 != 0 && delta - d <= k && k <= delta + d) { if (vUp[i - delta] <= vDown[i]) { return buildSnake(vUp[i - delta], k + start1 - start2, end1, end2); } } } // Up for (k = delta - d; k <= delta + d; k += 2) { // First step i = k + offset - delta; if (k === delta - d || k != delta + d && vUp[i + 1] <= vUp[i - 1]) { vUp[i] = vUp[i + 1] - 1; } else { vUp[i] = vUp[i - 1]; } x = vUp[i] - 1; y = x - start1 + start2 - k; while (x >= start1 && y >= start2 && left[x] === right[y]) { vUp[i] = x--; y--; } // Second step if (delta % 2 === 0 && -d <= k && k <= d) { if (vUp[i] <= vDown[i + delta]) { return buildSnake(vUp[i], k + start1 - start2, end1, end2); } } } } }; var script = []; buildScript(0, left.length, 0, right.length, script); return script; }; return { KEEP: KEEP, DELETE: DELETE, INSERT: INSERT, diff: diff }; } ); /** * Fragments.js * * Released under LGPL License. * Copyright (c) 1999-2017 Ephox Corp. All rights reserved * * License: http://www.tinymce.com/license * Contributing: http://www.tinymce.com/contributing */ /** * This module reads and applies html fragments from/to dom nodes. * * @class tinymce.undo.Fragments * @private */ define( 'tinymce.core.undo.Fragments', [ "tinymce.core.util.Arr", "tinymce.core.html.Entities", "tinymce.core.undo.Diff" ], function (Arr, Entities, Diff) { var getOuterHtml = function (elm) { if (elm.nodeType === 1) { return elm.outerHTML; } else if (elm.nodeType === 3) { return Entities.encodeRaw(elm.data, false); } else if (elm.nodeType === 8) { return ''; } return ''; }; var createFragment = function (html) { var frag, node, container; container = document.createElement("div"); frag = document.createDocumentFragment(); if (html) { container.innerHTML = html; } while ((node = container.firstChild)) { frag.appendChild(node); } return frag; }; var insertAt = function (elm, html, index) { var fragment = createFragment(html); if (elm.hasChildNodes() && index < elm.childNodes.length) { var target = elm.childNodes[index]; target.parentNode.insertBefore(fragment, target); } else { elm.appendChild(fragment); } }; var removeAt = function (elm, index) { if (elm.hasChildNodes() && index < elm.childNodes.length) { var target = elm.childNodes[index]; target.parentNode.removeChild(target); } }; var applyDiff = function (diff, elm) { var index = 0; Arr.each(diff, function (action) { if (action[0] === Diff.KEEP) { index++; } else if (action[0] === Diff.INSERT) { insertAt(elm, action[1], index); index++; } else if (action[0] === Diff.DELETE) { removeAt(elm, index); } }); }; var read = function (elm) { return Arr.filter(Arr.map(elm.childNodes, getOuterHtml), function (item) { return item.length > 0; }); }; var write = function (fragments, elm) { var currentFragments = Arr.map(elm.childNodes, getOuterHtml); applyDiff(Diff.diff(currentFragments, fragments), elm); return elm; }; return { read: read, write: write }; } ); /** * Levels.js * * Released under LGPL License. * Copyright (c) 1999-2017 Ephox Corp. All rights reserved * * License: http://www.tinymce.com/license * Contributing: http://www.tinymce.com/contributing */ /** * This module handles getting/setting undo levels to/from editor instances. * * @class tinymce.undo.Levels * @private */ define( 'tinymce.core.undo.Levels', [ "tinymce.core.util.Arr", "tinymce.core.undo.Fragments" ], function (Arr, Fragments) { var hasIframes = function (html) { return html.indexOf('') !== -1; }; var createFragmentedLevel = function (fragments) { return { type: 'fragmented', fragments: fragments, content: '', bookmark: null, beforeBookmark: null }; }; var createCompleteLevel = function (content) { return { type: 'complete', fragments: null, content: content, bookmark: null, beforeBookmark: null }; }; var createFromEditor = function (editor) { var fragments, content, trimmedFragments; fragments = Fragments.read(editor.getBody()); trimmedFragments = Arr.map(fragments, function (html) { return editor.serializer.trimContent(html); }); content = trimmedFragments.join(''); return hasIframes(content) ? createFragmentedLevel(trimmedFragments) : createCompleteLevel(content); }; var applyToEditor = function (editor, level, before) { if (level.type === 'fragmented') { Fragments.write(level.fragments, editor.getBody()); } else { editor.setContent(level.content, { format: 'raw' }); } editor.selection.moveToBookmark(before ? level.beforeBookmark : level.bookmark); }; var getLevelContent = function (level) { return level.type === 'fragmented' ? level.fragments.join('') : level.content; }; var isEq = function (level1, level2) { return !!level1 && !!level2 && getLevelContent(level1) === getLevelContent(level2); }; return { createFragmentedLevel: createFragmentedLevel, createCompleteLevel: createCompleteLevel, createFromEditor: createFromEditor, applyToEditor: applyToEditor, isEq: isEq }; } ); /** * UndoManager.js * * Released under LGPL License. * Copyright (c) 1999-2017 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.core.UndoManager', [ "tinymce.core.util.VK", "tinymce.core.util.Tools", "tinymce.core.undo.Levels" ], function (VK, Tools, Levels) { return function (editor) { var self = this, index = 0, data = [], beforeBookmark, isFirstTypedCharacter, locks = 0; var isUnlocked = function () { return locks === 0; }; var setTyping = function (typing) { if (isUnlocked()) { self.typing = typing; } }; function setDirty(state) { editor.setDirty(state); } function addNonTypingUndoLevel(e) { setTyping(false); self.add({}, e); } function endTyping() { if (self.typing) { setTyping(false); self.add(); } } // Add initial undo level when the editor is initialized editor.on('init', function () { self.add(); }); // Get position before an execCommand is processed editor.on('BeforeExecCommand', function (e) { var cmd = e.command; if (cmd !== 'Undo' && cmd !== 'Redo' && cmd !== 'mceRepaint') { endTyping(); self.beforeChange(); } }); // Add undo level after an execCommand call was made editor.on('ExecCommand', function (e) { var cmd = e.command; if (cmd !== 'Undo' && cmd !== 'Redo' && cmd !== 'mceRepaint') { addNonTypingUndoLevel(e); } }); editor.on('ObjectResizeStart Cut', function () { self.beforeChange(); }); editor.on('SaveContent ObjectResized blur', addNonTypingUndoLevel); editor.on('DragEnd', addNonTypingUndoLevel); editor.on('KeyUp', function (e) { var keyCode = e.keyCode; // If key is prevented then don't add undo level // This would happen on keyboard shortcuts for example if (e.isDefaultPrevented()) { return; } if ((keyCode >= 33 && keyCode <= 36) || (keyCode >= 37 && keyCode <= 40) || keyCode === 45 || e.ctrlKey) { addNonTypingUndoLevel(); editor.nodeChanged(); } if (keyCode === 46 || keyCode === 8) { editor.nodeChanged(); } // Fire a TypingUndo/Change event on the first character entered if (isFirstTypedCharacter && self.typing && Levels.isEq(Levels.createFromEditor(editor), data[0]) === false) { if (editor.isDirty() === false) { setDirty(true); 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(); setTyping(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 (isUnlocked()) { beforeBookmark = editor.selection.getBookmark(2, true); } }, /** * Adds a new undo level/snapshot to the undo list. * * @method add * @param {Object} level Optional undo level object to add. * @param {DOMEvent} event Optional event responsible for the creation of the undo level. * @return {Object} Undo level that got added or null it a level wasn't needed. */ add: function (level, event) { var i, settings = editor.settings, lastLevel, currentLevel; currentLevel = Levels.createFromEditor(editor); level = level || {}; level = Tools.extend(level, currentLevel); if (isUnlocked() === false || editor.removed) { return null; } lastLevel = data[index]; if (editor.fire('BeforeAddUndo', { level: level, lastLevel: lastLevel, originalEvent: event }).isDefaultPrevented()) { return null; } // Add undo level if needed if (lastLevel && Levels.isEq(lastLevel, level)) { return null; } // Set before bookmark on previous level if (data[index]) { data[index].beforeBookmark = beforeBookmark; } // Time to compress if (settings.custom_undo_redo_levels) { if (data.length > settings.custom_undo_redo_levels) { for (i = 0; i < data.length - 1; i++) { data[i] = data[i + 1]; } data.length--; index = data.length; } } // Get a non intrusive normalized bookmark level.bookmark = editor.selection.getBookmark(2, true); // Crop array if needed if (index < data.length - 1) { data.length = index + 1; } data.push(level); index = data.length - 1; var args = { level: level, lastLevel: lastLevel, originalEvent: event }; editor.fire('AddUndo', args); if (index > 0) { setDirty(true); editor.fire('change', args); } return level; }, /** * Undoes the last action. * * @method undo * @return {Object} Undo level or null if no undo was performed. */ undo: function () { var level; if (self.typing) { self.add(); self.typing = false; setTyping(false); } if (index > 0) { level = data[--index]; Levels.applyToEditor(editor, level, true); setDirty(true); editor.fire('undo', { level: level }); } return level; }, /** * Redoes the last action. * * @method redo * @return {Object} Redo level or null if no redo was performed. */ redo: function () { var level; if (index < data.length - 1) { level = data[++index]; Levels.applyToEditor(editor, level, false); setDirty(true); editor.fire('redo', { level: level }); } return level; }, /** * Removes all undo levels. * * @method clear */ clear: function () { data = []; index = 0; self.typing = false; self.data = data; editor.fire('ClearUndos'); }, /** * Returns true/false if the undo manager has any undo levels. * * @method hasUndo * @return {Boolean} true/false if the undo manager has any undo levels. */ hasUndo: function () { // Has undo levels or typing and content isn't the same as the initial level return index > 0 || (self.typing && data[0] && !Levels.isEq(Levels.createFromEditor(editor), data[0])); }, /** * Returns true/false if the undo manager has any redo levels. * * @method hasRedo * @return {Boolean} true/false if the undo manager has any redo levels. */ hasRedo: function () { return index < data.length - 1 && !self.typing; }, /** * Executes the specified mutator function as an undo transaction. The selection * before the modification will be stored to the undo stack and if the DOM changes * it will add a new undo level. Any logic within the translation that adds undo levels will * be ignored. So a translation can include calls to execCommand or editor.insertContent. * * @method transact * @param {function} callback Function that gets executed and has dom manipulation logic in it. * @return {Object} Undo level that got added or null it a level wasn't needed. */ transact: function (callback) { endTyping(); self.beforeChange(); self.ignore(callback); return self.add(); }, /** * Executes the specified mutator function as an undo transaction. But without adding an undo level. * Any logic within the translation that adds undo levels will be ignored. So a translation can * include calls to execCommand or editor.insertContent. * * @method ignore * @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. */ ignore: function (callback) { try { locks++; callback(); } finally { locks--; } }, /** * Adds an extra "hidden" undo level by first applying the first mutation and store that to the undo stack * then roll back that change and do the second mutation on top of the stack. This will produce an extra * undo level that the user doesn't see until they undo. * * @method extra * @param {function} callback1 Function that does mutation but gets stored as a "hidden" extra undo level. * @param {function} callback2 Function that does mutation but gets displayed to the user. */ extra: function (callback1, callback2) { var lastLevel, bookmark; if (self.transact(callback1)) { bookmark = data[index].bookmark; lastLevel = data[index - 1]; Levels.applyToEditor(editor, lastLevel, true); if (self.transact(callback2)) { data[index - 1].beforeBookmark = bookmark; } } } }; return self; }; } ); /** * NodePath.js * * Released under LGPL License. * Copyright (c) 1999-2017 Ephox Corp. All rights reserved * * License: http://www.tinymce.com/license * Contributing: http://www.tinymce.com/contributing */ /** * Handles paths of nodes within an element. * * @private * @class tinymce.dom.NodePath */ define( 'tinymce.core.dom.NodePath', [ "tinymce.core.dom.DOMUtils" ], function (DOMUtils) { function create(rootNode, targetNode, normalized) { var path = []; for (; targetNode && targetNode != rootNode; targetNode = targetNode.parentNode) { path.push(DOMUtils.nodeIndex(targetNode, normalized)); } return path; } function resolve(rootNode, path) { var i, node, children; for (node = rootNode, i = path.length - 1; i >= 0; i--) { children = node.childNodes; if (path[i] > children.length - 1) { return null; } node = children[path[i]]; } return node; } return { create: create, resolve: resolve }; } ); /** * Quirks.js * * Released under LGPL License. * Copyright (c) 1999-2017 Ephox Corp. All rights reserved * * License: http://www.tinymce.com/license * Contributing: http://www.tinymce.com/contributing * * @ignore-file */ /** * This file includes fixes for various browser quirks it's made to make it easy to add/remove browser specific fixes. * * @private * @class tinymce.util.Quirks */ define( 'tinymce.core.util.Quirks', [ "tinymce.core.util.VK", "tinymce.core.dom.RangeUtils", "tinymce.core.dom.TreeWalker", "tinymce.core.dom.NodePath", "tinymce.core.html.Node", "tinymce.core.html.Entities", "tinymce.core.Env", "tinymce.core.util.Tools", "tinymce.core.util.Delay", "tinymce.core.caret.CaretContainer", "tinymce.core.caret.CaretPosition", "tinymce.core.caret.CaretWalker" ], function (VK, RangeUtils, TreeWalker, NodePath, Node, Entities, Env, Tools, Delay, CaretContainer, CaretPosition, CaretWalker) { return function (editor) { var each = Tools.each; var BACKSPACE = VK.BACKSPACE, DELETE = VK.DELETE, dom = editor.dom, selection = editor.selection, settings = editor.settings, parser = editor.parser, serializer = editor.serializer; var isGecko = Env.gecko, isIE = Env.ie, isWebKit = Env.webkit; var mceInternalUrlPrefix = 'data:text/mce-internal,'; var mceInternalDataType = isIE ? 'Text' : 'URL'; /** * Executes a command with a specific state this can be to enable/disable browser editing features. */ function setEditorCommandState(cmd, state) { try { editor.getDoc().execCommand(cmd, false, state); } catch (ex) { // Ignore } } /** * Returns current IE document mode. */ function getDocumentMode() { var documentMode = editor.getDoc().documentMode; return documentMode ? documentMode : 6; } /** * Returns true/false if the event is prevented or not. * * @private * @param {Event} e Event object. * @return {Boolean} true/false if the event is prevented or not. */ function isDefaultPrevented(e) { return e.isDefaultPrevented(); } /** * Sets Text/URL data on the event's dataTransfer object to a special data:text/mce-internal url. * This is to workaround the inability to set custom contentType on IE and Safari. * The editor's selected content is encoded into this url so drag and drop between editors will work. * * @private * @param {DragEvent} e Event object */ function setMceInternalContent(e) { var selectionHtml, internalContent; if (e.dataTransfer) { if (editor.selection.isCollapsed() && e.target.tagName == 'IMG') { selection.select(e.target); } selectionHtml = editor.selection.getContent(); // Safari/IE doesn't support custom dataTransfer items so we can only use URL and Text if (selectionHtml.length > 0) { internalContent = mceInternalUrlPrefix + escape(editor.id) + ',' + escape(selectionHtml); e.dataTransfer.setData(mceInternalDataType, internalContent); } } } /** * Gets content of special data:text/mce-internal url on the event's dataTransfer object. * This is to workaround the inability to set custom contentType on IE and Safari. * The editor's selected content is encoded into this url so drag and drop between editors will work. * * @private * @param {DragEvent} e Event object * @returns {String} mce-internal content */ function getMceInternalContent(e) { var internalContent; if (e.dataTransfer) { internalContent = e.dataTransfer.getData(mceInternalDataType); if (internalContent && internalContent.indexOf(mceInternalUrlPrefix) >= 0) { internalContent = internalContent.substr(mceInternalUrlPrefix.length).split(','); return { id: unescape(internalContent[0]), html: unescape(internalContent[1]) }; } } return null; } /** * Inserts contents using the paste clipboard command if it's available if it isn't it will fallback * to the core command. * * @private * @param {String} content Content to insert at selection. * @param {Boolean} internal State if the paste is to be considered internal or external. */ function insertClipboardContents(content, internal) { if (editor.queryCommandSupported('mceInsertClipboardContent')) { editor.execCommand('mceInsertClipboardContent', false, { content: content, internal: internal }); } else { editor.execCommand('mceInsertContent', false, content); } } /** * Makes sure that the editor body becomes empty when backspace or delete is pressed in empty editors. * * For example: *|
* * 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(); editor.selection.select(target); 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)) { // We can't normalize on non collapsed ranges on keyboard events since that would cause // issues with moving the selection over empty paragraphs. See #TINY-1130 if (e.type !== 'keyup' || editor.selection.isCollapsed()) { 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, true); } } }); } function refreshContentEditable() { // No-op since Mozilla seems to have fixed the caret repaint issues } function isHidden() { var sel; if (!isGecko || editor.removed) { return 0; } // Weird, wheres that cursor selection? sel = editor.selection.getSel(); return (!sel || !sel.rangeCount || sel.rangeCount === 0); } // 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) { 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) { removeHrOnBackspace(); focusBody(); removeStylesWhenDeletingAcrossBlockElements(); setGeckoEditingOptions(); addBrAfterLastLinks(); showBrokenImageIcon(); blockCmdArrowNavigation(); disableBackspaceIntoATable(); } return { refreshContentEditable: refreshContentEditable, isHidden: isHidden }; }; } ); /** * InitContentBody.js * * Released under LGPL License. * Copyright (c) 1999-2017 Ephox Corp. All rights reserved * * License: http://www.tinymce.com/license * Contributing: http://www.tinymce.com/contributing */ define( 'tinymce.core.init.InitContentBody', [ 'ephox.sugar.api.dom.Insert', 'ephox.sugar.api.node.Element', 'ephox.sugar.api.properties.Attr', 'global!document', 'global!window', 'tinymce.core.api.Formatter', 'tinymce.core.caret.CaretContainerInput', 'tinymce.core.dom.DOMUtils', 'tinymce.core.dom.Selection', 'tinymce.core.dom.Serializer', 'tinymce.core.EditorUpload', 'tinymce.core.ErrorReporter', 'tinymce.core.ForceBlocks', 'tinymce.core.html.DomParser', 'tinymce.core.html.Node', 'tinymce.core.html.Schema', 'tinymce.core.keyboard.KeyboardOverrides', 'tinymce.core.NodeChange', 'tinymce.core.SelectionOverrides', 'tinymce.core.UndoManager', 'tinymce.core.util.Delay', 'tinymce.core.util.Quirks', 'tinymce.core.util.Tools' ], function ( Insert, Element, Attr, document, window, Formatter, CaretContainerInput, DOMUtils, Selection, Serializer, EditorUpload, ErrorReporter, ForceBlocks, DomParser, Node, Schema, KeyboardOverrides, NodeChange, SelectionOverrides, UndoManager, Delay, Quirks, Tools ) { var DOM = DOMUtils.DOM; var appendStyle = function (editor, text) { var head = Element.fromDom(editor.getDoc().head); var tag = Element.fromTag('style'); Attr.set(tag, 'type', 'text/css'); Insert.append(tag, Element.fromText(text)); Insert.append(head, tag); }; var createParser = function (editor) { var parser = new DomParser(editor.settings, editor.schema); // Convert src and href into data-mce-src, data-mce-href and data-mce-style parser.addAttributeFilter('src,href,style,tabindex', function (nodes, name) { var i = nodes.length, node, dom = editor.dom, value, internalName; while (i--) { node = nodes[i]; value = node.attr(name); internalName = 'data-mce-' + name; // Add internal attribute if we need to we don't on a refresh of the document if (!node.attributes.map[internalName]) { // Don't duplicate these since they won't get modified by any browser if (value.indexOf('data:') === 0 || value.indexOf('blob:') === 0) { continue; } if (name === "style") { value = dom.serializeStyle(dom.parseStyle(value), node.name); if (!value.length) { value = null; } node.attr(internalName, value); node.attr(name, value); } else if (name === "tabindex") { node.attr(internalName, value); node.attr(name, null); } else { node.attr(internalName, editor.convertURL(value, name, node.name)); } } } }); // Keep scripts from executing parser.addNodeFilter('script', function (nodes) { var i = nodes.length, node, type; while (i--) { node = nodes[i]; type = node.attr('type') || 'no/type'; if (type.indexOf('mce-') !== 0) { node.attr('type', 'mce-' + type); } } }); parser.addNodeFilter('#cdata', function (nodes) { var i = nodes.length, node; while (i--) { node = nodes[i]; node.type = 8; node.name = '#comment'; node.value = '[CDATA[' + node.value + ']]'; } }); parser.addNodeFilter('p,h1,h2,h3,h4,h5,h6,div', function (nodes) { var i = nodes.length, node, nonEmptyElements = editor.schema.getNonEmptyElements(); while (i--) { node = nodes[i]; if (node.isEmpty(nonEmptyElements) && node.getAll('br').length === 0) { node.append(new Node('br', 1)).shortEnded = true; } } }); return parser; }; var autoFocus = function (editor) { if (editor.settings.auto_focus) { Delay.setEditorTimeout(editor, function () { var focusEditor; if (editor.settings.auto_focus === true) { focusEditor = editor; } else { focusEditor = editor.editorManager.get(editor.settings.auto_focus); } if (!focusEditor.destroyed) { focusEditor.focus(); } }, 100); } }; var initEditor = function (editor) { editor.bindPendingEventDelegates(); editor.initialized = true; editor.fire('init'); editor.focus(true); editor.nodeChanged({ initial: true }); editor.execCallback('init_instance_callback', editor); autoFocus(editor); }; var getStyleSheetLoader = function (editor) { return editor.inline ? DOM.styleSheetLoader : editor.dom.styleSheetLoader; }; var initContentBody = function (editor, skipWrite) { var settings = editor.settings, targetElm = editor.getElement(), doc = editor.getDoc(), body, contentCssText; // Restore visibility on target element if (!settings.inline) { editor.getElement().style.visibility = editor.orgVisibility; } // Setup iframe body if (!skipWrite && !settings.content_editable) { doc.open(); doc.write(editor.iframeHTML); doc.close(); } if (settings.content_editable) { editor.on('remove', function () { var bodyEl = this.getBody(); DOM.removeClass(bodyEl, 'mce-content-body'); DOM.removeClass(bodyEl, 'mce-edit-focus'); DOM.setAttrib(bodyEl, 'contentEditable', null); }); DOM.addClass(targetElm, 'mce-content-body'); editor.contentDocument = doc = settings.content_document || document; editor.contentWindow = settings.content_window || window; editor.bodyElement = targetElm; // Prevent leak in IE settings.content_document = settings.content_window = null; // TODO: Fix this settings.root_name = targetElm.nodeName.toLowerCase(); } // It will not steal focus while setting contentEditable body = editor.getBody(); body.disabled = true; editor.readonly = settings.readonly; if (!editor.readonly) { if (editor.inline && DOM.getStyle(body, 'position', true) === 'static') { body.style.position = 'relative'; } body.contentEditable = editor.getParam('content_editable_state', true); } body.disabled = false; editor.editorUpload = new EditorUpload(editor); editor.schema = new Schema(settings); editor.dom = new DOMUtils(doc, { keep_values: true, url_converter: editor.convertURL, url_converter_scope: editor, hex_colors: settings.force_hex_style_colors, class_filter: settings.class_filter, update_styles: true, root_element: editor.inline ? editor.getBody() : null, collect: settings.content_editable, schema: editor.schema, onSetAttrib: function (e) { editor.fire('SetAttrib', e); } }); editor.parser = createParser(editor); editor.serializer = new Serializer(settings, editor); editor.selection = new Selection(editor.dom, editor.getWin(), editor.serializer, editor); editor.formatter = new Formatter(editor); editor.undoManager = new UndoManager(editor); editor._nodeChangeDispatcher = new NodeChange(editor); editor._selectionOverrides = new SelectionOverrides(editor); CaretContainerInput.setup(editor); KeyboardOverrides.setup(editor); ForceBlocks.setup(editor); editor.fire('PreInit'); if (!settings.browser_spellcheck && !settings.gecko_spellcheck) { doc.body.spellcheck = false; // Gecko DOM.setAttrib(body, "spellcheck", "false"); } editor.quirks = new Quirks(editor); editor.fire('PostRender'); if (settings.directionality) { body.dir = settings.directionality; } if (settings.nowrap) { body.style.whiteSpace = "nowrap"; } if (settings.protect) { editor.on('BeforeSetContent', function (e) { Tools.each(settings.protect, function (pattern) { e.content = e.content.replace(pattern, function (str) { return ''; }); }); }); } editor.on('SetContent', function () { editor.addVisual(editor.getBody()); }); // Remove empty contents if (settings.padd_empty_editor) { editor.on('PostProcess', function (e) { e.content = e.content.replace(/^(]*>( | |\s|\u00a0|
|)<\/p>[\r\n]*|
[\r\n]*)$/, '');
});
}
editor.load({ initial: true, format: 'html' });
editor.startContent = editor.getContent({ format: 'raw' });
editor.on('compositionstart compositionend', function (e) {
editor.composing = e.type === 'compositionstart';
});
// Add editor specific CSS styles
if (editor.contentStyles.length > 0) {
contentCssText = '';
Tools.each(editor.contentStyles, function (style) {
contentCssText += style + "\r\n";
});
editor.dom.addStyle(contentCssText);
}
getStyleSheetLoader(editor).loadAll(
editor.contentCSS,
function (_) {
initEditor(editor);
},
function (urls) {
initEditor(editor);
}
);
// Append specified content CSS last
if (settings.content_style) {
appendStyle(editor, settings.content_style);
}
};
return {
initContentBody: initContentBody
};
}
);
/**
* PluginManager.js
*
* Released under LGPL License.
* Copyright (c) 1999-2017 Ephox Corp. All rights reserved
*
* License: http://www.tinymce.com/license
* Contributing: http://www.tinymce.com/contributing
*/
define(
'tinymce.core.PluginManager',
[
'tinymce.core.AddOnManager'
],
function (AddOnManager) {
return AddOnManager.PluginManager;
}
);
/**
* ThemeManager.js
*
* Released under LGPL License.
* Copyright (c) 1999-2017 Ephox Corp. All rights reserved
*
* License: http://www.tinymce.com/license
* Contributing: http://www.tinymce.com/contributing
*/
define(
'tinymce.core.ThemeManager',
[
'tinymce.core.AddOnManager'
],
function (AddOnManager) {
return AddOnManager.ThemeManager;
}
);
/**
* Init.js
*
* Released under LGPL License.
* Copyright (c) 1999-2017 Ephox Corp. All rights reserved
*
* License: http://www.tinymce.com/license
* Contributing: http://www.tinymce.com/contributing
*/
define(
'tinymce.core.init.Init',
[
'global!document',
'global!window',
'tinymce.core.dom.DOMUtils',
'tinymce.core.Env',
'tinymce.core.init.InitContentBody',
'tinymce.core.PluginManager',
'tinymce.core.ThemeManager',
'tinymce.core.util.Tools',
'tinymce.core.util.Uuid'
],
function (document, window, DOMUtils, Env, InitContentBody, PluginManager, ThemeManager, Tools, Uuid) {
var DOM = DOMUtils.DOM;
var initPlugin = function (editor, initializedPlugins, plugin) {
var Plugin = PluginManager.get(plugin), pluginUrl, pluginInstance;
pluginUrl = PluginManager.urls[plugin] || editor.documentBaseUrl.replace(/\/$/, '');
plugin = Tools.trim(plugin);
if (Plugin && Tools.inArray(initializedPlugins, plugin) === -1) {
Tools.each(PluginManager.dependencies(plugin), function (dep) {
initPlugin(editor, initializedPlugins, dep);
});
if (editor.plugins[plugin]) {
return;
}
pluginInstance = new Plugin(editor, pluginUrl, editor.$);
editor.plugins[plugin] = pluginInstance;
if (pluginInstance.init) {
pluginInstance.init(editor, pluginUrl);
initializedPlugins.push(plugin);
}
}
};
var trimLegacyPrefix = function (name) {
// Themes and plugins can be prefixed with - to prevent them from being lazy loaded
return name.replace(/^\-/, '');
};
var initPlugins = function (editor) {
var initializedPlugins = [];
Tools.each(editor.settings.plugins.split(/[ ,]/), function (name) {
initPlugin(editor, initializedPlugins, trimLegacyPrefix(name));
});
};
var initTheme = function (editor) {
var Theme, settings = editor.settings;
if (settings.theme) {
if (typeof settings.theme != "function") {
settings.theme = trimLegacyPrefix(settings.theme);
Theme = ThemeManager.get(settings.theme);
editor.theme = new Theme(editor, ThemeManager.urls[settings.theme]);
if (editor.theme.init) {
editor.theme.init(editor, ThemeManager.urls[settings.theme] || editor.documentBaseUrl.replace(/\/$/, ''), editor.$);
}
} else {
editor.theme = settings.theme;
}
}
};
var measueBox = function (editor) {
var w, h, minHeight, re, o, settings = editor.settings, elm = editor.getElement();
// Measure box
if (settings.render_ui && editor.theme) {
editor.orgDisplay = elm.style.display;
if (typeof settings.theme != "function") {
w = settings.width || DOM.getStyle(elm, 'width') || '100%';
h = settings.height || DOM.getStyle(elm, '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 = editor.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(editor, elm);
if (o.editorContainer.nodeType) {
o.editorContainer.id = o.editorContainer.id || editor.id + "_parent";
}
if (o.iframeContainer.nodeType) {
o.iframeContainer.id = o.iframeContainer.id || editor.id + "_iframecontainer";
}
// Use specified iframe height or the targets offsetHeight
h = o.iframeHeight || elm.offsetHeight;
}
editor.editorContainer = o.editorContainer;
o.height = h;
}
return o;
};
var relaxDomain = function (editor, ifr) {
// Domain relaxing is required since the user has messed around with document.domain
// This only applies to IE 11 other browsers including Edge seems to handle document.domain
if (document.domain !== window.location.hostname && Env.ie && Env.ie < 12) {
var bodyUuid = Uuid.uuid('mce');
editor[bodyUuid] = function () {
InitContentBody.initContentBody(editor);
};
/*eslint no-script-url:0 */
var domainRelaxUrl = 'javascript:(function(){' +
'document.open();document.domain="' + document.domain + '";' +
'var ed = window.parent.tinymce.get("' + editor.id + '");document.write(ed.iframeHTML);' +
'document.close();ed.' + bodyUuid + '(true);})()';
DOM.setAttrib(ifr, 'src', domainRelaxUrl);
return true;
}
return false;
};
var createIframe = function (editor, o) {
var settings = editor.settings, bodyId, bodyClass;
editor.iframeHTML = settings.doctype + '
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 () { Render.render(this); }, /** * 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) { EditorFocus.focus(this, skipFocus); }, /** * 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) { if (text && Tools.is(text, 'string')) { var lang = this.settings.language || 'en', i18n = this.editorManager.i18n; text = i18n.data[lang + '.' + text] || text.replace(/\{\#([^\}]+)\}/g, function (a, b) { return i18n.data[lang + '.' + b] || '{#' + b + '}'; }); } return this.editorManager.translate(text); }, /** * Returns a language pack item by name/key. * * @method getLang * @param {String} name Name/key to get from the language pack. * @param {String} defaultVal Optional default value to retrieve. */ getLang: function (name, defaultVal) { return ( this.editorManager.i18n.data[(this.settings.language || 'en') + '.' + name] || (defaultVal !== undefined ? defaultVal : '{#' + name + '}') ); }, /** * Returns a configuration parameter by name. * * @method getParam * @param {String} name Configruation parameter to retrieve. * @param {String} defaultVal Optional default value to return. * @param {String} type Optional type parameter. * @return {String} Configuration parameter value or default value. * @example * // Returns a specific config value from the currently active editor * var someval = tinymce.activeEditor.getParam('myvalue'); * * // Returns a specific config value from a specific editor instance by id * var someval2 = tinymce.get('my_editor').getParam('myvalue'); */ getParam: function (name, defaultVal, type) { var value = name in this.settings ? this.settings[name] : defaultVal, output; if (type === 'hash') { output = {}; if (typeof value === 'string') { each(value.indexOf('=') > 0 ? value.split(/[;,](?![^=;,]*(?:[;,]|$))/) : value.split(','), function (value) { value = value.split('='); if (value.length > 1) { output[trim(value[0])] = trim(value[1]); } else { output[trim(value[0])] = trim(value); } }); } else { output = value; } return output; } return value; }, /** * Dispatches out a onNodeChange event to all observers. This method should be called when you * need to update the UI states or element path etc. * * @method nodeChanged * @param {Object} args Optional args to pass to NodeChange event handlers. */ nodeChanged: function (args) { this._nodeChangeDispatcher.nodeChanged(args); }, /** * Adds a button that later gets created by the theme in the editors toolbars. * * @method addButton * @param {String} name Button name to add. * @param {Object} settings Settings object with title, cmd etc. * @example * // Adds a custom button to the editor that inserts contents when clicked * tinymce.init({ * ... * * toolbar: 'example' * * setup: function(ed) { * ed.addButton('example', { * title: 'My title', * image: '../js/tinymce/plugins/example/img/example.gif', * onclick: function() { * ed.insertContent('Hello world!!'); * } * }); * } * }); */ addButton: function (name, settings) { var self = this; if (settings.cmd) { settings.onclick = function () { self.execCommand(settings.cmd); }; } if (!settings.text && !settings.icon) { settings.icon = name; } self.buttons = self.buttons || {}; settings.tooltip = settings.tooltip || settings.title; self.buttons[name] = settings; }, /** * Adds a sidebar for the editor instance. * * @method addSidebar * @param {String} name Sidebar name to add. * @param {Object} settings Settings object with icon, onshow etc. * @example * // Adds a custom sidebar that when clicked logs the panel element * tinymce.init({ * ... * setup: function(ed) { * ed.addSidebar('example', { * tooltip: 'My sidebar', * icon: 'my-side-bar', * onshow: function(api) { * console.log(api.element()); * } * }); * } * }); */ addSidebar: function (name, settings) { return Sidebar.add(this, name, settings); }, /** * Adds a menu item to be used in the menus of the theme. There might be multiple instances * of this menu item for example it might be used in the main menus of the theme but also in * the context menu so make sure that it's self contained and supports multiple instances. * * @method addMenuItem * @param {String} name Menu item name to add. * @param {Object} settings Settings object with title, cmd etc. * @example * // Adds a custom menu item to the editor that inserts contents when clicked * // The context option allows you to add the menu item to an existing default menu * tinymce.init({ * ... * * setup: function(ed) { * ed.addMenuItem('example', { * text: 'My menu item', * context: 'tools', * onclick: function() { * ed.insertContent('Hello world!!'); * } * }); * } * }); */ addMenuItem: function (name, settings) { var self = this; if (settings.cmd) { settings.onclick = function () { self.execCommand(settings.cmd); }; } self.menuItems = self.menuItems || {}; self.menuItems[name] = settings; }, /** * Adds a contextual toolbar to be rendered when the selector matches. * * @method addContextToolbar * @param {function/string} predicate Predicate that needs to return true if provided strings get converted into CSS predicates. * @param {String/Array} items String or array with items to add to the context toolbar. */ addContextToolbar: function (predicate, items) { var self = this, selector; self.contextToolbars = self.contextToolbars || []; // Convert selector to predicate if (typeof predicate == "string") { selector = predicate; predicate = function (elm) { return self.dom.is(elm, selector); }; } self.contextToolbars.push({ id: Uuid.uuid('mcet'), predicate: predicate, items: items }); }, /** * Adds a custom command to the editor, you can also override existing commands with this method. * The command that you add can be executed with execCommand. * * @method addCommand * @param {String} name Command name to add/override. * @param {addCommandCallback} callback Function to execute when the command occurs. * @param {Object} scope Optional scope to execute the function in. * @example * // Adds a custom command that later can be executed using execCommand * tinymce.init({ * ... * * setup: function(ed) { * // Register example command * ed.addCommand('mycommand', function(ui, v) { * ed.windowManager.alert('Hello world!! Selection: ' + ed.selection.getContent({format: 'text'})); * }); * } * }); */ addCommand: function (name, callback, scope) { /** * Callback function that gets called when a command is executed. * * @callback addCommandCallback * @param {Boolean} ui Display UI state true/false. * @param {Object} value Optional value for command. * @return {Boolean} True/false state if the command was handled or not. */ this.editorCommands.addCommand(name, callback, scope); }, /** * Adds a custom query state command to the editor, you can also override existing commands with this method. * The command that you add can be executed with queryCommandState function. * * @method addQueryStateHandler * @param {String} name Command name to add/override. * @param {addQueryStateHandlerCallback} callback Function to execute when the command state retrieval occurs. * @param {Object} scope Optional scope to execute the function in. */ addQueryStateHandler: function (name, callback, scope) { /** * Callback function that gets called when a queryCommandState is executed. * * @callback addQueryStateHandlerCallback * @return {Boolean} True/false state if the command is enabled or not like is it bold. */ this.editorCommands.addQueryStateHandler(name, callback, scope); }, /** * Adds a custom query value command to the editor, you can also override existing commands with this method. * The command that you add can be executed with queryCommandValue function. * * @method addQueryValueHandler * @param {String} name Command name to add/override. * @param {addQueryValueHandlerCallback} callback Function to execute when the command value retrieval occurs. * @param {Object} scope Optional scope to execute the function in. */ addQueryValueHandler: function (name, callback, scope) { /** * Callback function that gets called when a queryCommandValue is executed. * * @callback addQueryValueHandlerCallback * @return {Object} Value of the command or undefined. */ this.editorCommands.addQueryValueHandler(name, callback, scope); }, /** * Adds a keyboard shortcut for some command or function. * * @method addShortcut * @param {String} pattern Shortcut pattern. Like for example: ctrl+alt+o. * @param {String} desc Text description for the command. * @param {String/Function} cmdFunc Command name string or function to execute when the key is pressed. * @param {Object} sc Optional scope to execute the function in. * @return {Boolean} true/false state if the shortcut was added or not. */ addShortcut: function (pattern, desc, cmdFunc, scope) { this.shortcuts.add(pattern, desc, cmdFunc, scope); }, /** * Executes a command on the current instance. These commands can be TinyMCE internal commands prefixed with "mce" or * they can be build in browser commands such as "Bold". A compleate list of browser commands is available on MSDN or Mozilla.org. * This function will dispatch the execCommand function on each plugin, theme or the execcommand_callback option if none of these * return true it will handle the command as a internal browser command. * * @method execCommand * @param {String} cmd Command name to execute, for example mceLink or Bold. * @param {Boolean} ui True/false state if a UI (dialog) should be presented or not. * @param {mixed} value Optional command value, this can be anything. * @param {Object} args Optional arguments object. */ execCommand: function (cmd, ui, value, args) { return this.editorCommands.execCommand(cmd, ui, value, args); }, /** * Returns a command specific state, for example if bold is enabled or not. * * @method queryCommandState * @param {string} cmd Command to query state from. * @return {Boolean} Command specific state, for example if bold is enabled or not. */ queryCommandState: function (cmd) { return this.editorCommands.queryCommandState(cmd); }, /** * Returns a command specific value, for example the current font size. * * @method queryCommandValue * @param {string} cmd Command to query value from. * @return {Object} Command specific value, for example the current font size. */ queryCommandValue: function (cmd) { return this.editorCommands.queryCommandValue(cmd); }, /** * Returns true/false if the command is supported or not. * * @method queryCommandSupported * @param {String} cmd Command that we check support for. * @return {Boolean} true/false if the command is supported or not. */ queryCommandSupported: function (cmd) { return this.editorCommands.queryCommandSupported(cmd); }, /** * Shows the editor and hides any textarea/div that the editor is supposed to replace. * * @method show */ show: function () { var self = this; if (self.hidden) { self.hidden = false; if (self.inline) { self.getBody().contentEditable = true; } else { DOM.show(self.getContainer()); DOM.hide(self.id); } self.load(); self.fire('show'); } }, /** * Hides the editor and shows any textarea/div that the editor is supposed to replace. * * @method hide */ hide: function () { var self = this, doc = self.getDoc(); if (!self.hidden) { // Fixed bug where IE has a blinking cursor left from the editor if (ie && doc && !self.inline) { doc.execCommand('SelectAll'); } // We must save before we hide so Safari doesn't crash self.save(); if (self.inline) { self.getBody().contentEditable = false; // Make sure the editor gets blurred if (self == self.editorManager.focusedEditor) { self.editorManager.focusedEditor = null; } } else { DOM.hide(self.getContainer()); DOM.setStyle(self.id, 'display', self.orgDisplay); } self.hidden = true; self.fire('hide'); } }, /** * Returns true/false if the editor is hidden or not. * * @method isHidden * @return {Boolean} True/false if the editor is hidden or not. */ isHidden: function () { return !!this.hidden; }, /** * Sets the progress state, this will display a throbber/progess for the editor. * This is ideal for asynchronous operations like an AJAX save call. * * @method setProgressState * @param {Boolean} state Boolean state if the progress should be shown or hidden. * @param {Number} time Optional time to wait before the progress gets shown. * @return {Boolean} Same as the input state. * @example * // Show progress for the active editor * tinymce.activeEditor.setProgressState(true); * * // Hide progress for the active editor * tinymce.activeEditor.setProgressState(false); * * // Show progress after 3 seconds * tinymce.activeEditor.setProgressState(true, 3000); */ setProgressState: function (state, time) { this.fire('ProgressState', { state: state, time: time }); }, /** * Loads contents from the textarea or div element that got converted into an editor instance. * This method will move the contents from that textarea or div into the editor by using setContent * so all events etc that method has will get dispatched as well. * * @method load * @param {Object} args Optional content object, this gets passed around through the whole load process. * @return {String} HTML string that got set into the editor. */ load: function (args) { var self = this, elm = self.getElement(), html; if (self.removed) { return ''; } 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 || self.removed) { 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 ? '' : '