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.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); return DOMUtils; }); // Included from: js/tinymce/classes/dom/ScriptLoader.js /** * ScriptLoader.js * * Copyright, Moxiecode Systems AB * Released under LGPL License. * * License: http://www.tinymce.com/license * Contributing: http://www.tinymce.com/contributing */ /*globals console*/ /** * This class handles asynchronous/synchronous loading of JavaScript files it will execute callbacks * when various items gets loaded. This class is useful to load external JavaScript files. * * @class tinymce.dom.ScriptLoader * @example * // Load a script from a specific URL using the global script loader * tinymce.ScriptLoader.load('somescript.js'); * * // Load a script using a unique instance of the script loader * var scriptLoader = new tinymce.dom.ScriptLoader(); * * scriptLoader.load('somescript.js'); * * // Load multiple scripts * var scriptLoader = new tinymce.dom.ScriptLoader(); * * scriptLoader.add('somescript1.js'); * scriptLoader.add('somescript2.js'); * scriptLoader.add('somescript3.js'); * * scriptLoader.loadQueue(function() { * alert('All scripts are now loaded.'); * }); */ define("tinymce/dom/ScriptLoader", [ "tinymce/dom/DOMUtils", "tinymce/util/Tools" ], function(DOMUtils, Tools) { var DOM = DOMUtils.DOM; var each = Tools.each, grep = Tools.grep; function ScriptLoader() { var QUEUED = 0, LOADING = 1, LOADED = 2, states = {}, queue = [], scriptLoadedCallbacks = {}, queueLoadedCallbacks = [], loading = 0, undef; /** * Loads a specific script directly without adding it to the load queue. * * @method load * @param {String} url Absolute URL to script to add. * @param {function} callback Optional callback function to execute ones this script gets loaded. * @param {Object} scope Optional scope to execute callback in. */ function loadScript(url, callback) { var dom = DOM, elm, id; // Execute callback when script is loaded function done() { dom.remove(id); if (elm) { elm.onreadystatechange = elm.onload = elm = null; } callback(); } function error() { /*eslint no-console:0 */ // Report the error so it's easier for people to spot loading errors if (typeof console !== "undefined" && console.log) { console.log("Failed to load: " + url); } // We can't mark it as done if there is a load error since // A) We don't want to produce 404 errors on the server and // B) the onerror event won't fire on all browsers. // done(); } id = dom.uniqueId(); // Create new script element elm = document.createElement('script'); elm.id = id; elm.type = 'text/javascript'; elm.src = Tools._addCacheSuffix(url); // Seems that onreadystatechange works better on IE 10 onload seems to fire incorrectly if ("onreadystatechange" in elm) { elm.onreadystatechange = function() { if (/loaded|complete/.test(elm.readyState)) { done(); } }; } else { elm.onload = done; } // Add onerror event will get fired on some browsers but not all of them elm.onerror = error; // Add script to document (document.getElementsByTagName('head')[0] || document.body).appendChild(elm); } /** * Returns true/false if a script has been loaded or not. * * @method isDone * @param {String} url URL to check for. * @return {Boolean} true/false if the URL is loaded. */ this.isDone = function(url) { return states[url] == LOADED; }; /** * Marks a specific script to be loaded. This can be useful if a script got loaded outside * the script loader or to skip it from loading some script. * * @method markDone * @param {string} u Absolute URL to the script to mark as loaded. */ this.markDone = function(url) { states[url] = LOADED; }; /** * Adds a specific script to the load queue of the script loader. * * @method add * @param {String} url Absolute URL to script to add. * @param {function} callback Optional callback function to execute ones this script gets loaded. * @param {Object} scope Optional scope to execute callback in. */ this.add = this.load = function(url, callback, scope) { var state = states[url]; // Add url to load queue if (state == undef) { queue.push(url); states[url] = QUEUED; } if (callback) { // Store away callback for later execution if (!scriptLoadedCallbacks[url]) { scriptLoadedCallbacks[url] = []; } scriptLoadedCallbacks[url].push({ func: callback, scope: scope || this }); } }; /** * Starts the loading of the queue. * * @method loadQueue * @param {function} callback Optional callback to execute when all queued items are loaded. * @param {Object} scope Optional scope to execute the callback in. */ this.loadQueue = function(callback, scope) { this.loadScripts(queue, callback, scope); }; /** * Loads the specified queue of files and executes the callback ones they are loaded. * This method is generally not used outside this class but it might be useful in some scenarios. * * @method loadScripts * @param {Array} scripts Array of queue items to load. * @param {function} callback Optional callback to execute ones all items are loaded. * @param {Object} scope Optional scope to execute callback in. */ this.loadScripts = function(scripts, callback, scope) { var loadScripts; function execScriptLoadedCallbacks(url) { // Execute URL callback functions each(scriptLoadedCallbacks[url], function(callback) { callback.func.call(callback.scope); }); scriptLoadedCallbacks[url] = undef; } queueLoadedCallbacks.push({ func: callback, scope: scope || this }); loadScripts = function() { var loadingScripts = grep(scripts); // Current scripts has been handled scripts.length = 0; // Load scripts that needs to be loaded each(loadingScripts, function(url) { // Script is already loaded then execute script callbacks directly if (states[url] == LOADED) { execScriptLoadedCallbacks(url); return; } // Is script not loading then start loading it if (states[url] != LOADING) { states[url] = LOADING; loading++; loadScript(url, function() { states[url] = LOADED; loading--; execScriptLoadedCallbacks(url); // Load more scripts if they where added by the recently loaded script loadScripts(); }); } }); // No scripts are currently loading then execute all pending queue loaded callbacks if (!loading) { each(queueLoadedCallbacks, function(callback) { callback.func.call(callback.scope); }); queueLoadedCallbacks.length = 0; } }; loadScripts(); }; } ScriptLoader.ScriptLoader = new ScriptLoader(); return ScriptLoader; }); // Included from: js/tinymce/classes/AddOnManager.js /** * AddOnManager.js * * Copyright, Moxiecode Systems AB * Released under LGPL License. * * License: http://www.tinymce.com/license * Contributing: http://www.tinymce.com/contributing */ /** * This class handles the loading of themes/plugins or other add-ons and their language packs. * * @class tinymce.AddOnManager */ define("tinymce/AddOnManager", [ "tinymce/dom/ScriptLoader", "tinymce/util/Tools" ], function(ScriptLoader, Tools) { var each = Tools.each; function AddOnManager() { var self = this; self.items = []; self.urls = {}; self.lookup = {}; } AddOnManager.prototype = { /** * Returns the specified add on by the short name. * * @method get * @param {String} name Add-on to look for. * @return {tinymce.Theme/tinymce.Plugin} Theme or plugin add-on instance or undefined. */ get: function(name) { if (this.lookup[name]) { return this.lookup[name].instance; } else { 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; }, createUrl: function(baseUrl, dep) { if (typeof dep === "object") { return dep; } else { return {prefix: baseUrl.prefix, resource: dep, suffix: baseUrl.suffix}; } }, /** * Add a set of components that will make up the add-on. Using the url of the add-on name as the base url. * This should be used in development mode. A new compressor/javascript munger process will ensure that the * components are put together into the plugin.js file and compressed correctly. * * @method addComponents * @param {String} pluginName name of the plugin to load scripts from (will be used to get the base url for the plugins). * @param {Array} scripts Array containing the names of the scripts to load. */ addComponents: function(pluginName, scripts) { var pluginUrl = this.urls[pluginName]; each(scripts, function(script) { ScriptLoader.ScriptLoader.add(pluginUrl + "/" + script); }); }, /** * Loads an add-on from a specific url. * * @method load * @param {String} name Short name of the add-on that gets loaded. * @param {String} addOnUrl URL to the add-on that will get loaded. * @param {function} callback Optional callback to execute ones the add-on is loaded. * @param {Object} scope Optional scope to execute the callback in. * @example * // Loads a plugin from an external URL * tinymce.PluginManager.load('myplugin', '/some/dir/someplugin/plugin.js'); * * // Initialize TinyMCE * tinymce.init({ * ... * plugins: '-myplugin' // Don't try to load it again * }); */ load: function(name, addOnUrl, callback, scope) { var self = this, url = addOnUrl; function loadDependencies() { var dependencies = self.dependencies(name); each(dependencies, function(dep) { var newUrl = self.createUrl(addOnUrl, dep); self.load(newUrl.resource, newUrl, undefined, undefined); }); if (callback) { if (scope) { callback.call(scope); } else { callback.call(ScriptLoader); } } } if (self.urls[name]) { return; } if (typeof addOnUrl === "object") { url = addOnUrl.prefix + addOnUrl.resource + addOnUrl.suffix; } if (url.indexOf('/') !== 0 && url.indexOf('://') == -1) { url = AddOnManager.baseURL + '/' + url; } self.urls[name] = url.substring(0, url.lastIndexOf('/')); if (self.lookup[name]) { loadDependencies(); } else { ScriptLoader.ScriptLoader.add(url, loadDependencies, scope); } } }; AddOnManager.PluginManager = new AddOnManager(); AddOnManager.ThemeManager = new AddOnManager(); return AddOnManager; }); /** * TinyMCE theme class. * * @class tinymce.Theme */ /** * This method is responsible for rendering/generating the overall user interface with toolbars, buttons, iframe containers etc. * * @method renderUI * @param {Object} obj Object parameter containing the targetNode DOM node that will be replaced visually with an editor instance. * @return {Object} an object with items like iframeContainer, editorContainer, sizeContainer, deltaWidth, deltaHeight. */ /** * Plugin base class, this is a pseudo class that describes how a plugin is to be created for TinyMCE. The methods below are all optional. * * @class tinymce.Plugin * @example * tinymce.PluginManager.add('example', function(editor, url) { * // Add a button that opens a window * editor.addButton('example', { * text: 'My button', * icon: false, * onclick: function() { * // Open window * editor.windowManager.open({ * title: 'Example plugin', * body: [ * {type: 'textbox', name: 'title', label: 'Title'} * ], * onsubmit: function(e) { * // Insert content when the window form is submitted * editor.insertContent('Title: ' + e.data.title); * } * }); * } * }); * * // Adds a menu item to the tools menu * editor.addMenuItem('example', { * text: 'Example plugin', * context: 'tools', * onclick: function() { * // Open window with a specific url * editor.windowManager.open({ * title: 'TinyMCE site', * url: 'http://www.tinymce.com', * width: 800, * height: 600, * buttons: [{ * text: 'Close', * onclick: 'close' * }] * }); * } * }); * }); */ // Included from: js/tinymce/classes/dom/RangeUtils.js /** * RangeUtils.js * * Copyright, Moxiecode Systems AB * Released under LGPL License. * * License: http://www.tinymce.com/license * Contributing: http://www.tinymce.com/contributing */ /** * This class contains a few utility methods for ranges. * * @class tinymce.dom.RangeUtils */ define("tinymce/dom/RangeUtils", [ "tinymce/util/Tools", "tinymce/dom/TreeWalker" ], function(Tools, TreeWalker) { var each = Tools.each; function getEndChild(container, index) { var childNodes = container.childNodes; index--; if (index > childNodes.length - 1) { index = childNodes.length - 1; } else if (index < 0) { index = 0; } return childNodes[index] || container; } function RangeUtils(dom) { /** * Walks the specified range like object and executes the callback for each sibling collection it finds. * * @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.mce-item-selected,th.mce-item-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. * @return {Array} Array of collected siblings. */ function collectSiblings(node, name, end_node) { var siblings = []; for (; node && node != end_node; node = node[name]) { siblings.push(node); } return siblings; } /** * Find an end point this is the node just before the common ancestor root. * * @private * @param {Node} node Node to start at. * @param {Node} root Root/ancestor element to stop just before. * @return {Node} Node just before the root element. */ function findEndPoint(node, root) { do { if (node.parentNode == root) { return node; } node = node.parentNode; } while (node); } function walkBoundary(start_node, end_node, next) { var siblingName = next ? 'nextSibling' : 'previousSibling'; for (node = start_node, parent = node.parentNode; node && node != end_node; node = parent) { parent = node.parentNode; siblings = collectSiblings(node == start_node ? node : node[siblingName], siblingName); if (siblings.length) { if (!next) { siblings.reverse(); } callback(exclude(siblings)); } } } // If index based start position then resolve it if (startContainer.nodeType == 1 && startContainer.hasChildNodes()) { startContainer = startContainer.childNodes[startOffset]; } // If index based end position then resolve it if (endContainer.nodeType == 1 && endContainer.hasChildNodes()) { endContainer = getEndChild(endContainer, endOffset); } // Same container if (startContainer == endContainer) { return callback(exclude([startContainer])); } // Find common ancestor and end points ancestor = dom.findCommonAncestor(startContainer, endContainer); // Process left side for (node = startContainer; node; node = node.parentNode) { if (node === endContainer) { return walkBoundary(startContainer, ancestor, true); } if (node === ancestor) { break; } } // Process right side for (node = endContainer; node; node = node.parentNode) { if (node === startContainer) { return walkBoundary(endContainer, ancestor); } if (node === ancestor) { break; } } // Find start/end point startPoint = findEndPoint(startContainer, ancestor) || startContainer; endPoint = findEndPoint(endContainer, ancestor) || endContainer; // Walk left leaf walkBoundary(startContainer, startPoint, true); // Walk the middle from start to end point siblings = collectSiblings( startPoint == startContainer ? startPoint : startPoint.nextSibling, 'nextSibling', endPoint == endContainer ? endPoint.nextSibling : endPoint ); if (siblings.length) { callback(exclude(siblings)); } // Walk right leaf walkBoundary(endContainer, endPoint); }; /** * Splits the specified range at it's start/end points. * * @private * @param {Range/RangeObject} rng Range to split. * @return {Object} Range position object. */ this.split = function(rng) { var startContainer = rng.startContainer, startOffset = rng.startOffset, endContainer = rng.endContainer, endOffset = rng.endOffset; function splitText(node, offset) { return node.splitText(offset); } // Handle single text node if (startContainer == endContainer && startContainer.nodeType == 3) { if (startOffset > 0 && startOffset < startContainer.nodeValue.length) { endContainer = splitText(startContainer, startOffset); startContainer = endContainer.previousSibling; if (endOffset > startOffset) { endOffset = endOffset - startOffset; startContainer = endContainer = splitText(endContainer, endOffset).previousSibling; endOffset = endContainer.nodeValue.length; startOffset = 0; } else { endOffset = 0; } } } else { // Split startContainer text node if needed if (startContainer.nodeType == 3 && startOffset > 0 && startOffset < startContainer.nodeValue.length) { startContainer = splitText(startContainer, startOffset); startOffset = 0; } // Split endContainer text node if needed if (endContainer.nodeType == 3 && endOffset > 0 && endOffset < endContainer.nodeValue.length) { endContainer = splitText(endContainer, endOffset).previousSibling; endOffset = endContainer.nodeValue.length; } } return { startContainer: startContainer, startOffset: startOffset, endContainer: endContainer, endOffset: endOffset }; }; /** * Normalizes the specified range by finding the closest best suitable caret location. * * @private * @param {Range} rng Range to normalize. * @return {Boolean} True/false if the specified range was normalized or not. */ this.normalize = function(rng) { var normalized, collapsed; function normalizeEndPoint(start) { var container, offset, walker, body = dom.getRoot(), node, nonEmptyElementsMap; var directionLeft, isAfterNode; function 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 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:
|
|
x|
|
a
b
c will becomea
b
c
* * @example * var parser = new tinymce.html.DomParser({validate: true}, schema); * var rootNode = parser.parse('x
->x
function trim(rootBlockNode) { if (rootBlockNode) { node = rootBlockNode.firstChild; if (node && node.type == 3) { node.value = node.value.replace(startWhiteSpaceRegExp, ''); } node = rootBlockNode.lastChild; if (node && node.type == 3) { node.value = node.value.replace(endWhiteSpaceRegExp, ''); } } } // Check if rootBlock is valid within rootNode for example if P is valid in H1 if H1 is the contentEditabe root if (!schema.isValidChild(rootNode.name, rootBlockName.toLowerCase())) { return; } while (node) { next = node.next; if (node.type == 3 || (node.type == 1 && node.name !== 'p' && !blockElements[node.name] && !node.attr('data-mce-type'))) { if (!rootBlockNode) { // Create a new root block element rootBlockNode = createNode(rootBlockName, 1); rootBlockNode.attr(settings.forced_root_block_attrs); rootNode.insert(rootBlockNode, node); rootBlockNode.append(node); } else { rootBlockNode.append(node); } } else { trim(rootBlockNode); rootBlockNode = null; } node = next; } trim(rootBlockNode); } function createNode(name, type) { var node = new Node(name, type), list; if (name in nodeFilters) { list = matchedNodes[name]; if (list) { list.push(node); } else { matchedNodes[name] = [node]; } } return node; } function removeWhitespaceBefore(node) { var textNode, textVal, sibling; for (textNode = node.prev; textNode && textNode.type === 3;) { textVal = textNode.value.replace(endWhiteSpaceRegExp, ''); if (textVal.length > 0) { textNode.value = textVal; textNode = textNode.prev; } else { sibling = textNode.prev; textNode.remove(); textNode = sibling; } } } function cloneAndExcludeBlocks(input) { var name, output = {}; for (name in input) { if (name !== 'li' && name != 'p') { output[name] = input[name]; } } return output; } parser = new SaxParser({ validate: validate, allow_script_urls: settings.allow_script_urls, allow_conditional_comments: settings.allow_conditional_comments, // Exclude P and LI from DOM parsing since it's treated better by the DOM parser self_closing_elements: cloneAndExcludeBlocks(schema.getSelfClosingElements()), cdata: function(text) { node.append(createNode('#cdata', 4)).value = text; }, text: function(text, raw) { var textNode; // Trim all redundant whitespace on non white space elements if (!isInWhiteSpacePreservedElement) { text = text.replace(allWhiteSpaceRegExp, ' '); if (node.lastChild && blockElements[node.lastChild.name]) { text = text.replace(startWhiteSpaceRegExp, ''); } } // Do we need to create the node if (text.length !== 0) { textNode = createNode('#text', 3); textNode.raw = !!raw; node.append(textNode).value = text; } }, comment: function(text) { node.append(createNode('#comment', 8)).value = text; }, pi: function(name, text) { node.append(createNode(name, 7)).value = text; removeWhitespaceBefore(node); }, doctype: function(text) { var newNode; newNode = node.append(createNode('#doctype', 10)); newNode.value = text; removeWhitespaceBefore(node); }, start: function(name, attrs, empty) { var newNode, attrFiltersLen, elementRule, attrName, parent; elementRule = validate ? schema.getElementRule(name) : {}; if (elementRule) { newNode = createNode(elementRule.outputName || name, 1); newNode.attributes = attrs; newNode.shortEnded = empty; node.append(newNode); // Check if node is valid child of the parent node is the child is // unknown we don't collect it since it's probably a custom element parent = children[node.name]; if (parent && children[newNode.name] && !parent[newNode.name]) { invalidChildren.push(newNode); } attrFiltersLen = attributeFilters.length; while (attrFiltersLen--) { attrName = attributeFilters[attrFiltersLen].name; if (attrName in attrs.map) { list = matchedAttributes[attrName]; if (list) { list.push(newNode); } else { matchedAttributes[attrName] = [newNode]; } } } // Trim whitespace before block if (blockElements[name]) { removeWhitespaceBefore(newNode); } // Change current node if the element wasn't empty i.e nota
lastParent = node; while (parent && parent.firstChild === lastParent && parent.lastChild === lastParent) { lastParent = parent; if (blockElements[parent.name]) { break; } parent = parent.parent; } if (lastParent === parent) { textNode = new Node('#text', 3); textNode.value = '\u00a0'; node.replace(textNode); } } } }); } // Force anchor names closed, unless the setting "allow_html_in_named_anchor" is explicitly included. if (!settings.allow_html_in_named_anchor) { self.addAttributeFilter('id,name', function(nodes) { var i = nodes.length, sibling, prevSibling, parent, node; while (i--) { node = nodes[i]; if (node.name === 'a' && node.firstChild && !node.attr('href')) { parent = node.parent; // Move children after current node sibling = node.lastChild; do { prevSibling = sibling.prev; parent.insert(sibling, node); sibling = prevSibling; } while (sibling); } } }); } if (settings.validate && schema.getValidClasses()) { self.addAttributeFilter('class', function(nodes) { var i = nodes.length, node, classList, ci, className, classValue; var validClasses = schema.getValidClasses(), validClassesMap, valid; while (i--) { node = nodes[i]; classList = node.attr('class').split(' '); classValue = ''; for (ci = 0; ci < classList.length; ci++) { className = classList[ci]; valid = false; validClassesMap = validClasses['*']; if (validClassesMap && validClassesMap[className]) { valid = true; } validClassesMap = validClasses[node.name]; if (!valid && validClassesMap && validClassesMap[className]) { valid = true; } if (valid) { if (classValue) { classValue += ' '; } classValue += className; } } if (!classValue.length) { classValue = null; } node.attr('class', classValue); } }); } }; }); // Included from: js/tinymce/classes/html/Writer.js /** * Writer.js * * Copyright, Moxiecode Systems AB * Released under LGPL License. * * License: http://www.tinymce.com/license * Contributing: http://www.tinymce.com/contributing */ /** * This class is used to write HTML tags out it can be used with the Serializer or the SaxParser. * * @class tinymce.html.Writer * @example * var writer = new tinymce.html.Writer({indent: true}); * var parser = new tinymce.html.SaxParser(writer).parse('
.
*
* @method start
* @param {String} name Name of the element.
* @param {Array} attrs Optional attribute array or undefined if it hasn't any.
* @param {Boolean} empty Optional empty state if the tag should end like
.
*/
start: function(name, attrs, empty) {
var i, l, attr, value;
if (indent && indentBefore[name] && html.length > 0) {
value = html[html.length - 1];
if (value.length > 0 && value !== '\n') {
html.push('\n');
}
}
html.push('<', name);
if (attrs) {
for (i = 0, l = attrs.length; i < l; i++) {
attr = attrs[i];
html.push(' ', attr.name, '="', encode(attr.value, true), '"');
}
}
if (!empty || htmlOutput) {
html[html.length] = '>';
} else {
html[html.length] = ' />';
}
if (empty && indent && indentAfter[name] && html.length > 0) {
value = html[html.length - 1];
if (value.length > 0 && value !== '\n') {
html.push('\n');
}
}
},
/**
* Writes the a end element such as
text
')); * @class tinymce.html.Serializer * @version 3.4 */ define("tinymce/html/Serializer", [ "tinymce/html/Writer", "tinymce/html/Schema" ], function(Writer, Schema) { /** * Constructs a new Serializer instance. * * @constructor * @method Serializer * @param {Object} settings Name/value settings object. * @param {tinymce.html.Schema} schema Schema instance to use. */ return function(settings, schema) { var self = this, writer = new Writer(settings); settings = settings || {}; settings.validate = "validate" in settings ? settings.validate : true; self.schema = schema = schema || new Schema(); self.writer = writer; /** * Serializes the specified node into a string. * * @example * new tinymce.html.Serializer().serialize(new tinymce.html.DomParser().parse('text
')); * @method serialize * @param {tinymce.html.Node} node Node instance to serialize. * @return {String} String with HTML based on DOM tree. */ self.serialize = function(node) { var handlers, validate; validate = settings.validate; handlers = { // #text 3: function(node) { writer.text(node.value, node.raw); }, // #comment 8: function(node) { writer.comment(node.value); }, // Processing instruction 7: function(node) { writer.pi(node.name, node.value); }, // Doctype 10: function(node) { writer.doctype(node.value); }, // CDATA 4: function(node) { writer.cdata(node.value); }, // Document fragment 11: function(node) { if ((node = node.firstChild)) { do { walk(node); } while ((node = node.next)); } } }; writer.reset(); function walk(node) { var handler = handlers[node.type], name, isEmpty, attrs, attrName, attrValue, sortedAttrs, i, l, elementRule; if (!handler) { name = node.name; isEmpty = node.shortEnded; attrs = node.attributes; // Sort attributes if (validate && attrs && attrs.length > 1) { sortedAttrs = []; sortedAttrs.map = {}; elementRule = schema.getElementRule(node.name); for (i = 0, l = elementRule.attributesOrder.length; i < l; i++) { attrName = elementRule.attributesOrder[i]; if (attrName in attrs.map) { attrValue = attrs.map[attrName]; sortedAttrs.map[attrName] = attrValue; sortedAttrs.push({name: attrName, value: attrValue}); } } for (i = 0, l = attrs.length; i < l; i++) { attrName = attrs[i].name; if (!(attrName in sortedAttrs.map)) { attrValue = attrs.map[attrName]; sortedAttrs.map[attrName] = attrValue; sortedAttrs.push({name: attrName, value: attrValue}); } } attrs = sortedAttrs; } writer.start(node.name, attrs, isEmpty); if (!isEmpty) { if ((node = node.firstChild)) { do { walk(node); } while ((node = node.next)); } writer.end(name); } } else { handler(node); } } // Serialize element and treat all non elements as fragments if (node.type == 1 && !settings.inner) { walk(node); } else { handlers[11](node); } return writer.getContent(); }; }; }); // Included from: js/tinymce/classes/dom/Serializer.js /** * Serializer.js * * Copyright, Moxiecode Systems AB * Released under LGPL License. * * License: http://www.tinymce.com/license * Contributing: http://www.tinymce.com/contributing */ /** * This class is used to serialize DOM trees into a string. Consult the TinyMCE Wiki API for * more details and examples on how to use this class. * * @class tinymce.dom.Serializer */ define("tinymce/dom/Serializer", [ "tinymce/dom/DOMUtils", "tinymce/html/DomParser", "tinymce/html/Entities", "tinymce/html/Serializer", "tinymce/html/Node", "tinymce/html/Schema", "tinymce/Env", "tinymce/util/Tools" ], function(DOMUtils, DomParser, Entities, Serializer, Node, Schema, Env, Tools) { var each = Tools.each, trim = Tools.trim; var DOM = DOMUtils.DOM; /** * Constructs a new DOM serializer class. * * @constructor * @method Serializer * @param {Object} settings Serializer settings object. * @param {tinymce.Editor} editor Optional editor to bind events to and get schema/dom from. */ return function(settings, editor) { var dom, schema, htmlParser; if (editor) { dom = editor.dom; schema = editor.schema; } // Default DOM and Schema if they are undefined dom = dom || DOM; schema = schema || new Schema(settings); settings.entity_encoding = settings.entity_encoding || 'named'; settings.remove_trailing_brs = "remove_trailing_brs" in settings ? settings.remove_trailing_brs : true; htmlParser = new DomParser(settings, schema); // Convert tabindex back to elements when serializing contents htmlParser.addAttributeFilter('data-mce-tabindex', function(nodes, name) { var i = nodes.length, node; while (i--) { node = nodes[i]; node.attr('tabindex', node.attributes.map['data-mce-tabindex']); node.attr(name, null); } }); // Convert move data-mce-src, data-mce-href and data-mce-style into nodes or process them if needed htmlParser.addAttributeFilter('src,href,style', function(nodes, name) { var i = nodes.length, node, value, internalName = 'data-mce-' + name; var urlConverter = settings.url_converter, urlConverterScope = settings.url_converter_scope, undef; while (i--) { node = nodes[i]; value = node.attributes.map[internalName]; if (value !== undef) { // Set external name to internal value and remove internal node.attr(name, value.length > 0 ? value : null); node.attr(internalName, null); } else { // No internal attribute found then convert the value we have in the DOM value = node.attributes.map[name]; if (name === "style") { value = dom.serializeStyle(dom.parseStyle(value), node.name); } else if (urlConverter) { value = urlConverter.call(urlConverterScope, value, name, node.name); } node.attr(name, value.length > 0 ? value : null); } } }); // Remove internal classes mceItem<..> or mceSelected htmlParser.addAttributeFilter('class', function(nodes) { var i = nodes.length, node, value; while (i--) { node = nodes[i]; value = node.attr('class'); if (value) { value = node.attr('class').replace(/(?:^|\s)mce-item-\w+(?!\S)/g, ''); node.attr('class', value.length > 0 ? value : null); } } }); // Remove bookmark elements htmlParser.addAttributeFilter('data-mce-type', function(nodes, name, args) { var i = nodes.length, node; while (i--) { node = nodes[i]; if (node.attributes.map['data-mce-type'] === 'bookmark' && !args.cleanup) { node.remove(); } } }); htmlParser.addNodeFilter('noscript', function(nodes) { var i = nodes.length, node; while (i--) { node = nodes[i].firstChild; if (node) { node.value = Entities.decode(node.value); } } }); // Force script into CDATA sections and remove the mce- prefix also add comments around styles htmlParser.addNodeFilter('script,style', function(nodes, name) { var i = nodes.length, node, value, type; function trim(value) { /*jshint maxlen:255 */ /*eslint max-len:0 */ return value.replace(/()/g, '\n') .replace(/^[\r\n]*|[\r\n]*$/g, '') .replace(/^\s*(()?|\s*\/\/\s*\]\]>(-->)?|\/\/\s*(-->)?|\]\]>|\/\*\s*-->\s*\*\/|\s*-->\s*)\s*$/g, ''); } while (i--) { node = nodes[i]; value = node.firstChild ? node.firstChild.value : ''; if (name === "script") { // Remove mce- prefix from script elements and remove default type since the user specified // a script element without type attribute type = node.attr('type'); if (type) { node.attr('type', type == 'mce-no/type' ? null : type.replace(/^mce\-/, '')); } if (value.length > 0) { node.firstChild.value = '// '; } } else { if (value.length > 0) { node.firstChild.value = ''; } } } }); // Convert comments to cdata and handle protected comments htmlParser.addNodeFilter('#comment', function(nodes) { var i = nodes.length, node; while (i--) { node = nodes[i]; if (node.value.indexOf('[CDATA[') === 0) { node.name = '#cdata'; node.type = 4; node.value = node.value.replace(/^\[CDATA\[|\]\]$/g, ''); } else if (node.value.indexOf('mce:protected ') === 0) { node.name = "#text"; node.type = 3; node.raw = true; node.value = unescape(node.value).substr(14); } } }); htmlParser.addNodeFilter('xml:namespace,input', function(nodes, name) { var i = nodes.length, node; while (i--) { node = nodes[i]; if (node.type === 7) { node.remove(); } else if (node.type === 1) { if (name === "input" && !("type" in node.attributes.map)) { node.attr('type', 'text'); } } } }); // Fix list elements, TODO: Replace this later if (settings.fix_list_elements) { htmlParser.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); } } } }); } // Remove internal data attributes htmlParser.addAttributeFilter( 'data-mce-src,data-mce-href,data-mce-style,' + 'data-mce-selected,data-mce-expando,' + 'data-mce-type,data-mce-resize', function(nodes, name) { var i = nodes.length; while (i--) { nodes[i].attr(name, null); } } ); // Return public methods return { /** * Schema instance that was used to when the Serializer was constructed. * * @field {tinymce.html.Schema} schema */ schema: schema, /** * Adds a node filter function to the parser used by the serializer, the parser will collect the specified nodes by name * and then execute the callback ones it has finished parsing the document. * * @example * parser.addNodeFilter('p,h1', function(nodes, name) { * for (var i = 0; i < nodes.length; i++) { * console.log(nodes[i].name); * } * }); * @method addNodeFilter * @method {String} name Comma separated list of nodes to collect. * @param {function} callback Callback function to execute once it has collected nodes. */ addNodeFilter: htmlParser.addNodeFilter, /** * Adds a attribute filter function to the parser used by the serializer, the parser will * collect nodes that has the specified attributes * and then execute the callback ones it has finished parsing the document. * * @example * parser.addAttributeFilter('src,href', function(nodes, name) { * for (var i = 0; i < nodes.length; i++) { * console.log(nodes[i].name); * } * }); * @method addAttributeFilter * @method {String} name Comma separated list of nodes to collect. * @param {function} callback Callback function to execute once it has collected nodes. */ addAttributeFilter: htmlParser.addAttributeFilter, /** * Serializes the specified browser DOM node into a HTML string. * * @method serialize * @param {DOMNode} node DOM node to serialize. * @param {Object} args Arguments option that gets passed to event handlers. */ serialize: function(node, args) { var self = this, impl, doc, oldDoc, htmlSerializer, content; // Explorer won't clone contents of script and style and the // selected index of select elements are cleared on a clone operation. if (Env.ie && dom.select('script,style,select,map').length > 0) { content = node.innerHTML; node = node.cloneNode(false); dom.setHTML(node, content); } else { node = node.cloneNode(true); } // Nodes needs to be attached to something in WebKit/Opera // This fix will make DOM ranges and make Sizzle happy! impl = node.ownerDocument.implementation; if (impl.createHTMLDocument) { // Create an empty HTML document doc = impl.createHTMLDocument(""); // Add the element or it's children if it's a body element to the new document each(node.nodeName == 'BODY' ? node.childNodes : [node], function(node) { doc.body.appendChild(doc.importNode(node, true)); }); // Grab first child or body element for serialization if (node.nodeName != 'BODY') { node = doc.body.firstChild; } else { node = doc.body; } // set the new document in DOMUtils so createElement etc works oldDoc = dom.doc; dom.doc = doc; } args = args || {}; args.format = args.format || 'html'; // Don't wrap content if we want selected html if (args.selection) { args.forced_root_block = ''; } // Pre process if (!args.no_events) { args.node = node; self.onPreProcess(args); } // Setup serializer htmlSerializer = new Serializer(settings, schema); // Parse and serialize HTML args.content = htmlSerializer.serialize( htmlParser.parse(trim(args.getInner ? node.innerHTML : dom.getOuterHTML(node)), args) ); // Replace all BOM characters for now until we can find a better solution if (!args.cleanup) { args.content = args.content.replace(/\uFEFF/g, ''); } // Post process if (!args.no_events) { self.onPostProcess(args); } // Restore the old document if it was changed if (oldDoc) { dom.doc = oldDoc; } args.node = null; return args.content; }, /** * Adds valid elements rules to the serializers schema instance this enables you to specify things * like what elements should be outputted and what attributes specific elements might have. * Consult the Wiki for more details on this format. * * @method addRules * @param {String} rules Valid elements rules string to add to schema. */ addRules: function(rules) { schema.addValidElements(rules); }, /** * Sets the valid elements rules to the serializers schema instance this enables you to specify things * like what elements should be outputted and what attributes specific elements might have. * Consult the Wiki for more details on this format. * * @method setRules * @param {String} rules Valid elements rules string. */ setRules: function(rules) { schema.setValidElements(rules); }, onPreProcess: function(args) { if (editor) { editor.fire('PreProcess', args); } }, onPostProcess: function(args) { if (editor) { editor.fire('PostProcess', args); } } }; }; }); // Included from: js/tinymce/classes/dom/TridentSelection.js /** * TridentSelection.js * * Copyright, Moxiecode Systems AB * Released under LGPL License. * * License: http://www.tinymce.com/license * Contributing: http://www.tinymce.com/contributing */ /** * Selection class for old explorer versions. This one fakes the * native selection object available on modern browsers. * * @class tinymce.dom.TridentSelection */ define("tinymce/dom/TridentSelection", [], function() { function Selection(selection) { var self = this, dom = selection.dom, FALSE = false; function getPosition(rng, start) { var checkRng, startIndex = 0, endIndex, inside, children, child, offset, index, position = -1, parent; // Setup test range, collapse it and get the parent checkRng = rng.duplicate(); checkRng.collapse(start); parent = checkRng.parentElement(); // Check if the selection is within the right document if (parent.ownerDocument !== selection.dom.doc) { return; } // IE will report non editable elements as it's parent so look for an editable one while (parent.contentEditable === "false") { parent = parent.parentNode; } // If parent doesn't have any children then return that we are inside the element if (!parent.hasChildNodes()) { return {node: parent, inside: 1}; } // Setup node list and endIndex children = parent.children; endIndex = children.length - 1; // Perform a binary search for the position while (startIndex <= endIndex) { index = Math.floor((startIndex + endIndex) / 2); // Move selection to node and compare the ranges child = children[index]; checkRng.moveToElementText(child); position = checkRng.compareEndPoints(start ? 'StartToStart' : 'EndToEnd', rng); // Before/after or an exact match if (position > 0) { endIndex = index - 1; } else if (position < 0) { startIndex = index + 1; } else { return {node: child}; } } // Check if child position is before or we didn't find a position if (position < 0) { // No element child was found use the parent element and the offset inside that if (!child) { checkRng.moveToElementText(parent); checkRng.collapse(true); child = parent; inside = true; } else { checkRng.collapse(false); } // Walk character by character in text node until we hit the selected range endpoint, // hit the end of document or parent isn't the right one // We need to walk char by char since rng.text or rng.htmlText will trim line endings offset = 0; while (checkRng.compareEndPoints(start ? 'StartToStart' : 'StartToEnd', rng) !== 0) { if (checkRng.move('character', 1) === 0 || parent != checkRng.parentElement()) { break; } offset++; } } else { // Child position is after the selection endpoint checkRng.collapse(true); // Walk character by character in text node until we hit the selected range endpoint, hit // the end of document or parent isn't the right one offset = 0; while (checkRng.compareEndPoints(start ? 'StartToStart' : 'StartToEnd', rng) !== 0) { if (checkRng.move('character', -1) === 0 || parent != checkRng.parentElement()) { break; } offset++; } } return {node: child, position: position, offset: offset, inside: inside}; } // Returns a W3C DOM compatible range object by using the IE Range API function getRange() { var ieRange = selection.getRng(), domRange = dom.createRng(), element, collapsed, tmpRange, element2, bookmark; // If selection is outside the current document just return an empty range element = ieRange.item ? ieRange.item(0) : ieRange.parentElement(); if (element.ownerDocument != dom.doc) { return domRange; } collapsed = selection.isCollapsed(); // Handle control selection if (ieRange.item) { domRange.setStart(element.parentNode, dom.nodeIndex(element)); domRange.setEnd(domRange.startContainer, domRange.startOffset + 1); return domRange; } function findEndPoint(start) { var endPoint = getPosition(ieRange, start), container, offset, textNodeOffset = 0, sibling, undef, nodeValue; container = endPoint.node; offset = endPoint.offset; if (endPoint.inside && !container.hasChildNodes()) { domRange[start ? 'setStart' : 'setEnd'](container, 0); return; } if (offset === undef) { domRange[start ? 'setStartBefore' : 'setEndAfter'](container); return; } if (endPoint.position < 0) { sibling = endPoint.inside ? container.firstChild : container.nextSibling; if (!sibling) { domRange[start ? 'setStartAfter' : 'setEndAfter'](container); return; } if (!offset) { if (sibling.nodeType == 3) { domRange[start ? 'setStart' : 'setEnd'](sibling, 0); } else { domRange[start ? 'setStartBefore' : 'setEndBefore'](sibling); } return; } // Find the text node and offset while (sibling) { if (sibling.nodeType == 3) { nodeValue = sibling.nodeValue; textNodeOffset += nodeValue.length; // We are at or passed the position we where looking for if (textNodeOffset >= offset) { container = sibling; textNodeOffset -= offset; textNodeOffset = nodeValue.length - textNodeOffset; break; } } sibling = sibling.nextSibling; } } else { // Find the text node and offset sibling = container.previousSibling; if (!sibling) { return domRange[start ? 'setStartBefore' : 'setEndBefore'](container); } // If there isn't any text to loop then use the first position if (!offset) { if (container.nodeType == 3) { domRange[start ? 'setStart' : 'setEnd'](sibling, container.nodeValue.length); } else { domRange[start ? 'setStartAfter' : 'setEndAfter'](sibling); } return; } while (sibling) { if (sibling.nodeType == 3) { textNodeOffset += sibling.nodeValue.length; // We are at or passed the position we where looking for if (textNodeOffset >= offset) { container = sibling; textNodeOffset -= offset; break; } } sibling = sibling.previousSibling; } } domRange[start ? 'setStart' : 'setEnd'](container, textNodeOffset); } try { // Find start point findEndPoint(true); // Find end point if needed if (!collapsed) { findEndPoint(); } } catch (ex) { // IE has a nasty bug where text nodes might throw "invalid argument" when you // access the nodeValue or other properties of text nodes. This seems to happend when // text nodes are split into two nodes by a delete/backspace call. So lets detect it and try to fix it. if (ex.number == -2147024809) { // Get the current selection bookmark = self.getBookmark(2); // Get start element tmpRange = ieRange.duplicate(); tmpRange.collapse(true); element = tmpRange.parentElement(); // Get end element if (!collapsed) { tmpRange = ieRange.duplicate(); tmpRange.collapse(false); element2 = tmpRange.parentElement(); element2.innerHTML = element2.innerHTML; } // Remove the broken elements element.innerHTML = element.innerHTML; // Restore the selection self.moveToBookmark(bookmark); // Since the range has moved we need to re-get it ieRange = selection.getRng(); // Find start point findEndPoint(true); // Find end point if needed if (!collapsed) { findEndPoint(); } } else { throw ex; // Throw other errors } } return domRange; } this.getBookmark = function(type) { var rng = selection.getRng(), bookmark = {}; function getIndexes(node) { var parent, root, children, i, indexes = []; parent = node.parentNode; root = dom.getRoot().parentNode; while (parent != root && parent.nodeType !== 9) { children = parent.children; i = children.length; while (i--) { if (node === children[i]) { indexes.push(i); break; } } node = parent; parent = parent.parentNode; } return indexes; } function getBookmarkEndPoint(start) { var position; position = getPosition(rng, start); if (position) { return { position: position.position, offset: position.offset, indexes: getIndexes(position.node), inside: position.inside }; } } // Non ubstructive bookmark if (type === 2) { // Handle text selection if (!rng.item) { bookmark.start = getBookmarkEndPoint(true); if (!selection.isCollapsed()) { bookmark.end = getBookmarkEndPoint(); } } else { bookmark.start = {ctrl: true, indexes: getIndexes(rng.item(0))}; } } return bookmark; }; this.moveToBookmark = function(bookmark) { var rng, body = dom.doc.body; function resolveIndexes(indexes) { var node, i, idx, children; node = dom.getRoot(); for (i = indexes.length - 1; i >= 0; i--) { children = node.children; idx = indexes[i]; if (idx <= children.length - 1) { node = children[idx]; } } return node; } function setBookmarkEndPoint(start) { var endPoint = bookmark[start ? 'start' : 'end'], moveLeft, moveRng, undef, offset; if (endPoint) { moveLeft = endPoint.position > 0; moveRng = body.createTextRange(); moveRng.moveToElementText(resolveIndexes(endPoint.indexes)); offset = endPoint.offset; if (offset !== undef) { moveRng.collapse(endPoint.inside || moveLeft); moveRng.moveStart('character', moveLeft ? -offset : offset); } else { moveRng.collapse(start); } rng.setEndPoint(start ? 'StartToStart' : 'EndToStart', moveRng); if (start) { rng.collapse(true); } } } if (bookmark.start) { if (bookmark.start.ctrl) { rng = body.createControlRange(); rng.addElement(resolveIndexes(bookmark.start.indexes)); rng.select(); } else { rng = body.createTextRange(); setBookmarkEndPoint(true); setBookmarkEndPoint(); rng.select(); } } }; this.addRange = function(rng) { var ieRng, ctrlRng, startContainer, startOffset, endContainer, endOffset, sibling, doc = selection.dom.doc, body = doc.body, nativeRng, ctrlElm; function setEndPoint(start) { var container, offset, marker, tmpRng, nodes; marker = dom.create('a'); container = start ? startContainer : endContainer; offset = start ? startOffset : endOffset; tmpRng = ieRng.duplicate(); if (container == doc || container == doc.documentElement) { container = body; offset = 0; } if (container.nodeType == 3) { container.parentNode.insertBefore(marker, container); tmpRng.moveToElementText(marker); tmpRng.moveStart('character', offset); dom.remove(marker); ieRng.setEndPoint(start ? 'StartToStart' : 'EndToEnd', tmpRng); } else { nodes = container.childNodes; if (nodes.length) { if (offset >= nodes.length) { dom.insertAfter(marker, nodes[nodes.length - 1]); } else { container.insertBefore(marker, nodes[offset]); } tmpRng.moveToElementText(marker); } else if (container.canHaveHTML) { // Empty node selection for example|
would become this:|
sibling = startContainer.previousSibling; if (sibling && !sibling.hasChildNodes() && dom.isBlock(sibling)) { sibling.innerHTML = ''; } else { sibling = null; } startContainer.innerHTML = ''; ieRng.moveToElementText(startContainer.lastChild); ieRng.select(); dom.doc.selection.clear(); startContainer.innerHTML = ''; if (sibling) { sibling.innerHTML = ''; } return; } else { startOffset = dom.nodeIndex(startContainer); startContainer = startContainer.parentNode; } } if (startOffset == endOffset - 1) { try { ctrlElm = startContainer.childNodes[startOffset]; ctrlRng = body.createControlRange(); ctrlRng.addElement(ctrlElm); ctrlRng.select(); // Check if the range produced is on the correct element and is a control range // On IE 8 it will select the parent contentEditable container if you select an inner element see: #5398 nativeRng = selection.getRng(); if (nativeRng.item && ctrlElm === nativeRng.item(0)) { return; } } catch (ex) { // Ignore } } } // Set start/end point of selection setEndPoint(true); setEndPoint(); // Select the new range and scroll it into view ieRng.select(); }; // Expose range method this.getRangeAt = getRange; } return Selection; }); // Included from: js/tinymce/classes/util/VK.js /** * VK.js * * Copyright, Moxiecode Systems AB * Released under LGPL License. * * License: http://www.tinymce.com/license * Contributing: http://www.tinymce.com/contributing */ /** * This file exposes a set of the common KeyCodes for use. Please grow it as needed. */ define("tinymce/util/VK", [ "tinymce/Env" ], function(Env) { return { BACKSPACE: 8, DELETE: 46, DOWN: 40, ENTER: 13, LEFT: 37, RIGHT: 39, SPACEBAR: 32, TAB: 9, UP: 38, modifierPressed: function(e) { return e.shiftKey || e.ctrlKey || e.altKey || this.metaKeyPressed(e); }, metaKeyPressed: function(e) { // Check if ctrl or meta key is pressed. Edge case for AltGr on Windows where it produces ctrlKey+altKey states return (Env.mac ? e.metaKey : e.ctrlKey && !e.altKey); } }; }); // Included from: js/tinymce/classes/dom/ControlSelection.js /** * ControlSelection.js * * Copyright, Moxiecode Systems AB * Released under LGPL License. * * License: http://www.tinymce.com/license * Contributing: http://www.tinymce.com/contributing */ /** * This class handles control selection of elements. Controls are elements * that can be resized and needs to be selected as a whole. It adds custom resize handles * to all browser engines that support properly disabling the built in resize logic. * * @class tinymce.dom.ControlSelection */ define("tinymce/dom/ControlSelection", [ "tinymce/util/VK", "tinymce/util/Tools", "tinymce/Env" ], function(VK, Tools, Env) { return function(selection, editor) { var dom = editor.dom, each = Tools.each; var selectedElm, selectedElmGhost, resizeHelper, resizeHandles, selectedHandle, lastMouseDownEvent; var startX, startY, selectedElmX, selectedElmY, startW, startH, ratio, resizeStarted; var width, height, editableDoc = editor.getDoc(), rootDocument = document, isIE = Env.ie && Env.ie < 11; var abs = Math.abs, round = Math.round, rootElement = editor.getBody(), startScrollWidth, startScrollHeight; // Details about each resize handle how to scale etc resizeHandles = { // Name: x multiplier, y multiplier, delta size x, delta size y n: [0.5, 0, 0, -1], e: [1, 0.5, 1, 0], s: [0.5, 1, 0, 1], w: [0, 0.5, -1, 0], nw: [0, 0, -1, -1], ne: [1, 0, 1, -1], se: [1, 1, 1, 1], sw: [0, 1, -1, 1] }; // Add CSS for resize handles, cloned element and selected var rootClass = '.mce-content-body'; editor.contentStyles.push( rootClass + ' div.mce-resizehandle {' + 'position: absolute;' + 'border: 1px solid black;' + 'background: #FFF;' + 'width: 5px;' + 'height: 5px;' + 'z-index: 10000' + '}' + rootClass + ' .mce-resizehandle:hover {' + 'background: #000' + '}' + rootClass + ' img[data-mce-selected], hr[data-mce-selected] {' + 'outline: 1px solid black;' + 'resize: none' + // Have been talks about implementing this in browsers '}' + rootClass + ' .mce-clonedresizable {' + 'position: absolute;' + (Env.gecko ? '' : 'outline: 1px dashed black;') + // Gecko produces trails while resizing 'opacity: .5;' + 'filter: alpha(opacity=50);' + 'z-index: 10000' + '}' + rootClass + ' .mce-resize-helper {' + 'background: #555;' + 'background: rgba(0,0,0,0.75);' + 'border-radius: 3px;' + 'border: 1px;' + 'color: white;' + 'display: none;' + 'font-family: sans-serif;' + 'font-size: 12px;' + 'white-space: nowrap;' + 'line-height: 14px;' + 'margin: 5px 10px;' + 'padding: 5px;' + 'position: absolute;' + 'z-index: 10001' + '}' ); function isResizable(elm) { var selector = editor.settings.object_resizing; if (selector === false || Env.iOS) { return false; } if (typeof selector != 'string') { selector = 'table,img,div'; } if (elm.getAttribute('data-mce-resize') === 'false') { return false; } return editor.dom.is(elm, selector); } function resizeGhostElement(e) { var deltaX, deltaY, proportional; var resizeHelperX, resizeHelperY; // Calc new width/height deltaX = e.screenX - startX; deltaY = e.screenY - startY; // Calc new size width = deltaX * selectedHandle[2] + startW; height = deltaY * selectedHandle[3] + startH; // Never scale down lower than 5 pixels width = width < 5 ? 5 : width; height = height < 5 ? 5 : height; if (selectedElm.nodeName == "IMG" && editor.settings.resize_img_proportional !== false) { proportional = !VK.modifierPressed(e); } else { proportional = VK.modifierPressed(e) || (selectedElm.nodeName == "IMG" && selectedHandle[2] * selectedHandle[3] !== 0); } // Constrain proportions if (proportional) { if (abs(deltaX) > abs(deltaY)) { height = round(width * ratio); width = round(height / ratio); } else { width = round(height / ratio); height = round(width * ratio); } } // Update ghost size dom.setStyles(selectedElmGhost, { width: width, height: height }); // Update resize helper position resizeHelperX = selectedHandle.startPos.x + deltaX; resizeHelperY = selectedHandle.startPos.y + deltaY; resizeHelperX = resizeHelperX > 0 ? resizeHelperX : 0; resizeHelperY = resizeHelperY > 0 ? resizeHelperY : 0; dom.setStyles(resizeHelper, { left: resizeHelperX, top: resizeHelperY, display: 'block' }); resizeHelper.innerHTML = width + ' × ' + height; // Update ghost X position if needed if (selectedHandle[2] < 0 && selectedElmGhost.clientWidth <= width) { dom.setStyle(selectedElmGhost, 'left', selectedElmX + (startW - width)); } // Update ghost Y position if needed if (selectedHandle[3] < 0 && selectedElmGhost.clientHeight <= height) { dom.setStyle(selectedElmGhost, 'top', selectedElmY + (startH - height)); } // Calculate how must overflow we got deltaX = rootElement.scrollWidth - startScrollWidth; deltaY = rootElement.scrollHeight - startScrollHeight; // Re-position the resize helper based on the overflow if (deltaX + deltaY !== 0) { dom.setStyles(resizeHelper, { left: resizeHelperX - deltaX, top: resizeHelperY - deltaY }); } if (!resizeStarted) { editor.fire('ObjectResizeStart', {target: selectedElm, width: startW, height: startH}); resizeStarted = true; } } function endGhostResize() { resizeStarted = false; function setSizeProp(name, value) { if (value) { // Resize by using style or attribute if (selectedElm.style[name] || !editor.schema.isValid(selectedElm.nodeName.toLowerCase(), name)) { dom.setStyle(selectedElm, name, value); } else { dom.setAttrib(selectedElm, name, value); } } } // Set width/height properties setSizeProp('width', width); setSizeProp('height', height); dom.unbind(editableDoc, 'mousemove', resizeGhostElement); dom.unbind(editableDoc, 'mouseup', endGhostResize); if (rootDocument != editableDoc) { dom.unbind(rootDocument, 'mousemove', resizeGhostElement); dom.unbind(rootDocument, 'mouseup', endGhostResize); } // Remove ghost/helper and update resize handle positions dom.remove(selectedElmGhost); dom.remove(resizeHelper); if (!isIE || selectedElm.nodeName == "TABLE") { showResizeRect(selectedElm); } editor.fire('ObjectResized', {target: selectedElm, width: width, height: height}); dom.setAttrib(selectedElm, 'style', dom.getAttrib(selectedElm, 'style')); editor.nodeChanged(); } function showResizeRect(targetElm, mouseDownHandleName, mouseDownEvent) { var position, targetWidth, targetHeight, e, rect; unbindResizeHandleEvents(); // Get position and size of target position = dom.getPos(targetElm, rootElement); selectedElmX = position.x; selectedElmY = position.y; rect = targetElm.getBoundingClientRect(); // Fix for Gecko offsetHeight for table with caption targetWidth = rect.width || (rect.right - rect.left); targetHeight = rect.height || (rect.bottom - rect.top); // Reset width/height if user selects a new image/table if (selectedElm != targetElm) { detachResizeStartListener(); selectedElm = targetElm; width = height = 0; } // Makes it possible to disable resizing e = editor.fire('ObjectSelected', {target: targetElm}); if (isResizable(targetElm) && !e.isDefaultPrevented()) { each(resizeHandles, function(handle, name) { var handleElm, handlerContainerElm; function startDrag(e) { startX = e.screenX; startY = e.screenY; startW = selectedElm.clientWidth; startH = selectedElm.clientHeight; ratio = startH / startW; selectedHandle = handle; handle.startPos = { x: targetWidth * handle[0] + selectedElmX, y: targetHeight * handle[1] + selectedElmY }; startScrollWidth = rootElement.scrollWidth; startScrollHeight = rootElement.scrollHeight; selectedElmGhost = selectedElm.cloneNode(true); dom.addClass(selectedElmGhost, 'mce-clonedresizable'); dom.setAttrib(selectedElmGhost, 'data-mce-bogus', 'all'); selectedElmGhost.contentEditable = false; // Hides IE move layer cursor selectedElmGhost.unSelectabe = true; dom.setStyles(selectedElmGhost, { left: selectedElmX, top: selectedElmY, margin: 0 }); selectedElmGhost.removeAttribute('data-mce-selected'); rootElement.appendChild(selectedElmGhost); dom.bind(editableDoc, 'mousemove', resizeGhostElement); dom.bind(editableDoc, 'mouseup', endGhostResize); if (rootDocument != editableDoc) { dom.bind(rootDocument, 'mousemove', resizeGhostElement); dom.bind(rootDocument, 'mouseup', endGhostResize); } resizeHelper = dom.add(rootElement, 'div', { 'class': 'mce-resize-helper', 'data-mce-bogus': 'all' }, startW + ' × ' + startH); } if (mouseDownHandleName) { // Drag started by IE native resizestart if (name == mouseDownHandleName) { startDrag(mouseDownEvent); } return; } // Get existing or render resize handle handleElm = dom.get('mceResizeHandle' + name); if (!handleElm) { handlerContainerElm = rootElement; handleElm = dom.add(handlerContainerElm, 'div', { id: 'mceResizeHandle' + name, 'data-mce-bogus': 'all', 'class': 'mce-resizehandle', unselectable: true, style: 'cursor:' + name + '-resize; margin:0; padding:0' }); // Hides IE move layer cursor // If we set it on Chrome we get this wounderful bug: #6725 if (Env.ie) { handleElm.contentEditable = false; } } else { dom.show(handleElm); } if (!handle.elm) { dom.bind(handleElm, 'mousedown', function(e) { e.stopImmediatePropagation(); e.preventDefault(); startDrag(e); }); handle.elm = handleElm; } // Position element dom.setStyles(handleElm, { left: (targetWidth * handle[0] + selectedElmX) - (handleElm.offsetWidth / 2), top: (targetHeight * handle[1] + selectedElmY) - (handleElm.offsetHeight / 2) }); }); } else { hideResizeRect(); } selectedElm.setAttribute('data-mce-selected', '1'); } function hideResizeRect() { var name, handleElm; unbindResizeHandleEvents(); if (selectedElm) { selectedElm.removeAttribute('data-mce-selected'); } for (name in resizeHandles) { handleElm = dom.get('mceResizeHandle' + name); if (handleElm) { dom.unbind(handleElm); dom.remove(handleElm); } } } function updateResizeRect(e) { var startElm, controlElm; function isChildOrEqual(node, parent) { if (node) { do { if (node === parent) { return true; } } while ((node = node.parentNode)); } } // Ignore all events while resizing if (resizeStarted) { return; } // Remove data-mce-selected from all elements since they might have been copied using Ctrl+c/v each(dom.select('img[data-mce-selected],hr[data-mce-selected]'), function(img) { img.removeAttribute('data-mce-selected'); }); controlElm = e.type == 'mousedown' ? e.target : selection.getNode(); controlElm = dom.$(controlElm).closest(isIE ? 'table' : 'table,img,hr')[0]; if (isChildOrEqual(controlElm, rootElement)) { disableGeckoResize(); startElm = selection.getStart(true); if (isChildOrEqual(startElm, controlElm) && isChildOrEqual(selection.getEnd(true), controlElm)) { if (!isIE || (controlElm != startElm && startElm.nodeName !== 'IMG')) { showResizeRect(controlElm); return; } } } hideResizeRect(); } function attachEvent(elm, name, func) { if (elm && elm.attachEvent) { elm.attachEvent('on' + name, func); } } function detachEvent(elm, name, func) { if (elm && elm.detachEvent) { elm.detachEvent('on' + name, func); } } function resizeNativeStart(e) { var target = e.srcElement, pos, name, corner, cornerX, cornerY, relativeX, relativeY; pos = target.getBoundingClientRect(); relativeX = lastMouseDownEvent.clientX - pos.left; relativeY = lastMouseDownEvent.clientY - pos.top; // Figure out what corner we are draging on for (name in resizeHandles) { corner = resizeHandles[name]; cornerX = target.offsetWidth * corner[0]; cornerY = target.offsetHeight * corner[1]; if (abs(cornerX - relativeX) < 8 && abs(cornerY - relativeY) < 8) { selectedHandle = corner; break; } } // Remove native selection and let the magic begin resizeStarted = true; editor.fire('ObjectResizeStart', { target: selectedElm, width: selectedElm.clientWidth, height: selectedElm.clientHeight }); editor.getDoc().selection.empty(); showResizeRect(target, name, lastMouseDownEvent); } function nativeControlSelect(e) { var target = e.srcElement; if (target != selectedElm) { editor.fire('ObjectSelected', {target: target}); detachResizeStartListener(); if (target.id.indexOf('mceResizeHandle') === 0) { e.returnValue = false; return; } if (target.nodeName == 'IMG' || target.nodeName == 'TABLE') { hideResizeRect(); selectedElm = target; attachEvent(target, 'resizestart', resizeNativeStart); } } } function detachResizeStartListener() { detachEvent(selectedElm, 'resizestart', resizeNativeStart); } function unbindResizeHandleEvents() { for (var name in resizeHandles) { var handle = resizeHandles[name]; if (handle.elm) { dom.unbind(handle.elm); delete handle.elm; } } } function disableGeckoResize() { try { // Disable object resizing on Gecko editor.getDoc().execCommand('enableObjectResizing', false, false); } catch (ex) { // Ignore } } function controlSelect(elm) { var ctrlRng; if (!isIE) { return; } ctrlRng = editableDoc.body.createControlRange(); try { ctrlRng.addElement(elm); ctrlRng.select(); return true; } catch (ex) { // Ignore since the element can't be control selected for example a P tag } } editor.on('init', function() { if (isIE) { // Hide the resize rect on resize and reselect the image editor.on('ObjectResized', function(e) { if (e.target.nodeName != 'TABLE') { hideResizeRect(); controlSelect(e.target); } }); attachEvent(rootElement, 'controlselect', nativeControlSelect); editor.on('mousedown', function(e) { lastMouseDownEvent = e; }); } else { disableGeckoResize(); if (Env.ie >= 11) { // TODO: Drag/drop doesn't work editor.on('mouseup', function(e) { var nodeName = e.target.nodeName; if (!resizeStarted && /^(TABLE|IMG|HR)$/.test(nodeName)) { editor.selection.select(e.target, nodeName == 'TABLE'); editor.nodeChanged(); } }); editor.dom.bind(rootElement, 'mscontrolselect', function(e) { if (/^(TABLE|IMG|HR)$/.test(e.target.nodeName)) { e.preventDefault(); // This moves the selection from being a control selection to a text like selection like in WebKit #6753 // TODO: Fix this the day IE works like other browsers without this nasty native ugly control selections. if (e.target.tagName == 'IMG') { window.setTimeout(function() { editor.selection.select(e.target); }, 0); } } }); } } editor.on('nodechange ResizeEditor', updateResizeRect); // Update resize rect while typing in a table editor.on('keydown keyup', function(e) { if (selectedElm && selectedElm.nodeName == "TABLE") { updateResizeRect(e); } }); editor.on('hide', hideResizeRect); // Hide rect on focusout since it would float on top of windows otherwise //editor.on('focusout', hideResizeRect); }); editor.on('remove', unbindResizeHandleEvents); function destroy() { selectedElm = selectedElmGhost = null; if (isIE) { detachResizeStartListener(); detachEvent(rootElement, 'controlselect', nativeControlSelect); } } return { isResizable: isResizable, showResizeRect: showResizeRect, hideResizeRect: hideResizeRect, updateResizeRect: updateResizeRect, controlSelect: controlSelect, destroy: destroy }; }; }); // Included from: js/tinymce/classes/dom/BookmarkManager.js /** * BookmarkManager.js * * Copyright, Moxiecode Systems AB * Released under LGPL License. * * License: http://www.tinymce.com/license * Contributing: http://www.tinymce.com/contributing */ /** * This class handles selection bookmarks. * * @class tinymce.dom.BookmarkManager */ define("tinymce/dom/BookmarkManager", [ "tinymce/Env", "tinymce/util/Tools" ], function(Env, Tools) { /** * 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 index = 0; Tools.each(dom.select(name), function(node, i) { if (node == element) { index = i; } }); return index; } 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() { var rng = selection.getRng(true), root = dom.getRoot(), bookmark = {}; function getPoint(rng, start) { var container = rng[start ? 'startContainer' : 'endContainer'], offset = rng[start ? 'startOffset' : 'endOffset'], point = [], node, childNodes, after = 0; if (container.nodeType == 3) { if (normalized) { for (node = container.previousSibling; node && node.nodeType == 3; node = node.previousSibling) { offset += node.nodeValue.length; } } point.push(offset); } else { childNodes = container.childNodes; if (offset >= childNodes.length && childNodes.length) { after = 1; offset = Math.max(0, childNodes.length - 1); } point.push(dom.nodeIndex(childNodes[offset], normalized) + after); } for (; container && container != root; container = container.parentNode) { point.push(dom.nodeIndex(container, normalized)); } return point; } bookmark.start = getPoint(rng, true); if (!selection.isCollapsed()) { bookmark.end = getPoint(rng); } return bookmark; } if (type == 2) { element = selection.getNode(); name = element ? element.nodeName : null; if (name == 'IMG') { return {name: name, index: findIndex(name, element)}; } if (selection.tridentSel) { return selection.tridentSel.getBookmark(type); } return getLocation(); } // Handle simple range if (type) { return {rng: selection.getRng()}; } rng = selection.getRng(); id = dom.uniqueId(); collapsed = selection.isCollapsed(); styles = 'overflow:hidden;line-height:0px'; // Explorer method if (rng.duplicate || rng.item) { // Text selection if (!rng.item) { rng2 = rng.duplicate(); try { // Insert start marker rng.collapse(); rng.pasteHTML('' + chr + ''); // Insert end marker if (!collapsed) { rng2.collapse(false); // Detect the empty space after block elements in IE and move the // end back one character ] becomes]
rng.moveToElementText(rng2.parentElement()); if (rng.compareEndPoints('StartToEnd', rng2) === 0) { rng2.move('character', -1); } rng2.pasteHTML('' + chr + ''); } } catch (ex) { // IE might throw unspecified error so lets ignore it return null; } } else { // Control selection element = rng.item(0); name = element.nodeName; return {name: name, index: findIndex(name, element)}; } } else { element = selection.getNode(); name = element.nodeName; if (name == 'IMG') { return {name: name, index: findIndex(name, element)}; } // W3C method rng2 = normalizeTableCellSelection(rng.cloneRange()); // Insert end marker if (!collapsed) { rng2.collapse(false); rng2.insertNode(dom.create('span', {'data-mce-type': "bookmark", id: id + '_end', style: styles}, chr)); } rng = normalizeTableCellSelection(rng); rng.collapse(true); rng.insertNode(dom.create('span', {'data-mce-type': "bookmark", id: id + '_start', style: styles}, chr)); } selection.moveToBookmark({id: id, keep: 1}); return {id: id}; }; /** * Restores the selection to the specified bookmark. * * @method moveToBookmark * @param {Object} bookmark Bookmark to restore selection from. * @return {Boolean} true/false if it was successful or not. * @example * // Stores a bookmark of the current selection * var bm = tinymce.activeEditor.selection.getBookmark(); * * tinymce.activeEditor.setContent(tinymce.activeEditor.getContent() + 'Some new content'); * * // Restore the selection bookmark * tinymce.activeEditor.selection.moveToBookmark(bm); */ this.moveToBookmark = function(bookmark) { var rng, root, startContainer, endContainer, startOffset, endOffset; function setEndPoint(start) { var point = bookmark[start ? 'start' : 'end'], i, node, offset, children; if (point) { offset = point[0]; // Find container node for (node = root, i = point.length - 1; i >= 1; i--) { children = node.childNodes; if (point[i] > children.length - 1) { return; } node = children[point[i]]; } // Move text offset to best suitable location if (node.nodeType === 3) { offset = Math.min(point[0], node.nodeValue.length); } // Move element offset to best suitable location if (node.nodeType === 1) { offset = Math.min(point[0], node.childNodes.length); } // Set offset within container node if (start) { rng.setStart(node, offset); } else { rng.setEnd(node, offset); } } return true; } function restoreEndPoint(suffix) { var marker = dom.get(bookmark.id + '_' + suffix), node, idx, next, prev, keep = bookmark.keep; if (marker) { node = marker.parentNode; if (suffix == 'start') { if (!keep) { idx = dom.nodeIndex(marker); } else { node = marker.firstChild; idx = 1; } startContainer = endContainer = node; startOffset = endOffset = idx; } else { if (!keep) { idx = dom.nodeIndex(marker); } else { node = marker.firstChild; idx = 1; } endContainer = node; endOffset = idx; } if (!keep) { prev = marker.previousSibling; next = marker.nextSibling; // Remove all marker text nodes Tools.each(Tools.grep(marker.childNodes), function(node) { if (node.nodeType == 3) { node.nodeValue = node.nodeValue.replace(/\uFEFF/g, ''); } }); // Remove marker but keep children if for example contents where inserted into the marker // Also remove duplicated instances of the marker for example by a // split operation or by WebKit auto split on paste feature while ((marker = dom.get(bookmark.id + '_' + suffix))) { dom.remove(marker, 1); } // If siblings are text nodes then merge them unless it's Opera since it some how removes the node // and we are sniffing since adding a lot of detection code for a browser with 3% of the market // isn't worth the effort. Sorry, Opera but it's just a fact if (prev && next && prev.nodeType == next.nodeType && prev.nodeType == 3 && !Env.opera) { idx = prev.nodeValue.length; prev.appendData(next.nodeValue); dom.remove(next); if (suffix == 'start') { startContainer = endContainer = prev; startOffset = endOffset = idx; } else { endContainer = prev; endOffset = idx; } } } } } function addBogus(node) { // Adds a bogus BR element for empty block elements if (dom.isBlock(node) && !node.innerHTML && !Env.ie) { node.innerHTML = '*texttext*
! // This will reduce the number of wrapper elements that needs to be created // Move start point up the tree if (format[0].inline || format[0].block_expand) { if (!format[0].inline || (startContainer.nodeType != 3 || startOffset === 0)) { startContainer = findParentContainer(true); } if (!format[0].inline || (endContainer.nodeType != 3 || endOffset === endContainer.nodeValue.length)) { endContainer = findParentContainer(); } } // Expand start/end container to matching selector if (format[0].selector && format[0].expand !== FALSE && !format[0].inline) { // Find new startContainer/endContainer if there is better one startContainer = findSelectorEndPoint(startContainer, 'previousSibling'); endContainer = findSelectorEndPoint(endContainer, 'nextSibling'); } // Expand start/end container to matching block element or text node if (format[0].block || format[0].selector) { // Find new startContainer/endContainer if there is better one startContainer = findBlockEndPoint(startContainer, 'previousSibling'); endContainer = findBlockEndPoint(endContainer, 'nextSibling'); // Non block element then try to expand up the leaf if (format[0].block) { if (!isBlock(startContainer)) { startContainer = findParentContainer(true); } if (!isBlock(endContainer)) { endContainer = findParentContainer(); } } } // Setup index for startContainer if (startContainer.nodeType == 1) { startOffset = nodeIndex(startContainer); startContainer = startContainer.parentNode; } // Setup index for endContainer if (endContainer.nodeType == 1) { endOffset = nodeIndex(endContainer) + 1; endContainer = endContainer.parentNode; } // Return new range like object return { startContainer: startContainer, startOffset: startOffset, endContainer: endContainer, endOffset: endOffset }; } function isColorFormatAndAnchor(node, format) { return format.links && node.tagName == 'A'; } /** * Removes the specified format for the specified node. It will also remove the node if it doesn't have * any attributes if the format specifies it to do so. * * @private * @param {Object} format Format object with items to remove from node. * @param {Object} vars Name/value object with variables to apply to format. * @param {Node} node Node to remove the format styles on. * @param {Node} compare_node Optional compare node, if specified the styles will be compared to that node. * @return {Boolean} True/false if the node was removed or not. */ function removeFormat(format, vars, node, compare_node) { var i, attrs, stylesModified; // Check if node matches format if (!matchName(node, format) && !isColorFormatAndAnchor(node, format)) { return FALSE; } // Should we compare with format attribs and styles if (format.remove != 'all') { // Remove styles each(format.styles, function(value, name) { value = normalizeStyleValue(replaceVars(value, vars), name); // Indexed array if (typeof name === 'number') { name = value; compare_node = 0; } if (format.remove_similar || (!compare_node || isEq(getStyle(compare_node, name), value))) { dom.setStyle(node, name, ''); } stylesModified = 1; }); // Remove style attribute if it's empty if (stylesModified && dom.getAttrib(node, 'style') === '') { node.removeAttribute('style'); node.removeAttribute('data-mce-style'); } // Remove attributes each(format.attributes, function(value, name) { var valueOut; value = replaceVars(value, vars); // Indexed array if (typeof name === 'number') { name = value; compare_node = 0; } if (!compare_node || isEq(dom.getAttrib(compare_node, name), value)) { // Keep internal classes if (name == 'class') { value = dom.getAttrib(node, name); if (value) { // Build new class value where everything is removed except the internal prefixed classes valueOut = ''; each(value.split(/\s+/), function(cls) { if (/mce\-\w+/.test(cls)) { valueOut += (valueOut ? ' ' : '') + cls; } }); // We got some internal classes left if (valueOut) { dom.setAttrib(node, name, valueOut); return; } } } // IE6 has a bug where the attribute doesn't get removed correctly if (name == "class") { node.removeAttribute('className'); } // Remove mce prefixed attributes if (MCE_ATTR_RE.test(name)) { node.removeAttribute('data-mce-' + name); } node.removeAttribute(name); } }); // Remove classes each(format.classes, function(value) { value = replaceVars(value, vars); if (!compare_node || dom.hasClass(compare_node, value)) { dom.removeClass(node, value); } }); // Check for non internal attributes attrs = dom.getAttribs(node); for (i = 0; i < attrs.length; i++) { if (attrs[i].nodeName.indexOf('_') !== 0) { return FALSE; } } } // Remove the inline child if it's empty for example or if (format.remove != 'none') { removeNode(node, format); return TRUE; } } /** * Removes the node and wrap it's children in paragraphs before doing so or * appends BR elements to the beginning/end of the block element if forcedRootBlocks is disabled. * * If the div in the node below gets removed: * text|
formatNode.parentNode.replaceChild(caretContainer, formatNode); } else { // Insert caret container after the formated node dom.insertAfter(caretContainer, formatNode); } // Move selection to text node selection.setCursorLocation(node, 1); // If the formatNode is empty, we can remove it safely. if (dom.isEmpty(formatNode)) { dom.remove(formatNode); } } } // Checks if the parent caret container node isn't empty if that is the case it // will remove the bogus state on all children that isn't empty function unmarkBogusCaretParents() { var caretContainer; caretContainer = getParentCaretContainer(selection.getStart()); if (caretContainer && !dom.isEmpty(caretContainer)) { walk(caretContainer, function(node) { if (node.nodeType == 1 && node.id !== caretContainerId && !dom.isEmpty(node)) { dom.setAttrib(node, 'data-mce-bogus', null); } }, 'childNodes'); } } // Only bind the caret events once if (!ed._hasCaretEvents) { // Mark current caret container elements as bogus when getting the contents so we don't end up with empty elements markCaretContainersBogus = function() { var nodes = [], i; if (isCaretContainerEmpty(getParentCaretContainer(selection.getStart()), nodes)) { // Mark children i = nodes.length; while (i--) { dom.setAttrib(nodes[i], 'data-mce-bogus', '1'); } } }; disableCaretContainer = function(e) { var keyCode = e.keyCode; removeCaretContainer(); // Remove caret container on keydown and it's a backspace, enter or left/right arrow keys // Backspace key needs to check if the range is collapsed due to bug #6780 if ((keyCode == 8 && selection.isCollapsed()) || keyCode == 37 || keyCode == 39) { removeCaretContainer(getParentCaretContainer(selection.getStart())); } unmarkBogusCaretParents(); }; // Remove bogus state if they got filled by contents using editor.selection.setContent ed.on('SetContent', function(e) { if (e.selection) { unmarkBogusCaretParents(); } }); ed._hasCaretEvents = true; } // Do apply or remove caret format if (type == "apply") { applyCaretFormat(); } else { removeCaretFormat(); } } /** * Moves the start to the first suitable text node. */ function moveStart(rng) { var container = rng.startContainer, offset = rng.startOffset, isAtEndOfText, walker, node, nodes, tmpNode; // Convert text node into index if possible if (container.nodeType == 3 && offset >= container.nodeValue.length) { // Get the parent container location and walk from there offset = nodeIndex(container); container = container.parentNode; isAtEndOfText = true; } // Move startContainer/startOffset in to a suitable node if (container.nodeType == 1) { nodes = container.childNodes; container = nodes[Math.min(offset, nodes.length - 1)]; walker = new TreeWalker(container, dom.getParent(container, dom.isBlock)); // If offset is at end of the parent node walk to the next one if (offset > nodes.length - 1 || isAtEndOfText) { walker.next(); } for (node = walker.current(); node; node = walker.next()) { if (node.nodeType == 3 && !isWhiteSpaceNode(node)) { // IE has a "neat" feature where it moves the start node into the closest element // we can avoid this by inserting an element before it and then remove it after we set the selection tmpNode = dom.create('a', {'data-mce-bogus': 'all'}, INVISIBLE_CHAR); node.parentNode.insertBefore(tmpNode, node); // Set selection and remove tmpNode rng.setStart(node, 0); selection.setRng(rng); dom.remove(tmpNode); return; } } } } }; }); // Included from: js/tinymce/classes/UndoManager.js /** * UndoManager.js * * Copyright, Moxiecode Systems AB * Released under LGPL License. * * 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 build in undo/redo has major drawbacks a custom one was needed. * * @class tinymce.UndoManager */ define("tinymce/UndoManager", [ "tinymce/util/VK", "tinymce/Env", "tinymce/util/Tools", "tinymce/html/SaxParser" ], function(VK, Env, Tools, SaxParser) { var trim = Tools.trim, trimContentRegExp; trimContentRegExp = new RegExp([ ']+data-mce-bogus[^>]+>[\u200B\uFEFF]+<\\/span>', // Trim bogus spans like caret containers '\\s?data-mce-selected="[^"]+"' // Trim temporaty data-mce prefixed attributes like data-mce-selected ].join('|'), 'gi'); return function(editor) { var self = this, index = 0, data = [], beforeBookmark, isFirstTypedCharacter, locks = 0; // life ace var canAdd = true; /** * Returns a trimmed version of the editor contents to be used for the undo level. This * will remove any data-mce-bogus="all" marked elements since these are used for UI it will also * remove the data-mce-selected attributes used for selection of objects and caret containers. * It will keep all data-mce-bogus="1" elements since these can be used to place the caret etc and will * be removed by the serialization logic when you save. * * @private * @return {String} HTML contents of the editor excluding some internal bogus elements. */ function getContent() { // life ace if(window.LeaAce && window.getEditorContent) { return getEditorContent(); } var content = editor.getContent({format: 'raw', no_events: 1}); var bogusAllRegExp = /<(\w+) [^>]*data-mce-bogus="all"[^>]*>/g; var endTagIndex, index, matchLength, matches, shortEndedElements, schema = editor.schema; content = content.replace(trimContentRegExp, ''); shortEndedElements = schema.getShortEndedElements(); // Remove all bogus elements marked with "all" while ((matches = bogusAllRegExp.exec(content))) { index = bogusAllRegExp.lastIndex; matchLength = matches[0].length; if (shortEndedElements[matches[1]]) { endTagIndex = index; } else { endTagIndex = SaxParser.findEndTag(schema, content, index); } content = content.substring(0, index - matchLength) + content.substring(endTagIndex); bogusAllRegExp.lastIndex = index - matchLength; } return trim(content); } function addNonTypingUndoLevel(e) { self.typing = false; self.add({}, e); } // Add initial undo level when the editor is initialized editor.on('init', function() { self.add(); }); // Get position before an execCommand is processed editor.on('BeforeExecCommand', function(e) { var cmd = e.command; if (cmd != 'Undo' && cmd != 'Redo' && cmd != 'mceRepaint') { self.beforeChange(); } }); // Add undo level after an execCommand call was made editor.on('ExecCommand', function(e) { var cmd = e.command; if (cmd != 'Undo' && cmd != 'Redo' && cmd != 'mceRepaint') { addNonTypingUndoLevel(e); } }); editor.on('ObjectResizeStart', function() { self.beforeChange(); }); editor.on('SaveContent ObjectResized blur', addNonTypingUndoLevel); editor.on('DragEnd', addNonTypingUndoLevel); editor.on('KeyUp', function(e) { var keyCode = e.keyCode; // life ace // ctrl + shift + c, command + shift + c 代码 if((e.metaKey && e.shiftKey) || (e.ctrlKey && e.shiftKey)) { return; } // life ace // 在ace中回车也会加history if(keyCode == 13 && window.LeaAce && LeaAce.nowIsInAce()) { return; } if ((keyCode >= 33 && keyCode <= 36) || (keyCode >= 37 && keyCode <= 40) || keyCode == 45 || keyCode == 13 || e.ctrlKey) { addNonTypingUndoLevel(); editor.nodeChanged(); } if (keyCode == 46 || keyCode == 8 || (Env.mac && (keyCode == 91 || keyCode == 93))) { editor.nodeChanged(); } // Fire a TypingUndo event on the first character entered if (isFirstTypedCharacter && self.typing) { // Make the it dirty if the content was changed after typing the first character if (!editor.isDirty()) { editor.isNotDirty = !data[0] || getContent() == data[0].content; // Fire initial change event if (!editor.isNotDirty) { editor.fire('change', {level: data[0], lastLevel: null}); } } editor.fire('TypingUndo'); isFirstTypedCharacter = false; editor.nodeChanged(); } }); editor.on('KeyDown', function(e) { var keyCode = e.keyCode; // life ace // 在ace中回车也会加history if(keyCode == 13/* && LeaAce.nowIsInAce()*/) { return; } // Is caracter positon 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; } // life ace // ctrl + shift + c, command + shift + c 代码 if((e.metaKey && e.shiftKey) || (e.ctrlKey || e.shiftKey)) { return; } // If key isn't shift,ctrl,alt,capslock,metakey var modKey = VK.modifierPressed(e); if ((keyCode < 16 || keyCode > 20) && keyCode != 224 && keyCode != 91 && !self.typing && !modKey) { self.beforeChange(); self.typing = true; self.add({}, e); isFirstTypedCharacter = true; } }); editor.on('MouseDown', function(e) { if (self.typing) { addNonTypingUndoLevel(e); } }); // Add keyboard shortcuts for undo/redo keys editor.addShortcut('meta+z', '', 'Undo'); editor.addShortcut('meta+y,meta+shift+z', '', 'Redo'); editor.on('AddUndo Undo Redo ClearUndos', function(e) { if (!e.isDefaultPrevented()) { editor.nodeChanged(); } }); self = { // Explose for debugging reasons data: data, /** * State if the user is currently typing or not. This will add a typing operation into one undo * level instead of one new level for each keystroke. * * @field {Boolean} typing */ typing: false, /** * Stores away a bookmark to be used when performing an undo action so that the selection is before * the change has been made. * * @method beforeChange */ beforeChange: function() { if (!locks) { beforeBookmark = editor.selection.getBookmark(2, true); } }, // life ace setCanAdd: function(status) { canAdd = status; }, /** * 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) { // life ace if(!canAdd) { return; } var i, settings = editor.settings, lastLevel; level = level || {}; level.content = getContent(); if (locks || editor.removed) { return null; } lastLevel = data[index]; if (editor.fire('BeforeAddUndo', {level: level, lastLevel: lastLevel, originalEvent: event}).isDefaultPrevented()) { return null; } // Add undo level if needed if (lastLevel && lastLevel.content == level.content) { return null; } // Set before bookmark on previous level if (data[index]) { data[index].beforeBookmark = beforeBookmark; } // Time to compress if (settings.custom_undo_redo_levels) { if (data.length > settings.custom_undo_redo_levels) { for (i = 0; i < data.length - 1; i++) { data[i] = data[i + 1]; } data.length--; index = data.length; } } // Get a non intrusive normalized bookmark level.bookmark = editor.selection.getBookmark(2, true); // Crop array if needed if (index < data.length - 1) { data.length = index + 1; } data.push(level); index = data.length - 1; var args = {level: level, lastLevel: lastLevel, originalEvent: event}; editor.fire('AddUndo', args); if (index > 0) { editor.isNotDirty = false; editor.fire('change', args); } return level; }, /** * Undoes the last action. * * @method undo * @return {Object} Undo level or null if no undo was performed. */ undo: function() { var level; if (self.typing) { self.add(); self.typing = false; } if (index > 0) { level = data[--index]; // Undo to first index then set dirty state to false if (index === 0) { editor.isNotDirty = true; } editor.setContent(level.content, {format: 'raw'}); editor.selection.moveToBookmark(level.beforeBookmark); editor.fire('undo', {level: level}); } return level; }, /** * Redoes the last action. * * @method redo * @return {Object} Redo level or null if no redo was performed. */ redo: function() { var level; if (index < data.length - 1) { level = data[++index]; editor.setContent(level.content, {format: 'raw'}); editor.selection.moveToBookmark(level.bookmark); editor.fire('redo', {level: level}); } return level; }, /** * Removes all undo levels. * * @method clear */ clear: function() { data = []; index = 0; self.typing = false; editor.fire('ClearUndos'); }, /** * Returns true/false if the undo manager has any undo levels. * * @method hasUndo * @return {Boolean} true/false if the undo manager has any undo levels. */ hasUndo: function() { // Has undo levels or typing and content isn't the same as the initial level return index > 0 || (self.typing && data[0] && getContent() != data[0].content); }, /** * Returns true/false if the undo manager has any redo levels. * * @method hasRedo * @return {Boolean} true/false if the undo manager has any redo levels. */ hasRedo: function() { return index < data.length - 1 && !this.typing; }, /** * Executes the specified function in an undo transation. The selection * before the modification will be stored to the undo stack and if the DOM changes * it will add a new undo level. Any methods within the transation that adds undo levels will * be ignored. So a transation can include calls to execCommand or editor.insertContent. * * @method transact * @param {function} callback Function to execute dom manipulation logic in. */ transact: function(callback) { self.beforeChange(); try { locks++; callback(); } finally { locks--; } self.add(); } }; return self; }; }); // Included from: js/tinymce/classes/EnterKey.js /** * EnterKey.js * * Copyright, Moxiecode Systems AB * Released under LGPL License. * * License: http://www.tinymce.com/license * Contributing: http://www.tinymce.com/contributing */ /** * Contains logic for handling the enter key to split/generate block elements. */ define("tinymce/EnterKey", [ "tinymce/dom/TreeWalker", "tinymce/dom/RangeUtils", "tinymce/Env" ], function(TreeWalker, RangeUtils, Env) { var isIE = Env.ie && Env.ie < 11; return function(editor) { var dom = editor.dom, selection = editor.selection, settings = editor.settings; var undoManager = editor.undoManager, schema = editor.schema, nonEmptyElementsMap = schema.getNonEmptyElements(), moveCaretBeforeOnEnterElementsMap = schema.getMoveCaretBeforeOnEnterElements(); function handleEnterKey(evt) { var rng, tmpRng, editableRoot, container, offset, parentBlock, documentMode, shiftKey, newBlock, fragment, containerBlock, parentBlockName, containerBlockName, newBlockName, isAfterLastNodeInContainer; // Returns true if the block can be split into two blocks or not function canSplitBlock(node) { return node && dom.isBlock(node) && !/^(TD|TH|CAPTION|FORM)$/.test(node.nodeName) && !/^(fixed|absolute)/i.test(node.style.position) && dom.getContentEditable(node) !== "true"; } // Renders empty block on IE function renderBlockOnIE(block) { var oldRng; if (dom.isBlock(block)) { oldRng = selection.getRng(); block.appendChild(dom.create('span', null, '\u00a0')); selection.select(block); block.lastChild.outerHTML = ''; selection.setRng(oldRng); } } // Remove the first empty inline element of the block so this:x
becomes this:x
function trimInlineElementsOnLeftSideOfBlock(block) { var node = block, firstChilds = [], i; if (!node) { return; } // Find inner most first child ex:*
while ((node = node.firstChild)) { if (dom.isBlock(node)) { return; } if (node.nodeType == 1 && !nonEmptyElementsMap[node.nodeName.toLowerCase()]) { firstChilds.push(node); } } i = firstChilds.length; while (i--) { node = firstChilds[i]; if (!node.hasChildNodes() || (node.firstChild == node.lastChild && node.firstChild.nodeValue === '')) { dom.remove(node); } else { // Remove see #5381 if (node.nodeName == "A" && (node.innerText || node.textContent) === ' ') { dom.remove(node); } } } } // Moves the caret to a suitable position within the root for example in the first non // pure whitespace text node or before an image function moveToCaretPosition(root) { var walker, node, rng, lastNode = root, tempElm; function firstNonWhiteSpaceNodeSibling(node) { while (node) { if (node.nodeType == 1 || (node.nodeType == 3 && node.data && /[\r\n\s]/.test(node.data))) { return node; } node = node.nextSibling; } } if (!root) { return; } // Old IE versions doesn't properly render blocks with br elements in them // For exampletext|
text|text2
|
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) && 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()) { 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()}; fragment = parser.parse(value, parserArgs); markInlineFormatElements(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; } } } // If parser says valid we can insert the contents into that parent if (!parserArgs.invalid) { value = serializer.serialize(fragment); // Check if parent is empty or only has one BR element then set the innerHTML of that parent node = parentNode.firstChild; node2 = parentNode.lastChild; if (!node || (node === node2 && node.nodeName === 'BR')) { dom.setHTML(parentNode, value); } else { selection.setContent(value); } } else { // If the fragment was invalid within that context then we need // to parse and process the parent it's inserted into // Insert bookmark node and get the parent selection.setContent(bookmarkHtml); parentNode = selection.getNode(); rootNode = editor.getBody(); // Opera will return the document node when selection is in root if (parentNode.nodeType == 9) { parentNode = node = rootNode; } else { node = parentNode; } // Find the ancestor just before the root element while (node !== rootNode) { parentNode = node; node = node.parentNode; } // Get the outer/inner HTML depending on if we are in the root and parser and serialize that value = parentNode == rootNode ? rootNode.innerHTML : dom.getOuterHTML(parentNode); value = serializer.serialize( parser.parse( // Need to replace by using a function since $ in the contents would otherwise be a problem value.replace(//i, function() { return serializer.serialize(fragment); }) ) ); // life ace // 插入的时候把内的标签全清掉
// life 把间的代码拿出, 去掉标签之类的
// console.log(value);
//
xxx
//
xxx
// console.log('life');
// console.log(value);
value = value.replace(/]*?)>([\s\S]*?)<\/pre>/g, function(v, v1, v2) {
// v == "a, b, c
"
var hasBookmark = false;
var b = '';
if(v2.indexOf('id="mce_marker') != -1) { // mini后不是b了
hasBookmark = true;
}
v2 = v2.replace(/(<([^>]+)>)/gi, '').replace(/\s+$/, ''); // 把最后一个换行去掉
if(hasBookmark) {
v2 += b;
}
return "" + v2 + "
";
});
// console.log(value);
// 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();
marker = dom.get('mce_marker');
selection.scrollIntoView(marker);
// Move selection before marker and remove it
rng = dom.createRng();
// If previous sibling is a text node set the selection to the end of that node
node = marker.previousSibling;
if (node && node.nodeType == 3) {
rng.setStart(node, node.nodeValue.length);
// TODO: Why can't we normalize on IE
if (!isIE) {
node2 = marker.nextSibling;
if (node2 && node2.nodeType == 3) {
node.appendData(node2.data);
node2.parentNode.removeChild(node2);
}
}
} else {
// If the previous sibling isn't a text node or doesn't exist set the selection before the marker node
rng.setStartBefore(marker);
rng.setEndBefore(marker);
}
// Remove the marker node and set the new range
dom.remove(marker);
selection.setRng(rng);
// Dispatch after event and add any visual elements needed
editor.fire('SetContent', args);
editor.addVisual();
},
// life ace
// life 修改
// 之前不是这个版本, 为什么要改, 因为考虑到在pre下粘贴后全部内容会修改(ace不友好)
// 这里不是改全部的内容, 对ace好
mceInsertRawHTML: function(command, ui, value) {
var parser, serializer, parentNode, rootNode, fragment, args;
var marker, rng, node, node2, bookmarkHtml;
// Setup parser and serializer
parser = editor.parser;
serializer = new Serializer({}, editor.schema);
bookmarkHtml = '';
// Run beforeSetContent handlers on the HTML to be inserted
args = {content: value, format: 'html', selection: true};
editor.fire('BeforeSetContent', args);
value = args.content;
// Add caret at end of contents if it's missing
if (value.indexOf('{$caret}') == -1) {
value += '{$caret}';
}
// Replace the caret marker with a span bookmark element
value = value.replace(/\{\$caret\}/, bookmarkHtml);
// If selection is at | then move it into |
var body = editor.getBody();
if (dom.isBlock(body.firstChild) && dom.isEmpty(body.firstChild)) {
body.firstChild.appendChild(dom.doc.createTextNode('\u00a0'));
selection.select(body.firstChild, true);
dom.remove(body.firstChild.lastChild);
}
// Insert node maker where we will insert the new HTML and get it's parent
if (!selection.isCollapsed()) {
editor.getDoc().execCommand('Delete', false, null);
}
parentNode = selection.getNode();
// Parse the fragment within the context of the parent node
var parserArgs = {context: parentNode.nodeName.toLowerCase()};
fragment = parser.parse(value, parserArgs);
// Move the caret to a more suitable location
node = fragment.lastChild;
if (node.attr('id') == 'mce_marker') {
marker = node;
for (node = node.prev; node; node = node.walk(true)) {
if (node.type == 3 || !dom.isBlock(node.name)) {
node.parent.insert(marker, node, node.name === 'br');
break;
}
}
}
// If parser says valid we can insert the contents into that parent
if (!parserArgs.invalid) {
// Check if parent is empty or only has one BR element then set the innerHTML of that parent
node = parentNode.firstChild;
node2 = parentNode.lastChild;
if (!node || (node === node2 && node.nodeName === 'BR')) {
dom.setHTML(parentNode, value);
} else {
selection.setContent(value);
}
} else {
// If the fragment was invalid within that context then we need
// to parse and process the parent it's inserted into
// Insert bookmark node and get the parent
selection.setContent(bookmarkHtml);
parentNode = selection.getNode();
rootNode = editor.getBody();
// Opera will return the document node when selection is in root
if (parentNode.nodeType == 9) {
parentNode = node = rootNode;
} else {
node = parentNode;
}
// Find the ancestor just before the root element
while (node !== rootNode) {
parentNode = node;
node = node.parentNode;
}
// Get the outer/inner HTML depending on if we are in the root and parser and serialize that
value = parentNode == rootNode ? rootNode.innerHTML : dom.getOuterHTML(parentNode);
// 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);
}
}
marker = dom.get('mce_marker');
selection.scrollIntoView(marker);
// Move selection before marker and remove it
rng = dom.createRng();
// If previous sibling is a text node set the selection to the end of that node
node = marker.previousSibling;
if (node && node.nodeType == 3) {
rng.setStart(node, node.nodeValue.length);
// TODO: Why can't we normalize on IE
if (!isIE) {
node2 = marker.nextSibling;
if (node2 && node2.nodeType == 3) {
node.appendData(node2.data);
node2.parentNode.removeChild(node2);
}
}
} else {
// If the previous sibling isn't a text node or doesn't exist set the selection before the marker node
rng.setStartBefore(marker);
rng.setEndBefore(marker);
}
// Remove the marker node and set the new range
dom.remove(marker);
selection.setRng(rng);
// Dispatch after event and add any visual elements needed
editor.fire('SetContent', args);
editor.addVisual()
},
/* life
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 (element.nodeName != "LI") {
var indentStyleName = editor.getParam('indent_use_margin', false) ? 'margin' : 'padding';
indentStyleName += dom.getStyle(element, 'direction', true) == 'rtl' ? 'Right' : 'Left';
if (command == 'outdent') {
value = Math.max(0, parseInt(element.style[indentStyleName] || 0, 10) - intentValue);
dom.setStyle(element, indentStyleName, value ? value + indentUnit : '');
} else {
value = (parseInt(element.style[indentStyleName] || 0, 10) + intentValue) + indentUnit;
dom.setStyle(element, indentStyleName, value);
}
}
});
} else {
execNativeCommand(command);
}
},
mceRepaint: function() {
if (isGecko) {
try {
storeSelection(TRUE);
if (selection.getSel()) {
selection.getSel().selectAllChildren(editor.getBody());
}
selection.collapse(TRUE);
restoreSelection();
} catch (ex) {
// Ignore
}
}
},
InsertHorizontalRule: function() {
editor.execCommand('mceInsertContent', false, '
');
},
mceToggleVisualAid: function() {
editor.hasVisual = !editor.hasVisual;
editor.addVisual();
},
mceReplaceContent: function(command, ui, value) {
editor.execCommand('mceInsertContent', false, value.replace(/\{\$selection\}/g, selection.getContent({format: 'text'})));
},
mceInsertLink: function(command, ui, value) {
var anchor;
if (typeof value == 'string') {
value = {href: value};
}
anchor = dom.getParent(selection.getNode(), 'a');
// Spaces are never valid in URLs and it's a very common mistake for people to make so we fix it here.
value.href = value.href.replace(' ', '%20');
// Remove existing links if there could be child links or that the href isn't specified
if (!anchor || !value.href) {
formatter.remove('link');
}
// Apply new link to selection
if (value.href) {
formatter.apply('link', value, anchor);
}
},
selectAll: function() {
var root = dom.getRoot(), rng;
if (selection.getRng().setStart) {
rng = dom.createRng();
rng.setStart(root, 0);
rng.setEnd(root, root.childNodes.length);
selection.setRng(rng);
} else {
// IE will render it's own root level block elements and sometimes
// even put font elements in them when the user starts typing. So we need to
// move the selection to a more suitable element from this:
// | to this: |
rng = selection.getRng();
if (!rng.item) {
rng.moveToElementText(root);
rng.select();
}
}
},
"delete": function() {
execNativeCommand("Delete");
// Check if body is empty after the delete call if so then set the contents
// to an empty string and move the caret to any block produced by that operation
// this fixes the issue with root blocks not being properly produced after a delete call on IE
var body = editor.getBody();
if (dom.isEmpty(body)) {
editor.setContent('');
if (body.firstChild && dom.isBlock(body.firstChild)) {
editor.selection.setCursorLocation(body.firstChild, 0);
} else {
editor.selection.setCursorLocation(body, 0);
}
}
},
mceNewDocument: function() {
editor.setContent('');
},
InsertLineBreak: function(command, ui, value) {
// We load the current event in from EnterKey.js when appropriate to heed
// certain event-specific variations such as ctrl-enter in a list
var evt = value;
var brElm, extraBr, marker;
var rng = selection.getRng(true);
new RangeUtils(dom).normalize(rng);
var offset = rng.startOffset;
var container = rng.startContainer;
// Resolve node index
if (container.nodeType == 1 && container.hasChildNodes()) {
var isAfterLastNodeInContainer = offset > container.childNodes.length - 1;
container = container.childNodes[Math.min(offset, container.childNodes.length - 1)] || container;
if (isAfterLastNodeInContainer && container.nodeType == 3) {
offset = container.nodeValue.length;
} else {
offset = 0;
}
}
var parentBlock = dom.getParent(container, dom.isBlock);
var parentBlockName = parentBlock ? parentBlock.nodeName.toUpperCase() : ''; // IE < 9 & HTML5
var containerBlock = parentBlock ? dom.getParent(parentBlock.parentNode, dom.isBlock) : null;
var containerBlockName = containerBlock ? containerBlock.nodeName.toUpperCase() : ''; // IE < 9 & HTML5
// Enter inside block contained within a LI then split or insert before/after LI
var isControlKey = evt && evt.ctrlKey;
if (containerBlockName == 'LI' && !isControlKey) {
parentBlock = containerBlock;
parentBlockName = containerBlockName;
}
// Walks the parent block to the right and look for BR elements
function hasRightSideContent() {
var walker = new TreeWalker(container, parentBlock), node;
var nonEmptyElementsMap = editor.schema.getNonEmptyElements();
while ((node = walker.next())) {
if (nonEmptyElementsMap[node.nodeName.toLowerCase()] || node.length > 0) {
return true;
}
}
}
if (container && container.nodeType == 3 && offset >= container.nodeValue.length) {
// Insert extra BR element at the end block elements
if (!isOldIE && !hasRightSideContent()) {
brElm = dom.create('br');
rng.insertNode(brElm);
rng.setStartAfter(brElm);
rng.setEndAfter(brElm);
extraBr = true;
}
}
brElm = dom.create('br');
rng.insertNode(brElm);
// Rendering modes below IE8 doesn't display BR elements in PRE unless we have a \n before it
var documentMode = dom.doc.documentMode;
if (isOldIE && parentBlockName == 'PRE' && (!documentMode || documentMode < 8)) {
brElm.parentNode.insertBefore(dom.doc.createTextNode('\r'), brElm);
}
// Insert temp marker and scroll to that
marker = dom.create('span', {}, ' ');
brElm.parentNode.insertBefore(marker, brElm);
selection.scrollIntoView(marker);
dom.remove(marker);
if (!extraBr) {
rng.setStartAfter(brElm);
rng.setEndAfter(brElm);
} else {
rng.setStartBefore(brElm);
rng.setEndBefore(brElm);
}
selection.setRng(rng);
editor.undoManager.add();
return TRUE;
}
});
// Add queryCommandState overrides
addCommands({
// Override justify commands
'JustifyLeft,JustifyCenter,JustifyRight,JustifyFull': function(command) {
var name = 'align' + command.substring(7);
var nodes = selection.isCollapsed() ? [dom.getParent(selection.getNode(), dom.isBlock)] : selection.getSelectedBlocks();
var matches = map(nodes, function(node) {
return !!formatter.matchNode(node, name);
});
return inArray(matches, TRUE) !== -1;
},
'Bold,Italic,Underline,Strikethrough,Superscript,Subscript': function(command) {
return isFormatMatch(command);
},
mceBlockQuote: function() {
return isFormatMatch('blockquote');
},
Outdent: function() {
var node;
if (settings.inline_styles) {
if ((node = dom.getParent(selection.getStart(), dom.isBlock)) && parseInt(node.style.paddingLeft, 10) > 0) {
return TRUE;
}
if ((node = dom.getParent(selection.getEnd(), dom.isBlock)) && parseInt(node.style.paddingLeft, 10) > 0) {
return TRUE;
}
}
return (
queryCommandState('InsertUnorderedList') ||
queryCommandState('InsertOrderedList') ||
(!settings.inline_styles && !!dom.getParent(selection.getNode(), 'BLOCKQUOTE'))
);
},
'InsertUnorderedList,InsertOrderedList': function(command) {
var list = dom.getParent(selection.getNode(), 'ul,ol');
return list &&
(
command === 'insertunorderedlist' && list.tagName === 'UL' ||
command === 'insertorderedlist' && list.tagName === 'OL'
);
}
}, 'state');
// Add queryCommandValue overrides
addCommands({
'FontSize,FontName': function(command) {
var value = 0, parent;
if ((parent = dom.getParent(selection.getNode(), 'span'))) {
if (command == 'fontsize') {
value = parent.style.fontSize;
} else {
value = parent.style.fontFamily.replace(/, /g, ',').replace(/[\'\"]/g, '').toLowerCase();
}
}
return value;
}
}, 'value');
// Add undo manager logic
addCommands({
Undo: function() {
editor.undoManager.undo();
},
Redo: function() {
editor.undoManager.redo();
}
});
};
});
// Included from: js/tinymce/classes/util/URI.js
/**
* URI.js
*
* Copyright, Moxiecode Systems AB
* Released under LGPL License.
*
* License: http://www.tinymce.com/license
* Contributing: http://www.tinymce.com/contributing
*/
/**
* This class handles parsing, modification and serialization of URI/URL strings.
* @class tinymce.util.URI
*/
define("tinymce/util/URI", [
"tinymce/util/Tools"
], function(Tools) {
var each = Tools.each, trim = Tools.trim;
var queryParts = "source protocol authority userInfo user password host port relative path directory file query anchor".split(' ');
var DEFAULT_PORTS = {
'ftp': 21,
'http': 80,
'https': 443,
'mailto': 25
};
/**
* Constructs a new URI instance.
*
* @constructor
* @method URI
* @param {String} url URI string to parse.
* @param {Object} settings Optional settings object.
*/
function URI(url, settings) {
var self = this, baseUri, base_url;
url = trim(url);
settings = self.settings = settings || {};
baseUri = settings.base_uri;
// Strange app protocol that isn't http/https or local anchor
// For example: mailto,skype,tel etc.
if (/^([\w\-]+):([^\/]{2})/i.test(url) || /^\s*#/.test(url)) {
self.source = url;
return;
}
var isProtocolRelative = url.indexOf('//') === 0;
// Absolute path with no host, fake host and protocol
if (url.indexOf('/') === 0 && !isProtocolRelative) {
url = (baseUri ? baseUri.protocol || 'http' : 'http') + '://mce_host' + url;
}
// Relative path http:// or protocol relative //path
if (!/^[\w\-]*:?\/\//.test(url)) {
base_url = settings.base_uri ? settings.base_uri.path : new URI(location.href).directory;
if (settings.base_uri.protocol === "") {
url = '//mce_host' + self.toAbsPath(base_url, url);
} else {
url = /([^#?]*)([#?]?.*)/.exec(url);
url = ((baseUri && baseUri.protocol) || 'http') + '://mce_host' + self.toAbsPath(base_url, url[1]) + url[2];
}
}
// Parse URL (Credits goes to Steave, http://blog.stevenlevithan.com/archives/parseuri)
url = url.replace(/@@/g, '(mce_at)'); // Zope 3 workaround, they use @@something
/*jshint maxlen: 255 */
/*eslint max-len: 0 */
url = /^(?:(?![^:@]+:[^:@\/]*@)([^:\/?#.]+):)?(?:\/\/)?((?:(([^:@\/]*):?([^:@\/]*))?@)?([^:\/?#]*)(?::(\d*))?)(((\/(?:[^?#](?![^?#\/]*\.[^?#\/.]+(?:[?#]|$)))*\/?)?([^?#\/]*))(?:\?([^#]*))?(?:#(.*))?)/.exec(url);
each(queryParts, function(v, i) {
var part = url[i];
// Zope 3 workaround, they use @@something
if (part) {
part = part.replace(/\(mce_at\)/g, '@@');
}
self[v] = part;
});
if (baseUri) {
if (!self.protocol) {
self.protocol = baseUri.protocol;
}
if (!self.userInfo) {
self.userInfo = baseUri.userInfo;
}
if (!self.port && self.host === 'mce_host') {
self.port = baseUri.port;
}
if (!self.host || self.host === 'mce_host') {
self.host = baseUri.host;
}
self.source = '';
}
if (isProtocolRelative) {
self.protocol = '';
}
//t.path = t.path || '/';
}
URI.prototype = {
/**
* Sets the internal path part of the URI.
*
* @method setPath
* @param {string} path Path string to set.
*/
setPath: function(path) {
var self = this;
path = /^(.*?)\/?(\w+)?$/.exec(path);
// Update path parts
self.path = path[0];
self.directory = path[1];
self.file = path[2];
// Rebuild source
self.source = '';
self.getURI();
},
/**
* Converts the specified URI into a relative URI based on the current URI instance location.
*
* @method toRelative
* @param {String} uri URI to convert into a relative path/URI.
* @return {String} Relative URI from the point specified in the current URI instance.
* @example
* // Converts an absolute URL to an relative URL url will be somedir/somefile.htm
* var url = new tinymce.util.URI('http://www.site.com/dir/').toRelative('http://www.site.com/dir/somedir/somefile.htm');
*/
toRelative: function(uri) {
var self = this, output;
if (uri === "./") {
return uri;
}
uri = new URI(uri, {base_uri: self});
// Not on same domain/port or protocol
if ((uri.host != 'mce_host' && self.host != uri.host && uri.host) || self.port != uri.port ||
(self.protocol != uri.protocol && uri.protocol !== "")) {
return uri.getURI();
}
var tu = self.getURI(), uu = uri.getURI();
// Allow usage of the base_uri when relative_urls = true
if (tu == uu || (tu.charAt(tu.length - 1) == "/" && tu.substr(0, tu.length - 1) == uu)) {
return tu;
}
output = self.toRelPath(self.path, uri.path);
// Add query
if (uri.query) {
output += '?' + uri.query;
}
// Add anchor
if (uri.anchor) {
output += '#' + uri.anchor;
}
return output;
},
/**
* Converts the specified URI into a absolute URI based on the current URI instance location.
*
* @method toAbsolute
* @param {String} uri URI to convert into a relative path/URI.
* @param {Boolean} noHost No host and protocol prefix.
* @return {String} Absolute URI from the point specified in the current URI instance.
* @example
* // Converts an relative URL to an absolute URL url will be http://www.site.com/dir/somedir/somefile.htm
* var url = new tinymce.util.URI('http://www.site.com/dir/').toAbsolute('somedir/somefile.htm');
*/
toAbsolute: function(uri, noHost) {
uri = new URI(uri, {base_uri: this});
return uri.getURI(noHost && this.isSameOrigin(uri));
},
/**
* Determine whether the given URI has the same origin as this URI. Based on RFC-6454.
* Supports default ports for protocols listed in DEFAULT_PORTS. Unsupported protocols will fail safe: they
* won't match, if the port specifications differ.
*
* @method isSameOrigin
* @param {tinymce.util.URI} uri Uri instance to compare.
* @returns {Boolean} True if the origins are the same.
*/
isSameOrigin: function(uri) {
if (this.host == uri.host && this.protocol == uri.protocol) {
if (this.port == uri.port) {
return true;
}
var defaultPort = DEFAULT_PORTS[this.protocol];
if (defaultPort && ((this.port || defaultPort) == (uri.port || defaultPort))) {
return true;
}
}
return false;
},
/**
* Converts a absolute path into a relative path.
*
* @method toRelPath
* @param {String} base Base point to convert the path from.
* @param {String} path Absolute path to convert into a relative path.
*/
toRelPath: function(base, path) {
var items, breakPoint = 0, out = '', i, l;
// Split the paths
base = base.substring(0, base.lastIndexOf('/'));
base = base.split('/');
items = path.split('/');
if (base.length >= items.length) {
for (i = 0, l = base.length; i < l; i++) {
if (i >= items.length || base[i] != items[i]) {
breakPoint = i + 1;
break;
}
}
}
if (base.length < items.length) {
for (i = 0, l = items.length; i < l; i++) {
if (i >= base.length || base[i] != items[i]) {
breakPoint = i + 1;
break;
}
}
}
if (breakPoint === 1) {
return path;
}
for (i = 0, l = base.length - (breakPoint - 1); i < l; i++) {
out += "../";
}
for (i = breakPoint - 1, l = items.length; i < l; i++) {
if (i != breakPoint - 1) {
out += "/" + items[i];
} else {
out += items[i];
}
}
return out;
},
/**
* Converts a relative path into a absolute path.
*
* @method toAbsPath
* @param {String} base Base point to convert the path from.
* @param {String} path Relative path to convert into an absolute path.
*/
toAbsPath: function(base, path) {
var i, nb = 0, o = [], tr, outPath;
// Split paths
tr = /\/$/.test(path) ? '/' : '';
base = base.split('/');
path = path.split('/');
// Remove empty chunks
each(base, function(k) {
if (k) {
o.push(k);
}
});
base = o;
// Merge relURLParts chunks
for (i = path.length - 1, o = []; i >= 0; i--) {
// Ignore empty or .
if (path[i].length === 0 || path[i] === ".") {
continue;
}
// Is parent
if (path[i] === '..') {
nb++;
continue;
}
// Move up
if (nb > 0) {
nb--;
continue;
}
o.push(path[i]);
}
i = base.length - nb;
// If /a/b/c or /
if (i <= 0) {
outPath = o.reverse().join('/');
} else {
outPath = base.slice(0, i).join('/') + '/' + o.reverse().join('/');
}
// Add front / if it's needed
if (outPath.indexOf('/') !== 0) {
outPath = '/' + outPath;
}
// Add traling / if it's needed
if (tr && outPath.lastIndexOf('/') !== outPath.length - 1) {
outPath += tr;
}
return outPath;
},
/**
* Returns the full URI of the internal structure.
*
* @method getURI
* @param {Boolean} noProtoHost Optional no host and protocol part. Defaults to false.
*/
getURI: function(noProtoHost) {
var s, self = this;
// Rebuild source
if (!self.source || noProtoHost) {
s = '';
if (!noProtoHost) {
if (self.protocol) {
s += self.protocol + '://';
} else {
s += '//';
}
if (self.userInfo) {
s += self.userInfo + '@';
}
if (self.host) {
s += self.host;
}
if (self.port) {
s += ':' + self.port;
}
}
if (self.path) {
s += self.path;
}
if (self.query) {
s += '?' + self.query;
}
if (self.anchor) {
s += '#' + self.anchor;
}
self.source = s;
}
return self.source;
}
};
return URI;
});
// Included from: js/tinymce/classes/util/Class.js
/**
* Class.js
*
* Copyright 2003-2012, Moxiecode Systems AB, All rights reserved.
*/
/**
* This utilitiy class is used for easier inheritage.
*
* Features:
* * Exposed super functions: this._super();
* * Mixins
* * Dummy functions
* * Property functions: var value = object.value(); and object.value(newValue);
* * Static functions
* * Defaults settings
*/
define("tinymce/util/Class", [
"tinymce/util/Tools"
], function(Tools) {
var each = Tools.each, extend = Tools.extend;
var extendClass, initializing;
function Class() {
}
// Provides classical inheritance, based on code made by John Resig
Class.extend = extendClass = function(prop) {
var self = this, _super = self.prototype, prototype, name, member;
// The dummy class constructor
function Class() {
var i, mixins, mixin, self = this;
// All construction is actually done in the init method
if (!initializing) {
// Run class constuctor
if (self.init) {
self.init.apply(self, arguments);
}
// Run mixin constructors
mixins = self.Mixins;
if (mixins) {
i = mixins.length;
while (i--) {
mixin = mixins[i];
if (mixin.init) {
mixin.init.apply(self, arguments);
}
}
}
}
}
// Dummy function, needs to be extended in order to provide functionality
function dummy() {
return this;
}
// Creates a overloaded method for the class
// this enables you to use this._super(); to call the super function
function createMethod(name, fn) {
return function() {
var self = this, tmp = self._super, ret;
self._super = _super[name];
ret = fn.apply(self, arguments);
self._super = tmp;
return ret;
};
}
// Instantiate a base class (but only create the instance,
// don't run the init constructor)
initializing = true;
/*eslint new-cap:0 */
prototype = new self();
initializing = false;
// Add mixins
if (prop.Mixins) {
each(prop.Mixins, function(mixin) {
mixin = mixin;
for (var name in mixin) {
if (name !== "init") {
prop[name] = mixin[name];
}
}
});
if (_super.Mixins) {
prop.Mixins = _super.Mixins.concat(prop.Mixins);
}
}
// Generate dummy methods
if (prop.Methods) {
each(prop.Methods.split(','), function(name) {
prop[name] = dummy;
});
}
// Generate property methods
if (prop.Properties) {
each(prop.Properties.split(','), function(name) {
var fieldName = '_' + name;
prop[name] = function(value) {
var self = this, undef;
// Set value
if (value !== undef) {
self[fieldName] = value;
return self;
}
// Get value
return self[fieldName];
};
});
}
// Static functions
if (prop.Statics) {
each(prop.Statics, function(func, name) {
Class[name] = func;
});
}
// Default settings
if (prop.Defaults && _super.Defaults) {
prop.Defaults = extend({}, _super.Defaults, prop.Defaults);
}
// Copy the properties over onto the new prototype
for (name in prop) {
member = prop[name];
if (typeof member == "function" && _super[name]) {
prototype[name] = createMethod(name, member);
} else {
prototype[name] = member;
}
}
// Populate our constructed prototype object
Class.prototype = prototype;
// Enforce the constructor to be what we expect
Class.constructor = Class;
// And make this class extendible
Class.extend = extendClass;
return Class;
};
return Class;
});
// Included from: js/tinymce/classes/util/EventDispatcher.js
/**
* EventDispatcher.js
*
* Copyright, Moxiecode Systems AB
* Released under LGPL License.
*
* License: http://www.tinymce.com/license
* Contributing: http://www.tinymce.com/contributing
*/
/**
* This class lets you add/remove and fire events by name on the specified scope. This makes
* it easy to add event listener logic to any class.
*
* @class tinymce.util.EventDispatcher
* @example
* var eventDispatcher = new EventDispatcher();
*
* eventDispatcher.on('click', function() {console.log('data');});
* eventDispatcher.fire('click', {data: 123});
*/
define("tinymce/util/EventDispatcher", [
"tinymce/util/Tools"
], function(Tools) {
var nativeEvents = Tools.makeMap(
"focus blur focusin focusout click dblclick mousedown mouseup mousemove mouseover beforepaste paste cut copy selectionchange " +
"mouseout mouseenter mouseleave wheel keydown keypress keyup input contextmenu dragstart dragend dragover " +
"draggesture dragdrop drop drag submit " +
"compositionstart compositionend compositionupdate touchstart touchend",
' '
);
function Dispatcher(settings) {
var self = this, scope, bindings = {}, toggleEvent;
function returnFalse() {
return false;
}
function returnTrue() {
return true;
}
settings = settings || {};
scope = settings.scope || self;
toggleEvent = settings.toggleEvent || returnFalse;
/**
* Fires the specified event by name.
*
* @method fire
* @param {String} name Name of the event to fire.
* @param {Object?} args Event arguments.
* @return {Object} Event args instance passed in.
* @example
* instance.fire('event', {...});
*/
function fire(name, args) {
var handlers, i, l, callback;
name = name.toLowerCase();
args = args || {};
args.type = name;
// Setup target is there isn't one
if (!args.target) {
args.target = scope;
}
// Add event delegation methods if they are missing
if (!args.preventDefault) {
// Add preventDefault method
args.preventDefault = function() {
args.isDefaultPrevented = returnTrue;
};
// Add stopPropagation
args.stopPropagation = function() {
args.isPropagationStopped = returnTrue;
};
// Add stopImmediatePropagation
args.stopImmediatePropagation = function() {
args.isImmediatePropagationStopped = returnTrue;
};
// Add event delegation states
args.isDefaultPrevented = returnFalse;
args.isPropagationStopped = returnFalse;
args.isImmediatePropagationStopped = returnFalse;
}
if (settings.beforeFire) {
settings.beforeFire(args);
}
handlers = bindings[name];
if (handlers) {
for (i = 0, l = handlers.length; i < l; i++) {
callback = handlers[i];
// Unbind handlers marked with "once"
if (callback.once) {
off(name, callback.func);
}
// Stop immediate propagation if needed
if (args.isImmediatePropagationStopped()) {
args.stopPropagation();
return args;
}
// If callback returns false then prevent default and stop all propagation
if (callback.func.call(scope, args) === false) {
args.preventDefault();
return args;
}
}
}
return args;
}
/**
* Binds an event listener to a specific event by name.
*
* @method on
* @param {String} name Event name or space separated list of events to bind.
* @param {callback} callback Callback to be executed when the event occurs.
* @param {Boolean} first Optional flag if the event should be prepended. Use this with care.
* @return {Object} Current class instance.
* @example
* instance.on('event', function(e) {
* // Callback logic
* });
*/
function on(name, callback, prepend, extra) {
var handlers, names, i;
if (callback === false) {
callback = returnFalse;
}
if (callback) {
callback = {
func: callback
};
if (extra) {
Tools.extend(callback, extra);
}
names = name.toLowerCase().split(' ');
i = names.length;
while (i--) {
name = names[i];
handlers = bindings[name];
if (!handlers) {
handlers = bindings[name] = [];
toggleEvent(name, true);
}
if (prepend) {
handlers.unshift(callback);
} else {
handlers.push(callback);
}
}
}
return self;
}
/**
* Unbinds an event listener to a specific event by name.
*
* @method off
* @param {String?} name Name of the event to unbind.
* @param {callback?} callback Callback to unbind.
* @return {Object} Current class instance.
* @example
* // Unbind specific callback
* instance.off('event', handler);
*
* // Unbind all listeners by name
* instance.off('event');
*
* // Unbind all events
* instance.off();
*/
function off(name, callback) {
var i, handlers, bindingName, names, hi;
if (name) {
names = name.toLowerCase().split(' ');
i = names.length;
while (i--) {
name = names[i];
handlers = bindings[name];
// Unbind all handlers
if (!name) {
for (bindingName in bindings) {
toggleEvent(bindingName, false);
delete bindings[bindingName];
}
return self;
}
if (handlers) {
// Unbind all by name
if (!callback) {
handlers.length = 0;
} else {
// Unbind specific ones
hi = handlers.length;
while (hi--) {
if (handlers[hi].func === callback) {
handlers = handlers.slice(0, hi).concat(handlers.slice(hi + 1));
bindings[name] = handlers;
}
}
}
if (!handlers.length) {
toggleEvent(name, false);
delete bindings[name];
}
}
}
} else {
for (name in bindings) {
toggleEvent(name, false);
}
bindings = {};
}
return self;
}
/**
* Binds an event listener to a specific event by name
* and automatically unbind the event once the callback fires.
*
* @method once
* @param {String} name Event name or space separated list of events to bind.
* @param {callback} callback Callback to be executed when the event occurs.
* @param {Boolean} first Optional flag if the event should be prepended. Use this with care.
* @return {Object} Current class instance.
* @example
* instance.once('event', function(e) {
* // Callback logic
* });
*/
function once(name, callback, prepend) {
return on(name, callback, prepend, {once: true});
}
/**
* Returns true/false if the dispatcher has a event of the specified name.
*
* @method has
* @param {String} name Name of the event to check for.
* @return {Boolean} true/false if the event exists or not.
*/
function has(name) {
name = name.toLowerCase();
return !(!bindings[name] || bindings[name].length === 0);
}
// Expose
self.fire = fire;
self.on = on;
self.off = off;
self.once = once;
self.has = has;
}
/**
* Returns true/false if the specified event name is a native browser event or not.
*
* @method isNative
* @param {String} name Name to check if it's native.
* @return {Boolean} true/false if the event is native or not.
* @static
*/
Dispatcher.isNative = function(name) {
return !!nativeEvents[name.toLowerCase()];
};
return Dispatcher;
});
// Included from: js/tinymce/classes/ui/Selector.js
/**
* Selector.js
*
* Copyright, Moxiecode Systems AB
* Released under LGPL License.
*
* 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:
* element:not()
* element:first
* element:last
* element:odd
* element:even
* element element
* element > element
*
* @class tinymce.ui.Selector
*/
define("tinymce/ui/Selector", [
"tinymce/util/Class"
], function(Class) {
"use strict";
/**
* Produces an array with a unique set of objects. It will not compare the values
* but the references of the objects.
*
* @private
* @method unqiue
* @param {Array} array Array to make into an array with unique items.
* @return {Array} Array with unique items.
*/
function unique(array) {
var uniqueItems = [], i = array.length, item;
while (i--) {
item = array[i];
if (!item.__checked) {
uniqueItems.push(item);
item.__checked = 1;
}
}
i = uniqueItems.length;
while (i--) {
delete uniqueItems[i].__checked;
}
return uniqueItems;
}
var expression = /^([\w\\*]+)?(?:#([\w\\]+))?(?:\.([\w\\\.]+))?(?:\[\@?([\w\\]+)([\^\$\*!~]?=)([\w\\]+)\])?(?:\:(.+))?/i;
/*jshint maxlen:255 */
/*eslint max-len:0 */
var chunker = /((?:\((?:\([^()]+\)|[^()]+)+\)|\[(?:\[[^\[\]]*\]|['"][^'"]*['"]|[^\[\]'"]+)+\]|\\.|[^ >+~,(\[\\]+)+|[>+~])(\s*,\s*)?((?:.|\r|\n)*)/g,
whiteSpace = /^\s*|\s*$/g,
Collection;
var Selector = Class.extend({
/**
* Constructs a new Selector instance.
*
* @constructor
* @method init
* @param {String} selector CSS like selector expression.
*/
init: function(selector) {
var match = this.match;
function compileNameFilter(name) {
if (name) {
name = name.toLowerCase();
return function(item) {
return name === '*' || item.type === name;
};
}
}
function compileIdFilter(id) {
if (id) {
return function(item) {
return item._name === id;
};
}
}
function compileClassesFilter(classes) {
if (classes) {
classes = classes.split('.');
return function(item) {
var i = classes.length;
while (i--) {
if (!item.hasClass(classes[i])) {
return false;
}
}
return true;
};
}
}
function compileAttrFilter(name, cmp, check) {
if (name) {
return function(item) {
var value = item[name] ? item[name]() : '';
return !cmp ? !!check :
cmp === "=" ? value === check :
cmp === "*=" ? value.indexOf(check) >= 0 :
cmp === "~=" ? (" " + value + " ").indexOf(" " + check + " ") >= 0 :
cmp === "!=" ? value != check :
cmp === "^=" ? value.indexOf(check) === 0 :
cmp === "$=" ? value.substr(value.length - check.length) === check :
false;
};
}
}
function compilePsuedoFilter(name) {
var notSelectors;
if (name) {
name = /(?:not\((.+)\))|(.+)/i.exec(name);
if (!name[1]) {
name = name[2];
return function(item, index, length) {
return name === 'first' ? index === 0 :
name === 'last' ? index === length - 1 :
name === 'even' ? index % 2 === 0 :
name === 'odd' ? index % 2 === 1 :
item[name] ? item[name]() :
false;
};
} else {
// Compile not expression
notSelectors = parseChunks(name[1], []);
return function(item) {
return !match(item, notSelectors);
};
}
}
}
function compile(selector, filters, direct) {
var parts;
function add(filter) {
if (filter) {
filters.push(filter);
}
}
// Parse expression into parts
parts = expression.exec(selector.replace(whiteSpace, ''));
add(compileNameFilter(parts[1]));
add(compileIdFilter(parts[2]));
add(compileClassesFilter(parts[3]));
add(compileAttrFilter(parts[4], parts[5], parts[6]));
add(compilePsuedoFilter(parts[7]));
// Mark the filter with psuedo for performance
filters.psuedo = !!parts[7];
filters.direct = direct;
return filters;
}
// Parser logic based on Sizzle by John Resig
function parseChunks(selector, selectors) {
var parts = [], extra, matches, i;
do {
chunker.exec("");
matches = chunker.exec(selector);
if (matches) {
selector = matches[3];
parts.push(matches[1]);
if (matches[2]) {
extra = matches[3];
break;
}
}
} while (matches);
if (extra) {
parseChunks(extra, selectors);
}
selector = [];
for (i = 0; i < parts.length; i++) {
if (parts[i] != '>') {
selector.push(compile(parts[i], [], parts[i - 1] === '>'));
}
}
selectors.push(selector);
return selectors;
}
this._selectors = parseChunks(selector, []);
},
/**
* Returns true/false if the selector matches the specified control.
*
* @method match
* @param {tinymce.ui.Control} control Control to match agains the selector.
* @param {Array} selectors Optional array of selectors, mostly used internally.
* @return {Boolean} true/false state if the control matches or not.
*/
match: function(control, selectors) {
var i, l, si, sl, selector, fi, fl, filters, index, length, siblings, count, item;
selectors = selectors || this._selectors;
for (i = 0, l = selectors.length; i < l; i++) {
selector = selectors[i];
sl = selector.length;
item = control;
count = 0;
for (si = sl - 1; si >= 0; si--) {
filters = selector[si];
while (item) {
// Find the index and length since a psuedo filter like :first needs it
if (filters.psuedo) {
siblings = item.parent().items();
index = length = siblings.length;
while (index--) {
if (siblings[index] === item) {
break;
}
}
}
for (fi = 0, fl = filters.length; fi < fl; fi++) {
if (!filters[fi](item, index, length)) {
fi = fl + 1;
break;
}
}
if (fi === fl) {
count++;
break;
} else {
// If it didn't match the right most expression then
// break since it's no point looking at the parents
if (si === sl - 1) {
break;
}
}
item = item.parent();
}
}
// If we found all selectors then return true otherwise continue looking
if (count === sl) {
return true;
}
}
return false;
},
/**
* Returns a tinymce.ui.Collection with matches of the specified selector inside the specified container.
*
* @method find
* @param {tinymce.ui.Control} container Container to look for items in.
* @return {tinymce.ui.Collection} Collection with matched elements.
*/
find: function(container) {
var matches = [], i, l, selectors = this._selectors;
function collect(items, selector, index) {
var i, l, fi, fl, item, filters = selector[index];
for (i = 0, l = items.length; i < l; i++) {
item = items[i];
// Run each filter agains the item
for (fi = 0, fl = filters.length; fi < fl; fi++) {
if (!filters[fi](item, i, l)) {
fi = fl + 1;
break;
}
}
// All filters matched the item
if (fi === fl) {
// Matched item is on the last expression like: panel toolbar [button]
if (index == selector.length - 1) {
matches.push(item);
} else {
// Collect next expression type
if (item.items) {
collect(item.items(), selector, index + 1);
}
}
} else if (filters.direct) {
return;
}
// Collect child items
if (item.items) {
collect(item.items(), selector, index);
}
}
}
if (container.items) {
for (i = 0, l = selectors.length; i < l; i++) {
collect(container.items(), selectors[i], 0);
}
// Unique the matches if needed
if (l > 1) {
matches = unique(matches);
}
}
// Fix for circular reference
if (!Collection) {
// TODO: Fix me!
Collection = Selector.Collection;
}
return new Collection(matches);
}
});
return Selector;
});
// Included from: js/tinymce/classes/ui/Collection.js
/**
* Collection.js
*
* Copyright, Moxiecode Systems AB
* Released under LGPL License.
*
* License: http://www.tinymce.com/license
* Contributing: http://www.tinymce.com/contributing
*/
/**
* Control collection, this class contains control instances and it enables you to
* perform actions on all the contained items. This is very similar to how jQuery works.
*
* @example
* someCollection.show().disabled(true);
*
* @class tinymce.ui.Collection
*/
define("tinymce/ui/Collection", [
"tinymce/util/Tools",
"tinymce/ui/Selector",
"tinymce/util/Class"
], function(Tools, Selector, Class) {
"use strict";
var Collection, proto, push = Array.prototype.push, slice = Array.prototype.slice;
proto = {
/**
* Current number of contained control instances.
*
* @field length
* @type Number
*/
length: 0,
/**
* Constructor for the collection.
*
* @constructor
* @method init
* @param {Array} items Optional array with items to add.
*/
init: function(items) {
if (items) {
this.add(items);
}
},
/**
* Adds new items to the control collection.
*
* @method add
* @param {Array} items Array if items to add to collection.
* @return {tinymce.ui.Collection} Current collection instance.
*/
add: function(items) {
var self = this;
// Force single item into array
if (!Tools.isArray(items)) {
if (items instanceof Collection) {
self.add(items.toArray());
} else {
push.call(self, items);
}
} else {
push.apply(self, items);
}
return self;
},
/**
* Sets the contents of the collection. This will remove any existing items
* and replace them with the ones specified in the input array.
*
* @method set
* @param {Array} items Array with items to set into the Collection.
* @return {tinymce.ui.Collection} Collection instance.
*/
set: function(items) {
var self = this, len = self.length, i;
self.length = 0;
self.add(items);
// Remove old entries
for (i = self.length; i < len; i++) {
delete self[i];
}
return self;
},
/**
* Filters the collection item based on the specified selector expression or selector function.
*
* @method filter
* @param {String} selector Selector expression to filter items by.
* @return {tinymce.ui.Collection} Collection containing the filtered items.
*/
filter: function(selector) {
var self = this, i, l, matches = [], item, match;
// Compile string into selector expression
if (typeof selector === "string") {
selector = new Selector(selector);
match = function(item) {
return selector.match(item);
};
} else {
// Use selector as matching function
match = selector;
}
for (i = 0, l = self.length; i < l; i++) {
item = self[i];
if (match(item)) {
matches.push(item);
}
}
return new Collection(matches);
},
/**
* Slices the items within the collection.
*
* @method slice
* @param {Number} index Index to slice at.
* @param {Number} len Optional length to slice.
* @return {tinymce.ui.Collection} Current collection.
*/
slice: function() {
return new Collection(slice.apply(this, arguments));
},
/**
* Makes the current collection equal to the specified index.
*
* @method eq
* @param {Number} index Index of the item to set the collection to.
* @return {tinymce.ui.Collection} Current collection.
*/
eq: function(index) {
return index === -1 ? this.slice(index) : this.slice(index, +index + 1);
},
/**
* Executes the specified callback on each item in collection.
*
* @method each
* @param {function} callback Callback to execute for each item in collection.
* @return {tinymce.ui.Collection} Current collection instance.
*/
each: function(callback) {
Tools.each(this, callback);
return this;
},
/**
* Returns an JavaScript array object of the contents inside the collection.
*
* @method toArray
* @return {Array} Array with all items from collection.
*/
toArray: function() {
return Tools.toArray(this);
},
/**
* Finds the index of the specified control or return -1 if it isn't in the collection.
*
* @method indexOf
* @param {Control} ctrl Control instance to look for.
* @return {Number} Index of the specified control or -1.
*/
indexOf: function(ctrl) {
var self = this, i = self.length;
while (i--) {
if (self[i] === ctrl) {
break;
}
}
return i;
},
/**
* Returns a new collection of the contents in reverse order.
*
* @method reverse
* @return {tinymce.ui.Collection} Collection instance with reversed items.
*/
reverse: function() {
return new Collection(Tools.toArray(this).reverse());
},
/**
* Returns true/false if the class exists or not.
*
* @method hasClass
* @param {String} cls Class to check for.
* @return {Boolean} true/false state if the class exists or not.
*/
hasClass: function(cls) {
return this[0] ? this[0].hasClass(cls) : false;
},
/**
* Sets/gets the specific property on the items in the collection. The same as executing control.();
*
* @method prop
* @param {String} name Property name to get/set.
* @param {Object} value Optional object value to set.
* @return {tinymce.ui.Collection} Current collection instance or value of the first item on a get operation.
*/
prop: function(name, value) {
var self = this, undef, item;
if (value !== undef) {
self.each(function(item) {
if (item[name]) {
item[name](value);
}
});
return self;
}
item = self[0];
if (item && item[name]) {
return item[name]();
}
},
/**
* Executes the specific function name with optional arguments an all items in collection if it exists.
*
* @example collection.exec("myMethod", arg1, arg2, arg3);
* @method exec
* @param {String} name Name of the function to execute.
* @param {Object} ... Multiple arguments to pass to each function.
* @return {tinymce.ui.Collection} Current collection.
*/
exec: function(name) {
var self = this, args = Tools.toArray(arguments).slice(1);
self.each(function(item) {
if (item[name]) {
item[name].apply(item, args);
}
});
return self;
},
/**
* Remove all items from collection and DOM.
*
* @method remove
* @return {tinymce.ui.Collection} Current collection.
*/
remove: function() {
var i = this.length;
while (i--) {
this[i].remove();
}
return this;
}
/**
* Fires the specified event by name and arguments on the control. This will execute all
* bound event handlers.
*
* @method fire
* @param {String} name Name of the event to fire.
* @param {Object} args Optional arguments to pass to the event.
* @return {tinymce.ui.Collection} Current collection instance.
*/
// fire: function(event, args) {}, -- Generated by code below
/**
* Binds a callback to the specified event. This event can both be
* native browser events like "click" or custom ones like PostRender.
*
* The callback function will have two parameters the first one being the control that received the event
* the second one will be the event object either the browsers native event object or a custom JS object.
*
* @method on
* @param {String} name Name of the event to bind. For example "click".
* @param {String/function} callback Callback function to execute ones the event occurs.
* @return {tinymce.ui.Collection} Current collection instance.
*/
// on: function(name, callback) {}, -- Generated by code below
/**
* Unbinds the specified event and optionally a specific callback. If you omit the name
* parameter all event handlers will be removed. If you omit the callback all event handles
* by the specified name will be removed.
*
* @method off
* @param {String} name Optional name for the event to unbind.
* @param {function} callback Optional callback function to unbind.
* @return {tinymce.ui.Collection} Current collection instance.
*/
// off: function(name, callback) {}, -- Generated by code below
/**
* Shows the items in the current collection.
*
* @method show
* @return {tinymce.ui.Collection} Current collection instance.
*/
// show: function() {}, -- Generated by code below
/**
* Hides the items in the current collection.
*
* @method hide
* @return {tinymce.ui.Collection} Current collection instance.
*/
// hide: function() {}, -- Generated by code below
/**
* Sets/gets the text contents of the items in the current collection.
*
* @method text
* @return {tinymce.ui.Collection} Current collection instance or text value of the first item on a get operation.
*/
// text: function(value) {}, -- Generated by code below
/**
* Sets/gets the name contents of the items in the current collection.
*
* @method name
* @return {tinymce.ui.Collection} Current collection instance or name value of the first item on a get operation.
*/
// name: function(value) {}, -- Generated by code below
/**
* Sets/gets the disabled state on the items in the current collection.
*
* @method disabled
* @return {tinymce.ui.Collection} Current collection instance or disabled state of the first item on a get operation.
*/
// disabled: function(state) {}, -- Generated by code below
/**
* Sets/gets the active state on the items in the current collection.
*
* @method active
* @return {tinymce.ui.Collection} Current collection instance or active state of the first item on a get operation.
*/
// active: function(state) {}, -- Generated by code below
/**
* Sets/gets the selected state on the items in the current collection.
*
* @method selected
* @return {tinymce.ui.Collection} Current collection instance or selected state of the first item on a get operation.
*/
// selected: function(state) {}, -- Generated by code below
/**
* Sets/gets the selected state on the items in the current collection.
*
* @method visible
* @return {tinymce.ui.Collection} Current collection instance or visible state of the first item on a get operation.
*/
// visible: function(state) {}, -- Generated by code below
/**
* Adds a class to all items in the collection.
*
* @method addClass
* @param {String} cls Class to add to each item.
* @return {tinymce.ui.Collection} Current collection instance.
*/
// addClass: function(cls) {}, -- Generated by code below
/**
* Removes the specified class from all items in collection.
*
* @method removeClass
* @param {String} cls Class to remove from each item.
* @return {tinymce.ui.Collection} Current collection instance.
*/
// removeClass: function(cls) {}, -- Generated by code below
};
// Extend tinymce.ui.Collection prototype with some generated control specific methods
Tools.each('fire on off show hide addClass removeClass append prepend before after reflow'.split(' '), function(name) {
proto[name] = function() {
var args = Tools.toArray(arguments);
this.each(function(ctrl) {
if (name in ctrl) {
ctrl[name].apply(ctrl, args);
}
});
return this;
};
});
// Extend tinymce.ui.Collection prototype with some property methods
Tools.each('text name disabled active selected checked visible parent value data'.split(' '), function(name) {
proto[name] = function(value) {
return this.prop(name, value);
};
});
// Create class based on the new prototype
Collection = Class.extend(proto);
// Stick Collection into Selector to prevent circual references
Selector.Collection = Collection;
return Collection;
});
// Included from: js/tinymce/classes/ui/DomUtils.js
/**
* DOMUtils.js
*
* Copyright, Moxiecode Systems AB
* Released under LGPL License.
*
* License: http://www.tinymce.com/license
* Contributing: http://www.tinymce.com/contributing
*/
define("tinymce/ui/DomUtils", [
"tinymce/util/Tools",
"tinymce/dom/DOMUtils"
], function(Tools, DOMUtils) {
"use strict";
var count = 0;
return {
id: function() {
return 'mceu_' + (count++);
},
createFragment: function(html) {
return DOMUtils.DOM.createFragment(html);
},
getWindowSize: function() {
return DOMUtils.DOM.getViewPort();
},
getSize: function(elm) {
var width, height;
if (elm.getBoundingClientRect) {
var rect = elm.getBoundingClientRect();
width = Math.max(rect.width || (rect.right - rect.left), elm.offsetWidth);
height = Math.max(rect.height || (rect.bottom - rect.bottom), elm.offsetHeight);
} else {
width = elm.offsetWidth;
height = elm.offsetHeight;
}
return {width: width, height: height};
},
getPos: function(elm, root) {
return DOMUtils.DOM.getPos(elm, root);
},
getViewPort: function(win) {
return DOMUtils.DOM.getViewPort(win);
},
get: function(id) {
return document.getElementById(id);
},
addClass: function(elm, cls) {
return DOMUtils.DOM.addClass(elm, cls);
},
removeClass: function(elm, cls) {
return DOMUtils.DOM.removeClass(elm, cls);
},
hasClass: function(elm, cls) {
return DOMUtils.DOM.hasClass(elm, cls);
},
toggleClass: function(elm, cls, state) {
return DOMUtils.DOM.toggleClass(elm, cls, state);
},
css: function(elm, name, value) {
return DOMUtils.DOM.setStyle(elm, name, value);
},
getRuntimeStyle: function(elm, name) {
return DOMUtils.DOM.getStyle(elm, name, true);
},
on: function(target, name, callback, scope) {
return DOMUtils.DOM.bind(target, name, callback, scope);
},
off: function(target, name, callback) {
return DOMUtils.DOM.unbind(target, name, callback);
},
fire: function(target, name, args) {
return DOMUtils.DOM.fire(target, name, args);
},
innerHtml: function(elm, html) {
// Workaround for in bug on IE 8 #6178
DOMUtils.DOM.setHTML(elm, html);
}
};
});
// Included from: js/tinymce/classes/ui/Control.js
/**
* Control.js
*
* Copyright, Moxiecode Systems AB
* Released under LGPL License.
*
* License: http://www.tinymce.com/license
* Contributing: http://www.tinymce.com/contributing
*/
/*eslint consistent-this:0 */
/**
* This is the base class for all controls and containers. All UI control instances inherit
* from this one as it has the base logic needed by all of them.
*
* @class tinymce.ui.Control
*/
define("tinymce/ui/Control", [
"tinymce/util/Class",
"tinymce/util/Tools",
"tinymce/util/EventDispatcher",
"tinymce/ui/Collection",
"tinymce/ui/DomUtils"
], function(Class, Tools, EventDispatcher, Collection, DomUtils) {
"use strict";
var hasMouseWheelEventSupport = "onmousewheel" in document;
var hasWheelEventSupport = false;
var classPrefix = "mce-";
function getEventDispatcher(obj) {
if (!obj._eventDispatcher) {
obj._eventDispatcher = new EventDispatcher({
scope: obj,
toggleEvent: function(name, state) {
if (state && EventDispatcher.isNative(name)) {
if (!obj._nativeEvents) {
obj._nativeEvents = {};
}
obj._nativeEvents[name] = true;
if (obj._rendered) {
obj.bindPendingEvents();
}
}
}
});
}
return obj._eventDispatcher;
}
var Control = Class.extend({
Statics: {
classPrefix: classPrefix
},
isRtl: function() {
return Control.rtl;
},
/**
* Class/id prefix to use for all controls.
*
* @final
* @field {String} classPrefix
*/
classPrefix: classPrefix,
/**
* Constructs a new control instance with the specified settings.
*
* @constructor
* @param {Object} settings Name/value object with settings.
* @setting {String} style Style CSS properties to add.
* @setting {String} border Border box values example: 1 1 1 1
* @setting {String} padding Padding box values example: 1 1 1 1
* @setting {String} margin Margin box values example: 1 1 1 1
* @setting {Number} minWidth Minimal width for the control.
* @setting {Number} minHeight Minimal height for the control.
* @setting {String} classes Space separated list of classes to add.
* @setting {String} role WAI-ARIA role to use for control.
* @setting {Boolean} hidden Is the control hidden by default.
* @setting {Boolean} disabled Is the control disabled by default.
* @setting {String} name Name of the control instance.
*/
init: function(settings) {
var self = this, classes, i;
self.settings = settings = Tools.extend({}, self.Defaults, settings);
// Initial states
self._id = settings.id || DomUtils.id();
self._text = self._name = '';
self._width = self._height = 0;
self._aria = {role: settings.role};
this._elmCache = {};
// Setup classes
classes = settings.classes;
if (classes) {
classes = classes.split(' ');
classes.map = {};
i = classes.length;
while (i--) {
classes.map[classes[i]] = true;
}
}
self._classes = classes || [];
self.visible(true);
// Set some properties
Tools.each('title text width height name classes visible disabled active value'.split(' '), function(name) {
var value = settings[name], undef;
if (value !== undef) {
self[name](value);
} else if (self['_' + name] === undef) {
self['_' + name] = false;
}
});
self.on('click', function() {
if (self.disabled()) {
return false;
}
});
// TODO: Is this needed duplicate code see above?
if (settings.classes) {
Tools.each(settings.classes.split(' '), function(cls) {
self.addClass(cls);
});
}
/**
* Name/value object with settings for the current control.
*
* @field {Object} settings
*/
self.settings = settings;
self._borderBox = self.parseBox(settings.border);
self._paddingBox = self.parseBox(settings.padding);
self._marginBox = self.parseBox(settings.margin);
if (settings.hidden) {
self.hide();
}
},
// Will generate getter/setter methods for these properties
Properties: 'parent,title,text,width,height,disabled,active,name,value',
// Will generate empty dummy functions for these
Methods: 'renderHtml',
/**
* Returns the root element to render controls into.
*
* @method getContainerElm
* @return {Element} HTML DOM element to render into.
*/
getContainerElm: function() {
return document.body;
},
/**
* Returns a control instance for the current DOM element.
*
* @method getParentCtrl
* @param {Element} elm HTML dom element to get parent control from.
* @return {tinymce.ui.Control} Control instance or undefined.
*/
getParentCtrl: function(elm) {
var ctrl, lookup = this.getRoot().controlIdLookup;
while (elm && lookup) {
ctrl = lookup[elm.id];
if (ctrl) {
break;
}
elm = elm.parentNode;
}
return ctrl;
},
/**
* Parses the specified box value. A box value contains 1-4 properties in clockwise order.
*
* @method parseBox
* @param {String/Number} value Box value "0 1 2 3" or "0" etc.
* @return {Object} Object with top/right/bottom/left properties.
* @private
*/
parseBox: function(value) {
var len, radix = 10;
if (!value) {
return;
}
if (typeof value === "number") {
value = value || 0;
return {
top: value,
left: value,
bottom: value,
right: value
};
}
value = value.split(' ');
len = value.length;
if (len === 1) {
value[1] = value[2] = value[3] = value[0];
} else if (len === 2) {
value[2] = value[0];
value[3] = value[1];
} else if (len === 3) {
value[3] = value[1];
}
return {
top: parseInt(value[0], radix) || 0,
right: parseInt(value[1], radix) || 0,
bottom: parseInt(value[2], radix) || 0,
left: parseInt(value[3], radix) || 0
};
},
borderBox: function() {
return this._borderBox;
},
paddingBox: function() {
return this._paddingBox;
},
marginBox: function() {
return this._marginBox;
},
measureBox: function(elm, prefix) {
function getStyle(name) {
var defaultView = document.defaultView;
if (defaultView) {
// Remove camelcase
name = name.replace(/[A-Z]/g, function(a) {
return '-' + a;
});
return defaultView.getComputedStyle(elm, null).getPropertyValue(name);
}
return elm.currentStyle[name];
}
function getSide(name) {
var val = parseFloat(getStyle(name), 10);
return isNaN(val) ? 0 : val;
}
return {
top: getSide(prefix + "TopWidth"),
right: getSide(prefix + "RightWidth"),
bottom: getSide(prefix + "BottomWidth"),
left: getSide(prefix + "LeftWidth")
};
},
/**
* Initializes the current controls layout rect.
* This will be executed by the layout managers to determine the
* default minWidth/minHeight etc.
*
* @method initLayoutRect
* @return {Object} Layout rect instance.
*/
initLayoutRect: function() {
var self = this, settings = self.settings, borderBox, layoutRect;
var elm = self.getEl(), width, height, minWidth, minHeight, autoResize;
var startMinWidth, startMinHeight, initialSize;
// Measure the current element
borderBox = self._borderBox = self._borderBox || self.measureBox(elm, 'border');
self._paddingBox = self._paddingBox || self.measureBox(elm, 'padding');
self._marginBox = self._marginBox || self.measureBox(elm, 'margin');
initialSize = DomUtils.getSize(elm);
// Setup minWidth/minHeight and width/height
startMinWidth = settings.minWidth;
startMinHeight = settings.minHeight;
minWidth = startMinWidth || initialSize.width;
minHeight = startMinHeight || initialSize.height;
width = settings.width;
height = settings.height;
autoResize = settings.autoResize;
autoResize = typeof autoResize != "undefined" ? autoResize : !width && !height;
width = width || minWidth;
height = height || minHeight;
var deltaW = borderBox.left + borderBox.right;
var deltaH = borderBox.top + borderBox.bottom;
var maxW = settings.maxWidth || 0xFFFF;
var maxH = settings.maxHeight || 0xFFFF;
// Setup initial layout rect
self._layoutRect = layoutRect = {
x: settings.x || 0,
y: settings.y || 0,
w: width,
h: height,
deltaW: deltaW,
deltaH: deltaH,
contentW: width - deltaW,
contentH: height - deltaH,
innerW: width - deltaW,
innerH: height - deltaH,
startMinWidth: startMinWidth || 0,
startMinHeight: startMinHeight || 0,
minW: Math.min(minWidth, maxW),
minH: Math.min(minHeight, maxH),
maxW: maxW,
maxH: maxH,
autoResize: autoResize,
scrollW: 0
};
self._lastLayoutRect = {};
return layoutRect;
},
/**
* Getter/setter for the current layout rect.
*
* @method layoutRect
* @param {Object} [newRect] Optional new layout rect.
* @return {tinymce.ui.Control/Object} Current control or rect object.
*/
layoutRect: function(newRect) {
var self = this, curRect = self._layoutRect, lastLayoutRect, size, deltaWidth, deltaHeight, undef, repaintControls;
// Initialize default layout rect
if (!curRect) {
curRect = self.initLayoutRect();
}
// Set new rect values
if (newRect) {
// Calc deltas between inner and outer sizes
deltaWidth = curRect.deltaW;
deltaHeight = curRect.deltaH;
// Set x position
if (newRect.x !== undef) {
curRect.x = newRect.x;
}
// Set y position
if (newRect.y !== undef) {
curRect.y = newRect.y;
}
// Set minW
if (newRect.minW !== undef) {
curRect.minW = newRect.minW;
}
// Set minH
if (newRect.minH !== undef) {
curRect.minH = newRect.minH;
}
// Set new width and calculate inner width
size = newRect.w;
if (size !== undef) {
size = size < curRect.minW ? curRect.minW : size;
size = size > curRect.maxW ? curRect.maxW : size;
curRect.w = size;
curRect.innerW = size - deltaWidth;
}
// Set new height and calculate inner height
size = newRect.h;
if (size !== undef) {
size = size < curRect.minH ? curRect.minH : size;
size = size > curRect.maxH ? curRect.maxH : size;
curRect.h = size;
curRect.innerH = size - deltaHeight;
}
// Set new inner width and calculate width
size = newRect.innerW;
if (size !== undef) {
size = size < curRect.minW - deltaWidth ? curRect.minW - deltaWidth : size;
size = size > curRect.maxW - deltaWidth ? curRect.maxW - deltaWidth : size;
curRect.innerW = size;
curRect.w = size + deltaWidth;
}
// Set new height and calculate inner height
size = newRect.innerH;
if (size !== undef) {
size = size < curRect.minH - deltaHeight ? curRect.minH - deltaHeight : size;
size = size > curRect.maxH - deltaHeight ? curRect.maxH - deltaHeight : size;
curRect.innerH = size;
curRect.h = size + deltaHeight;
}
// Set new contentW
if (newRect.contentW !== undef) {
curRect.contentW = newRect.contentW;
}
// Set new contentH
if (newRect.contentH !== undef) {
curRect.contentH = newRect.contentH;
}
// Compare last layout rect with the current one to see if we need to repaint or not
lastLayoutRect = self._lastLayoutRect;
if (lastLayoutRect.x !== curRect.x || lastLayoutRect.y !== curRect.y ||
lastLayoutRect.w !== curRect.w || lastLayoutRect.h !== curRect.h) {
repaintControls = Control.repaintControls;
if (repaintControls) {
if (repaintControls.map && !repaintControls.map[self._id]) {
repaintControls.push(self);
repaintControls.map[self._id] = true;
}
}
lastLayoutRect.x = curRect.x;
lastLayoutRect.y = curRect.y;
lastLayoutRect.w = curRect.w;
lastLayoutRect.h = curRect.h;
}
return self;
}
return curRect;
},
/**
* Repaints the control after a layout operation.
*
* @method repaint
*/
repaint: function() {
var self = this, style, bodyStyle, rect, borderBox, borderW = 0, borderH = 0, lastRepaintRect, round;
// Use Math.round on all values on IE < 9
round = !document.createRange ? Math.round : function(value) {
return value;
};
style = self.getEl().style;
rect = self._layoutRect;
lastRepaintRect = self._lastRepaintRect || {};
borderBox = self._borderBox;
borderW = borderBox.left + borderBox.right;
borderH = borderBox.top + borderBox.bottom;
if (rect.x !== lastRepaintRect.x) {
style.left = round(rect.x) + 'px';
lastRepaintRect.x = rect.x;
}
if (rect.y !== lastRepaintRect.y) {
style.top = round(rect.y) + 'px';
lastRepaintRect.y = rect.y;
}
if (rect.w !== lastRepaintRect.w) {
style.width = round(rect.w - borderW) + 'px';
lastRepaintRect.w = rect.w;
}
if (rect.h !== lastRepaintRect.h) {
style.height = round(rect.h - borderH) + 'px';
lastRepaintRect.h = rect.h;
}
// Update body if needed
if (self._hasBody && rect.innerW !== lastRepaintRect.innerW) {
bodyStyle = self.getEl('body').style;
bodyStyle.width = round(rect.innerW) + 'px';
lastRepaintRect.innerW = rect.innerW;
}
if (self._hasBody && rect.innerH !== lastRepaintRect.innerH) {
bodyStyle = bodyStyle || self.getEl('body').style;
bodyStyle.height = round(rect.innerH) + 'px';
lastRepaintRect.innerH = rect.innerH;
}
self._lastRepaintRect = lastRepaintRect;
self.fire('repaint', {}, false);
},
/**
* Binds a callback to the specified event. This event can both be
* native browser events like "click" or custom ones like PostRender.
*
* The callback function will be passed a DOM event like object that enables yout do stop propagation.
*
* @method on
* @param {String} name Name of the event to bind. For example "click".
* @param {String/function} callback Callback function to execute ones the event occurs.
* @return {tinymce.ui.Control} Current control object.
*/
on: function(name, callback) {
var self = this;
function resolveCallbackName(name) {
var callback, scope;
if (typeof name != 'string') {
return name;
}
return function(e) {
if (!callback) {
self.parentsAndSelf().each(function(ctrl) {
var callbacks = ctrl.settings.callbacks;
if (callbacks && (callback = callbacks[name])) {
scope = ctrl;
return false;
}
});
}
return callback.call(scope, e);
};
}
getEventDispatcher(self).on(name, resolveCallbackName(callback));
return self;
},
/**
* Unbinds the specified event and optionally a specific callback. If you omit the name
* parameter all event handlers will be removed. If you omit the callback all event handles
* by the specified name will be removed.
*
* @method off
* @param {String} [name] Name for the event to unbind.
* @param {function} [callback] Callback function to unbind.
* @return {mxex.ui.Control} Current control object.
*/
off: function(name, callback) {
getEventDispatcher(this).off(name, callback);
return this;
},
/**
* Fires the specified event by name and arguments on the control. This will execute all
* bound event handlers.
*
* @method fire
* @param {String} name Name of the event to fire.
* @param {Object} [args] Arguments to pass to the event.
* @param {Boolean} [bubble] Value to control bubbeling. Defaults to true.
* @return {Object} Current arguments object.
*/
fire: function(name, args, bubble) {
var self = this;
args = args || {};
if (!args.control) {
args.control = self;
}
args = getEventDispatcher(self).fire(name, args);
// Bubble event up to parents
if (bubble !== false && self.parent) {
var parent = self.parent();
while (parent && !args.isPropagationStopped()) {
parent.fire(name, args, false);
parent = parent.parent();
}
}
return args;
},
/**
* Returns true/false if the specified event has any listeners.
*
* @method hasEventListeners
* @param {String} name Name of the event to check for.
* @return {Boolean} True/false state if the event has listeners.
*/
hasEventListeners: function(name) {
return getEventDispatcher(this).has(name);
},
/**
* Returns a control collection with all parent controls.
*
* @method parents
* @param {String} selector Optional selector expression to find parents.
* @return {tinymce.ui.Collection} Collection with all parent controls.
*/
parents: function(selector) {
var self = this, ctrl, parents = new Collection();
// Add each parent to collection
for (ctrl = self.parent(); ctrl; ctrl = ctrl.parent()) {
parents.add(ctrl);
}
// Filter away everything that doesn't match the selector
if (selector) {
parents = parents.filter(selector);
}
return parents;
},
/**
* Returns the current control and it's parents.
*
* @method parentsAndSelf
* @param {String} selector Optional selector expression to find parents.
* @return {tinymce.ui.Collection} Collection with all parent controls.
*/
parentsAndSelf: function(selector) {
return new Collection(this).add(this.parents(selector));
},
/**
* Returns the control next to the current control.
*
* @method next
* @return {tinymce.ui.Control} Next control instance.
*/
next: function() {
var parentControls = this.parent().items();
return parentControls[parentControls.indexOf(this) + 1];
},
/**
* Returns the control previous to the current control.
*
* @method prev
* @return {tinymce.ui.Control} Previous control instance.
*/
prev: function() {
var parentControls = this.parent().items();
return parentControls[parentControls.indexOf(this) - 1];
},
/**
* Find the common ancestor for two control instances.
*
* @method findCommonAncestor
* @param {tinymce.ui.Control} ctrl1 First control.
* @param {tinymce.ui.Control} ctrl2 Second control.
* @return {tinymce.ui.Control} Ancestor control instance.
*/
findCommonAncestor: function(ctrl1, ctrl2) {
var parentCtrl;
while (ctrl1) {
parentCtrl = ctrl2;
while (parentCtrl && ctrl1 != parentCtrl) {
parentCtrl = parentCtrl.parent();
}
if (ctrl1 == parentCtrl) {
break;
}
ctrl1 = ctrl1.parent();
}
return ctrl1;
},
/**
* Returns true/false if the specific control has the specific class.
*
* @method hasClass
* @param {String} cls Class to check for.
* @param {String} [group] Sub element group name.
* @return {Boolean} True/false if the control has the specified class.
*/
hasClass: function(cls, group) {
var classes = this._classes[group || 'control'];
cls = this.classPrefix + cls;
return classes && !!classes.map[cls];
},
/**
* Adds the specified class to the control
*
* @method addClass
* @param {String} cls Class to check for.
* @param {String} [group] Sub element group name.
* @return {tinymce.ui.Control} Current control object.
*/
addClass: function(cls, group) {
var self = this, classes, elm;
cls = this.classPrefix + cls;
classes = self._classes[group || 'control'];
if (!classes) {
classes = [];
classes.map = {};
self._classes[group || 'control'] = classes;
}
if (!classes.map[cls]) {
classes.map[cls] = cls;
classes.push(cls);
if (self._rendered) {
elm = self.getEl(group);
if (elm) {
elm.className = classes.join(' ');
}
}
}
return self;
},
/**
* Removes the specified class from the control.
*
* @method removeClass
* @param {String} cls Class to remove.
* @param {String} [group] Sub element group name.
* @return {tinymce.ui.Control} Current control object.
*/
removeClass: function(cls, group) {
var self = this, classes, i, elm;
cls = this.classPrefix + cls;
classes = self._classes[group || 'control'];
if (classes && classes.map[cls]) {
delete classes.map[cls];
i = classes.length;
while (i--) {
if (classes[i] === cls) {
classes.splice(i, 1);
}
}
}
if (self._rendered) {
elm = self.getEl(group);
if (elm) {
elm.className = classes.join(' ');
}
}
return self;
},
/**
* Toggles the specified class on the control.
*
* @method toggleClass
* @param {String} cls Class to remove.
* @param {Boolean} state True/false state to add/remove class.
* @param {String} [group] Sub element group name.
* @return {tinymce.ui.Control} Current control object.
*/
toggleClass: function(cls, state, group) {
var self = this;
if (state) {
self.addClass(cls, group);
} else {
self.removeClass(cls, group);
}
return self;
},
/**
* Returns the class string for the specified group name.
*
* @method classes
* @param {String} [group] Group to get clases by.
* @return {String} Classes for the specified group.
*/
classes: function(group) {
var classes = this._classes[group || 'control'];
return classes ? classes.join(' ') : '';
},
/**
* Sets the inner HTML of the control element.
*
* @method innerHtml
* @param {String} html Html string to set as inner html.
* @return {tinymce.ui.Control} Current control object.
*/
innerHtml: function(html) {
DomUtils.innerHtml(this.getEl(), html);
return this;
},
/**
* Returns the control DOM element or sub element.
*
* @method getEl
* @param {String} [suffix] Suffix to get element by.
* @return {Element} HTML DOM element for the current control or it's children.
*/
getEl: function(suffix) {
var id = suffix ? this._id + '-' + suffix : this._id;
if (!this._elmCache[id]) {
this._elmCache[id] = DomUtils.get(id);
}
return this._elmCache[id];
},
/**
* Sets/gets the visible for the control.
*
* @method visible
* @param {Boolean} state Value to set to control.
* @return {Boolean/tinymce.ui.Control} Current control on a set operation or current state on a get.
*/
visible: function(state) {
var self = this, parentCtrl;
if (typeof state !== "undefined") {
if (self._visible !== state) {
if (self._rendered) {
self.getEl().style.display = state ? '' : 'none';
}
self._visible = state;
// Parent container needs to reflow
parentCtrl = self.parent();
if (parentCtrl) {
parentCtrl._lastRect = null;
}
self.fire(state ? 'show' : 'hide');
}
return self;
}
return self._visible;
},
/**
* Sets the visible state to true.
*
* @method show
* @return {tinymce.ui.Control} Current control instance.
*/
show: function() {
return this.visible(true);
},
/**
* Sets the visible state to false.
*
* @method hide
* @return {tinymce.ui.Control} Current control instance.
*/
hide: function() {
return this.visible(false);
},
/**
* Focuses the current control.
*
* @method focus
* @return {tinymce.ui.Control} Current control instance.
*/
focus: function() {
try {
this.getEl().focus();
} catch (ex) {
// Ignore IE error
}
return this;
},
/**
* Blurs the current control.
*
* @method blur
* @return {tinymce.ui.Control} Current control instance.
*/
blur: function() {
this.getEl().blur();
return this;
},
/**
* Sets the specified aria property.
*
* @method aria
* @param {String} name Name of the aria property to set.
* @param {String} value Value of the aria property.
* @return {tinymce.ui.Control} Current control instance.
*/
aria: function(name, value) {
var self = this, elm = self.getEl(self.ariaTarget);
if (typeof value === "undefined") {
return self._aria[name];
} else {
self._aria[name] = value;
}
if (self._rendered) {
elm.setAttribute(name == 'role' ? name : 'aria-' + name, value);
}
return self;
},
/**
* Encodes the specified string with HTML entities. It will also
* translate the string to different languages.
*
* @method encode
* @param {String/Object/Array} text Text to entity encode.
* @param {Boolean} [translate=true] False if the contents shouldn't be translated.
* @return {String} Encoded and possible traslated string.
*/
encode: function(text, translate) {
if (translate !== false) {
text = this.translate(text);
}
return (text || '').replace(/[&<>"]/g, function(match) {
return '' + match.charCodeAt(0) + ';';
});
},
/**
* Returns the translated string.
*
* @method translate
* @param {String} text Text to translate.
* @return {String} Translated string or the same as the input.
*/
translate: function(text) {
return Control.translate ? Control.translate(text) : text;
},
/**
* Adds items before the current control.
*
* @method before
* @param {Array/tinymce.ui.Collection} items Array of items to prepend before this control.
* @return {tinymce.ui.Control} Current control instance.
*/
before: function(items) {
var self = this, parent = self.parent();
if (parent) {
parent.insert(items, parent.items().indexOf(self), true);
}
return self;
},
/**
* Adds items after the current control.
*
* @method after
* @param {Array/tinymce.ui.Collection} items Array of items to append after this control.
* @return {tinymce.ui.Control} Current control instance.
*/
after: function(items) {
var self = this, parent = self.parent();
if (parent) {
parent.insert(items, parent.items().indexOf(self));
}
return self;
},
/**
* Removes the current control from DOM and from UI collections.
*
* @method remove
* @return {tinymce.ui.Control} Current control instance.
*/
remove: function() {
var self = this, elm = self.getEl(), parent = self.parent(), newItems, i;
if (self.items) {
var controls = self.items().toArray();
i = controls.length;
while (i--) {
controls[i].remove();
}
}
if (parent && parent.items) {
newItems = [];
parent.items().each(function(item) {
if (item !== self) {
newItems.push(item);
}
});
parent.items().set(newItems);
parent._lastRect = null;
}
if (self._eventsRoot && self._eventsRoot == self) {
DomUtils.off(elm);
}
var lookup = self.getRoot().controlIdLookup;
if (lookup) {
delete lookup[self._id];
}
if (elm && elm.parentNode) {
elm.parentNode.removeChild(elm);
}
self._rendered = false;
return self;
},
/**
* Renders the control before the specified element.
*
* @method renderBefore
* @param {Element} elm Element to render before.
* @return {tinymce.ui.Control} Current control instance.
*/
renderBefore: function(elm) {
var self = this;
elm.parentNode.insertBefore(DomUtils.createFragment(self.renderHtml()), elm);
self.postRender();
return self;
},
/**
* Renders the control to the specified element.
*
* @method renderBefore
* @param {Element} elm Element to render to.
* @return {tinymce.ui.Control} Current control instance.
*/
renderTo: function(elm) {
var self = this;
elm = elm || self.getContainerElm();
elm.appendChild(DomUtils.createFragment(self.renderHtml()));
self.postRender();
return self;
},
/**
* Post render method. Called after the control has been rendered to the target.
*
* @method postRender
* @return {tinymce.ui.Control} Current control instance.
*/
postRender: function() {
var self = this, settings = self.settings, elm, box, parent, name, parentEventsRoot;
// Bind on settings
for (name in settings) {
if (name.indexOf("on") === 0) {
self.on(name.substr(2), settings[name]);
}
}
if (self._eventsRoot) {
for (parent = self.parent(); !parentEventsRoot && parent; parent = parent.parent()) {
parentEventsRoot = parent._eventsRoot;
}
if (parentEventsRoot) {
for (name in parentEventsRoot._nativeEvents) {
self._nativeEvents[name] = true;
}
}
}
self.bindPendingEvents();
if (settings.style) {
elm = self.getEl();
if (elm) {
elm.setAttribute('style', settings.style);
elm.style.cssText = settings.style;
}
}
if (!self._visible) {
DomUtils.css(self.getEl(), 'display', 'none');
}
if (self.settings.border) {
box = self.borderBox();
DomUtils.css(self.getEl(), {
'border-top-width': box.top,
'border-right-width': box.right,
'border-bottom-width': box.bottom,
'border-left-width': box.left
});
}
// Add instance to lookup
var root = self.getRoot();
if (!root.controlIdLookup) {
root.controlIdLookup = {};
}
root.controlIdLookup[self._id] = self;
for (var key in self._aria) {
self.aria(key, self._aria[key]);
}
self.fire('postrender', {}, false);
},
/**
* Scrolls the current control into view.
*
* @method scrollIntoView
* @param {String} align Alignment in view top|center|bottom.
* @return {tinymce.ui.Control} Current control instance.
*/
scrollIntoView: function(align) {
function getOffset(elm, rootElm) {
var x, y, parent = elm;
x = y = 0;
while (parent && parent != rootElm && parent.nodeType) {
x += parent.offsetLeft || 0;
y += parent.offsetTop || 0;
parent = parent.offsetParent;
}
return {x: x, y: y};
}
var elm = this.getEl(), parentElm = elm.parentNode;
var x, y, width, height, parentWidth, parentHeight;
var pos = getOffset(elm, parentElm);
x = pos.x;
y = pos.y;
width = elm.offsetWidth;
height = elm.offsetHeight;
parentWidth = parentElm.clientWidth;
parentHeight = parentElm.clientHeight;
if (align == "end") {
x -= parentWidth - width;
y -= parentHeight - height;
} else if (align == "center") {
x -= (parentWidth / 2) - (width / 2);
y -= (parentHeight / 2) - (height / 2);
}
parentElm.scrollLeft = x;
parentElm.scrollTop = y;
return this;
},
/**
* Binds pending DOM events.
*
* @private
*/
bindPendingEvents: function() {
var self = this, i, l, parents, eventRootCtrl, nativeEvents, name;
function delegate(e) {
var control = self.getParentCtrl(e.target);
if (control) {
control.fire(e.type, e);
}
}
function mouseLeaveHandler() {
var ctrl = eventRootCtrl._lastHoverCtrl;
if (ctrl) {
ctrl.fire("mouseleave", {target: ctrl.getEl()});
ctrl.parents().each(function(ctrl) {
ctrl.fire("mouseleave", {target: ctrl.getEl()});
});
eventRootCtrl._lastHoverCtrl = null;
}
}
function mouseEnterHandler(e) {
var ctrl = self.getParentCtrl(e.target), lastCtrl = eventRootCtrl._lastHoverCtrl, idx = 0, i, parents, lastParents;
// Over on a new control
if (ctrl !== lastCtrl) {
eventRootCtrl._lastHoverCtrl = ctrl;
parents = ctrl.parents().toArray().reverse();
parents.push(ctrl);
if (lastCtrl) {
lastParents = lastCtrl.parents().toArray().reverse();
lastParents.push(lastCtrl);
for (idx = 0; idx < lastParents.length; idx++) {
if (parents[idx] !== lastParents[idx]) {
break;
}
}
for (i = lastParents.length - 1; i >= idx; i--) {
lastCtrl = lastParents[i];
lastCtrl.fire("mouseleave", {
target: lastCtrl.getEl()
});
}
}
for (i = idx; i < parents.length; i++) {
ctrl = parents[i];
ctrl.fire("mouseenter", {
target: ctrl.getEl()
});
}
}
}
function fixWheelEvent(e) {
e.preventDefault();
if (e.type == "mousewheel") {
e.deltaY = -1 / 40 * e.wheelDelta;
if (e.wheelDeltaX) {
e.deltaX = -1 / 40 * e.wheelDeltaX;
}
} else {
e.deltaX = 0;
e.deltaY = e.detail;
}
e = self.fire("wheel", e);
}
self._rendered = true;
nativeEvents = self._nativeEvents;
if (nativeEvents) {
// Find event root element if it exists
parents = self.parents().toArray();
parents.unshift(self);
for (i = 0, l = parents.length; !eventRootCtrl && i < l; i++) {
eventRootCtrl = parents[i]._eventsRoot;
}
// Event root wasn't found the use the root control
if (!eventRootCtrl) {
eventRootCtrl = parents[parents.length - 1] || self;
}
// Set the eventsRoot property on children that didn't have it
self._eventsRoot = eventRootCtrl;
for (l = i, i = 0; i < l; i++) {
parents[i]._eventsRoot = eventRootCtrl;
}
var eventRootDelegates = eventRootCtrl._delegates;
if (!eventRootDelegates) {
eventRootDelegates = eventRootCtrl._delegates = {};
}
// Bind native event delegates
for (name in nativeEvents) {
if (!nativeEvents) {
return false;
}
if (name === "wheel" && !hasWheelEventSupport) {
if (hasMouseWheelEventSupport) {
DomUtils.on(self.getEl(), "mousewheel", fixWheelEvent);
} else {
DomUtils.on(self.getEl(), "DOMMouseScroll", fixWheelEvent);
}
continue;
}
// Special treatment for mousenter/mouseleave since these doesn't bubble
if (name === "mouseenter" || name === "mouseleave") {
// Fake mousenter/mouseleave
if (!eventRootCtrl._hasMouseEnter) {
DomUtils.on(eventRootCtrl.getEl(), "mouseleave", mouseLeaveHandler);
DomUtils.on(eventRootCtrl.getEl(), "mouseover", mouseEnterHandler);
eventRootCtrl._hasMouseEnter = 1;
}
} else if (!eventRootDelegates[name]) {
DomUtils.on(eventRootCtrl.getEl(), name, delegate);
eventRootDelegates[name] = true;
}
// Remove the event once it's bound
nativeEvents[name] = false;
}
}
},
getRoot: function() {
var ctrl = this, rootControl, parents = [];
while (ctrl) {
if (ctrl.rootControl) {
rootControl = ctrl.rootControl;
break;
}
parents.push(ctrl);
rootControl = ctrl;
ctrl = ctrl.parent();
}
if (!rootControl) {
rootControl = this;
}
var i = parents.length;
while (i--) {
parents[i].rootControl = rootControl;
}
return rootControl;
},
/**
* Reflows the current control and it's parents.
* This should be used after you for example append children to the current control so
* that the layout managers know that they need to reposition everything.
*
* @example
* container.append({type: 'button', text: 'My button'}).reflow();
*
* @method reflow
* @return {tinymce.ui.Control} Current control instance.
*/
reflow: function() {
this.repaint();
return this;
}
/**
* Sets/gets the parent container for the control.
*
* @method parent
* @param {tinymce.ui.Container} parent Optional parent to set.
* @return {tinymce.ui.Control} Parent control or the current control on a set action.
*/
// parent: function(parent) {} -- Generated
/**
* Sets/gets the text for the control.
*
* @method text
* @param {String} value Value to set to control.
* @return {String/tinymce.ui.Control} Current control on a set operation or current value on a get.
*/
// text: function(value) {} -- Generated
/**
* Sets/gets the width for the control.
*
* @method width
* @param {Number} value Value to set to control.
* @return {Number/tinymce.ui.Control} Current control on a set operation or current value on a get.
*/
// width: function(value) {} -- Generated
/**
* Sets/gets the height for the control.
*
* @method height
* @param {Number} value Value to set to control.
* @return {Number/tinymce.ui.Control} Current control on a set operation or current value on a get.
*/
// height: function(value) {} -- Generated
/**
* Sets/gets the disabled state on the control.
*
* @method disabled
* @param {Boolean} state Value to set to control.
* @return {Boolean/tinymce.ui.Control} Current control on a set operation or current state on a get.
*/
// disabled: function(state) {} -- Generated
/**
* Sets/gets the active for the control.
*
* @method active
* @param {Boolean} state Value to set to control.
* @return {Boolean/tinymce.ui.Control} Current control on a set operation or current state on a get.
*/
// active: function(state) {} -- Generated
/**
* Sets/gets the name for the control.
*
* @method name
* @param {String} value Value to set to control.
* @return {String/tinymce.ui.Control} Current control on a set operation or current value on a get.
*/
// name: function(value) {} -- Generated
/**
* Sets/gets the title for the control.
*
* @method title
* @param {String} value Value to set to control.
* @return {String/tinymce.ui.Control} Current control on a set operation or current value on a get.
*/
// title: function(value) {} -- Generated
});
return Control;
});
// Included from: js/tinymce/classes/ui/Factory.js
/**
* Factory.js
*
* Copyright, Moxiecode Systems AB
* Released under LGPL License.
*
* License: http://www.tinymce.com/license
* Contributing: http://www.tinymce.com/contributing
*/
/*global tinymce:true */
/**
* This class is a factory for control instances. This enables you
* to create instances of controls without having to require the UI controls directly.
*
* It also allow you to override or add new control types.
*
* @class tinymce.ui.Factory
*/
define("tinymce/ui/Factory", [], function() {
"use strict";
var types = {}, namespaceInit;
return {
/**
* Adds a new control instance type to the factory.
*
* @method add
* @param {String} type Type name for example "button".
* @param {function} typeClass Class type function.
*/
add: function(type, typeClass) {
types[type.toLowerCase()] = typeClass;
},
/**
* Returns true/false if the specified type exists or not.
*
* @method has
* @param {String} type Type to look for.
* @return {Boolean} true/false if the control by name exists.
*/
has: function(type) {
return !!types[type.toLowerCase()];
},
/**
* Creates a new control instance based on the settings provided. The instance created will be
* based on the specified type property it can also create whole structures of components out of
* the specified JSON object.
*
* @example
* tinymce.ui.Factory.create({
* type: 'button',
* text: 'Hello world!'
* });
*
* @method create
* @param {Object/String} settings Name/Value object with items used to create the type.
* @return {tinymce.ui.Control} Control instance based on the specified type.
*/
create: function(type, settings) {
var ControlType, name, namespace;
// Build type lookup
if (!namespaceInit) {
namespace = tinymce.ui;
for (name in namespace) {
types[name.toLowerCase()] = namespace[name];
}
namespaceInit = true;
}
// If string is specified then use it as the type
if (typeof type == 'string') {
settings = settings || {};
settings.type = type;
} else {
settings = type;
type = settings.type;
}
// Find control type
type = type.toLowerCase();
ControlType = types[type];
// #if debug
if (!ControlType) {
throw new Error("Could not find control by type: " + type);
}
// #endif
ControlType = new ControlType(settings);
ControlType.type = type; // Set the type on the instance, this will be used by the Selector engine
return ControlType;
}
};
});
// Included from: js/tinymce/classes/ui/KeyboardNavigation.js
/**
* KeyboardNavigation.js
*
* Copyright, Moxiecode Systems AB
* Released under LGPL License.
*
* License: http://www.tinymce.com/license
* Contributing: http://www.tinymce.com/contributing
*/
/**
* This class handles keyboard navigation of controls and elements.
*
* @class tinymce.ui.KeyboardNavigation
*/
define("tinymce/ui/KeyboardNavigation", [
], function() {
"use strict";
/**
* This class handles all keyboard navigation for WAI-ARIA support. Each root container
* gets an instance of this class.
*
* @constructor
*/
return function(settings) {
var root = settings.root, focusedElement, focusedControl;
try {
focusedElement = document.activeElement;
} catch (ex) {
// IE sometimes fails to return a proper element
focusedElement = document.body;
}
focusedControl = root.getParentCtrl(focusedElement);
/**
* Returns the currently focused elements wai aria role of the currently
* focused element or specified element.
*
* @private
* @param {Element} elm Optional element to get role from.
* @return {String} Role of specified element.
*/
function getRole(elm) {
elm = elm || focusedElement;
return elm && elm.getAttribute('role');
}
/**
* Returns the wai role of the parent element of the currently
* focused element or specified element.
*
* @private
* @param {Element} elm Optional element to get parent role from.
* @return {String} Role of the first parent that has a role.
*/
function getParentRole(elm) {
var role, parent = elm || focusedElement;
while ((parent = parent.parentNode)) {
if ((role = getRole(parent))) {
return role;
}
}
}
/**
* Returns a wai aria property by name for example aria-selected.
*
* @private
* @param {String} name Name of the aria property to get for example "disabled".
* @return {String} Aria property value.
*/
function getAriaProp(name) {
var elm = focusedElement;
if (elm) {
return elm.getAttribute('aria-' + name);
}
}
/**
* Is the element a text input element or not.
*
* @private
* @param {Element} elm Element to check if it's an text input element or not.
* @return {Boolean} True/false if the element is a text element or not.
*/
function isTextInputElement(elm) {
var tagName = elm.tagName.toUpperCase();
// Notice: since type can be "email" etc we don't check the type
// So all input elements gets treated as text input elements
return tagName == "INPUT" || tagName == "TEXTAREA";
}
/**
* Returns true/false if the specified element can be focused or not.
*
* @private
* @param {Element} elm DOM element to check if it can be focused or not.
* @return {Boolean} True/false if the element can have focus.
*/
function canFocus(elm) {
if (isTextInputElement(elm) && !elm.hidden) {
return true;
}
if (/^(button|menuitem|checkbox|tab|menuitemcheckbox|option|gridcell)$/.test(getRole(elm))) {
return true;
}
return false;
}
/**
* Returns an array of focusable visible elements within the specified container element.
*
* @private
* @param {Element} elm DOM element to find focusable elements within.
* @return {Array} Array of focusable elements.
*/
function getFocusElements(elm) {
var elements = [];
function collect(elm) {
if (elm.nodeType != 1 || elm.style.display == 'none') {
return;
}
if (canFocus(elm)) {
elements.push(elm);
}
for (var i = 0; i < elm.childNodes.length; i++) {
collect(elm.childNodes[i]);
}
}
collect(elm || root.getEl());
return elements;
}
/**
* Returns the navigation root control for the specified control. The navigation root
* is the control that the keyboard navigation gets scoped to for example a menubar or toolbar group.
* It will look for parents of the specified target control or the currently focused control if this option is omitted.
*
* @private
* @param {tinymce.ui.Control} targetControl Optional target control to find root of.
* @return {tinymce.ui.Control} Navigation root control.
*/
function getNavigationRoot(targetControl) {
var navigationRoot, controls;
targetControl = targetControl || focusedControl;
controls = targetControl.parents().toArray();
controls.unshift(targetControl);
for (var i = 0; i < controls.length; i++) {
navigationRoot = controls[i];
if (navigationRoot.settings.ariaRoot) {
break;
}
}
return navigationRoot;
}
/**
* Focuses the first item in the specified targetControl element or the last aria index if the
* navigation root has the ariaRemember option enabled.
*
* @private
* @param {tinymce.ui.Control} targetControl Target control to focus the first item in.
*/
function focusFirst(targetControl) {
var navigationRoot = getNavigationRoot(targetControl);
var focusElements = getFocusElements(navigationRoot.getEl());
if (navigationRoot.settings.ariaRemember && "lastAriaIndex" in navigationRoot) {
moveFocusToIndex(navigationRoot.lastAriaIndex, focusElements);
} else {
moveFocusToIndex(0, focusElements);
}
}
/**
* Moves the focus to the specified index within the elements list.
* This will scope the index to the size of the element list if it changed.
*
* @private
* @param {Number} idx Specified index to move to.
* @param {Array} elements Array with dom elements to move focus within.
* @return {Number} Input index or a changed index if it was out of range.
*/
function moveFocusToIndex(idx, elements) {
if (idx < 0) {
idx = elements.length - 1;
} else if (idx >= elements.length) {
idx = 0;
}
if (elements[idx]) {
elements[idx].focus();
}
return idx;
}
/**
* Moves the focus forwards or backwards.
*
* @private
* @param {Number} dir Direction to move in positive means forward, negative means backwards.
* @param {Array} elements Optional array of elements to move within defaults to the current navigation roots elements.
*/
function moveFocus(dir, elements) {
var idx = -1, navigationRoot = getNavigationRoot();
elements = elements || getFocusElements(navigationRoot.getEl());
for (var i = 0; i < elements.length; i++) {
if (elements[i] === focusedElement) {
idx = i;
}
}
idx += dir;
navigationRoot.lastAriaIndex = moveFocusToIndex(idx, elements);
}
/**
* Moves the focus to the left this is called by the left key.
*
* @private
*/
function left() {
var parentRole = getParentRole();
if (parentRole == "tablist") {
moveFocus(-1, getFocusElements(focusedElement.parentNode));
} else if (focusedControl.parent().submenu) {
cancel();
} else {
moveFocus(-1);
}
}
/**
* Moves the focus to the right this is called by the right key.
*
* @private
*/
function right() {
var role = getRole(), parentRole = getParentRole();
if (parentRole == "tablist") {
moveFocus(1, getFocusElements(focusedElement.parentNode));
} else if (role == "menuitem" && parentRole == "menu" && getAriaProp('haspopup')) {
enter();
} else {
moveFocus(1);
}
}
/**
* Moves the focus to the up this is called by the up key.
*
* @private
*/
function up() {
moveFocus(-1);
}
/**
* Moves the focus to the up this is called by the down key.
*
* @private
*/
function down() {
var role = getRole(), parentRole = getParentRole();
if (role == "menuitem" && parentRole == "menubar") {
enter();
} else if (role == "button" && getAriaProp('haspopup')) {
enter({key: 'down'});
} else {
moveFocus(1);
}
}
/**
* Moves the focus to the next item or previous item depending on shift key.
*
* @private
* @param {DOMEvent} e DOM event object.
*/
function tab(e) {
var parentRole = getParentRole();
if (parentRole == "tablist") {
var elm = getFocusElements(focusedControl.getEl('body'))[0];
if (elm) {
elm.focus();
}
} else {
moveFocus(e.shiftKey ? -1 : 1);
}
}
/**
* Calls the cancel event on the currently focused control. This is normally done using the Esc key.
*
* @private
*/
function cancel() {
focusedControl.fire('cancel');
}
/**
* Calls the click event on the currently focused control. This is normally done using the Enter/Space keys.
*
* @private
* @param {Object} aria Optional aria data to pass along with the enter event.
*/
function enter(aria) {
aria = aria || {};
focusedControl.fire('click', {target: focusedElement, aria: aria});
}
root.on('keydown', function(e) {
function handleNonTabOrEscEvent(e, handler) {
// Ignore non tab keys for text elements
if (isTextInputElement(focusedElement)) {
return;
}
if (handler(e) !== false) {
e.preventDefault();
}
}
if (e.isDefaultPrevented()) {
return;
}
switch (e.keyCode) {
case 37: // DOM_VK_LEFT
handleNonTabOrEscEvent(e, left);
break;
case 39: // DOM_VK_RIGHT
handleNonTabOrEscEvent(e, right);
break;
case 38: // DOM_VK_UP
handleNonTabOrEscEvent(e, up);
break;
case 40: // DOM_VK_DOWN
handleNonTabOrEscEvent(e, down);
break;
case 27: // DOM_VK_ESCAPE
cancel();
break;
case 14: // DOM_VK_ENTER
case 13: // DOM_VK_RETURN
case 32: // DOM_VK_SPACE
handleNonTabOrEscEvent(e, enter);
break;
case 9: // DOM_VK_TAB
if (tab(e) !== false) {
e.preventDefault();
}
break;
}
});
root.on('focusin', function(e) {
focusedElement = e.target;
focusedControl = e.control;
});
return {
focusFirst: focusFirst
};
};
});
// Included from: js/tinymce/classes/ui/Container.js
/**
* Container.js
*
* Copyright, Moxiecode Systems AB
* Released under LGPL License.
*
* License: http://www.tinymce.com/license
* Contributing: http://www.tinymce.com/contributing
*/
/**
* Container control. This is extended by all controls that can have
* children such as panels etc. You can also use this class directly as an
* generic container instance. The container doesn't have any specific role or style.
*
* @-x-less Container.less
* @class tinymce.ui.Container
* @extends tinymce.ui.Control
*/
define("tinymce/ui/Container", [
"tinymce/ui/Control",
"tinymce/ui/Collection",
"tinymce/ui/Selector",
"tinymce/ui/Factory",
"tinymce/ui/KeyboardNavigation",
"tinymce/util/Tools",
"tinymce/ui/DomUtils"
], function(Control, Collection, Selector, Factory, KeyboardNavigation, Tools, DomUtils) {
"use strict";
var selectorCache = {};
return Control.extend({
layout: '',
innerClass: 'container-inner',
/**
* Constructs a new control instance with the specified settings.
*
* @constructor
* @param {Object} settings Name/value object with settings.
* @setting {Array} items Items to add to container in JSON format or control instances.
* @setting {String} layout Layout manager by name to use.
* @setting {Object} defaults Default settings to apply to all items.
*/
init: function(settings) {
var self = this;
self._super(settings);
settings = self.settings;
self._fixed = settings.fixed;
self._items = new Collection();
if (self.isRtl()) {
self.addClass('rtl');
}
self.addClass('container');
self.addClass('container-body', 'body');
if (settings.containerCls) {
self.addClass(settings.containerCls);
}
self._layout = Factory.create((settings.layout || self.layout) + 'layout');
if (self.settings.items) {
self.add(self.settings.items);
}
// TODO: Fix this!
self._hasBody = true;
},
/**
* Returns a collection of child items that the container currently have.
*
* @method items
* @return {tinymce.ui.Collection} Control collection direct child controls.
*/
items: function() {
return this._items;
},
/**
* Find child controls by selector.
*
* @method find
* @param {String} selector Selector CSS pattern to find children by.
* @return {tinymce.ui.Collection} Control collection with child controls.
*/
find: function(selector) {
selector = selectorCache[selector] = selectorCache[selector] || new Selector(selector);
return selector.find(this);
},
/**
* Adds one or many items to the current container. This will create instances of
* the object representations if needed.
*
* @method add
* @param {Array/Object/tinymce.ui.Control} items Array or item that will be added to the container.
* @return {tinymce.ui.Collection} Current collection control.
*/
add: function(items) {
var self = this;
self.items().add(self.create(items)).parent(self);
return self;
},
/**
* Focuses the current container instance. This will look
* for the first control in the container and focus that.
*
* @method focus
* @param {Boolean} keyboard Optional true/false if the focus was a keyboard focus or not.
* @return {tinymce.ui.Collection} Current instance.
*/
focus: function(keyboard) {
var self = this, focusCtrl, keyboardNav, items;
if (keyboard) {
keyboardNav = self.keyboardNav || self.parents().eq(-1)[0].keyboardNav;
if (keyboardNav) {
keyboardNav.focusFirst(self);
return;
}
}
items = self.find('*');
// TODO: Figure out a better way to auto focus alert dialog buttons
if (self.statusbar) {
items.add(self.statusbar.items());
}
items.each(function(ctrl) {
if (ctrl.settings.autofocus) {
focusCtrl = null;
return false;
}
if (ctrl.canFocus) {
focusCtrl = focusCtrl || ctrl;
}
});
if (focusCtrl) {
focusCtrl.focus();
}
return self;
},
/**
* Replaces the specified child control with a new control.
*
* @method replace
* @param {tinymce.ui.Control} oldItem Old item to be replaced.
* @param {tinymce.ui.Control} newItem New item to be inserted.
*/
replace: function(oldItem, newItem) {
var ctrlElm, items = this.items(), i = items.length;
// Replace the item in collection
while (i--) {
if (items[i] === oldItem) {
items[i] = newItem;
break;
}
}
if (i >= 0) {
// Remove new item from DOM
ctrlElm = newItem.getEl();
if (ctrlElm) {
ctrlElm.parentNode.removeChild(ctrlElm);
}
// Remove old item from DOM
ctrlElm = oldItem.getEl();
if (ctrlElm) {
ctrlElm.parentNode.removeChild(ctrlElm);
}
}
// Adopt the item
newItem.parent(this);
},
/**
* Creates the specified items. If any of the items is plain JSON style objects
* it will convert these into real tinymce.ui.Control instances.
*
* @method create
* @param {Array} items Array of items to convert into control instances.
* @return {Array} Array with control instances.
*/
create: function(items) {
var self = this, settings, ctrlItems = [];
// Non array structure, then force it into an array
if (!Tools.isArray(items)) {
items = [items];
}
// Add default type to each child control
Tools.each(items, function(item) {
if (item) {
// Construct item if needed
if (!(item instanceof Control)) {
// Name only then convert it to an object
if (typeof item == "string") {
item = {type: item};
}
// Create control instance based on input settings and default settings
settings = Tools.extend({}, self.settings.defaults, item);
item.type = settings.type = settings.type || item.type || self.settings.defaultType ||
(settings.defaults ? settings.defaults.type : null);
item = Factory.create(settings);
}
ctrlItems.push(item);
}
});
return ctrlItems;
},
/**
* Renders new control instances.
*
* @private
*/
renderNew: function() {
var self = this;
// Render any new items
self.items().each(function(ctrl, index) {
var containerElm, fragment;
ctrl.parent(self);
if (!ctrl._rendered) {
containerElm = self.getEl('body');
fragment = DomUtils.createFragment(ctrl.renderHtml());
// Insert or append the item
if (containerElm.hasChildNodes() && index <= containerElm.childNodes.length - 1) {
containerElm.insertBefore(fragment, containerElm.childNodes[index]);
} else {
containerElm.appendChild(fragment);
}
ctrl.postRender();
}
});
self._layout.applyClasses(self);
self._lastRect = null;
return self;
},
/**
* Appends new instances to the current container.
*
* @method append
* @param {Array/tinymce.ui.Collection} items Array if controls to append.
* @return {tinymce.ui.Container} Current container instance.
*/
append: function(items) {
return this.add(items).renderNew();
},
/**
* Prepends new instances to the current container.
*
* @method prepend
* @param {Array/tinymce.ui.Collection} items Array if controls to prepend.
* @return {tinymce.ui.Container} Current container instance.
*/
prepend: function(items) {
var self = this;
self.items().set(self.create(items).concat(self.items().toArray()));
return self.renderNew();
},
/**
* Inserts an control at a specific index.
*
* @method insert
* @param {Array/tinymce.ui.Collection} items Array if controls to insert.
* @param {Number} index Index to insert controls at.
* @param {Boolean} [before=false] Inserts controls before the index.
*/
insert: function(items, index, before) {
var self = this, curItems, beforeItems, afterItems;
items = self.create(items);
curItems = self.items();
if (!before && index < curItems.length - 1) {
index += 1;
}
if (index >= 0 && index < curItems.length) {
beforeItems = curItems.slice(0, index).toArray();
afterItems = curItems.slice(index).toArray();
curItems.set(beforeItems.concat(items, afterItems));
}
return self.renderNew();
},
/**
* Populates the form fields from the specified JSON data object.
*
* Control items in the form that matches the data will have it's value set.
*
* @method fromJSON
* @param {Object} data JSON data object to set control values by.
* @return {tinymce.ui.Container} Current form instance.
*/
fromJSON: function(data) {
var self = this;
for (var name in data) {
self.find('#' + name).value(data[name]);
}
return self;
},
/**
* Serializes the form into a JSON object by getting all items
* that has a name and a value.
*
* @method toJSON
* @return {Object} JSON object with form data.
*/
toJSON: function() {
var self = this, data = {};
self.find('*').each(function(ctrl) {
var name = ctrl.name(), value = ctrl.value();
if (name && typeof value != "undefined") {
data[name] = value;
}
});
return data;
},
preRender: function() {
},
/**
* Renders the control as a HTML string.
*
* @method renderHtml
* @return {String} HTML representing the control.
*/
renderHtml: function() {
var self = this, layout = self._layout, role = this.settings.role;
self.preRender();
layout.preRender(self);
return (
'' +
'' +
(self.settings.html || '') + layout.renderHtml(self) +
'' +
''
);
},
/**
* Post render method. Called after the control has been rendered to the target.
*
* @method postRender
* @return {tinymce.ui.Container} Current combobox instance.
*/
postRender: function() {
var self = this, box;
self.items().exec('postRender');
self._super();
self._layout.postRender(self);
self._rendered = true;
if (self.settings.style) {
DomUtils.css(self.getEl(), self.settings.style);
}
if (self.settings.border) {
box = self.borderBox();
DomUtils.css(self.getEl(), {
'border-top-width': box.top,
'border-right-width': box.right,
'border-bottom-width': box.bottom,
'border-left-width': box.left
});
}
if (!self.parent()) {
self.keyboardNav = new KeyboardNavigation({
root: self
});
}
return self;
},
/**
* Initializes the current controls layout rect.
* This will be executed by the layout managers to determine the
* default minWidth/minHeight etc.
*
* @method initLayoutRect
* @return {Object} Layout rect instance.
*/
initLayoutRect: function() {
var self = this, layoutRect = self._super();
// Recalc container size by asking layout manager
self._layout.recalc(self);
return layoutRect;
},
/**
* Recalculates the positions of the controls in the current container.
* This is invoked by the reflow method and shouldn't be called directly.
*
* @method recalc
*/
recalc: function() {
var self = this, rect = self._layoutRect, lastRect = self._lastRect;
if (!lastRect || lastRect.w != rect.w || lastRect.h != rect.h) {
self._layout.recalc(self);
rect = self.layoutRect();
self._lastRect = {x: rect.x, y: rect.y, w: rect.w, h: rect.h};
return true;
}
},
/**
* Reflows the current container and it's children and possible parents.
* This should be used after you for example append children to the current control so
* that the layout managers know that they need to reposition everything.
*
* @example
* container.append({type: 'button', text: 'My button'}).reflow();
*
* @method reflow
* @return {tinymce.ui.Container} Current container instance.
*/
reflow: function() {
var i;
if (this.visible()) {
Control.repaintControls = [];
Control.repaintControls.map = {};
this.recalc();
i = Control.repaintControls.length;
while (i--) {
Control.repaintControls[i].repaint();
}
// TODO: Fix me!
if (this.settings.layout !== "flow" && this.settings.layout !== "stack") {
this.repaint();
}
Control.repaintControls = [];
}
return this;
}
});
});
// Included from: js/tinymce/classes/ui/DragHelper.js
/**
* DragHelper.js
*
* Copyright, Moxiecode Systems AB
* Released under LGPL License.
*
* License: http://www.tinymce.com/license
* Contributing: http://www.tinymce.com/contributing
*/
/**
* Drag/drop helper class.
*
* @example
* var dragHelper = new tinymce.ui.DragHelper('mydiv', {
* start: function(evt) {
* },
*
* drag: function(evt) {
* },
*
* end: function(evt) {
* }
* });
*
* @class tinymce.ui.DragHelper
*/
define("tinymce/ui/DragHelper", [
"tinymce/ui/DomUtils"
], function(DomUtils) {
"use strict";
function getDocumentSize() {
var doc = document, documentElement, body, scrollWidth, clientWidth;
var offsetWidth, scrollHeight, clientHeight, offsetHeight, max = Math.max;
documentElement = doc.documentElement;
body = doc.body;
scrollWidth = max(documentElement.scrollWidth, body.scrollWidth);
clientWidth = max(documentElement.clientWidth, body.clientWidth);
offsetWidth = max(documentElement.offsetWidth, body.offsetWidth);
scrollHeight = max(documentElement.scrollHeight, body.scrollHeight);
clientHeight = max(documentElement.clientHeight, body.clientHeight);
offsetHeight = max(documentElement.offsetHeight, body.offsetHeight);
return {
width: scrollWidth < offsetWidth ? clientWidth : scrollWidth,
height: scrollHeight < offsetHeight ? clientHeight : scrollHeight
};
}
return function(id, settings) {
var eventOverlayElm, doc = document, downButton, start, stop, drag, startX, startY;
settings = settings || {};
function getHandleElm() {
return doc.getElementById(settings.handle || id);
}
start = function(e) {
var docSize = getDocumentSize(), handleElm, cursor;
e.preventDefault();
downButton = e.button;
handleElm = getHandleElm();
startX = e.screenX;
startY = e.screenY;
// Grab cursor from handle
if (window.getComputedStyle) {
cursor = window.getComputedStyle(handleElm, null).getPropertyValue("cursor");
} else {
cursor = handleElm.runtimeStyle.cursor;
}
// Create event overlay and add it to document
eventOverlayElm = doc.createElement('div');
DomUtils.css(eventOverlayElm, {
position: "absolute",
top: 0, left: 0,
width: docSize.width,
height: docSize.height,
zIndex: 0x7FFFFFFF,
opacity: 0.0001,
cursor: cursor
});
doc.body.appendChild(eventOverlayElm);
// Bind mouse events
DomUtils.on(doc, 'mousemove', drag);
DomUtils.on(doc, 'mouseup', stop);
// Begin drag
settings.start(e);
};
drag = function(e) {
if (e.button !== downButton) {
return stop(e);
}
e.deltaX = e.screenX - startX;
e.deltaY = e.screenY - startY;
e.preventDefault();
settings.drag(e);
};
stop = function(e) {
DomUtils.off(doc, 'mousemove', drag);
DomUtils.off(doc, 'mouseup', stop);
eventOverlayElm.parentNode.removeChild(eventOverlayElm);
if (settings.stop) {
settings.stop(e);
}
};
/**
* Destroys the drag/drop helper instance.
*
* @method destroy
*/
this.destroy = function() {
DomUtils.off(getHandleElm());
};
DomUtils.on(getHandleElm(), 'mousedown', start);
};
});
// Included from: js/tinymce/classes/ui/Scrollable.js
/**
* Scrollable.js
*
* Copyright, Moxiecode Systems AB
* Released under LGPL License.
*
* License: http://www.tinymce.com/license
* Contributing: http://www.tinymce.com/contributing
*/
/**
* This mixin makes controls scrollable using custom scrollbars.
*
* @-x-less Scrollable.less
* @mixin tinymce.ui.Scrollable
*/
define("tinymce/ui/Scrollable", [
"tinymce/ui/DomUtils",
"tinymce/ui/DragHelper"
], function(DomUtils, DragHelper) {
"use strict";
return {
init: function() {
var self = this;
self.on('repaint', self.renderScroll);
},
renderScroll: function() {
var self = this, margin = 2;
function repaintScroll() {
var hasScrollH, hasScrollV, bodyElm;
function repaintAxis(axisName, posName, sizeName, contentSizeName, hasScroll, ax) {
var containerElm, scrollBarElm, scrollThumbElm;
var containerSize, scrollSize, ratio, rect;
var posNameLower, sizeNameLower;
scrollBarElm = self.getEl('scroll' + axisName);
if (scrollBarElm) {
posNameLower = posName.toLowerCase();
sizeNameLower = sizeName.toLowerCase();
if (self.getEl('absend')) {
DomUtils.css(self.getEl('absend'), posNameLower, self.layoutRect()[contentSizeName] - 1);
}
if (!hasScroll) {
DomUtils.css(scrollBarElm, 'display', 'none');
return;
}
DomUtils.css(scrollBarElm, 'display', 'block');
containerElm = self.getEl('body');
scrollThumbElm = self.getEl('scroll' + axisName + "t");
containerSize = containerElm["client" + sizeName] - (margin * 2);
containerSize -= hasScrollH && hasScrollV ? scrollBarElm["client" + ax] : 0;
scrollSize = containerElm["scroll" + sizeName];
ratio = containerSize / scrollSize;
rect = {};
rect[posNameLower] = containerElm["offset" + posName] + margin;
rect[sizeNameLower] = containerSize;
DomUtils.css(scrollBarElm, rect);
rect = {};
rect[posNameLower] = containerElm["scroll" + posName] * ratio;
rect[sizeNameLower] = containerSize * ratio;
DomUtils.css(scrollThumbElm, rect);
}
}
bodyElm = self.getEl('body');
hasScrollH = bodyElm.scrollWidth > bodyElm.clientWidth;
hasScrollV = bodyElm.scrollHeight > bodyElm.clientHeight;
repaintAxis("h", "Left", "Width", "contentW", hasScrollH, "Height");
repaintAxis("v", "Top", "Height", "contentH", hasScrollV, "Width");
}
function addScroll() {
function addScrollAxis(axisName, posName, sizeName, deltaPosName, ax) {
var scrollStart, axisId = self._id + '-scroll' + axisName, prefix = self.classPrefix;
self.getEl().appendChild(DomUtils.createFragment(
' '
));
self.draghelper = new DragHelper(axisId + 't', {
start: function() {
scrollStart = self.getEl('body')["scroll" + posName];
DomUtils.addClass(DomUtils.get(axisId), prefix + 'active');
},
drag: function(e) {
var ratio, hasScrollH, hasScrollV, containerSize, layoutRect = self.layoutRect();
hasScrollH = layoutRect.contentW > layoutRect.innerW;
hasScrollV = layoutRect.contentH > layoutRect.innerH;
containerSize = self.getEl('body')["client" + sizeName] - (margin * 2);
containerSize -= hasScrollH && hasScrollV ? self.getEl('scroll' + axisName)["client" + ax] : 0;
ratio = containerSize / self.getEl('body')["scroll" + sizeName];
self.getEl('body')["scroll" + posName] = scrollStart + (e["delta" + deltaPosName] / ratio);
},
stop: function() {
DomUtils.removeClass(DomUtils.get(axisId), prefix + 'active');
}
});
/*
self.on('click', function(e) {
if (e.target.id == self._id + '-scrollv') {
}
});*/
}
self.addClass('scroll');
addScrollAxis("v", "Top", "Height", "Y", "Width");
addScrollAxis("h", "Left", "Width", "X", "Height");
}
if (self.settings.autoScroll) {
if (!self._hasScroll) {
self._hasScroll = true;
addScroll();
self.on('wheel', function(e) {
var bodyEl = self.getEl('body');
bodyEl.scrollLeft += (e.deltaX || 0) * 10;
bodyEl.scrollTop += e.deltaY * 10;
repaintScroll();
});
DomUtils.on(self.getEl('body'), "scroll", repaintScroll);
}
repaintScroll();
}
}
};
});
// Included from: js/tinymce/classes/ui/Panel.js
/**
* Panel.js
*
* Copyright, Moxiecode Systems AB
* Released under LGPL License.
*
* License: http://www.tinymce.com/license
* Contributing: http://www.tinymce.com/contributing
*/
/**
* Creates a new panel.
*
* @-x-less Panel.less
* @class tinymce.ui.Panel
* @extends tinymce.ui.Container
* @mixes tinymce.ui.Scrollable
*/
define("tinymce/ui/Panel", [
"tinymce/ui/Container",
"tinymce/ui/Scrollable"
], function(Container, Scrollable) {
"use strict";
return Container.extend({
Defaults: {
layout: 'fit',
containerCls: 'panel'
},
Mixins: [Scrollable],
/**
* Renders the control as a HTML string.
*
* @method renderHtml
* @return {String} HTML representing the control.
*/
renderHtml: function() {
var self = this, layout = self._layout, innerHtml = self.settings.html;
self.preRender();
layout.preRender(self);
if (typeof innerHtml == "undefined") {
innerHtml = (
'' +
layout.renderHtml(self) +
''
);
} else {
if (typeof innerHtml == 'function') {
innerHtml = innerHtml.call(self);
}
self._hasBody = false;
}
return (
'' +
(self._preBodyHtml || '') +
innerHtml +
''
);
}
});
});
// Included from: js/tinymce/classes/ui/Movable.js
/**
* Movable.js
*
* Copyright, Moxiecode Systems AB
* Released under LGPL License.
*
* License: http://www.tinymce.com/license
* Contributing: http://www.tinymce.com/contributing
*/
/**
* Movable mixin. Makes controls movable absolute and relative to other elements.
*
* @mixin tinymce.ui.Movable
*/
define("tinymce/ui/Movable", [
"tinymce/ui/DomUtils"
], function(DomUtils) {
"use strict";
function calculateRelativePosition(ctrl, targetElm, rel) {
var ctrlElm, pos, x, y, selfW, selfH, targetW, targetH, viewport, size;
viewport = DomUtils.getViewPort();
// Get pos of target
pos = DomUtils.getPos(targetElm);
x = pos.x;
y = pos.y;
if (ctrl._fixed && DomUtils.getRuntimeStyle(document.body, 'position') == 'static') {
x -= viewport.x;
y -= viewport.y;
}
// Get size of self
ctrlElm = ctrl.getEl();
size = DomUtils.getSize(ctrlElm);
selfW = size.width;
selfH = size.height;
// Get size of target
size = DomUtils.getSize(targetElm);
targetW = size.width;
targetH = size.height;
// Parse align string
rel = (rel || '').split('');
// Target corners
if (rel[0] === 'b') {
y += targetH;
}
if (rel[1] === 'r') {
x += targetW;
}
if (rel[0] === 'c') {
y += Math.round(targetH / 2);
}
if (rel[1] === 'c') {
x += Math.round(targetW / 2);
}
// Self corners
if (rel[3] === 'b') {
y -= selfH;
}
if (rel[4] === 'r') {
x -= selfW;
}
if (rel[3] === 'c') {
y -= Math.round(selfH / 2);
}
if (rel[4] === 'c') {
x -= Math.round(selfW / 2);
}
return {
x: x,
y: y,
w: selfW,
h: selfH
};
}
return {
/**
* Tests various positions to get the most suitable one.
*
* @method testMoveRel
* @param {DOMElement} elm Element to position against.
* @param {Array} rels Array with relative positions.
* @return {String} Best suitable relative position.
*/
testMoveRel: function(elm, rels) {
var viewPortRect = DomUtils.getViewPort();
for (var i = 0; i < rels.length; i++) {
var pos = calculateRelativePosition(this, elm, rels[i]);
if (this._fixed) {
if (pos.x > 0 && pos.x + pos.w < viewPortRect.w && pos.y > 0 && pos.y + pos.h < viewPortRect.h) {
return rels[i];
}
} else {
if (pos.x > viewPortRect.x && pos.x + pos.w < viewPortRect.w + viewPortRect.x &&
pos.y > viewPortRect.y && pos.y + pos.h < viewPortRect.h + viewPortRect.y) {
return rels[i];
}
}
}
return rels[0];
},
/**
* Move relative to the specified element.
*
* @method moveRel
* @param {Element} elm Element to move relative to.
* @param {String} rel Relative mode. For example: br-tl.
* @return {tinymce.ui.Control} Current control instance.
*/
moveRel: function(elm, rel) {
if (typeof rel != 'string') {
rel = this.testMoveRel(elm, rel);
}
var pos = calculateRelativePosition(this, elm, rel);
return this.moveTo(pos.x, pos.y);
},
/**
* Move by a relative x, y values.
*
* @method moveBy
* @param {Number} dx Relative x position.
* @param {Number} dy Relative y position.
* @return {tinymce.ui.Control} Current control instance.
*/
moveBy: function(dx, dy) {
var self = this, rect = self.layoutRect();
self.moveTo(rect.x + dx, rect.y + dy);
return self;
},
/**
* Move to absolute position.
*
* @method moveTo
* @param {Number} x Absolute x position.
* @param {Number} y Absolute y position.
* @return {tinymce.ui.Control} Current control instance.
*/
moveTo: function(x, y) {
var self = this;
// TODO: Move this to some global class
function contrain(value, max, size) {
if (value < 0) {
return 0;
}
if (value + size > max) {
value = max - size;
return value < 0 ? 0 : value;
}
return value;
}
if (self.settings.constrainToViewport) {
var viewPortRect = DomUtils.getViewPort(window);
var layoutRect = self.layoutRect();
x = contrain(x, viewPortRect.w + viewPortRect.x, layoutRect.w);
y = contrain(y, viewPortRect.h + viewPortRect.y, layoutRect.h);
}
if (self._rendered) {
self.layoutRect({x: x, y: y}).repaint();
} else {
self.settings.x = x;
self.settings.y = y;
}
self.fire('move', {x: x, y: y});
return self;
}
};
});
// Included from: js/tinymce/classes/ui/Resizable.js
/**
* Resizable.js
*
* Copyright, Moxiecode Systems AB
* Released under LGPL License.
*
* License: http://www.tinymce.com/license
* Contributing: http://www.tinymce.com/contributing
*/
/**
* Resizable mixin. Enables controls to be resized.
*
* @mixin tinymce.ui.Resizable
*/
define("tinymce/ui/Resizable", [
"tinymce/ui/DomUtils"
], function(DomUtils) {
"use strict";
return {
/**
* Resizes the control to contents.
*
* @method resizeToContent
*/
resizeToContent: function() {
this._layoutRect.autoResize = true;
this._lastRect = null;
this.reflow();
},
/**
* Resizes the control to a specific width/height.
*
* @method resizeTo
* @param {Number} w Control width.
* @param {Number} h Control height.
* @return {tinymce.ui.Control} Current control instance.
*/
resizeTo: function(w, h) {
// TODO: Fix hack
if (w <= 1 || h <= 1) {
var rect = DomUtils.getWindowSize();
w = w <= 1 ? w * rect.w : w;
h = h <= 1 ? h * rect.h : h;
}
this._layoutRect.autoResize = false;
return this.layoutRect({minW: w, minH: h, w: w, h: h}).reflow();
},
/**
* Resizes the control to a specific relative width/height.
*
* @method resizeBy
* @param {Number} dw Relative control width.
* @param {Number} dh Relative control height.
* @return {tinymce.ui.Control} Current control instance.
*/
resizeBy: function(dw, dh) {
var self = this, rect = self.layoutRect();
return self.resizeTo(rect.w + dw, rect.h + dh);
}
};
});
// Included from: js/tinymce/classes/ui/FloatPanel.js
/**
* FloatPanel.js
*
* Copyright, Moxiecode Systems AB
* Released under LGPL License.
*
* License: http://www.tinymce.com/license
* Contributing: http://www.tinymce.com/contributing
*/
/**
* This class creates a floating panel.
*
* @-x-less FloatPanel.less
* @class tinymce.ui.FloatPanel
* @extends tinymce.ui.Panel
* @mixes tinymce.ui.Movable
* @mixes tinymce.ui.Resizable
*/
define("tinymce/ui/FloatPanel", [
"tinymce/ui/Panel",
"tinymce/ui/Movable",
"tinymce/ui/Resizable",
"tinymce/ui/DomUtils"
], function(Panel, Movable, Resizable, DomUtils) {
"use strict";
var documentClickHandler, documentScrollHandler, windowResizeHandler, visiblePanels = [];
var zOrder = [], hasModal;
function bindDocumentClickHandler() {
function isChildOf(ctrl, parent) {
while (ctrl) {
if (ctrl == parent) {
return true;
}
ctrl = ctrl.parent();
}
}
if (!documentClickHandler) {
documentClickHandler = function(e) {
// Gecko fires click event and in the wrong order on Mac so lets normalize
if (e.button == 2) {
return;
}
// Hide any float panel when a click is out side that float panel and the
// float panels direct parent for example a click on a menu button
var i = visiblePanels.length;
while (i--) {
var panel = visiblePanels[i], clickCtrl = panel.getParentCtrl(e.target);
if (panel.settings.autohide) {
if (clickCtrl) {
if (isChildOf(clickCtrl, panel) || panel.parent() === clickCtrl) {
continue;
}
}
e = panel.fire('autohide', {target: e.target});
if (!e.isDefaultPrevented()) {
panel.hide();
}
}
}
};
DomUtils.on(document, 'click', documentClickHandler);
}
}
function bindDocumentScrollHandler() {
if (!documentScrollHandler) {
documentScrollHandler = function() {
var i;
i = visiblePanels.length;
while (i--) {
repositionPanel(visiblePanels[i]);
}
};
DomUtils.on(window, 'scroll', documentScrollHandler);
}
}
function bindWindowResizeHandler() {
if (!windowResizeHandler) {
var docElm = document.documentElement, clientWidth = docElm.clientWidth, clientHeight = docElm.clientHeight;
windowResizeHandler = function() {
// Workaround for #7065 IE 7 fires resize events event though the window wasn't resized
if (!document.all || clientWidth != docElm.clientWidth || clientHeight != docElm.clientHeight) {
clientWidth = docElm.clientWidth;
clientHeight = docElm.clientHeight;
FloatPanel.hideAll();
}
};
DomUtils.on(window, 'resize', windowResizeHandler);
}
}
/**
* Repositions the panel to the top of page if the panel is outside of the visual viewport. It will
* also reposition all child panels of the current panel.
*/
function repositionPanel(panel) {
var scrollY = DomUtils.getViewPort().y;
function toggleFixedChildPanels(fixed, deltaY) {
var parent;
for (var i = 0; i < visiblePanels.length; i++) {
if (visiblePanels[i] != panel) {
parent = visiblePanels[i].parent();
while (parent && (parent = parent.parent())) {
if (parent == panel) {
visiblePanels[i].fixed(fixed).moveBy(0, deltaY).repaint();
}
}
}
}
}
if (panel.settings.autofix) {
if (!panel._fixed) {
panel._autoFixY = panel.layoutRect().y;
if (panel._autoFixY < scrollY) {
panel.fixed(true).layoutRect({y: 0}).repaint();
toggleFixedChildPanels(true, scrollY - panel._autoFixY);
}
} else {
if (panel._autoFixY > scrollY) {
panel.fixed(false).layoutRect({y: panel._autoFixY}).repaint();
toggleFixedChildPanels(false, panel._autoFixY - scrollY);
}
}
}
}
function addRemove(add, ctrl) {
var i, zIndex = FloatPanel.zIndex || 0xFFFF, topModal;
if (add) {
zOrder.push(ctrl);
} else {
i = zOrder.length;
while (i--) {
if (zOrder[i] === ctrl) {
zOrder.splice(i, 1);
}
}
}
if (zOrder.length) {
for (i = 0; i < zOrder.length; i++) {
if (zOrder[i].modal) {
zIndex++;
topModal = zOrder[i];
}
zOrder[i].getEl().style.zIndex = zIndex;
zOrder[i].zIndex = zIndex;
zIndex++;
}
}
var modalBlockEl = document.getElementById(ctrl.classPrefix + 'modal-block');
if (topModal) {
DomUtils.css(modalBlockEl, 'z-index', topModal.zIndex - 1);
} else if (modalBlockEl) {
modalBlockEl.parentNode.removeChild(modalBlockEl);
hasModal = false;
}
FloatPanel.currentZIndex = zIndex;
}
var FloatPanel = Panel.extend({
Mixins: [Movable, Resizable],
/**
* Constructs a new control instance with the specified settings.
*
* @constructor
* @param {Object} settings Name/value object with settings.
* @setting {Boolean} autohide Automatically hide the panel.
*/
init: function(settings) {
var self = this;
self._super(settings);
self._eventsRoot = self;
self.addClass('floatpanel');
// Hide floatpanes on click out side the root button
if (settings.autohide) {
bindDocumentClickHandler();
bindWindowResizeHandler();
visiblePanels.push(self);
}
if (settings.autofix) {
bindDocumentScrollHandler();
self.on('move', function() {
repositionPanel(this);
});
}
self.on('postrender show', function(e) {
if (e.control == self) {
var modalBlockEl, prefix = self.classPrefix;
if (self.modal && !hasModal) {
modalBlockEl = DomUtils.createFragment('');
modalBlockEl = modalBlockEl.firstChild;
self.getContainerElm().appendChild(modalBlockEl);
setTimeout(function() {
DomUtils.addClass(modalBlockEl, prefix + 'in');
DomUtils.addClass(self.getEl(), prefix + 'in');
}, 0);
hasModal = true;
}
addRemove(true, self);
}
});
self.on('show', function() {
self.parents().each(function(ctrl) {
if (ctrl._fixed) {
self.fixed(true);
return false;
}
});
});
if (settings.popover) {
self._preBodyHtml = '';
self.addClass('popover').addClass('bottom').addClass(self.isRtl() ? 'end' : 'start');
}
},
fixed: function(state) {
var self = this;
if (self._fixed != state) {
if (self._rendered) {
var viewport = DomUtils.getViewPort();
if (state) {
self.layoutRect().y -= viewport.y;
} else {
self.layoutRect().y += viewport.y;
}
}
self.toggleClass('fixed', state);
self._fixed = state;
}
return self;
},
/**
* Shows the current float panel.
*
* @method show
* @return {tinymce.ui.FloatPanel} Current floatpanel instance.
*/
show: function() {
var self = this, i, state = self._super();
i = visiblePanels.length;
while (i--) {
if (visiblePanels[i] === self) {
break;
}
}
if (i === -1) {
visiblePanels.push(self);
}
return state;
},
/**
* Hides the current float panel.
*
* @method hide
* @return {tinymce.ui.FloatPanel} Current floatpanel instance.
*/
hide: function() {
removeVisiblePanel(this);
addRemove(false, this);
return this._super();
},
/**
* Hide all visible float panels with he autohide setting enabled. This is for
* manually hiding floating menus or panels.
*
* @method hideAll
*/
hideAll: function() {
FloatPanel.hideAll();
},
/**
* Closes the float panel. This will remove the float panel from page and fire the close event.
*
* @method close
*/
close: function() {
var self = this;
if (!self.fire('close').isDefaultPrevented()) {
self.remove();
addRemove(false, self);
}
return self;
},
/**
* Removes the float panel from page.
*
* @method remove
*/
remove: function() {
removeVisiblePanel(this);
this._super();
},
postRender: function() {
var self = this;
if (self.settings.bodyRole) {
this.getEl('body').setAttribute('role', self.settings.bodyRole);
}
return self._super();
}
});
/**
* Hide all visible float panels with he autohide setting enabled. This is for
* manually hiding floating menus or panels.
*
* @static
* @method hideAll
*/
FloatPanel.hideAll = function() {
var i = visiblePanels.length;
while (i--) {
var panel = visiblePanels[i];
if (panel && panel.settings.autohide) {
panel.hide();
visiblePanels.splice(i, 1);
}
}
};
function removeVisiblePanel(panel) {
var i;
i = visiblePanels.length;
while (i--) {
if (visiblePanels[i] === panel) {
visiblePanels.splice(i, 1);
}
}
i = zOrder.length;
while (i--) {
if (zOrder[i] === panel) {
zOrder.splice(i, 1);
}
}
}
return FloatPanel;
});
// Included from: js/tinymce/classes/ui/Window.js
/**
* Window.js
*
* Copyright, Moxiecode Systems AB
* Released under LGPL License.
*
* License: http://www.tinymce.com/license
* Contributing: http://www.tinymce.com/contributing
*/
/**
* Creates a new window.
*
* @-x-less Window.less
* @class tinymce.ui.Window
* @extends tinymce.ui.FloatPanel
*/
define("tinymce/ui/Window", [
"tinymce/ui/FloatPanel",
"tinymce/ui/Panel",
"tinymce/ui/DomUtils",
"tinymce/ui/DragHelper"
], function(FloatPanel, Panel, DomUtils, DragHelper) {
"use strict";
var Window = FloatPanel.extend({
modal: true,
Defaults: {
border: 1,
layout: 'flex',
containerCls: 'panel',
role: 'dialog',
callbacks: {
submit: function() {
this.fire('submit', {data: this.toJSON()});
},
close: function() {
this.close();
}
}
},
/**
* Constructs a instance with the specified settings.
*
* @constructor
* @param {Object} settings Name/value object with settings.
*/
init: function(settings) {
var self = this;
self._super(settings);
if (self.isRtl()) {
self.addClass('rtl');
}
self.addClass('window');
self._fixed = true;
// Create statusbar
if (settings.buttons) {
self.statusbar = new Panel({
layout: 'flex',
border: '1 0 0 0',
spacing: 3,
padding: 10,
align: 'center',
pack: self.isRtl() ? 'start' : 'end',
defaults: {
type: 'button'
},
items: settings.buttons
});
self.statusbar.addClass('foot');
self.statusbar.parent(self);
}
self.on('click', function(e) {
if (e.target.className.indexOf(self.classPrefix + 'close') != -1) {
self.close();
}
});
self.on('cancel', function() {
self.close();
});
self.aria('describedby', self.describedBy || self._id + '-none');
self.aria('label', settings.title);
self._fullscreen = false;
},
/**
* Recalculates the positions of the controls in the current container.
* This is invoked by the reflow method and shouldn't be called directly.
*
* @method recalc
*/
recalc: function() {
var self = this, statusbar = self.statusbar, layoutRect, width, x, needsRecalc;
if (self._fullscreen) {
self.layoutRect(DomUtils.getWindowSize());
self.layoutRect().contentH = self.layoutRect().innerH;
}
self._super();
layoutRect = self.layoutRect();
// Resize window based on title width
if (self.settings.title && !self._fullscreen) {
width = layoutRect.headerW;
if (width > layoutRect.w) {
x = layoutRect.x - Math.max(0, width / 2);
self.layoutRect({w: width, x: x});
needsRecalc = true;
}
}
// Resize window based on statusbar width
if (statusbar) {
statusbar.layoutRect({w: self.layoutRect().innerW}).recalc();
width = statusbar.layoutRect().minW + layoutRect.deltaW;
if (width > layoutRect.w) {
x = layoutRect.x - Math.max(0, width - layoutRect.w);
self.layoutRect({w: width, x: x});
needsRecalc = true;
}
}
// Recalc body and disable auto resize
if (needsRecalc) {
self.recalc();
}
},
/**
* Initializes the current controls layout rect.
* This will be executed by the layout managers to determine the
* default minWidth/minHeight etc.
*
* @method initLayoutRect
* @return {Object} Layout rect instance.
*/
initLayoutRect: function() {
var self = this, layoutRect = self._super(), deltaH = 0, headEl;
// Reserve vertical space for title
if (self.settings.title && !self._fullscreen) {
headEl = self.getEl('head');
var size = DomUtils.getSize(headEl);
layoutRect.headerW = size.width;
layoutRect.headerH = size.height;
deltaH += layoutRect.headerH;
}
// Reserve vertical space for statusbar
if (self.statusbar) {
deltaH += self.statusbar.layoutRect().h;
}
layoutRect.deltaH += deltaH;
layoutRect.minH += deltaH;
//layoutRect.innerH -= deltaH;
layoutRect.h += deltaH;
var rect = DomUtils.getWindowSize();
layoutRect.x = Math.max(0, rect.w / 2 - layoutRect.w / 2);
layoutRect.y = Math.max(0, rect.h / 2 - layoutRect.h / 2);
return layoutRect;
},
/**
* Renders the control as a HTML string.
*
* @method renderHtml
* @return {String} HTML representing the control.
*/
renderHtml: function() {
var self = this, layout = self._layout, id = self._id, prefix = self.classPrefix;
var settings = self.settings, headerHtml = '', footerHtml = '', html = settings.html;
self.preRender();
layout.preRender(self);
if (settings.title) {
headerHtml = (
'' +
'' + self.encode(settings.title) + '' +
'' +
'' +
''
);
}
if (settings.url) {
html = '';
}
if (typeof html == "undefined") {
html = layout.renderHtml(self);
}
if (self.statusbar) {
footerHtml = self.statusbar.renderHtml();
}
return (
'' +
'' +
headerHtml +
'' +
html +
'' +
footerHtml +
'' +
''
);
},
/**
* Switches the window fullscreen mode.
*
* @method fullscreen
* @param {Boolean} state True/false state.
* @return {tinymce.ui.Window} Current window instance.
*/
fullscreen: function(state) {
var self = this, documentElement = document.documentElement, slowRendering, prefix = self.classPrefix, layoutRect;
if (state != self._fullscreen) {
DomUtils.on(window, 'resize', function() {
var time;
if (self._fullscreen) {
// Time the layout time if it's to slow use a timeout to not hog the CPU
if (!slowRendering) {
time = new Date().getTime();
var rect = DomUtils.getWindowSize();
self.moveTo(0, 0).resizeTo(rect.w, rect.h);
if ((new Date().getTime()) - time > 50) {
slowRendering = true;
}
} else {
if (!self._timer) {
self._timer = setTimeout(function() {
var rect = DomUtils.getWindowSize();
self.moveTo(0, 0).resizeTo(rect.w, rect.h);
self._timer = 0;
}, 50);
}
}
}
});
layoutRect = self.layoutRect();
self._fullscreen = state;
if (!state) {
self._borderBox = self.parseBox(self.settings.border);
self.getEl('head').style.display = '';
layoutRect.deltaH += layoutRect.headerH;
DomUtils.removeClass(documentElement, prefix + 'fullscreen');
DomUtils.removeClass(document.body, prefix + 'fullscreen');
self.removeClass('fullscreen');
self.moveTo(self._initial.x, self._initial.y).resizeTo(self._initial.w, self._initial.h);
} else {
self._initial = {x: layoutRect.x, y: layoutRect.y, w: layoutRect.w, h: layoutRect.h};
self._borderBox = self.parseBox('0');
self.getEl('head').style.display = 'none';
layoutRect.deltaH -= layoutRect.headerH + 2;
DomUtils.addClass(documentElement, prefix + 'fullscreen');
DomUtils.addClass(document.body, prefix + 'fullscreen');
self.addClass('fullscreen');
var rect = DomUtils.getWindowSize();
self.moveTo(0, 0).resizeTo(rect.w, rect.h);
}
}
return self.reflow();
},
/**
* Called after the control has been rendered.
*
* @method postRender
*/
postRender: function() {
var self = this, startPos;
setTimeout(function() {
self.addClass('in');
}, 0);
self._super();
if (self.statusbar) {
self.statusbar.postRender();
}
self.focus();
this.dragHelper = new DragHelper(self._id + '-dragh', {
start: function() {
startPos = {
x: self.layoutRect().x,
y: self.layoutRect().y
};
},
drag: function(e) {
self.moveTo(startPos.x + e.deltaX, startPos.y + e.deltaY);
}
});
self.on('submit', function(e) {
if (!e.isDefaultPrevented()) {
self.close();
}
});
},
/**
* Fires a submit event with the serialized form.
*
* @method submit
* @return {Object} Event arguments object.
*/
submit: function() {
return this.fire('submit', {data: this.toJSON()});
},
/**
* Removes the current control from DOM and from UI collections.
*
* @method remove
* @return {tinymce.ui.Control} Current control instance.
*/
remove: function() {
var self = this, prefix = self.classPrefix;
self.dragHelper.destroy();
self._super();
if (self.statusbar) {
this.statusbar.remove();
}
if (self._fullscreen) {
DomUtils.removeClass(document.documentElement, prefix + 'fullscreen');
DomUtils.removeClass(document.body, prefix + 'fullscreen');
}
},
/**
* Returns the contentWindow object of the iframe if it exists.
*
* @method getContentWindow
* @return {Window} window object or null.
*/
getContentWindow: function() {
var ifr = this.getEl().getElementsByTagName('iframe')[0];
return ifr ? ifr.contentWindow : null;
}
});
return Window;
});
// Included from: js/tinymce/classes/ui/MessageBox.js
/**
* MessageBox.js
*
* Copyright, Moxiecode Systems AB
* Released under LGPL License.
*
* License: http://www.tinymce.com/license
* Contributing: http://www.tinymce.com/contributing
*/
/**
* This class is used to create MessageBoxes like alerts/confirms etc.
*
* @class tinymce.ui.MessageBox
* @extends tinymce.ui.Window
*/
define("tinymce/ui/MessageBox", [
"tinymce/ui/Window"
], function(Window) {
"use strict";
var MessageBox = Window.extend({
/**
* Constructs a instance with the specified settings.
*
* @constructor
* @param {Object} settings Name/value object with settings.
*/
init: function(settings) {
settings = {
border: 1,
padding: 20,
layout: 'flex',
pack: "center",
align: "center",
containerCls: 'panel',
autoScroll: true,
buttons: {type: "button", text: "Ok", action: "ok"},
items: {
type: "label",
multiline: true,
maxWidth: 500,
maxHeight: 200
}
};
this._super(settings);
},
Statics: {
/**
* Ok buttons constant.
*
* @static
* @final
* @field {Number} OK
*/
OK: 1,
/**
* Ok/cancel buttons constant.
*
* @static
* @final
* @field {Number} OK_CANCEL
*/
OK_CANCEL: 2,
/**
* yes/no buttons constant.
*
* @static
* @final
* @field {Number} YES_NO
*/
YES_NO: 3,
/**
* yes/no/cancel buttons constant.
*
* @static
* @final
* @field {Number} YES_NO_CANCEL
*/
YES_NO_CANCEL: 4,
/**
* Constructs a new message box and renders it to the body element.
*
* @static
* @method msgBox
* @param {Object} settings Name/value object with settings.
*/
msgBox: function(settings) {
var buttons, callback = settings.callback || function() {};
function createButton(text, status, primary) {
return {
type: "button",
text: text,
subtype: primary ? 'primary' : '',
onClick: function(e) {
e.control.parents()[1].close();
callback(status);
}
};
}
switch (settings.buttons) {
case MessageBox.OK_CANCEL:
buttons = [
createButton('Ok', true, true),
createButton('Cancel', false)
];
break;
case MessageBox.YES_NO:
case MessageBox.YES_NO_CANCEL:
buttons = [
createButton('Yes', 1, true),
createButton('No', 0)
];
if (settings.buttons == MessageBox.YES_NO_CANCEL) {
buttons.push(createButton('Cancel', -1));
}
break;
default:
buttons = [
createButton('Ok', true, true)
];
break;
}
return new Window({
padding: 20,
x: settings.x,
y: settings.y,
minWidth: 300,
minHeight: 100,
layout: "flex",
pack: "center",
align: "center",
buttons: buttons,
title: settings.title,
role: 'alertdialog',
items: {
type: "label",
multiline: true,
maxWidth: 500,
maxHeight: 200,
text: settings.text
},
onPostRender: function() {
this.aria('describedby', this.items()[0]._id);
},
onClose: settings.onClose,
onCancel: function() {
callback(false);
}
}).renderTo(document.body).reflow();
},
/**
* Creates a new alert dialog.
*
* @method alert
* @param {Object} settings Settings for the alert dialog.
* @param {function} [callback] Callback to execute when the user makes a choice.
*/
alert: function(settings, callback) {
if (typeof settings == "string") {
settings = {text: settings};
}
settings.callback = callback;
return MessageBox.msgBox(settings);
},
/**
* Creates a new confirm dialog.
*
* @method confirm
* @param {Object} settings Settings for the confirm dialog.
* @param {function} [callback] Callback to execute when the user makes a choice.
*/
confirm: function(settings, callback) {
if (typeof settings == "string") {
settings = {text: settings};
}
settings.callback = callback;
settings.buttons = MessageBox.OK_CANCEL;
return MessageBox.msgBox(settings);
}
}
});
return MessageBox;
});
// Included from: js/tinymce/classes/WindowManager.js
/**
* WindowManager.js
*
* Copyright, Moxiecode Systems AB
* Released under LGPL License.
*
* License: http://www.tinymce.com/license
* Contributing: http://www.tinymce.com/contributing
*/
/**
* This class handles the creation of native windows and dialogs. This class can be extended to provide for example inline dialogs.
*
* @class tinymce.WindowManager
* @example
* // Opens a new dialog with the file.htm file and the size 320x240
* // It also adds a custom parameter this can be retrieved by using tinyMCEPopup.getWindowArg inside the dialog.
* tinymce.activeEditor.windowManager.open({
* url: 'file.htm',
* width: 320,
* height: 240
* }, {
* custom_param: 1
* });
*
* // Displays an alert box using the active editors window manager instance
* tinymce.activeEditor.windowManager.alert('Hello world!');
*
* // Displays an confirm box and an alert message will be displayed depending on what you choose in the confirm
* tinymce.activeEditor.windowManager.confirm("Do you want to do something", function(s) {
* if (s)
* tinymce.activeEditor.windowManager.alert("Ok");
* else
* tinymce.activeEditor.windowManager.alert("Cancel");
* });
*/
define("tinymce/WindowManager", [
"tinymce/ui/Window",
"tinymce/ui/MessageBox"
], function(Window, MessageBox) {
return function(editor) {
var self = this, windows = [];
function getTopMostWindow() {
if (windows.length) {
return windows[windows.length - 1];
}
}
self.windows = windows;
editor.on('remove', function() {
var i = windows.length;
while (i--) {
windows[i].close();
}
});
/**
* Opens a new window.
*
* @method open
* @param {Object} args Optional name/value settings collection contains things like width/height/url etc.
* @option {String} title Window title.
* @option {String} file URL of the file to open in the window.
* @option {Number} width Width in pixels.
* @option {Number} height Height in pixels.
* @option {Boolean} autoScroll Specifies whether the popup window can have scrollbars if required (i.e. content
* larger than the popup size specified).
*/
self.open = function(args, params) {
var win;
editor.editorManager.setActive(editor);
args.title = args.title || ' ';
// Handle URL
args.url = args.url || args.file; // Legacy
if (args.url) {
args.width = parseInt(args.width || 320, 10);
args.height = parseInt(args.height || 240, 10);
}
// Handle body
if (args.body) {
args.items = {
defaults: args.defaults,
type: args.bodyType || 'form',
items: args.body
};
}
if (!args.url && !args.buttons) {
args.buttons = [
{text: 'Ok', subtype: 'primary', onclick: function() {
win.find('form')[0].submit();
}},
{text: 'Cancel', onclick: function() {
win.close();
}}
];
}
win = new Window(args);
windows.push(win);
win.on('close', function() {
var i = windows.length;
while (i--) {
if (windows[i] === win) {
windows.splice(i, 1);
}
}
if (!windows.length) {
editor.focus();
}
});
// Handle data
if (args.data) {
win.on('postRender', function() {
this.find('*').each(function(ctrl) {
var name = ctrl.name();
if (name in args.data) {
ctrl.value(args.data[name]);
}
});
});
}
// store args and parameters
win.features = args || {};
win.params = params || {};
// Takes a snapshot in the FocusManager of the selection before focus is lost to dialog
if (windows.length === 1) {
editor.nodeChanged();
}
return win.renderTo().reflow();
};
/**
* Creates a alert dialog. Please don't use the blocking behavior of this
* native version use the callback method instead then it can be extended.
*
* @method alert
* @param {String} message Text to display in the new alert dialog.
* @param {function} callback Callback function to be executed after the user has selected ok.
* @param {Object} scope Optional scope to execute the callback in.
* @example
* // Displays an alert box using the active editors window manager instance
* tinymce.activeEditor.windowManager.alert('Hello world!');
*/
self.alert = function(message, callback, scope) {
MessageBox.alert(message, function() {
if (callback) {
callback.call(scope || this);
} else {
editor.focus();
}
});
};
/**
* Creates a confirm dialog. Please don't use the blocking behavior of this
* native version use the callback method instead then it can be extended.
*
* @method confirm
* @param {String} messageText to display in the new confirm dialog.
* @param {function} callback Callback function to be executed after the user has selected ok or cancel.
* @param {Object} scope Optional scope to execute the callback in.
* @example
* // Displays an confirm box and an alert message will be displayed depending on what you choose in the confirm
* tinymce.activeEditor.windowManager.confirm("Do you want to do something", function(s) {
* if (s)
* tinymce.activeEditor.windowManager.alert("Ok");
* else
* tinymce.activeEditor.windowManager.alert("Cancel");
* });
*/
self.confirm = function(message, callback, scope) {
MessageBox.confirm(message, function(state) {
callback.call(scope || this, state);
});
};
/**
* Closes the top most window.
*
* @method close
*/
self.close = function() {
if (getTopMostWindow()) {
getTopMostWindow().close();
}
};
/**
* Returns the params of the last window open call. This can be used in iframe based
* dialog to get params passed from the tinymce plugin.
*
* @example
* var dialogArguments = top.tinymce.activeEditor.windowManager.getParams();
*
* @method getParams
* @return {Object} Name/value object with parameters passed from windowManager.open call.
*/
self.getParams = function() {
return getTopMostWindow() ? getTopMostWindow().params : null;
};
/**
* Sets the params of the last opened window.
*
* @method setParams
* @param {Object} params Params object to set for the last opened window.
*/
self.setParams = function(params) {
if (getTopMostWindow()) {
getTopMostWindow().params = params;
}
};
/**
* Returns the currently opened window objects.
*
* @method getWindows
* @return {Array} Array of the currently opened windows.
*/
self.getWindows = function() {
return windows;
};
};
});
// Included from: js/tinymce/classes/util/Quirks.js
/**
* Quirks.js
*
* Copyright, Moxiecode Systems AB
* Released under LGPL License.
*
* 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.
*
* @class tinymce.util.Quirks
*/
define("tinymce/util/Quirks", [
"tinymce/util/VK",
"tinymce/dom/RangeUtils",
"tinymce/dom/TreeWalker",
"tinymce/html/Node",
"tinymce/html/Entities",
"tinymce/Env",
"tinymce/util/Tools"
], function(VK, RangeUtils, TreeWalker, Node, Entities, Env, Tools) {
return function(editor) {
var each = Tools.each, $ = editor.$;
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 setMceInteralContent(e) {
var selectionHtml;
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) {
e.dataTransfer.setData(mceInternalDataType, mceInternalUrlPrefix + escape(selectionHtml));
}
}
}
/**
* 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, content;
if (e.dataTransfer) {
internalContent = e.dataTransfer.getData(mceInternalDataType);
if (internalContent && internalContent.indexOf(mceInternalUrlPrefix) >= 0) {
content = unescape(internalContent.substr(mceInternalUrlPrefix.length));
}
}
return content;
}
/**
* 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.
*/
function insertClipboardContents(content) {
if (editor.queryCommandSupported('mceInsertClipboardContent')) {
editor.execCommand('mceInsertClipboardContent', false, {content: content});
} else {
editor.execCommand('mceInsertContent', false, content);
}
}
/**
* Fixes a WebKit bug when deleting contents using backspace or delete key.
* WebKit will produce a span element if you delete across two block elements.
*
* Example:
* a
|b
*
* Will produce this on backspace:
* ab
*
* This fixes the backspace to produce:
* a|b
*
* See bug: https://bugs.webkit.org/show_bug.cgi?id=45784
*
* This fixes the following delete scenarios:
* 1. Delete by pressing backspace key.
* 2. Delete by pressing delete key.
* 3. Delete by pressing backspace key with ctrl/cmd (Word delete).
* 4. Delete by pressing delete key with ctrl/cmd (Word delete).
* 5. Delete by drag/dropping contents inside the editor.
* 6. Delete by using Cut Ctrl+X/Cmd+X.
* 7. Delete by selecting contents and writing a character.
*
* This code is a ugly hack since writing full custom delete logic for just this bug
* fix seemed like a huge task. I hope we can remove this before the year 2030.
*/
function cleanupStylesWhenDeleting() {
var doc = editor.getDoc(), dom = editor.dom, selection = editor.selection;
var MutationObserver = window.MutationObserver, olderWebKit, dragStartRng;
// Add mini polyfill for older WebKits
// TODO: Remove this when old Safari versions gets updated
if (!MutationObserver) {
olderWebKit = true;
MutationObserver = function() {
var records = [], target;
function nodeInsert(e) {
var target = e.relatedNode || e.target;
records.push({target: target, addedNodes: [target]});
}
function attrModified(e) {
var target = e.relatedNode || e.target;
records.push({target: target, attributeName: e.attrName});
}
this.observe = function(node) {
target = node;
target.addEventListener('DOMSubtreeModified', nodeInsert, false);
target.addEventListener('DOMNodeInsertedIntoDocument', nodeInsert, false);
target.addEventListener('DOMNodeInserted', nodeInsert, false);
target.addEventListener('DOMAttrModified', attrModified, false);
};
this.disconnect = function() {
target.removeEventListener('DOMSubtreeModified', nodeInsert, false);
target.removeEventListener('DOMNodeInsertedIntoDocument', nodeInsert, false);
target.removeEventListener('DOMNodeInserted', nodeInsert, false);
target.removeEventListener('DOMAttrModified', attrModified, false);
};
this.takeRecords = function() {
return records;
};
};
}
function isTrailingBr(node) {
var blockElements = dom.schema.getBlockElements(), rootNode = editor.getBody();
if (node.nodeName != 'BR') {
return false;
}
for (node = node; node != rootNode && !blockElements[node.nodeName]; node = node.parentNode) {
if (node.nextSibling) {
return false;
}
}
return true;
}
function findCaretNode(node, forward, startNode) {
var walker, current, nonEmptyElements;
nonEmptyElements = dom.schema.getNonEmptyElements();
walker = new TreeWalker(startNode || node, node);
while ((current = walker[forward ? 'next' : 'prev']())) {
if (nonEmptyElements[current.nodeName] && !isTrailingBr(current)) {
return current;
}
if (current.nodeType == 3 && current.data.length > 0) {
return current;
}
}
}
function deleteRangeBetweenTextBlocks(rng) {
var startBlock, endBlock, caretNodeBefore, caretNodeAfter, textBlockElements;
if (rng.collapsed) {
return;
}
startBlock = dom.getParent(RangeUtils.getNode(rng.startContainer, rng.startOffset), dom.isBlock);
endBlock = dom.getParent(RangeUtils.getNode(rng.endContainer, rng.endOffset), dom.isBlock);
textBlockElements = editor.schema.getTextBlockElements();
if (startBlock == endBlock) {
return;
}
if (!textBlockElements[startBlock.nodeName] || !textBlockElements[endBlock.nodeName]) {
return;
}
if (dom.getContentEditable(startBlock) === "false" || dom.getContentEditable(endBlock) === "false") {
return;
}
rng.deleteContents();
caretNodeBefore = findCaretNode(startBlock, false);
caretNodeAfter = findCaretNode(endBlock, true);
if (!dom.isEmpty(endBlock)) {
$(startBlock).append(endBlock.childNodes);
}
$(endBlock).remove();
if (caretNodeBefore) {
if (caretNodeBefore.nodeType == 1) {
if (caretNodeBefore.nodeName == "BR") {
rng.setStartBefore(caretNodeBefore);
rng.setEndBefore(caretNodeBefore);
} else {
rng.setStartAfter(caretNodeBefore);
rng.setEndAfter(caretNodeBefore);
}
} else {
rng.setStart(caretNodeBefore, caretNodeBefore.data.length);
rng.setEnd(caretNodeBefore, caretNodeBefore.data.length);
}
} else if (caretNodeAfter) {
if (caretNodeAfter.nodeType == 1) {
rng.setStartBefore(caretNodeAfter);
rng.setEndBefore(caretNodeAfter);
} else {
rng.setStart(caretNodeAfter, 0);
rng.setEnd(caretNodeAfter, 0);
}
}
selection.setRng(rng);
return true;
}
function expandBetweenBlocks(rng, isForward) {
var caretNode, targetCaretNode, textBlock, targetTextBlock, container, offset;
if (!rng.collapsed) {
return rng;
}
container = rng.startContainer;
offset = rng.startOffset;
if (container.nodeType == 3) {
if (isForward) {
if (offset < container.data.length) {
return rng;
}
} else {
if (offset > 0) {
return rng;
}
}
}
caretNode = RangeUtils.getNode(rng.startContainer, rng.startOffset);
textBlock = dom.getParent(caretNode, dom.isBlock);
targetCaretNode = findCaretNode(editor.getBody(), isForward, caretNode);
targetTextBlock = dom.getParent(targetCaretNode, dom.isBlock);
if (!caretNode || !targetCaretNode) {
return rng;
}
if (textBlock != targetTextBlock) {
if (!isForward) {
if (targetCaretNode.nodeType == 1) {
if (targetCaretNode.nodeName == "BR") {
rng.setStartBefore(targetCaretNode);
} else {
rng.setStartAfter(targetCaretNode);
}
} else {
rng.setStart(targetCaretNode, targetCaretNode.data.length);
}
if (caretNode.nodeType == 1) {
rng.setEnd(caretNode, 0);
} else {
rng.setEndBefore(caretNode);
}
} else {
if (caretNode.nodeType == 1) {
if (caretNode.nodeName == "BR") {
rng.setStartBefore(caretNode);
} else {
rng.setStartAfter(caretNode);
}
} else {
rng.setStart(caretNode, caretNode.data.length);
}
if (targetCaretNode.nodeType == 1) {
rng.setEnd(targetCaretNode, 0);
} else {
rng.setEndBefore(targetCaretNode);
}
}
}
return rng;
}
function handleTextBlockMergeDelete(isForward) {
var rng = selection.getRng();
rng = expandBetweenBlocks(rng, isForward);
if (deleteRangeBetweenTextBlocks(rng)) {
return true;
}
}
function customDelete(isForward) {
var mutationObserver, rng, caretElement;
if (handleTextBlockMergeDelete(isForward)) {
return;
}
Tools.each(editor.getBody().getElementsByTagName('*'), function(elm) {
// Mark existing spans
if (elm.tagName == 'SPAN') {
elm.setAttribute('mce-data-marked', 1);
}
// Make sure all elements has a data-mce-style attribute
if (!elm.hasAttribute('data-mce-style') && elm.hasAttribute('style')) {
editor.dom.setAttrib(elm, 'style', editor.dom.getAttrib(elm, 'style'));
}
});
// Observe added nodes and style attribute changes
mutationObserver = new MutationObserver(function() {});
mutationObserver.observe(editor.getDoc(), {
childList: true,
attributes: true,
subtree: true,
attributeFilter: ['style']
});
editor.getDoc().execCommand(isForward ? 'ForwardDelete' : 'Delete', false, null);
rng = editor.selection.getRng();
caretElement = rng.startContainer.parentNode;
Tools.each(mutationObserver.takeRecords(), function(record) {
if (!dom.isChildOf(record.target, editor.getBody())) {
return;
}
// Restore style attribute to previous value
if (record.attributeName == "style") {
var oldValue = record.target.getAttribute('data-mce-style');
if (oldValue) {
record.target.setAttribute("style", oldValue);
} else {
record.target.removeAttribute("style");
}
}
// Remove all spans that isn't maked and retain selection
Tools.each(record.addedNodes, function(node) {
if (node.nodeName == "SPAN" && !node.getAttribute('mce-data-marked')) {
var offset, container;
if (node == caretElement) {
offset = rng.startOffset;
container = node.firstChild;
}
dom.remove(node, true);
if (container) {
rng.setStart(container, offset);
rng.setEnd(container, offset);
editor.selection.setRng(rng);
}
}
});
});
mutationObserver.disconnect();
// Remove any left over marks
Tools.each(editor.dom.select('span[mce-data-marked]'), function(span) {
span.removeAttribute('mce-data-marked');
});
}
editor.on('keydown', function(e) {
var isForward = e.keyCode == DELETE, isMetaOrCtrl = e.ctrlKey || e.metaKey;
if (!isDefaultPrevented(e) && (isForward || e.keyCode == BACKSPACE)) {
var rng = editor.selection.getRng(), container = rng.startContainer, offset = rng.startOffset;
// Ignore non meta delete in the where there is text before/after the caret
if (!isMetaOrCtrl && rng.collapsed && container.nodeType == 3) {
if (isForward ? offset < container.data.length : offset > 0) {
return;
}
}
e.preventDefault();
if (isMetaOrCtrl) {
editor.selection.getSel().modify("extend", isForward ? "forward" : "backward", e.metaKey ? "lineboundary" : "word");
}
customDelete(isForward);
}
});
// Handle case where text is deleted by typing over
editor.on('keypress', function(e) {
if (!isDefaultPrevented(e) && !selection.isCollapsed() && e.charCode && !VK.metaKeyPressed(e)) {
var rng, currentFormatNodes, fragmentNode, blockParent, caretNode, charText;
rng = editor.selection.getRng();
charText = String.fromCharCode(e.charCode);
e.preventDefault();
// Keep track of current format nodes
currentFormatNodes = $(rng.startContainer).parents().filter(function(idx, node) {
return !!editor.schema.getTextInlineElements()[node.nodeName];
});
customDelete(true);
// Check if the browser removed them
currentFormatNodes = currentFormatNodes.filter(function(idx, node) {
return !$.contains(editor.getBody(), node);
});
// Then re-add them
if (currentFormatNodes.length) {
fragmentNode = dom.createFragment();
currentFormatNodes.each(function(idx, formatNode) {
formatNode = formatNode.cloneNode(false);
if (fragmentNode.hasChildNodes()) {
formatNode.appendChild(fragmentNode.firstChild);
fragmentNode.appendChild(formatNode);
} else {
caretNode = formatNode;
fragmentNode.appendChild(formatNode);
}
fragmentNode.appendChild(formatNode);
});
caretNode.appendChild(editor.getDoc().createTextNode(charText));
// Prevent edge case where older WebKit would add an extra BR element
blockParent = dom.getParent(rng.startContainer, dom.isBlock);
if (dom.isEmpty(blockParent)) {
$(blockParent).empty().append(fragmentNode);
} else {
rng.insertNode(fragmentNode);
}
rng.setStart(caretNode.firstChild, 1);
rng.setEnd(caretNode.firstChild, 1);
editor.selection.setRng(rng);
} else {
editor.selection.setContent(charText);
}
}
});
editor.addCommand('Delete', function() {
customDelete();
});
editor.addCommand('ForwardDelete', function() {
customDelete(true);
});
// Older WebKits doesn't properly handle the clipboard so we can't add the rest
if (olderWebKit) {
return;
}
editor.on('dragstart', function(e) {
dragStartRng = selection.getRng();
setMceInteralContent(e);
});
editor.on('drop', function(e) {
if (!isDefaultPrevented(e)) {
var internalContent = getMceInternalContent(e);
if (internalContent) {
e.preventDefault();
// Safari has a weird issue where drag/dropping images sometimes
// produces a green plus icon. When this happens the caretRangeFromPoint
// will return "null" even though the x, y coordinate is correct.
// But if we detach the insert from the drop event we will get a proper range
window.setTimeout(function() {
var pointRng = RangeUtils.getCaretRangeFromPoint(e.x, e.y, doc);
if (dragStartRng) {
selection.setRng(dragStartRng);
dragStartRng = null;
}
customDelete();
selection.setRng(pointRng);
insertClipboardContents(internalContent);
}, 0);
}
}
});
editor.on('cut', function(e) {
if (!isDefaultPrevented(e) && e.clipboardData) {
e.preventDefault();
e.clipboardData.clearData();
e.clipboardData.setData('text/html', editor.selection.getContent());
e.clipboardData.setData('text/plain', editor.selection.getContent({format: 'text'}));
customDelete(true);
}
});
}
/**
* Makes sure that the editor body becomes empty when backspace or delete is pressed in empty editors.
*
* For example:
*
|
*
* Or:
* |
*
* Or:
* []
*/
function emptyEditorWhenDeleting() {
function serializeRng(rng) {
var body = dom.create("body");
var contents = rng.cloneContents();
body.appendChild(contents);
return selection.serializer.serialize(body, {format: 'html'});
}
function allContentsSelected(rng) {
if (!rng.setStart) {
if (rng.item) {
return false;
}
var bodyRng = rng.duplicate();
bodyRng.moveToElementText(editor.getBody());
return RangeUtils.compareRanges(rng, bodyRng);
}
var selection = serializeRng(rng);
var allRng = dom.createRng();
allRng.selectNode(editor.getBody());
var allSelection = serializeRng(allRng);
return selection === allSelection;
}
editor.on('keydown', function(e) {
var keyCode = e.keyCode, isCollapsed, body;
// Empty the editor if it's needed for example backspace at |
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
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) {
if (e.target == editor.getDoc().documentElement) {
editor.getBody().focus();
if (e.type == 'mousedown') {
// 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(selection.getRng());
}
}
});
}
}
/**
* 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
setTimeout(function() {
body.focus();
}, 0);
}
});
}
}
/**
* 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)) {
e.preventDefault();
selection.getSel().setBaseAndExtent(target, 0, target, 1);
editor.nodeChanged();
}
if (target.nodeName == 'A' && dom.hasClass(target, 'mce-item-anchor')) {
e.preventDefault();
selection.select(target);
}
});
}
/**
* Fixes a Gecko bug where the style attribute gets added to the wrong element when deleting between two block elements.
*
* Fixes do backspace/delete on this:
* bla[ck
r]ed
*
* Would become:
* bla|ed
*
* Instead of:
* bla|ed
*/
function removeStylesWhenDeletingAcrossBlockElements() {
function getAttributeApplyFunction() {
var template = dom.getAttribs(selection.getStart().cloneNode(false));
return function() {
var target = selection.getStart();
if (target !== editor.getBody()) {
dom.setAttrib(target, "style", null);
each(template, function(attr) {
target.setAttributeNode(attr.cloneNode(true));
});
}
};
}
function isSelectionAcrossElements() {
return !selection.isCollapsed() &&
dom.getParent(selection.getStart(), dom.isBlock) != dom.getParent(selection.getEnd(), dom.isBlock);
}
editor.on('keypress', function(e) {
var applyAttributes;
if (!isDefaultPrevented(e) && (e.keyCode == 8 || e.keyCode == 46) && isSelectionAcrossElements()) {
applyAttributes = getAttributeApplyFunction();
editor.getDoc().execCommand('delete', false, null);
applyAttributes();
e.preventDefault();
return false;
}
});
dom.bind(editor.getDoc(), 'cut', function(e) {
var applyAttributes;
if (!isDefaultPrevented(e) && isSelectionAcrossElements()) {
applyAttributes = getAttributeApplyFunction();
setTimeout(function() {
applyAttributes();
}, 0);
}
});
}
/**
* 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:
* |x
*
* Becomes:
* |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() {
editor._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);
});
}
}
/**
* Removes ghost selections from images/tables on Gecko.
*/
function removeGhostSelection() {
editor.on('Undo Redo SetContent', function(e) {
if (!e.initial) {
editor.execCommand('mceRepaint');
}
});
}
/**
* Deletes the selected image on IE instead of navigating to previous page.
*/
function deleteControlItemOnBackSpace() {
editor.on('keydown', function(e) {
var rng;
if (!isDefaultPrevented(e) && e.keyCode == BACKSPACE) {
rng = editor.getDoc().selection.createRange();
if (rng && rng.item) {
e.preventDefault();
editor.undoManager.beforeChange();
dom.remove(rng.item(0));
editor.undoManager.add();
}
}
});
}
/**
* IE10 doesn't properly render block elements with the right height until you add contents to them.
* This fixes that by adding a padding-right to all empty text block elements.
* See: https://connect.microsoft.com/IE/feedback/details/743881
*/
function renderEmptyBlocksFix() {
var emptyBlocksCSS;
// IE10+
if (getDocumentMode() >= 10) {
emptyBlocksCSS = '';
each('p div h1 h2 h3 h4 h5 h6'.split(' '), function(name, i) {
emptyBlocksCSS += (i > 0 ? ',' : '') + name + ':empty';
});
editor.contentStyles.push(emptyBlocksCSS + '{padding-right: 1px !important}');
}
}
/**
* Old IE versions can't retain contents within noscript elements so this logic will store the contents
* as a attribute and the insert that value as it's raw text when the DOM is serialized.
*/
function keepNoScriptContents() {
if (getDocumentMode() < 9) {
parser.addNodeFilter('noscript', function(nodes) {
var i = nodes.length, node, textNode;
while (i--) {
node = nodes[i];
textNode = node.firstChild;
if (textNode) {
node.attr('data-mce-innertext', textNode.value);
}
}
});
serializer.addNodeFilter('noscript', function(nodes) {
var i = nodes.length, node, textNode, value;
while (i--) {
node = nodes[i];
textNode = nodes[i].firstChild;
if (textNode) {
textNode.value = Entities.decode(textNode.value);
} else {
// Old IE can't retain noscript value so an attribute is used to store it
value = node.attributes.map['data-mce-innertext'];
if (value) {
node.attr('data-mce-innertext', null);
textNode = new Node('#text', 3);
textNode.value = value;
textNode.raw = true;
node.append(textNode);
}
}
}
});
}
}
/**
* IE has an issue where you can't select/move the caret by clicking outside the body if the document is in standards mode.
*/
function fixCaretSelectionOfDocumentElementOnIe() {
var doc = dom.doc, body = doc.body, started, startRng, htmlElm;
// Return range from point or null if it failed
function rngFromPoint(x, y) {
var rng = body.createTextRange();
try {
rng.moveToPoint(x, y);
} catch (ex) {
// IE sometimes throws and exception, so lets just ignore it
rng = null;
}
return rng;
}
// Fires while the selection is changing
function selectionChange(e) {
var pointRng;
// Check if the button is down or not
if (e.button) {
// Create range from mouse position
pointRng = rngFromPoint(e.x, e.y);
if (pointRng) {
// Check if pointRange is before/after selection then change the endPoint
if (pointRng.compareEndPoints('StartToStart', startRng) > 0) {
pointRng.setEndPoint('StartToStart', startRng);
} else {
pointRng.setEndPoint('EndToEnd', startRng);
}
pointRng.select();
}
} else {
endSelection();
}
}
// Removes listeners
function endSelection() {
var rng = doc.selection.createRange();
// If the range is collapsed then use the last start range
if (startRng && !rng.item && rng.compareEndPoints('StartToEnd', rng) === 0) {
startRng.select();
}
dom.unbind(doc, 'mouseup', endSelection);
dom.unbind(doc, 'mousemove', selectionChange);
startRng = started = 0;
}
// Make HTML element unselectable since we are going to handle selection by hand
doc.documentElement.unselectable = true;
// Detect when user selects outside BODY
dom.bind(doc, 'mousedown contextmenu', function(e) {
if (e.target.nodeName === 'HTML') {
if (started) {
endSelection();
}
// Detect vertical scrollbar, since IE will fire a mousedown on the scrollbar and have target set as HTML
htmlElm = doc.documentElement;
if (htmlElm.scrollHeight > htmlElm.clientHeight) {
return;
}
started = 1;
// Setup start position
startRng = rngFromPoint(e.x, e.y);
if (startRng) {
// Listen for selection change events
dom.bind(doc, 'mouseup', endSelection);
dom.bind(doc, 'mousemove', selectionChange);
dom.getRoot().focus();
startRng.select();
}
}
});
}
/**
* Fixes selection issues where the caret can be placed between two inline elements like a|b
* this fix will lean the caret right into the closest inline element.
*/
function normalizeSelection() {
// Normalize selection for example a|a becomes a|a except for Ctrl+A since it selects everything
editor.on('keyup focusin mouseup', function(e) {
if (e.keyCode != 65 || !VK.metaKeyPressed(e)) {
selection.normalize();
}
}, true);
}
/**
* Forces Gecko to render a broken image icon if it fails to load an image.
*/
function showBrokenImageIcon() {
editor.contentStyles.push(
'img:-moz-broken {' +
'-moz-force-broken-image-icon:1;' +
'min-width:24px;' +
'min-height:24px' +
'}'
);
}
/**
* iOS has a bug where it's impossible to type if the document has a touchstart event
* bound and the user touches the document while having the on screen keyboard visible.
*
* The touch event moves the focus to the parent document while having the caret inside the iframe
* this fix moves the focus back into the iframe document.
*/
function restoreFocusOnKeyDown() {
if (!editor.inline) {
editor.on('keydown', function() {
if (document.activeElement == document.body) {
editor.getWin().focus();
}
});
}
}
/**
* IE 11 has an annoying issue where you can't move focus into the editor
* by clicking on the white area HTML element. We used to be able to to fix this with
* the fixCaretSelectionOfDocumentElementOnIe fix. But since M$ removed the selection
* object it's not possible anymore. So we need to hack in a ungly CSS to force the
* body to be at least 150px. If the user clicks the HTML element out side this 150px region
* we simply move the focus into the first paragraph. Not ideal since you loose the
* positioning of the caret but goot enough for most cases.
*/
function bodyHeight() {
if (!editor.inline) {
editor.contentStyles.push('body {min-height: 150px}');
editor.on('click', function(e) {
if (e.target.nodeName == 'HTML') {
var rng;
// 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.keyCode == 37 || e.keyCode == 39)) {
e.preventDefault();
editor.selection.getSel().modify('move', e.keyCode == 37 ? 'backward' : 'forward', 'word');
}
});
}
}
/**
* Disables the autolinking in IE 9+ this is then re-enabled by the autolink plugin.
*/
function disableAutoUrlDetect() {
setEditorCommandState("AutoUrlDetect", false);
}
/**
* 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. However is we add a BR element to the body then remove it
* it doesn't seem to add these BR elements makes sence right?!
*
* Example of what happens: text becomes text
*/
function doubleTrailingBrElements() {
if (!editor.inline) {
editor.on('focus blur beforegetcontent', function() {
var br = editor.dom.create('br');
editor.getBody().appendChild(br);
br.parentNode.removeChild(br);
}, true);
}
}
/**
* iOS 7.1 introduced two new bugs:
* 1) It's possible to open links within a contentEditable area by clicking on them.
* 2) If you hold down the finger it will display the link/image touch callout menu.
*/
function tapLinksAndImages() {
editor.on('click', function(e) {
var elm = e.target;
do {
if (elm.tagName === 'A') {
e.preventDefault();
return;
}
} while ((elm = elm.parentNode));
});
editor.contentStyles.push('.mce-content-body {-webkit-touch-callout: none}');
}
/**
* 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) {
setMceInteralContent(e);
});
editor.on('drop', function(e) {
if (!isDefaultPrevented(e)) {
var internalContent = getMceInternalContent(e);
if (internalContent) {
e.preventDefault();
var rng = RangeUtils.getCaretRangeFromPoint(e.x, e.y, editor.getDoc());
selection.setRng(rng);
insertClipboardContents(internalContent);
}
}
});
}
// All browsers
removeBlockQuoteOnBackSpace();
emptyEditorWhenDeleting();
normalizeSelection();
// WebKit
if (isWebKit) {
cleanupStylesWhenDeleting();
inputMethodFocus();
selectControlElements();
setDefaultBlockType();
blockFormSubmitInsideEditor();
disableBackspaceIntoATable();
removeAppleInterchangeBrs();
touchClickEvent();
// iOS
if (Env.iOS) {
restoreFocusOnKeyDown();
bodyHeight();
tapLinksAndImages();
} else {
selectAll();
}
}
// IE
if (isIE && Env.ie < 11) {
removeHrOnBackspace();
ensureBodyHasRoleApplication();
addNewLinesBeforeBrInPre();
removePreSerializedStylesWhenSelectingControls();
deleteControlItemOnBackSpace();
renderEmptyBlocksFix();
keepNoScriptContents();
fixCaretSelectionOfDocumentElementOnIe();
}
if (Env.ie >= 11) {
bodyHeight();
doubleTrailingBrElements();
disableBackspaceIntoATable();
}
if (Env.ie) {
selectAll();
disableAutoUrlDetect();
ieInternalDragAndDrop();
}
// Gecko
if (isGecko) {
removeHrOnBackspace();
focusBody();
removeStylesWhenDeletingAcrossBlockElements();
setGeckoEditingOptions();
addBrAfterLastLinks();
removeGhostSelection();
showBrokenImageIcon();
blockCmdArrowNavigation();
disableBackspaceIntoATable();
}
};
});
// Included from: js/tinymce/classes/util/Observable.js
/**
* Observable.js
*
* Copyright, Moxiecode Systems AB
* Released under LGPL License.
*
* License: http://www.tinymce.com/license
* Contributing: http://www.tinymce.com/contributing
*/
/**
* This mixin will add event binding logic to classes.
*
* @mixin tinymce.util.Observable
*/
define("tinymce/util/Observable", [
"tinymce/util/EventDispatcher"
], function(EventDispatcher) {
function getEventDispatcher(obj) {
if (!obj._eventDispatcher) {
obj._eventDispatcher = new EventDispatcher({
scope: obj,
toggleEvent: function(name, state) {
if (EventDispatcher.isNative(name) && obj.toggleNativeEvent) {
obj.toggleNativeEvent(name, state);
}
}
});
}
return obj._eventDispatcher;
}
return {
/**
* Fires the specified event by name.
*
* @method fire
* @param {String} name Name of the event to fire.
* @param {Object?} args Event arguments.
* @param {Boolean?} bubble True/false if the event is to be bubbled.
* @return {Object} Event args instance passed in.
* @example
* instance.fire('event', {...});
*/
fire: function(name, args, bubble) {
var self = this;
// Prevent all events except the remove event after the instance has been removed
if (self.removed && name !== "remove") {
return args;
}
args = getEventDispatcher(self).fire(name, args, bubble);
// Bubble event up to parents
if (bubble !== false && self.parent) {
var parent = self.parent();
while (parent && !args.isPropagationStopped()) {
parent.fire(name, args, false);
parent = parent.parent();
}
}
return args;
},
/**
* Binds an event listener to a specific event by name.
*
* @method on
* @param {String} name Event name or space separated list of events to bind.
* @param {callback} callback Callback to be executed when the event occurs.
* @param {Boolean} first Optional flag if the event should be prepended. Use this with care.
* @return {Object} Current class instance.
* @example
* instance.on('event', function(e) {
* // Callback logic
* });
*/
on: function(name, callback, prepend) {
return getEventDispatcher(this).on(name, callback, prepend);
},
/**
* Unbinds an event listener to a specific event by name.
*
* @method off
* @param {String?} name Name of the event to unbind.
* @param {callback?} callback Callback to unbind.
* @return {Object} Current class instance.
* @example
* // Unbind specific callback
* instance.off('event', handler);
*
* // Unbind all listeners by name
* instance.off('event');
*
* // Unbind all events
* instance.off();
*/
off: function(name, callback) {
return getEventDispatcher(this).off(name, callback);
},
/**
* Bind the event callback and once it fires the callback is removed.
*
* @method once
* @param {String} name Name of the event to bind.
* @param {callback} callback Callback to bind only once.
* @return {Object} Current class instance.
*/
once: function(name, callback) {
return getEventDispatcher(this).once(name, callback);
},
/**
* Returns true/false if the object has a event of the specified name.
*
* @method hasEventListeners
* @param {String} name Name of the event to check for.
* @return {Boolean} true/false if the event exists or not.
*/
hasEventListeners: function(name) {
return getEventDispatcher(this).has(name);
}
};
});
// Included from: js/tinymce/classes/EditorObservable.js
/**
* EditorObservable.js
*
* Copyright, Moxiecode Systems AB
* Released under LGPL License.
*
* License: http://www.tinymce.com/license
* Contributing: http://www.tinymce.com/contributing
*/
/**
* This mixin contains the event logic for the tinymce.Editor class.
*
* @mixin tinymce.EditorObservable
* @extends tinymce.util.Observable
*/
define("tinymce/EditorObservable", [
"tinymce/util/Observable",
"tinymce/dom/DOMUtils",
"tinymce/util/Tools"
], function(Observable, DOMUtils, Tools) {
var DOM = DOMUtils.DOM, customEventRootDelegates;
/**
* Returns the event target so for the specified event. Some events fire
* only on document, some fire on documentElement etc. This also handles the
* custom event root setting where it returns that element instead of the body.
*
* @private
* @param {tinymce.Editor} editor Editor instance to get event target from.
* @param {String} eventName Name of the event for example "click".
* @return {Element/Document} HTML Element or document target to bind on.
*/
function getEventTarget(editor, eventName) {
if (eventName == 'selectionchange') {
return editor.getDoc();
}
// Need to bind mousedown/mouseup etc to document not body in iframe mode
// Since the user might click on the HTML element not the BODY
if (!editor.inline && /^mouse|click|contextmenu|drop|dragover|dragend/.test(eventName)) {
return editor.getDoc().documentElement;
}
// Bind to event root instead of body if it's defined
if (editor.settings.event_root) {
if (!editor.eventRoot) {
editor.eventRoot = DOM.select(editor.settings.event_root)[0];
}
return editor.eventRoot;
}
return editor.getBody();
}
/**
* Binds a event delegate for the specified name this delegate will fire
* the event to the editor dispatcher.
*
* @private
* @param {tinymce.Editor} editor Editor instance to get event target from.
* @param {String} eventName Name of the event for example "click".
*/
function bindEventDelegate(editor, eventName) {
var eventRootElm = getEventTarget(editor, eventName), delegate;
if (!editor.delegates) {
editor.delegates = {};
}
if (editor.delegates[eventName]) {
return;
}
if (editor.settings.event_root) {
if (!customEventRootDelegates) {
customEventRootDelegates = {};
editor.editorManager.on('removeEditor', function() {
var name;
if (!editor.editorManager.activeEditor) {
if (customEventRootDelegates) {
for (name in customEventRootDelegates) {
editor.dom.unbind(getEventTarget(editor, name));
}
customEventRootDelegates = null;
}
}
});
}
if (customEventRootDelegates[eventName]) {
return;
}
delegate = function(e) {
var target = e.target, editors = editor.editorManager.editors, i = editors.length;
while (i--) {
var body = editors[i].getBody();
if (body === target || DOM.isChildOf(target, body)) {
if (!editors[i].hidden) {
editors[i].fire(eventName, e);
}
}
}
};
customEventRootDelegates[eventName] = delegate;
DOM.bind(eventRootElm, eventName, delegate);
} else {
delegate = function(e) {
if (!editor.hidden) {
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;
if (self.settings.readonly) {
return;
}
// Never bind focus/blur since the FocusManager fakes those
if (name == "focus" || name == "blur") {
return;
}
if (state) {
if (self.initialized) {
bindEventDelegate(self, name);
} else {
if (!self._pendingNativeEvents) {
self._pendingNativeEvents = [name];
} else {
self._pendingNativeEvents.push(name);
}
}
} else if (self.initialized) {
self.dom.unbind(getEventTarget(self, name), name, self.delegates[name]);
delete self.delegates[name];
}
},
/**
* Unbinds all native event handlers that means delegates, custom events bound using the Events API etc.
*
* @private
*/
unbindAllNativeEvents: function() {
var self = this, name;
if (self.delegates) {
for (name in self.delegates) {
self.dom.unbind(getEventTarget(self, name), name, self.delegates[name]);
}
delete self.delegates;
}
if (!self.inline) {
self.getBody().onload = null;
self.dom.unbind(self.getWin());
self.dom.unbind(self.getDoc());
}
self.dom.unbind(self.getBody());
self.dom.unbind(self.getContainer());
}
};
EditorObservable = Tools.extend({}, Observable, EditorObservable);
return EditorObservable;
});
// Included from: js/tinymce/classes/Shortcuts.js
/**
* Shortcuts.js
*
* Copyright, Moxiecode Systems AB
* Released under LGPL License.
*
* License: http://www.tinymce.com/license
* Contributing: http://www.tinymce.com/contributing
*/
/**
* Contains all logic for handling of keyboard shortcuts.
*
* @example
* editor.shortcuts.add('ctrl+a', function() {});
* editor.shortcuts.add('meta+a', function() {}); // "meta" maps to Command on Mac and Ctrl on PC
* editor.shortcuts.add('ctrl+alt+a', function() {});
* editor.shortcuts.add('access+a', function() {}); // "access" maps to ctrl+alt on Mac and shift+alt on PC
*/
define("tinymce/Shortcuts", [
"tinymce/util/Tools",
"tinymce/Env"
], function(Tools, Env) {
var each = Tools.each, explode = Tools.explode;
var keyCodeLookup = {
"f9": 120,
"f10": 121,
"f11": 122
};
var modifierNames = Tools.makeMap('alt,ctrl,shift,meta,access');
return function(editor) {
var self = this, shortcuts = {};
function createShortcut(pattern, desc, cmdFunc, scope) {
var id, key, shortcut;
shortcut = {
func: cmdFunc,
scope: scope || editor,
desc: editor.translate(desc)
};
// Parse modifiers and keys ctrl+alt+b for example
each(explode(pattern, '+'), function(value) {
if (value in modifierNames) {
shortcut[value] = true;
} else {
// Allow numeric keycodes like ctrl+219 for ctrl+[
if (/^[0-9]{2,}$/.test(value)) {
shortcut.keyCode = parseInt(value, 10);
} else {
shortcut.charCode = value.charCodeAt(0);
shortcut.keyCode = keyCodeLookup[value] || value.toUpperCase().charCodeAt(0);
}
}
});
// Generate unique id for modifier combination and set default state for unused modifiers
id = [shortcut.keyCode];
for (key in modifierNames) {
if (shortcut[key]) {
id.push(key);
} else {
shortcut[key] = false;
}
}
shortcut.id = id.join(',');
// Handle special access modifier differently depending on Mac/Win
if (shortcut.access) {
shortcut.alt = true;
if (Env.mac) {
shortcut.ctrl = true;
} else {
shortcut.shift = true;
}
}
// Handle special meta modifier differently depending on Mac/Win
if (shortcut.meta) {
if (Env.mac) {
shortcut.meta = true;
} else {
shortcut.ctrl = true;
shortcut.meta = false;
}
}
return shortcut;
}
editor.on('keyup keypress keydown', function(e) {
if ((e.altKey || e.ctrlKey || e.metaKey) && !e.isDefaultPrevented()) {
each(shortcuts, function(shortcut) {
if (shortcut.ctrl != e.ctrlKey || shortcut.meta != e.metaKey) {
return;
}
if (shortcut.alt != e.altKey || shortcut.shift != e.shiftKey) {
return;
}
if (e.keyCode == shortcut.keyCode || (e.charCode && e.charCode == shortcut.charCode)) {
e.preventDefault();
if (e.type == "keydown") {
shortcut.func.call(shortcut.scope);
}
return true;
}
});
}
});
/**
* 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.
*/
self.add = function(pattern, desc, cmdFunc, scope) {
var cmd;
cmd = cmdFunc;
if (typeof cmdFunc === 'string') {
cmdFunc = function() {
editor.execCommand(cmd, false, null);
};
} else if (Tools.isArray(cmd)) {
cmdFunc = function() {
editor.execCommand(cmd[0], cmd[1], cmd[2]);
};
}
each(explode(pattern.toLowerCase()), function(pattern) {
var shortcut = createShortcut(pattern, desc, cmdFunc, scope);
shortcuts[shortcut.id] = shortcut;
});
return true;
};
/**
* Remove a keyboard shortcut by pattern.
*
* @method remove
* @param {String} pattern Shortcut pattern. Like for example: ctrl+alt+o.
* @return {Boolean} true/false state if the shortcut was removed or not.
*/
self.remove = function(pattern) {
var shortcut = createShortcut(pattern);
if (shortcuts[shortcut.id]) {
delete shortcuts[shortcut.id];
return true;
}
return false;
};
};
});
// Included from: js/tinymce/classes/Editor.js
/**
* Editor.js
*
* Copyright, Moxiecode Systems AB
* Released under LGPL License.
*
* License: http://www.tinymce.com/license
* Contributing: http://www.tinymce.com/contributing
*/
/*jshint scripturl:true */
/**
* Include the base event class documentation.
*
* @include ../../../tools/docs/tinymce.Event.js
*/
/**
* This class contains the core logic for a TinyMCE editor.
*
* @class tinymce.Editor
* @mixes tinymce.util.Observable
* @example
* // Add a class to all paragraphs in the editor.
* tinymce.activeEditor.dom.addClass(tinymce.activeEditor.dom.select('p'), 'someclass');
*
* // Gets the current editors selection as text
* tinymce.activeEditor.selection.getContent({format: 'text'});
*
* // Creates a new editor instance
* var ed = new tinymce.Editor('textareaid', {
* some_setting: 1
* }, tinymce.EditorManager);
*
* // Select each item the user clicks on
* ed.on('click', function(e) {
* ed.selection.select(e.target);
* });
*
* ed.render();
*/
define("tinymce/Editor", [
"tinymce/dom/DOMUtils",
"tinymce/dom/DomQuery",
"tinymce/AddOnManager",
"tinymce/NodeChange",
"tinymce/html/Node",
"tinymce/dom/Serializer",
"tinymce/html/Serializer",
"tinymce/dom/Selection",
"tinymce/Formatter",
"tinymce/UndoManager",
"tinymce/EnterKey",
"tinymce/ForceBlocks",
"tinymce/EditorCommands",
"tinymce/util/URI",
"tinymce/dom/ScriptLoader",
"tinymce/dom/EventUtils",
"tinymce/WindowManager",
"tinymce/html/Schema",
"tinymce/html/DomParser",
"tinymce/util/Quirks",
"tinymce/Env",
"tinymce/util/Tools",
"tinymce/EditorObservable",
"tinymce/Shortcuts"
], function(
DOMUtils, DomQuery, AddOnManager, NodeChange, Node, DomSerializer, Serializer,
Selection, Formatter, UndoManager, EnterKey, ForceBlocks, EditorCommands,
URI, ScriptLoader, EventUtils, WindowManager,
Schema, DomParser, Quirks, Env, Tools, EditorObservable, Shortcuts
) {
// life
/*
if(typeof LeaAce == "undefined") {
window.LeaAce = {
isInAce: function() {},
nowIsInAce: function(){}
};
}
*/
// Shorten these names
var DOM = DOMUtils.DOM, ThemeManager = AddOnManager.ThemeManager, PluginManager = AddOnManager.PluginManager;
var extend = Tools.extend, each = Tools.each, explode = Tools.explode;
var inArray = Tools.inArray, trim = Tools.trim, resolve = Tools.resolve;
var Event = EventUtils.Event;
var isGecko = Env.gecko, ie = Env.ie;
/**
* Include documentation for all the events.
*
* @include ../../../tools/docs/tinymce.Editor.js
*/
/**
* Constructs a editor instance by id.
*
* @constructor
* @method Editor
* @param {String} id Unique id for the editor.
* @param {Object} settings Settings for the editor.
* @param {tinymce.EditorManager} editorManager EditorManager instance.
* @author Moxiecode
*/
function Editor(id, settings, editorManager) {
var self = this, documentBaseUrl, baseUri;
documentBaseUrl = self.documentBaseUrl = editorManager.documentBaseURL;
baseUri = editorManager.baseURI;
/**
* Name/value collection with editor settings.
*
* @property settings
* @type Object
* @example
* // Get the value of the theme setting
* tinymce.activeEditor.windowManager.alert("You are using the " + tinymce.activeEditor.settings.theme + " theme");
*/
self.settings = settings = extend({
id: id,
theme: 'modern',
delta_width: 0,
delta_height: 0,
popup_css: '',
plugins: '',
document_base_url: documentBaseUrl,
add_form_submit_trigger: true,
submit_patch: true,
add_unload_trigger: true,
convert_urls: true,
relative_urls: true,
remove_script_host: true,
object_resizing: true,
doctype: '',
visual: true,
font_size_style_values: 'xx-small,x-small,small,medium,large,x-large,xx-large',
// See: http://www.w3.org/TR/CSS2/fonts.html#propdef-font-size
font_size_legacy_values: 'xx-small,small,medium,large,x-large,xx-large,300%',
forced_root_block: 'p',
hidden_input: true,
padd_empty_editor: true,
render_ui: true,
indentation: '30px',
inline_styles: true,
convert_fonts_to_spans: true,
indent: 'simple',
indent_before: 'p,h1,h2,h3,h4,h5,h6,blockquote,div,title,style,pre,script,td,th,ul,ol,li,dl,dt,dd,area,table,thead,' +
'tfoot,tbody,tr,section,article,hgroup,aside,figure,option,optgroup,datalist',
indent_after: 'p,h1,h2,h3,h4,h5,h6,blockquote,div,title,style,pre,script,td,th,ul,ol,li,dl,dt,dd,area,table,thead,' +
'tfoot,tbody,tr,section,article,hgroup,aside,figure,option,optgroup,datalist',
validate: true,
entity_encoding: 'named',
url_converter: self.convertURL,
url_converter_scope: self,
ie7_compat: true
}, settings);
AddOnManager.language = settings.language || 'en';
AddOnManager.languageLoad = settings.language_load;
AddOnManager.baseURL = editorManager.baseURL;
/**
* Editor instance id, normally the same as the div/textarea that was replaced.
*
* @property id
* @type String
*/
self.id = settings.id = id;
/**
* State to force the editor to return false on a isDirty call.
*
* @property isNotDirty
* @type Boolean
* @example
* function ajaxSave() {
* var ed = tinymce.get('elm1');
*
* // Save contents using some XHR call
* alert(ed.getContent());
*
* ed.isNotDirty = true; // Force not dirty state
* }
*/
self.isNotDirty = true;
/**
* Name/Value object containting plugin instances.
*
* @property plugins
* @type Object
* @example
* // Execute a method inside a plugin directly
* tinymce.activeEditor.plugins.someplugin.someMethod();
*/
self.plugins = {};
/**
* URI object to document configured for the TinyMCE instance.
*
* @property documentBaseURI
* @type tinymce.util.URI
* @example
* // Get relative URL from the location of document_base_url
* tinymce.activeEditor.documentBaseURI.toRelative('/somedir/somefile.htm');
*
* // Get absolute URL from the location of document_base_url
* tinymce.activeEditor.documentBaseURI.toAbsolute('somefile.htm');
*/
self.documentBaseURI = new URI(settings.document_base_url || documentBaseUrl, {
base_uri: baseUri
});
/**
* URI object to current document that holds the TinyMCE editor instance.
*
* @property baseURI
* @type tinymce.util.URI
* @example
* // Get relative URL from the location of the API
* tinymce.activeEditor.baseURI.toRelative('/somedir/somefile.htm');
*
* // Get absolute URL from the location of the API
* tinymce.activeEditor.baseURI.toAbsolute('somefile.htm');
*/
self.baseURI = baseUri;
/**
* Array with CSS files to load into the iframe.
*
* @property contentCSS
* @type Array
*/
self.contentCSS = [];
/**
* Array of CSS styles to add to head of document when the editor loads.
*
* @property contentStyles
* @type Array
*/
self.contentStyles = [];
// Creates all events like onClick, onSetContent etc see Editor.Events.js for the actual logic
self.shortcuts = new Shortcuts(self);
self.loadedCSS = {};
self.editorCommands = new EditorCommands(self);
if (settings.target) {
self.targetElm = settings.target;
}
self.suffix = editorManager.suffix;
self.editorManager = editorManager;
self.inline = settings.inline;
if (settings.cache_suffix) {
Env.cacheSuffix = settings.cache_suffix.replace(/^[\?\&]+/, '');
}
// Call setup
editorManager.fire('SetupEditor', self);
self.execCallback('setup', self);
/**
* Dom query instance with default scope to the editor document and default element is the body of the editor.
*
* @property $
* @type tinymce.dom.DomQuery
* @example
* tinymce.activeEditor.$('p').css('color', 'red');
* tinymce.activeEditor.$().append('new
');
*/
self.$ = DomQuery.overrideDefaults(function() {
return {
context: self.inline ? self.getBody() : self.getDoc(),
element: self.getBody()
};
});
}
Editor.prototype = {
/**
* Renderes the editor/adds it to the page.
*
* @method render
*/
render: function() {
var self = this, settings = self.settings, id = self.id, suffix = self.suffix;
function readyHandler() {
DOM.unbind(window, 'ready', readyHandler);
self.render();
}
// Page is not loaded yet, wait for it
if (!Event.domLoaded) {
DOM.bind(window, 'ready', readyHandler);
return;
}
// Element not found, then skip initialization
if (!self.getElement()) {
return;
}
// No editable support old iOS versions etc
if (!Env.contentEditable) {
return;
}
// Hide target element early to prevent content flashing
if (!settings.inline) {
self.orgVisibility = self.getElement().style.visibility;
self.getElement().style.visibility = 'hidden';
} else {
self.inline = true;
}
var form = self.getElement().form || DOM.getParent(id, 'form');
if (form) {
self.formElement = form;
// Add hidden input for non input elements inside form elements
if (settings.hidden_input && !/TEXTAREA|INPUT/i.test(self.getElement().nodeName)) {
DOM.insertAfter(DOM.create('input', {type: 'hidden', name: id}), id);
self.hasHiddenInput = true;
}
// Pass submit/reset from form to editor instance
self.formEventDelegate = function(e) {
self.fire(e.type, e);
};
DOM.bind(form, 'submit reset', self.formEventDelegate);
// Reset contents in editor when the form is reset
self.on('reset', function() {
self.setContent(self.startContent, {format: 'raw'});
});
// Check page uses id="submit" or name="submit" for it's submit button
if (settings.submit_patch && !form.submit.nodeType && !form.submit.length && !form._mceOldSubmit) {
form._mceOldSubmit = form.submit;
form.submit = function() {
self.editorManager.triggerSave();
self.isNotDirty = true;
return form._mceOldSubmit(form);
};
}
}
/**
* Window manager reference, use this to open new windows and dialogs.
*
* @property windowManager
* @type tinymce.WindowManager
* @example
* // Shows an alert message
* tinymce.activeEditor.windowManager.alert('Hello world!');
*
* // Opens a new dialog with the file.htm file and the size 320x240
* // It also adds a custom parameter this can be retrieved by using tinyMCEPopup.getWindowArg inside the dialog.
* tinymce.activeEditor.windowManager.open({
* url: 'file.htm',
* width: 320,
* height: 240
* }, {
* custom_param: 1
* });
*/
self.windowManager = new WindowManager(self);
if (settings.encoding == 'xml') {
self.on('GetContent', function(e) {
if (e.save) {
e.content = DOM.encode(e.content);
}
});
}
if (settings.add_form_submit_trigger) {
self.on('submit', function() {
if (self.initialized) {
self.save();
}
});
}
if (settings.add_unload_trigger) {
self._beforeUnload = function() {
if (self.initialized && !self.destroyed && !self.isHidden()) {
self.save({format: 'raw', no_events: true, set_dirty: false});
}
};
self.editorManager.on('BeforeUnload', self._beforeUnload);
}
// Load scripts
function loadScripts() {
var scriptLoader = ScriptLoader.ScriptLoader;
if (settings.language && settings.language != 'en' && !settings.language_url) {
settings.language_url = self.editorManager.baseURL + '/langs/' + settings.language + '.js';
}
if (settings.language_url) {
scriptLoader.add(settings.language_url);
}
if (settings.theme && typeof settings.theme != "function" &&
settings.theme.charAt(0) != '-' && !ThemeManager.urls[settings.theme]) {
var themeUrl = settings.theme_url;
if (themeUrl) {
themeUrl = self.documentBaseURI.toAbsolute(themeUrl);
} else {
themeUrl = 'themes/' + settings.theme + '/theme' + suffix + '.js';
}
ThemeManager.load(settings.theme, themeUrl);
}
if (Tools.isArray(settings.plugins)) {
settings.plugins = settings.plugins.join(' ');
}
each(settings.external_plugins, function(url, name) {
PluginManager.load(name, url);
settings.plugins += ' ' + name;
});
each(settings.plugins.split(/[ ,]/), function(plugin) {
plugin = trim(plugin);
if (plugin && !PluginManager.urls[plugin]) {
if (plugin.charAt(0) == '-') {
plugin = plugin.substr(1, plugin.length);
var dependencies = PluginManager.dependencies(plugin);
each(dependencies, function(dep) {
var defaultSettings = {
prefix: 'plugins/',
resource: dep,
suffix: '/plugin' + suffix + '.js'
};
dep = PluginManager.createUrl(defaultSettings, dep);
PluginManager.load(dep.resource, dep);
});
} else {
PluginManager.load(plugin, {
prefix: 'plugins/',
resource: plugin,
suffix: '/plugin' + suffix + '.js'
});
}
}
});
scriptLoader.loadQueue(function() {
if (!self.removed) {
self.init();
}
});
}
loadScripts();
},
/**
* Initializes the editor this will be called automatically when
* all plugins/themes and language packs are loaded by the rendered method.
* This method will setup the iframe and create the theme and plugin instances.
*
* @method init
*/
init: function() {
var self = this, settings = self.settings, elm = self.getElement();
var w, h, minHeight, n, o, Theme, url, bodyId, bodyClass, re, i, initializedPlugins = [];
this.editorManager.i18n.setCode(settings.language);
self.rtl = this.editorManager.i18n.rtl;
self.editorManager.add(self);
settings.aria_label = settings.aria_label || DOM.getAttrib(elm, 'aria-label', self.getLang('aria.rich_text_area'));
/**
* Reference to the theme instance that was used to generate the UI.
*
* @property theme
* @type tinymce.Theme
* @example
* // Executes a method on the theme directly
* tinymce.activeEditor.theme.someMethod();
*/
if (settings.theme) {
if (typeof settings.theme != "function") {
settings.theme = settings.theme.replace(/-/, '');
Theme = ThemeManager.get(settings.theme);
self.theme = new Theme(self, ThemeManager.urls[settings.theme]);
if (self.theme.init) {
self.theme.init(self, ThemeManager.urls[settings.theme] || self.documentBaseUrl.replace(/\/$/, ''), self.$);
}
} else {
self.theme = settings.theme;
}
}
function initPlugin(plugin) {
var Plugin = PluginManager.get(plugin), pluginUrl, pluginInstance;
pluginUrl = PluginManager.urls[plugin] || self.documentBaseUrl.replace(/\/$/, '');
plugin = trim(plugin);
if (Plugin && inArray(initializedPlugins, plugin) === -1) {
each(PluginManager.dependencies(plugin), function(dep) {
initPlugin(dep);
});
pluginInstance = new Plugin(self, pluginUrl, self.$);
self.plugins[plugin] = pluginInstance;
if (pluginInstance.init) {
pluginInstance.init(self, pluginUrl);
initializedPlugins.push(plugin);
}
}
}
// Create all plugins
each(settings.plugins.replace(/\-/g, '').split(/[ ,]/), initPlugin);
// Measure box
if (settings.render_ui && self.theme) {
self.orgDisplay = elm.style.display;
if (typeof settings.theme != "function") {
w = settings.width || elm.style.width || elm.offsetWidth;
h = settings.height || elm.style.height || elm.offsetHeight;
minHeight = settings.min_height || 100;
re = /^[0-9\.]+(|px)$/i;
if (re.test('' + w)) {
w = Math.max(parseInt(w, 10), 100);
}
if (re.test('' + h)) {
h = Math.max(parseInt(h, 10), minHeight);
}
// Render UI
o = self.theme.renderUI({
targetNode: elm,
width: w,
height: h,
deltaWidth: settings.delta_width,
deltaHeight: settings.delta_height
});
// Resize editor
if (!settings.content_editable) {
h = (o.iframeHeight || h) + (typeof h == 'number' ? (o.deltaHeight || 0) : '');
if (h < minHeight) {
h = minHeight;
}
}
} else {
o = settings.theme(self, elm);
// Convert element type to id:s
if (o.editorContainer.nodeType) {
o.editorContainer = o.editorContainer.id = o.editorContainer.id || self.id + "_parent";
}
// Convert element type to id:s
if (o.iframeContainer.nodeType) {
o.iframeContainer = o.iframeContainer.id = o.iframeContainer.id || self.id + "_iframecontainer";
}
// Use specified iframe height or the targets offsetHeight
h = o.iframeHeight || elm.offsetHeight;
}
self.editorContainer = o.editorContainer;
}
// Load specified content CSS last
if (settings.content_css) {
each(explode(settings.content_css), function(u) {
self.contentCSS.push(self.documentBaseURI.toAbsolute(u));
});
}
// Load specified content CSS last
if (settings.content_style) {
self.contentStyles.push(settings.content_style);
}
// Content editable mode ends here
if (settings.content_editable) {
elm = n = o = null; // Fix IE leak
return self.initContentBody();
}
self.iframeHTML = settings.doctype + '';
// We only need to override paths if we have to
// IE has a bug where it remove site absolute urls to relative ones if this is specified
if (settings.document_base_url != self.documentBaseUrl) {
self.iframeHTML += ' ';
}
// IE8 doesn't support carets behind images setting ie7_compat would force IE8+ to run in IE7 compat mode.
if (!Env.caretAfter && settings.ie7_compat) {
self.iframeHTML += '';
}
self.iframeHTML += '';
// Load the CSS by injecting them into the HTML this will reduce "flicker"
for (i = 0; i < self.contentCSS.length; i++) {
var cssUrl = self.contentCSS[i];
self.iframeHTML += (
''
);
self.loadedCSS[cssUrl] = true;
}
bodyId = settings.body_id || 'tinymce';
if (bodyId.indexOf('=') != -1) {
bodyId = self.getParam('body_id', '', 'hash');
bodyId = bodyId[self.id] || bodyId;
}
bodyClass = settings.body_class || '';
if (bodyClass.indexOf('=') != -1) {
bodyClass = self.getParam('body_class', '', 'hash');
bodyClass = bodyClass[self.id] || '';
}
if (settings.content_security_policy) {
self.iframeHTML += '';
}
self.iframeHTML += '
';
/*eslint no-script-url:0 */
var domainRelaxUrl = 'javascript:(function(){' +
'document.open();document.domain="' + document.domain + '";' +
'var ed = window.parent.tinymce.get("' + self.id + '");document.write(ed.iframeHTML);' +
'document.close();ed.initContentBody(true);})()';
// Domain relaxing is required since the user has messed around with document.domain
if (document.domain != location.hostname) {
url = domainRelaxUrl;
}
// Create iframe
// TODO: ACC add the appropriate description on this.
var ifr = DOM.create('iframe', {
id: self.id + "_ifr",
//src: url || 'javascript:""', // Workaround for HTTPS warning in IE6/7
frameBorder: '0',
allowTransparency: "true",
// life
title: self.editorManager.translate("Leanote Editor"),
style: {
width: '100%',
height: h,
display: 'block' // Important for Gecko to render the iframe correctly
}
});
ifr.onload = function() {
ifr.onload = null;
self.fire("load");
};
DOM.setAttrib(ifr, "src", url || 'javascript:""');
self.contentAreaContainer = o.iframeContainer;
self.iframeElement = ifr;
n = DOM.add(o.iframeContainer, ifr);
// Try accessing the document this will fail on IE when document.domain is set to the same as location.hostname
// Then we have to force domain relaxing using the domainRelaxUrl approach very ugly!!
if (ie) {
try {
self.getDoc();
} catch (e) {
n.src = url = domainRelaxUrl;
}
}
if (o.editorContainer) {
DOM.get(o.editorContainer).style.display = self.orgDisplay;
self.hidden = DOM.isHidden(o.editorContainer);
}
self.getElement().style.display = 'none';
DOM.setAttrib(self.id, 'aria-hidden', true);
if (!url) {
self.initContentBody();
}
elm = n = o = null; // Cleanup
},
/**
* This method get called by the init method ones the iframe is loaded.
* It will fill the iframe with contents, setups DOM and selection objects for the iframe.
*
* @method initContentBody
* @private
*/
initContentBody: function(skipWrite) {
var self = this, settings = self.settings, targetElm = self.getElement(), doc = self.getDoc(), body, contentCssText;
// Restore visibility on target element
if (!settings.inline) {
self.getElement().style.visibility = self.orgVisibility;
}
// Setup iframe body
if (!skipWrite && !settings.content_editable) {
doc.open();
doc.write(self.iframeHTML);
doc.close();
}
if (settings.content_editable) {
self.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');
self.contentDocument = doc = settings.content_document || document;
self.contentWindow = settings.content_window || window;
self.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 = self.getBody();
body.disabled = true;
if (!settings.readonly) {
if (self.inline && DOM.getStyle(body, 'position', true) == 'static') {
body.style.position = 'relative';
}
body.contentEditable = self.getParam('content_editable_state', true);
}
body.disabled = false;
/**
* Schema instance, enables you to validate elements and it's children.
*
* @property schema
* @type tinymce.html.Schema
*/
self.schema = new Schema(settings);
/**
* DOM instance for the editor.
*
* @property dom
* @type tinymce.dom.DOMUtils
* @example
* // Adds a class to all paragraphs within the editor
* tinymce.activeEditor.dom.addClass(tinymce.activeEditor.dom.select('p'), 'someclass');
*/
self.dom = new DOMUtils(doc, {
keep_values: true,
url_converter: self.convertURL,
url_converter_scope: self,
hex_colors: settings.force_hex_style_colors,
class_filter: settings.class_filter,
update_styles: true,
root_element: self.inline ? self.getBody() : null,
collect: settings.content_editable,
schema: self.schema,
onSetAttrib: function(e) {
self.fire('SetAttrib', e);
}
});
/**
* HTML parser will be used when contents is inserted into the editor.
*
* @property parser
* @type tinymce.html.DomParser
*/
self.parser = new DomParser(settings, self.schema);
// Convert src and href into data-mce-src, data-mce-href and data-mce-style
self.parser.addAttributeFilter('src,href,style,tabindex', function(nodes, name) {
var i = nodes.length, node, dom = self.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]) {
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, self.convertURL(value, name, node.name));
}
}
}
});
// Keep scripts from executing
self.parser.addNodeFilter('script', function(nodes) {
var i = nodes.length, node;
while (i--) {
node = nodes[i];
node.attr('type', 'mce-' + (node.attr('type') || 'no/type'));
}
});
self.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 + ']]';
}
});
self.parser.addNodeFilter('p,h1,h2,h3,h4,h5,h6,div', function(nodes) {
var i = nodes.length, node, nonEmptyElements = self.schema.getNonEmptyElements();
while (i--) {
node = nodes[i];
if (node.isEmpty(nonEmptyElements)) {
node.append(new Node('br', 1)).shortEnded = true;
}
}
});
/**
* DOM serializer for the editor. Will be used when contents is extracted from the editor.
*
* @property serializer
* @type tinymce.dom.Serializer
* @example
* // Serializes the first paragraph in the editor into a string
* tinymce.activeEditor.serializer.serialize(tinymce.activeEditor.dom.select('p')[0]);
*/
self.serializer = new DomSerializer(settings, self);
/**
* Selection instance for the editor.
*
* @property selection
* @type tinymce.dom.Selection
* @example
* // Sets some contents to the current selection in the editor
* tinymce.activeEditor.selection.setContent('Some contents');
*
* // Gets the current selection
* alert(tinymce.activeEditor.selection.getContent());
*
* // Selects the first paragraph found
* tinymce.activeEditor.selection.select(tinymce.activeEditor.dom.select('p')[0]);
*/
self.selection = new Selection(self.dom, self.getWin(), self.serializer, self);
/**
* Formatter instance.
*
* @property formatter
* @type tinymce.Formatter
*/
self.formatter = new Formatter(self);
/**
* Undo manager instance, responsible for handling undo levels.
*
* @property undoManager
* @type tinymce.UndoManager
* @example
* // Undoes the last modification to the editor
* tinymce.activeEditor.undoManager.undo();
*/
self.undoManager = new UndoManager(self);
self.forceBlocks = new ForceBlocks(self);
self.enterKey = new EnterKey(self);
self._nodeChangeDispatcher = new NodeChange(self);
self.fire('PreInit');
if (!settings.browser_spellcheck && !settings.gecko_spellcheck) {
doc.body.spellcheck = false; // Gecko
DOM.setAttrib(body, "spellcheck", "false");
}
self.fire('PostRender');
self.quirks = new Quirks(self);
if (settings.directionality) {
body.dir = settings.directionality;
}
if (settings.nowrap) {
body.style.whiteSpace = "nowrap";
}
if (settings.protect) {
self.on('BeforeSetContent', function(e) {
each(settings.protect, function(pattern) {
e.content = e.content.replace(pattern, function(str) {
return '';
});
});
});
}
self.on('SetContent', function() {
self.addVisual(self.getBody());
});
// Remove empty contents
if (settings.padd_empty_editor) {
self.on('PostProcess', function(e) {
e.content = e.content.replace(/^(]*>( | |\s|\u00a0|)<\/p>[\r\n]*|
[\r\n]*)$/, '');
});
}
self.load({initial: true, format: 'html'});
self.startContent = self.getContent({format: 'raw'});
/**
* Is set to true after the editor instance has been initialized
*
* @property initialized
* @type Boolean
* @example
* function isEditorInitialized(editor) {
* return editor && editor.initialized;
* }
*/
self.initialized = true;
self.bindPendingEventDelegates();
self.fire('init');
self.focus(true);
self.nodeChanged({initial: true});
self.execCallback('init_instance_callback', self);
// Add editor specific CSS styles
if (self.contentStyles.length > 0) {
contentCssText = '';
each(self.contentStyles, function(style) {
contentCssText += style + "\r\n";
});
self.dom.addStyle(contentCssText);
}
// Load specified content CSS last
each(self.contentCSS, function(cssUrl) {
if (!self.loadedCSS[cssUrl]) {
self.dom.loadCSS(cssUrl);
self.loadedCSS[cssUrl] = true;
}
});
// Handle auto focus
if (settings.auto_focus) {
setTimeout(function() {
var editor;
if (settings.auto_focus === true) {
editor = self;
} else {
editor = self.editorManager.get(settings.auto_focus);
}
editor.focus();
}, 100);
}
// Clean up references for IE
targetElm = doc = body = null;
},
/**
* Focuses/activates the editor. This will set this editor as the activeEditor in the tinymce collection
* it will also place DOM focus inside the editor.
*
* @method focus
* @param {Boolean} skipFocus Skip DOM focus. Just set is as the active editor.
*/
focus: function(skipFocus) {
var self = this, selection = self.selection, contentEditable = self.settings.content_editable, rng;
var controlElm, doc = self.getDoc(), body;
if (!skipFocus) {
// Get selected control element
rng = selection.getRng();
if (rng.item) {
controlElm = rng.item(0);
}
self._refreshContentEditable();
// Focus the window iframe
if (!contentEditable) {
// WebKit needs this call to fire focusin event properly see #5948
// But Opera pre Blink engine will produce an empty selection so skip Opera
if (!Env.opera) {
self.getBody().focus();
}
self.getWin().focus();
}
// Focus the body as well since it's contentEditable
if (isGecko || contentEditable) {
body = self.getBody();
// Check for setActive since it doesn't scroll to the element
if (body.setActive) {
// IE 11 sometimes throws "Invalid function" then fallback to focus
try {
body.setActive();
} catch (ex) {
body.focus();
}
} else {
body.focus();
}
if (contentEditable) {
selection.normalize();
}
}
// Restore selected control element
// This is needed when for example an image is selected within a
// layer a call to focus will then remove the control selection
if (controlElm && controlElm.ownerDocument == doc) {
rng = doc.body.createControlRange();
rng.addElement(controlElm);
rng.select();
}
}
self.editorManager.setActive(self);
},
/**
* Executes a legacy callback. This method is useful to call old 2.x option callbacks.
* There new event model is a better way to add callback so this method might be removed in the future.
*
* @method execCallback
* @param {String} name Name of the callback to execute.
* @return {Object} Return value passed from callback function.
*/
execCallback: function(name) {
var self = this, callback = self.settings[name], scope;
if (!callback) {
return;
}
// Look through lookup
if (self.callbackLookup && (scope = self.callbackLookup[name])) {
callback = scope.func;
scope = scope.scope;
}
if (typeof callback === 'string') {
scope = callback.replace(/\.\w+$/, '');
scope = scope ? resolve(scope) : 0;
callback = resolve(callback);
self.callbackLookup = self.callbackLookup || {};
self.callbackLookup[name] = {func: callback, scope: scope};
}
return callback.apply(scope || self, Array.prototype.slice.call(arguments, 1));
},
/**
* Translates the specified string by replacing variables with language pack items it will also check if there is
* a key mathcin the input.
*
* @method translate
* @param {String} text String to translate by the language pack data.
* @return {String} Translated string.
*/
translate: function(text) {
var lang = this.settings.language || 'en', i18n = this.editorManager.i18n;
if (!text) {
return '';
}
return i18n.data[lang + '.' + text] || text.replace(/\{\#([^\}]+)\}/g, function(a, b) {
return i18n.data[lang + '.' + b] || '{#' + b + '}';
});
},
/**
* 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 retrive.
*/
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 retrive.
* @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;
},
/**
* Distpaches out a onNodeChange event to all observers. This method should be called when you
* need to update the UI states or element path etc.
*
* @method nodeChanged
* @param {Object} args Optional args to pass to NodeChange event handlers.
*/
nodeChanged: function(args) {
this._nodeChangeDispatcher.nodeChanged(args);
},
/**
* Adds a button that later gets created by the theme in the editors toolbars.
*
* @method addButton
* @param {String} name Button name to add.
* @param {Object} settings Settings object with title, cmd etc.
* @example
* // Adds a custom button to the editor that inserts contents when clicked
* tinymce.init({
* ...
*
* toolbar: 'example'
*
* setup: function(ed) {
* ed.addButton('example', {
* title: 'My title',
* image: '../js/tinymce/plugins/example/img/example.gif',
* onclick: function() {
* ed.insertContent('Hello world!!');
* }
* });
* }
* });
*/
addButton: function(name, settings) {
var self = this;
if (settings.cmd) {
settings.onclick = function() {
self.execCommand(settings.cmd);
};
}
if (!settings.text && !settings.icon) {
settings.icon = name;
}
self.buttons = self.buttons || {};
settings.tooltip = settings.tooltip || settings.title;
self.buttons[name] = settings;
},
/**
* Adds a menu item to be used in the menus of the theme. There might be multiple instances
* of this menu item for example it might be used in the main menus of the theme but also in
* the context menu so make sure that it's self contained and supports multiple instances.
*
* @method addMenuItem
* @param {String} name Menu item name to add.
* @param {Object} settings Settings object with title, cmd etc.
* @example
* // Adds a custom menu item to the editor that inserts contents when clicked
* // The context option allows you to add the menu item to an existing default menu
* tinymce.init({
* ...
*
* setup: function(ed) {
* ed.addMenuItem('example', {
* text: 'My menu item',
* context: 'tools',
* onclick: function() {
* ed.insertContent('Hello world!!');
* }
* });
* }
* });
*/
addMenuItem: function(name, settings) {
var self = this;
if (settings.cmd) {
settings.onclick = function() {
self.execCommand(settings.cmd);
};
}
self.menuItems = self.menuItems || {};
self.menuItems[name] = settings;
},
/**
* Adds a 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 retrival 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 retrival 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 asycronous operations like an AJAX save call.
*
* @method setProgressState
* @param {Boolean} state Boolean state if the progress should be shown or hidden.
* @param {Number} time Optional time to wait before the progress gets shown.
* @return {Boolean} Same as the input state.
* @example
* // Show progress for the active editor
* tinymce.activeEditor.setProgressState(true);
*
* // Hide progress for the active editor
* tinymce.activeEditor.setProgressState(false);
*
* // Show progress after 3 seconds
* tinymce.activeEditor.setProgressState(true, 3000);
*/
setProgressState: function(state, time) {
this.fire('ProgressState', {state: state, time: time});
},
/**
* Loads contents from the textarea or div element that got converted into an editor instance.
* This method will move the contents from that textarea or div into the editor by using setContent
* so all events etc that method has will get dispatched as well.
*
* @method load
* @param {Object} args Optional content object, this gets passed around through the whole load process.
* @return {String} HTML string that got set into the editor.
*/
load: function(args) {
var self = this, elm = self.getElement(), html;
if (elm) {
args = args || {};
args.load = true;
html = self.setContent(elm.value !== undefined ? elm.value : elm.innerHTML, args);
args.element = elm;
if (!args.no_events) {
self.fire('LoadContent', args);
}
args.element = elm = null;
return html;
}
},
/**
* Saves the contents from a editor out to the textarea or div element that got converted into an editor instance.
* This method will move the HTML contents from the editor into that textarea or div by getContent
* so all events etc that method has will get dispatched as well.
*
* @method save
* @param {Object} args Optional content object, this gets passed around through the whole save process.
* @return {String} HTML string that got set into the textarea/div.
*/
save: function(args) {
var self = this, elm = self.getElement(), html, form;
if (!elm || !self.initialized) {
return;
}
args = args || {};
args.save = true;
args.element = elm;
html = args.content = self.getContent(args);
if (!args.no_events) {
self.fire('SaveContent', args);
}
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.isNotDirty = true;
}
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;
/**
* life ace
*/
// 先destroy之前的ace
if(window.LeaAce && window.LeaAce.canAce) { // 有些地方不用, 比如单页面
var everContent = $(self.getBody());
if(everContent) {
LeaAce.destroyAceFromContent(everContent);
}
}
// end
// 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)) {
forcedRootBlockName = self.settings.forced_root_block;
// Check if forcedRootBlock is configured and that the block is a valid child of the body
if (forcedRootBlockName && self.schema.isValidChild(body.nodeName.toLowerCase(), forcedRootBlockName.toLowerCase())) {
// Padd with bogus BR elements on modern browsers and IE 7 and 8 since they don't render empty P tags properly
content = ie && ie < 11 ? '' : '
';
content = self.dom.createHTML(forcedRootBlockName, self.settings.forced_root_block_attrs, content);
} else if (!ie) {
// We need to add a BR when forced_root_block is disabled on non IE browsers to place the caret
content = '
';
}
self.dom.setHTML(body, content);
self.fire('SetContent', args);
} else {
// Parse and serialize the html
if (args.format !== 'raw') {
content = new Serializer({}, self.schema).serialize(
self.parser.parse(content, {isRootContent: true})
);
}
// Set the new cleaned contents to the editor
args.content = trim(content);
self.dom.setHTML(body, args.content);
// Do post processing
if (!args.no_events) {
self.fire('SetContent', args);
}
// Don't normalize selection if the focused element isn't the body in
// content editable mode since it will steal focus otherwise
/*if (!self.settings.content_editable || document.activeElement === self.getBody()) {
self.selection.normalize();
}*/
}
/**
* life ace
*/
if(window.LeaAce && window.LeaAce.canAce) {
if(LeaAce.canAce() && LeaAce.isAce) {
try {
LeaAce.initAceFromContent(self);
} catch(e) {
log(e);
}
} else {
// 为了在firefox下有正常的显示
$("#editorContent pre").removeClass("ace-tomorrow ace_editor");
}
}
// end
return args.content;
},
/**
* Gets the content from the editor instance, this will cleanup the content before it gets returned using
* the different cleanup rules options.
*
* @method getContent
* @param {Object} args Optional content object, this gets passed around through the whole get process.
* @return {String} Cleaned content string, normally HTML contents.
* @example
* // Get the HTML contents of the currently active editor
* console.debug(tinymce.activeEditor.getContent());
*
* // Get the raw contents of the currently active editor
* tinymce.activeEditor.getContent({format: 'raw'});
*
* // Get content of a specific editor:
* tinymce.get('content id').getContent()
*/
getContent: function(args) {
var self = this, content, body = self.getBody();
// Setup args object
args = args || {};
args.format = args.format || 'html';
args.get = true;
args.getInner = true;
// Do preprocessing
if (!args.no_events) {
self.fire('BeforeGetContent', args);
}
// Get raw contents or by default the cleaned contents
if (args.format == 'raw') {
content = body.innerHTML;
} else if (args.format == 'text') {
content = body.innerText || body.textContent;
} else {
content = self.serializer.serialize(body, args);
}
// Trim whitespace in beginning/end of HTML
if (args.format != 'text') {
args.content = trim(content);
} else {
args.content = content;
}
// Do post processing
if (!args.no_events) {
self.fire('GetContent', args);
}
return args.content;
},
/**
* Inserts content at caret position.
*
* @method insertContent
* @param {String} content Content to insert.
* @param {Object} args Optional args to pass to insert call.
*/
insertContent: function(content, args) {
if (args) {
content = extend({content: content}, args);
}
this.execCommand('mceInsertContent', false, content);
},
// life
insertRawContent: function(content) {
this.execCommand('mceInsertRawHTML', false, content);
},
/**
* Returns true/false if the editor is dirty or not. It will get dirty if the user has made modifications to the contents.
*
* @method isDirty
* @return {Boolean} True/false if the editor is dirty or not. It will get dirty if the user has made modifications to the contents.
* @example
* if (tinymce.activeEditor.isDirty())
* alert("You must save your contents.");
*/
isDirty: function() {
return !this.isNotDirty;
},
/**
* Returns the editors container element. The container element wrappes in
* all the elements added to the page for the editor. Such as UI, iframe etc.
*
* @method getContainer
* @return {Element} HTML DOM element for the editor container.
*/
getContainer: function() {
var self = this;
if (!self.container) {
self.container = DOM.get(self.editorContainer || self.id + '_parent');
}
return self.container;
},
/**
* Returns the editors content area container element. The this element is the one who
* holds the iframe or the editable element.
*
* @method getContentAreaContainer
* @return {Element} HTML DOM element for the editor area container.
*/
getContentAreaContainer: function() {
return this.contentAreaContainer;
},
/**
* Returns the target element/textarea that got replaced with a TinyMCE editor instance.
*
* @method getElement
* @return {Element} HTML DOM element for the replaced element.
*/
getElement: function() {
if (!this.targetElm) {
this.targetElm = DOM.get(this.id);
}
return this.targetElm;
},
/**
* Returns the iframes window object.
*
* @method getWin
* @return {Window} Iframe DOM window object.
*/
getWin: function() {
var self = this, elm;
if (!self.contentWindow) {
elm = self.iframeElement;
if (elm) {
self.contentWindow = elm.contentWindow;
}
}
return self.contentWindow;
},
/**
* Returns the iframes document object.
*
* @method getDoc
* @return {Document} Iframe DOM document object.
*/
getDoc: function() {
var self = this, win;
if (!self.contentDocument) {
win = self.getWin();
if (win) {
self.contentDocument = win.document;
}
}
return self.contentDocument;
},
/**
* Returns the root element of the editable area.
* For a non-inline iframe-based editor, returns the iframe's body element.
*
* @method getBody
* @return {Element} The root element of the editable area.
*/
getBody: function() {
return this.bodyElement || this.getDoc().body;
},
/**
* URL converter function this gets executed each time a user adds an img, a or
* any other element that has a URL in it. This will be called both by the DOM and HTML
* manipulation functions.
*
* @method convertURL
* @param {string} url URL to convert.
* @param {string} name Attribute name src, href etc.
* @param {string/HTMLElement} elm Tag name or HTML DOM element depending on HTML or DOM insert.
* @return {string} Converted URL string.
*/
convertURL: function(url, name, elm) {
var self = this, settings = self.settings;
// Use callback instead
if (settings.urlconverter_callback) {
return self.execCallback('urlconverter_callback', url, elm, true, name);
}
// Don't convert link href since thats the CSS files that gets loaded into the editor also skip local file URLs
if (!settings.convert_urls || (elm && elm.nodeName == 'LINK') || url.indexOf('file:') === 0 || url.length === 0) {
return url;
}
// Convert to relative
if (settings.relative_urls) {
return self.documentBaseURI.toRelative(url);
}
// Convert to absolute
url = self.documentBaseURI.toAbsolute(url, settings.remove_script_host);
return url;
},
/**
* Adds visual aid for tables, anchors etc so they can be more easily edited inside the editor.
*
* @method addVisual
* @param {Element} elm Optional root element to loop though to find tables etc that needs the visual aid.
*/
addVisual: function(elm) {
var self = this, settings = self.settings, dom = self.dom, cls;
elm = elm || self.getBody();
if (self.hasVisual === undefined) {
self.hasVisual = settings.visual;
}
each(dom.select('table,a', elm), function(elm) {
var value;
switch (elm.nodeName) {
case 'TABLE':
cls = settings.visual_table_class || 'mce-item-table';
value = dom.getAttrib(elm, 'border');
if ((!value || value == '0') && self.hasVisual) {
dom.addClass(elm, cls);
} else {
dom.removeClass(elm, cls);
}
return;
case 'A':
if (!dom.getAttrib(elm, 'href', false)) {
value = dom.getAttrib(elm, 'name') || elm.id;
cls = settings.visual_anchor_class || 'mce-item-anchor';
if (value && self.hasVisual) {
dom.addClass(elm, cls);
} else {
dom.removeClass(elm, cls);
}
}
return;
}
});
self.fire('VisualAid', {element: elm, hasVisual: self.hasVisual});
},
/**
* Removes the editor from the dom and tinymce collection.
*
* @method remove
*/
remove: function() {
var self = this;
if (!self.removed) {
self.save();
self.removed = 1;
self.unbindAllNativeEvents();
// Remove any hidden input
if (self.hasHiddenInput) {
DOM.remove(self.getElement().nextSibling);
}
if (!self.inline) {
// IE 9 has a bug where the selection stops working if you place the
// caret inside the editor then remove the iframe
if (ie && ie < 10) {
self.getDoc().execCommand('SelectAll', false, null);
}
DOM.setStyle(self.id, 'display', self.orgDisplay);
self.getBody().onload = null; // Prevent #6816
}
self.fire('remove');
self.editorManager.remove(self);
DOM.remove(self.getContainer());
self.destroy();
}
},
/**
* Destroys the editor instance by removing all events, element references or other resources
* that could leak memory. This method will be called automatically when the page is unloaded
* but you can also call it directly if you know what you are doing.
*
* @method destroy
* @param {Boolean} automatic Optional state if the destroy is an automatic destroy or user called one.
*/
destroy: function(automatic) {
var self = this, form;
// One time is enough
if (self.destroyed) {
return;
}
// If user manually calls destroy and not remove
// Users seems to have logic that calls destroy instead of remove
if (!automatic && !self.removed) {
self.remove();
return;
}
if (!automatic) {
self.editorManager.off('beforeunload', self._beforeUnload);
// Manual destroy
if (self.theme && self.theme.destroy) {
self.theme.destroy();
}
// Destroy controls, selection and dom
self.selection.destroy();
self.dom.destroy();
}
form = self.formElement;
if (form) {
if (form._mceOldSubmit) {
form.submit = form._mceOldSubmit;
form._mceOldSubmit = null;
}
DOM.unbind(form, 'submit reset', self.formEventDelegate);
}
self.contentAreaContainer = self.formElement = self.container = self.editorContainer = null;
self.bodyElement = self.contentDocument = self.contentWindow = null;
self.iframeElement = self.targetElm = null;
if (self.selection) {
self.selection = self.selection.win = self.selection.dom = self.selection.dom.doc = null;
}
self.destroyed = 1;
},
// Internal functions
_refreshContentEditable: function() {
var self = this, body, parent;
// Check if the editor was hidden and the re-initalize contentEditable mode by removing and adding the body again
if (self._isHidden()) {
body = self.getBody();
parent = body.parentNode;
parent.removeChild(body);
parent.appendChild(body);
body.focus();
}
},
_isHidden: function() {
var sel;
if (!isGecko) {
return 0;
}
// Weird, wheres that cursor selection?
sel = this.selection.getSel();
return (!sel || !sel.rangeCount || sel.rangeCount === 0);
}
};
extend(Editor.prototype, EditorObservable);
return Editor;
});
// Included from: js/tinymce/classes/util/I18n.js
/**
* I18n.js
*
* Copyright, Moxiecode Systems AB
* Released under LGPL License.
*
* License: http://www.tinymce.com/license
* Contributing: http://www.tinymce.com/contributing
*/
/**
* I18n class that handles translation of TinyMCE UI.
* Uses po style with csharp style parameters.
*
* @class tinymce.util.I18n
*/
define("tinymce/util/I18n", [], function() {
"use strict";
var data = {}, code = "en";
return {
/**
* Sets the current language code.
*
* @method setCode
* @param {String} newCode Current language code.
*/
setCode: function(newCode) {
if (newCode) {
code = newCode;
this.rtl = this.data[newCode] ? this.data[newCode]._dir === 'rtl' : false;
}
},
/**
* Returns the current language code.
*
* @return {String} Current language code.
*/
getCode: function() {
return code;
},
/**
* Property gets set to true if a RTL language pack was loaded.
*
* @property rtl
* @type Boolean
*/
rtl: false,
/**
* Adds translations for a specific language code.
*
* @method add
* @param {String} code Language code like sv_SE.
* @param {Array} items Name/value array with English en_US to sv_SE.
*/
add: function(code, items) {
var langData = data[code];
if (!langData) {
data[code] = langData = {};
}
for (var name in items) {
langData[name] = items[name];
}
this.setCode(code);
},
/**
* Translates the specified text.
*
* It has a few formats:
* I18n.translate("Text");
* I18n.translate(["Text {0}/{1}", 0, 1]);
* I18n.translate({raw: "Raw string"});
*
* @method translate
* @param {String/Object/Array} text Text to translate.
* @return {String} String that got translated.
*/
translate: function(text) {
var langData;
langData = data[code];
if (!langData) {
langData = {};
}
if (typeof text == "undefined") {
return text;
}
if (typeof text != "string" && text.raw) {
return text.raw;
}
if (text.push) {
var values = text.slice(1);
text = (langData[text[0]] || text[0]).replace(/\{([0-9]+)\}/g, function(match1, match2) {
return values[match2];
});
}
return (langData[text] || text).replace(/{context:\w+}$/, '');
},
data: data
};
});
// Included from: js/tinymce/classes/FocusManager.js
/**
* FocusManager.js
*
* Copyright, Moxiecode Systems AB
* Released under LGPL License.
*
* License: http://www.tinymce.com/license
* Contributing: http://www.tinymce.com/contributing
*/
/**
* This class manages the focus/blur state of the editor. This class is needed since some
* browsers fire false focus/blur states when the selection is moved to a UI dialog or similar.
*
* This class will fire two events focus and blur on the editor instances that got affected.
* It will also handle the restore of selection when the focus is lost and returned.
*
* @class tinymce.FocusManager
*/
define("tinymce/FocusManager", [
"tinymce/dom/DOMUtils",
"tinymce/Env"
], function(DOMUtils, Env) {
var selectionChangeHandler, documentFocusInHandler, documentMouseUpHandler, DOM = DOMUtils.DOM;
/**
* Constructs a new focus manager instance.
*
* @constructor FocusManager
* @param {tinymce.EditorManager} editorManager Editor manager instance to handle focus for.
*/
function FocusManager(editorManager) {
function getActiveElement() {
try {
return document.activeElement;
} catch (ex) {
// IE sometimes fails to get the activeElement when resizing table
// TODO: Investigate this
return document.body;
}
}
// We can't store a real range on IE 11 since it gets mutated so we need to use a bookmark object
// TODO: Move this to a separate range utils class since it's it's logic is present in Selection as well.
function createBookmark(dom, rng) {
if (rng && rng.startContainer) {
// Verify that the range is within the root of the editor
if (!dom.isChildOf(rng.startContainer, dom.getRoot()) || !dom.isChildOf(rng.endContainer, dom.getRoot())) {
return;
}
return {
startContainer: rng.startContainer,
startOffset: rng.startOffset,
endContainer: rng.endContainer,
endOffset: rng.endOffset
};
}
return rng;
}
function bookmarkToRng(editor, bookmark) {
var rng;
if (bookmark.startContainer) {
rng = editor.getDoc().createRange();
rng.setStart(bookmark.startContainer, bookmark.startOffset);
rng.setEnd(bookmark.endContainer, bookmark.endOffset);
} else {
rng = bookmark;
}
return rng;
}
function isUIElement(elm) {
return !!DOM.getParent(elm, FocusManager.isEditorUIElement);
}
function registerEvents(e) {
var editor = e.editor;
editor.on('init', function() {
// Gecko/WebKit has ghost selections in iframes and IE only has one selection per browser tab
if (editor.inline || Env.ie) {
// Use the onbeforedeactivate event when available since it works better see #7023
if ("onbeforedeactivate" in document && Env.ie < 9) {
editor.dom.bind(editor.getBody(), 'beforedeactivate', function(e) {
if (e.target != editor.getBody()) {
return;
}
try {
editor.lastRng = editor.selection.getRng();
} catch (ex) {
// IE throws "Unexcpected call to method or property access" some times so lets ignore it
}
});
} else {
// On other browsers take snapshot on nodechange in inline mode since they have Ghost selections for iframes
editor.on('nodechange mouseup keyup', function(e) {
var node = getActiveElement();
// Only act on manual nodechanges
if (e.type == 'nodechange' && e.selectionChange) {
return;
}
// IE 11 reports active element as iframe not body of iframe
if (node && node.id == editor.id + '_ifr') {
node = editor.getBody();
}
if (editor.dom.isChildOf(node, editor.getBody())) {
editor.lastRng = editor.selection.getRng();
}
});
}
// Handles the issue with WebKit not retaining selection within inline document
// If the user releases the mouse out side the body since a mouse up event wont occur on the body
if (Env.webkit && !selectionChangeHandler) {
selectionChangeHandler = function() {
var activeEditor = editorManager.activeEditor;
if (activeEditor && activeEditor.selection) {
var rng = activeEditor.selection.getRng();
// Store when it's non collapsed
if (rng && !rng.collapsed) {
editor.lastRng = rng;
}
}
};
DOM.bind(document, 'selectionchange', selectionChangeHandler);
}
}
});
editor.on('setcontent', function() {
editor.lastRng = null;
});
// Remove last selection bookmark on mousedown see #6305
editor.on('mousedown', function() {
editor.selection.lastFocusBookmark = null;
});
editor.on('focusin', function() {
var focusedEditor = editorManager.focusedEditor;
if (editor.selection.lastFocusBookmark) {
editor.selection.setRng(bookmarkToRng(editor, editor.selection.lastFocusBookmark));
editor.selection.lastFocusBookmark = null;
}
if (focusedEditor != editor) {
if (focusedEditor) {
focusedEditor.fire('blur', {focusedEditor: editor});
}
editorManager.setActive(editor);
editorManager.focusedEditor = editor;
editor.fire('focus', {blurredEditor: focusedEditor});
editor.focus(true);
}
editor.lastRng = null;
});
editor.on('focusout', function() {
window.setTimeout(function() {
var focusedEditor = editorManager.focusedEditor;
// Still the same editor the the blur was outside any editor UI
if (!isUIElement(getActiveElement()) && focusedEditor == editor) {
editor.fire('blur', {focusedEditor: null});
editorManager.focusedEditor = null;
// Make sure selection is valid could be invalid if the editor is blured and removed before the timeout occurs
if (editor.selection) {
editor.selection.lastFocusBookmark = null;
}
}
}, 0);
});
// Check if focus is moved to an element outside the active editor by checking if the target node
// isn't within the body of the activeEditor nor a UI element such as a dialog child control
if (!documentFocusInHandler) {
documentFocusInHandler = function(e) {
var activeEditor = editorManager.activeEditor;
if (activeEditor && e.target.ownerDocument == document) {
// Check to make sure we have a valid selection don't update the bookmark if it's
// a focusin to the body of the editor see #7025
if (activeEditor.selection && e.target != activeEditor.getBody()) {
activeEditor.selection.lastFocusBookmark = createBookmark(activeEditor.dom, activeEditor.lastRng);
}
// Fire a blur event if the element isn't a UI element
if (e.target != document.body && !isUIElement(e.target) && editorManager.focusedEditor == activeEditor) {
activeEditor.fire('blur', {focusedEditor: null});
editorManager.focusedEditor = null;
}
}
};
DOM.bind(document, 'focusin', documentFocusInHandler);
}
// Handle edge case when user starts the selection inside the editor and releases
// the mouse outside the editor producing a new selection. This weird workaround is needed since
// Gecko doesn't have the "selectionchange" event we need to do this. Fixes: #6843
if (editor.inline && !documentMouseUpHandler) {
documentMouseUpHandler = function(e) {
var activeEditor = editorManager.activeEditor;
if (activeEditor.inline && !activeEditor.dom.isChildOf(e.target, activeEditor.getBody())) {
var rng = activeEditor.selection.getRng();
if (!rng.collapsed) {
activeEditor.lastRng = rng;
}
}
};
DOM.bind(document, 'mouseup', documentMouseUpHandler);
}
}
function unregisterDocumentEvents(e) {
if (editorManager.focusedEditor == e.editor) {
editorManager.focusedEditor = null;
}
if (!editorManager.activeEditor) {
DOM.unbind(document, 'selectionchange', selectionChangeHandler);
DOM.unbind(document, 'focusin', documentFocusInHandler);
DOM.unbind(document, 'mouseup', documentMouseUpHandler);
selectionChangeHandler = documentFocusInHandler = documentMouseUpHandler = null;
}
}
editorManager.on('AddEditor', registerEvents);
editorManager.on('RemoveEditor', unregisterDocumentEvents);
}
/**
* Returns true if the specified element is part of the UI for example an button or text input.
*
* @method isEditorUIElement
* @param {Element} elm Element to check if it's part of the UI or not.
* @return {Boolean} True/false state if the element is part of the UI or not.
*/
FocusManager.isEditorUIElement = function(elm) {
// Needs to be converted to string since svg can have focus: #6776
return elm.className.toString().indexOf('mce-') !== -1;
};
return FocusManager;
});
// Included from: js/tinymce/classes/EditorManager.js
/**
* EditorManager.js
*
* Copyright, Moxiecode Systems AB
* Released under LGPL License.
*
* License: http://www.tinymce.com/license
* Contributing: http://www.tinymce.com/contributing
*/
/**
* This class used as a factory for manager for tinymce.Editor instances.
*
* @example
* tinymce.EditorManager.init({});
*
* @class tinymce.EditorManager
* @mixes tinymce.util.Observable
* @static
*/
define("tinymce/EditorManager", [
"tinymce/Editor",
"tinymce/dom/DomQuery",
"tinymce/dom/DOMUtils",
"tinymce/util/URI",
"tinymce/Env",
"tinymce/util/Tools",
"tinymce/util/Observable",
"tinymce/util/I18n",
"tinymce/FocusManager"
], function(Editor, DomQuery, DOMUtils, URI, Env, Tools, Observable, I18n, FocusManager) {
var DOM = DOMUtils.DOM;
var explode = Tools.explode, each = Tools.each, extend = Tools.extend;
var instanceCounter = 0, beforeUnloadDelegate, EditorManager;
function removeEditorFromList(editor) {
var editors = EditorManager.editors, removedFromList;
delete editors[editor.id];
for (var i = 0; i < editors.length; i++) {
if (editors[i] == editor) {
editors.splice(i, 1);
removedFromList = true;
break;
}
}
// Select another editor since the active one was removed
if (EditorManager.activeEditor == editor) {
EditorManager.activeEditor = editors[0];
}
// Clear focusedEditor if necessary, so that we don't try to blur the destroyed editor
if (EditorManager.focusedEditor == editor) {
EditorManager.focusedEditor = null;
}
return removedFromList;
}
function purgeDestroyedEditor(editor) {
// User has manually destroyed the editor lets clean up the mess
if (editor && !(editor.getContainer() || editor.getBody()).parentNode) {
removeEditorFromList(editor);
editor.unbindAllNativeEvents();
editor.destroy(true);
editor = null;
}
return editor;
}
EditorManager = {
/**
* Dom query instance.
*
* @property $
* @type tinymce.dom.DomQuery
*/
$: DomQuery,
/**
* Major version of TinyMCE build.
*
* @property majorVersion
* @type String
*/
majorVersion: '4',
/**
* Minor version of TinyMCE build.
*
* @property minorVersion
* @type String
*/
minorVersion: '1.9',
/**
* Release date of TinyMCE build.
*
* @property releaseDate
* @type String
*/
releaseDate: '2015-03-10',
/**
* Collection of editor instances.
*
* @property editors
* @type Object
* @example
* for (edId in tinymce.editors)
* tinymce.editors[edId].save();
*/
editors: [],
/**
* Collection of language pack data.
*
* @property i18n
* @type Object
*/
i18n: I18n,
/**
* Currently active editor instance.
*
* @property activeEditor
* @type tinymce.Editor
* @example
* tinyMCE.activeEditor.selection.getContent();
* tinymce.EditorManager.activeEditor.selection.getContent();
*/
activeEditor: null,
setup: function() {
var self = this, baseURL, documentBaseURL, suffix = "", preInit, src;
// Get base URL for the current document
documentBaseURL = document.location.href;
// Check if the URL is a document based format like: http://site/dir/file and file:///
// leave other formats like applewebdata://... intact
if (/^[^:]+:\/\/\/?[^\/]+\//.test(documentBaseURL)) {
documentBaseURL = documentBaseURL.replace(/[\?#].*$/, '').replace(/[\/\\][^\/]+$/, '');
if (!/[\/\\]$/.test(documentBaseURL)) {
documentBaseURL += '/';
}
}
// If tinymce is defined and has a base use that or use the old tinyMCEPreInit
preInit = window.tinymce || window.tinyMCEPreInit;
if (preInit) {
baseURL = preInit.base || preInit.baseURL;
suffix = preInit.suffix;
} else {
// Get base where the tinymce script is located
var scripts = document.getElementsByTagName('script');
for (var i = 0; i < scripts.length; i++) {
src = scripts[i].src;
// Script types supported:
// tinymce.js tinymce.min.js tinymce.dev.js
// tinymce.jquery.js tinymce.jquery.min.js tinymce.jquery.dev.js
// tinymce.full.js tinymce.full.min.js tinymce.full.dev.js
if (/tinymce(\.full|\.jquery|)(\.min|\.dev|)\.js/.test(src)) {
if (src.indexOf('.min') != -1) {
suffix = '.min';
}
baseURL = src.substring(0, src.lastIndexOf('/'));
break;
}
}
// We didn't find any baseURL by looking at the script elements
// Try to use the document.currentScript as a fallback
if (!baseURL && document.currentScript) {
src = document.currentScript.src;
if (src.indexOf('.min') != -1) {
suffix = '.min';
}
baseURL = src.substring(0, src.lastIndexOf('/'));
}
}
/**
* Base URL where the root directory if TinyMCE is located.
*
* @property baseURL
* @type String
*/
self.baseURL = new URI(documentBaseURL).toAbsolute(baseURL);
/**
* Document base URL where the current document is located.
*
* @property documentBaseURL
* @type String
*/
self.documentBaseURL = documentBaseURL;
/**
* Absolute baseURI for the installation path of TinyMCE.
*
* @property baseURI
* @type tinymce.util.URI
*/
self.baseURI = new URI(self.baseURL);
/**
* Current suffix to add to each plugin/theme that gets loaded for example ".min".
*
* @property suffix
* @type String
*/
self.suffix = suffix;
self.focusManager = new FocusManager(self);
},
/**
* Initializes a set of editors. This method will create editors based on various settings.
*
* @method init
* @param {Object} settings Settings object to be passed to each editor instance.
* @example
* // Initializes a editor using the longer method
* tinymce.EditorManager.init({
* some_settings : 'some value'
* });
*
* // Initializes a editor instance using the shorter version
* tinyMCE.init({
* some_settings : 'some value'
* });
*/
init: function(settings) {
var self = this, editors = [];
function createId(elm) {
var id = elm.id;
// Use element id, or unique name or generate a unique id
if (!id) {
id = elm.name;
if (id && !DOM.get(id)) {
id = elm.name;
} else {
// Generate unique name
id = DOM.uniqueId();
}
elm.setAttribute('id', id);
}
return id;
}
function createEditor(id, settings, targetElm) {
if (!purgeDestroyedEditor(self.get(id))) {
var editor = new Editor(id, settings, self);
editor.targetElm = editor.targetElm || targetElm;
editors.push(editor);
editor.render();
}
}
function execCallback(name) {
var callback = settings[name];
if (!callback) {
return;
}
return callback.apply(self, Array.prototype.slice.call(arguments, 2));
}
function hasClass(elm, className) {
return className.constructor === RegExp ? className.test(elm.className) : DOM.hasClass(elm, className);
}
function readyHandler() {
var l, co;
DOM.unbind(window, 'ready', readyHandler);
execCallback('onpageload');
if (settings.types) {
// Process type specific selector
each(settings.types, function(type) {
each(DOM.select(type.selector), function(elm) {
createEditor(createId(elm), extend({}, settings, type), elm);
});
});
return;
} else if (settings.selector) {
// Process global selector
each(DOM.select(settings.selector), function(elm) {
createEditor(createId(elm), settings, elm);
});
return;
} else if (settings.target) {
createEditor(createId(settings.target), settings);
}
// Fallback to old setting
switch (settings.mode) {
case "exact":
l = settings.elements || '';
if (l.length > 0) {
each(explode(l), function(id) {
var elm;
if ((elm = DOM.get(id))) {
createEditor(id, settings, elm);
} else {
each(document.forms, function(f) {
each(f.elements, function(e) {
if (e.name === id) {
id = 'mce_editor_' + instanceCounter++;
DOM.setAttrib(e, 'id', id);
createEditor(id, settings, e);
}
});
});
}
});
}
break;
case "textareas":
case "specific_textareas":
each(DOM.select('textarea'), function(elm) {
if (settings.editor_deselector && hasClass(elm, settings.editor_deselector)) {
return;
}
if (!settings.editor_selector || hasClass(elm, settings.editor_selector)) {
createEditor(createId(elm), settings, elm);
}
});
break;
}
// Call onInit when all editors are initialized
if (settings.oninit) {
l = co = 0;
each(editors, function(ed) {
co++;
if (!ed.initialized) {
// Wait for it
ed.on('init', function() {
l++;
// All done
if (l == co) {
execCallback('oninit');
}
});
} else {
l++;
}
// All done
if (l == co) {
execCallback('oninit');
}
});
}
}
self.settings = settings;
DOM.bind(window, 'ready', readyHandler);
},
/**
* Returns a editor instance by id.
*
* @method get
* @param {String/Number} id Editor instance id or index to return.
* @return {tinymce.Editor} Editor instance to return.
* @example
* // Adds an onclick event to an editor by id (shorter version)
* tinymce.get('mytextbox').on('click', function(e) {
* ed.windowManager.alert('Hello world!');
* });
*
* // Adds an onclick event to an editor by id (longer version)
* tinymce.EditorManager.get('mytextbox').on('click', function(e) {
* ed.windowManager.alert('Hello world!');
* });
*/
get: function(id) {
if (!arguments.length) {
return this.editors;
}
return id in this.editors ? this.editors[id] : null;
},
/**
* Adds an editor instance to the editor collection. This will also set it as the active editor.
*
* @method add
* @param {tinymce.Editor} editor Editor instance to add to the collection.
* @return {tinymce.Editor} The same instance that got passed in.
*/
add: function(editor) {
var self = this, editors = self.editors;
// Add named and index editor instance
editors[editor.id] = editor;
editors.push(editor);
// Doesn't call setActive method since we don't want
// to fire a bunch of activate/deactivate calls while initializing
self.activeEditor = editor;
/**
* Fires when an editor is added to the EditorManager collection.
*
* @event AddEditor
* @param {Object} e Event arguments.
*/
self.fire('AddEditor', {editor: editor});
if (!beforeUnloadDelegate) {
beforeUnloadDelegate = function() {
self.fire('BeforeUnload');
};
DOM.bind(window, 'beforeunload', beforeUnloadDelegate);
}
return editor;
},
/**
* Creates an editor instance and adds it to the EditorManager collection.
*
* @method createEditor
* @param {String} id Instance id to use for editor.
* @param {Object} settings Editor instance settings.
* @return {tinymce.Editor} Editor instance that got created.
*/
createEditor: function(id, settings) {
return this.add(new Editor(id, settings, this));
},
/**
* Removes a editor or editors form page.
*
* @example
* // Remove all editors bound to divs
* tinymce.remove('div');
*
* // Remove all editors bound to textareas
* tinymce.remove('textarea');
*
* // Remove all editors
* tinymce.remove();
*
* // Remove specific instance by id
* tinymce.remove('#id');
*
* @method remove
* @param {tinymce.Editor/String/Object} [selector] CSS selector or editor instance to remove.
* @return {tinymce.Editor} The editor that got passed in will be return if it was found otherwise null.
*/
remove: function(selector) {
var self = this, i, editors = self.editors, editor;
// Remove all editors
if (!selector) {
for (i = editors.length - 1; i >= 0; i--) {
self.remove(editors[i]);
}
return;
}
// Remove editors by selector
if (typeof selector == "string") {
selector = selector.selector || selector;
each(DOM.select(selector), function(elm) {
editor = editors[elm.id];
if (editor) {
self.remove(editor);
}
});
return;
}
// Remove specific editor
editor = selector;
// Not in the collection
if (!editors[editor.id]) {
return null;
}
/**
* Fires when an editor is removed from EditorManager collection.
*
* @event RemoveEditor
* @param {Object} e Event arguments.
*/
if (removeEditorFromList(editor)) {
self.fire('RemoveEditor', {editor: editor});
}
if (!editors.length) {
DOM.unbind(window, 'beforeunload', beforeUnloadDelegate);
}
editor.remove();
return editor;
},
/**
* Executes a specific command on the currently active editor.
*
* @method execCommand
* @param {String} c Command to perform for example Bold.
* @param {Boolean} u Optional boolean state if a UI should be presented for the command or not.
* @param {String} v Optional value parameter like for example an URL to a link.
* @return {Boolean} true/false if the command was executed or not.
*/
execCommand: function(cmd, ui, value) {
var self = this, editor = self.get(value);
// Manager commands
switch (cmd) {
case "mceAddEditor":
if (!self.get(value)) {
new Editor(value, self.settings, self).render();
}
return true;
case "mceRemoveEditor":
if (editor) {
editor.remove();
}
return true;
case 'mceToggleEditor':
if (!editor) {
self.execCommand('mceAddEditor', 0, value);
return true;
}
if (editor.isHidden()) {
editor.show();
} else {
editor.hide();
}
return true;
}
// Run command on active editor
if (self.activeEditor) {
return self.activeEditor.execCommand(cmd, ui, value);
}
return false;
},
/**
* Calls the save method on all editor instances in the collection. This can be useful when a form is to be submitted.
*
* @method triggerSave
* @example
* // Saves all contents
* tinyMCE.triggerSave();
*/
triggerSave: function() {
each(this.editors, function(editor) {
editor.save();
});
},
/**
* Adds a language pack, this gets called by the loaded language files like en.js.
*
* @method addI18n
* @param {String} code Optional language code.
* @param {Object} items Name/value object with translations.
*/
addI18n: function(code, items) {
I18n.add(code, items);
},
/**
* Translates the specified string using the language pack items.
*
* @method translate
* @param {String/Array/Object} text String to translate
* @return {String} Translated string.
*/
translate: function(text) {
return I18n.translate(text);
},
/**
* Sets the active editor instance and fires the deactivate/activate events.
*
* @method setActive
* @param {tinymce.Editor} editor Editor instance to set as the active instance.
*/
setActive: function(editor) {
var activeEditor = this.activeEditor;
if (this.activeEditor != editor) {
if (activeEditor) {
activeEditor.fire('deactivate', {relatedTarget: editor});
}
editor.fire('activate', {relatedTarget: activeEditor});
}
this.activeEditor = editor;
}
};
extend(EditorManager, Observable);
EditorManager.setup();
// Export EditorManager as tinymce/tinymce in global namespace
window.tinymce = window.tinyMCE = EditorManager;
return EditorManager;
});
// Included from: js/tinymce/classes/LegacyInput.js
/**
* LegacyInput.js
*
* Copyright, Moxiecode Systems AB
* Released under LGPL License.
*
* License: http://www.tinymce.com/license
* Contributing: http://www.tinymce.com/contributing
*/
define("tinymce/LegacyInput", [
"tinymce/EditorManager",
"tinymce/util/Tools"
], function(EditorManager, Tools) {
var each = Tools.each, explode = Tools.explode;
EditorManager.on('AddEditor', function(e) {
var editor = e.editor;
editor.on('preInit', function() {
var filters, fontSizes, dom, settings = editor.settings;
function replaceWithSpan(node, styles) {
each(styles, function(value, name) {
if (value) {
dom.setStyle(node, name, value);
}
});
dom.rename(node, 'span');
}
function convert(e) {
dom = editor.dom;
if (settings.convert_fonts_to_spans) {
each(dom.select('font,u,strike', e.node), function(node) {
filters[node.nodeName.toLowerCase()](dom, node);
});
}
}
if (settings.inline_styles) {
fontSizes = explode(settings.font_size_legacy_values);
filters = {
font: function(dom, node) {
replaceWithSpan(node, {
backgroundColor: node.style.backgroundColor,
color: node.color,
fontFamily: node.face,
fontSize: fontSizes[parseInt(node.size, 10) - 1]
});
},
u: function(dom, node) {
// HTML5 allows U element
if (editor.settings.schema === "html4") {
replaceWithSpan(node, {
textDecoration: 'underline'
});
}
},
strike: function(dom, node) {
replaceWithSpan(node, {
textDecoration: 'line-through'
});
}
};
editor.on('PreProcess SetContent', convert);
}
});
});
});
// Included from: js/tinymce/classes/util/XHR.js
/**
* XHR.js
*
* Copyright, Moxiecode Systems AB
* Released under LGPL License.
*
* License: http://www.tinymce.com/license
* Contributing: http://www.tinymce.com/contributing
*/
/**
* This class enables you to send XMLHTTPRequests cross browser.
* @class tinymce.util.XHR
* @mixes tinymce.util.Observable
* @static
* @example
* // Sends a low level Ajax request
* tinymce.util.XHR.send({
* url: 'someurl',
* success: function(text) {
* console.debug(text);
* }
* });
*
* // Add custom header to XHR request
* tinymce.util.XHR.on('beforeSend', function(e) {
* e.xhr.setRequestHeader('X-Requested-With', 'Something');
* });
*/
define("tinymce/util/XHR", [
"tinymce/util/Observable",
"tinymce/util/Tools"
], function(Observable, Tools) {
var XHR = {
/**
* Sends a XMLHTTPRequest.
* Consult the Wiki for details on what settings this method takes.
*
* @method send
* @param {Object} settings Object will target URL, callbacks and other info needed to make the request.
*/
send: function(settings) {
var xhr, count = 0;
function ready() {
if (!settings.async || xhr.readyState == 4 || count++ > 10000) {
if (settings.success && count < 10000 && xhr.status == 200) {
settings.success.call(settings.success_scope, '' + xhr.responseText, xhr, settings);
} else if (settings.error) {
settings.error.call(settings.error_scope, count > 10000 ? 'TIMED_OUT' : 'GENERAL', xhr, settings);
}
xhr = null;
} else {
setTimeout(ready, 10);
}
}
// Default settings
settings.scope = settings.scope || this;
settings.success_scope = settings.success_scope || settings.scope;
settings.error_scope = settings.error_scope || settings.scope;
settings.async = settings.async === false ? false : true;
settings.data = settings.data || '';
xhr = new XMLHttpRequest();
if (xhr) {
if (xhr.overrideMimeType) {
xhr.overrideMimeType(settings.content_type);
}
xhr.open(settings.type || (settings.data ? 'POST' : 'GET'), settings.url, settings.async);
if (settings.crossDomain) {
xhr.withCredentials = true;
}
if (settings.content_type) {
xhr.setRequestHeader('Content-Type', settings.content_type);
}
xhr.setRequestHeader('X-Requested-With', 'XMLHttpRequest');
xhr = XHR.fire('beforeSend', {xhr: xhr, settings: settings}).xhr;
xhr.send(settings.data);
// Syncronous request
if (!settings.async) {
return ready();
}
// Wait for response, onReadyStateChange can not be used since it leaks memory in IE
setTimeout(ready, 10);
}
}
};
Tools.extend(XHR, Observable);
return XHR;
});
// Included from: js/tinymce/classes/util/JSON.js
/**
* JSON.js
*
* Copyright, Moxiecode Systems AB
* Released under LGPL License.
*
* License: http://www.tinymce.com/license
* Contributing: http://www.tinymce.com/contributing
*/
/**
* JSON parser and serializer class.
*
* @class tinymce.util.JSON
* @static
* @example
* // JSON parse a string into an object
* var obj = tinymce.util.JSON.parse(somestring);
*
* // JSON serialize a object into an string
* var str = tinymce.util.JSON.serialize(obj);
*/
define("tinymce/util/JSON", [], function() {
function serialize(o, quote) {
var i, v, t, name;
quote = quote || '"';
if (o === null) {
return 'null';
}
t = typeof o;
if (t == 'string') {
v = '\bb\tt\nn\ff\rr\""\'\'\\\\';
return quote + o.replace(/([\u0080-\uFFFF\x00-\x1f\"\'\\])/g, function(a, b) {
// Make sure single quotes never get encoded inside double quotes for JSON compatibility
if (quote === '"' && a === "'") {
return a;
}
i = v.indexOf(b);
if (i + 1) {
return '\\' + v.charAt(i + 1);
}
a = b.charCodeAt().toString(16);
return '\\u' + '0000'.substring(a.length) + a;
}) + quote;
}
if (t == 'object') {
if (o.hasOwnProperty && Object.prototype.toString.call(o) === '[object Array]') {
for (i = 0, v = '['; i < o.length; i++) {
v += (i > 0 ? ',' : '') + serialize(o[i], quote);
}
return v + ']';
}
v = '{';
for (name in o) {
if (o.hasOwnProperty(name)) {
v += typeof o[name] != 'function' ? (v.length > 1 ? ',' + quote : quote) + name +
quote + ':' + serialize(o[name], quote) : '';
}
}
return v + '}';
}
return '' + o;
}
return {
/**
* Serializes the specified object as a JSON string.
*
* @method serialize
* @param {Object} obj Object to serialize as a JSON string.
* @param {String} quote Optional quote string defaults to ".
* @return {string} JSON string serialized from input.
*/
serialize: serialize,
/**
* Unserializes/parses the specified JSON string into a object.
*
* @method parse
* @param {string} s JSON String to parse into a JavaScript object.
* @return {Object} Object from input JSON string or undefined if it failed.
*/
parse: function(text) {
try {
// Trick uglify JS
return window[String.fromCharCode(101) + 'val']('(' + text + ')');
} catch (ex) {
// Ignore
}
}
/**#@-*/
};
});
// Included from: js/tinymce/classes/util/JSONRequest.js
/**
* JSONRequest.js
*
* Copyright, Moxiecode Systems AB
* Released under LGPL License.
*
* License: http://www.tinymce.com/license
* Contributing: http://www.tinymce.com/contributing
*/
/**
* This class enables you to use JSON-RPC to call backend methods.
*
* @class tinymce.util.JSONRequest
* @example
* var json = new tinymce.util.JSONRequest({
* url: 'somebackend.php'
* });
*
* // Send RPC call 1
* json.send({
* method: 'someMethod1',
* params: ['a', 'b'],
* success: function(result) {
* console.dir(result);
* }
* });
*
* // Send RPC call 2
* json.send({
* method: 'someMethod2',
* params: ['a', 'b'],
* success: function(result) {
* console.dir(result);
* }
* });
*/
define("tinymce/util/JSONRequest", [
"tinymce/util/JSON",
"tinymce/util/XHR",
"tinymce/util/Tools"
], function(JSON, XHR, Tools) {
var extend = Tools.extend;
function JSONRequest(settings) {
this.settings = extend({}, settings);
this.count = 0;
}
/**
* Simple helper function to send a JSON-RPC request without the need to initialize an object.
* Consult the Wiki API documentation for more details on what you can pass to this function.
*
* @method sendRPC
* @static
* @param {Object} o Call object where there are three field id, method and params this object should also contain callbacks etc.
*/
JSONRequest.sendRPC = function(o) {
return new JSONRequest().send(o);
};
JSONRequest.prototype = {
/**
* Sends a JSON-RPC call. Consult the Wiki API documentation for more details on what you can pass to this function.
*
* @method send
* @param {Object} args Call object where there are three field id, method and params this object should also contain callbacks etc.
*/
send: function(args) {
var ecb = args.error, scb = args.success;
args = extend(this.settings, args);
args.success = function(c, x) {
c = JSON.parse(c);
if (typeof c == 'undefined') {
c = {
error: 'JSON Parse error.'
};
}
if (c.error) {
ecb.call(args.error_scope || args.scope, c.error, x);
} else {
scb.call(args.success_scope || args.scope, c.result);
}
};
args.error = function(ty, x) {
if (ecb) {
ecb.call(args.error_scope || args.scope, ty, x);
}
};
args.data = JSON.serialize({
id: args.id || 'c' + (this.count++),
method: args.method,
params: args.params
});
// JSON content type for Ruby on rails. Bug: #1883287
args.content_type = 'application/json';
XHR.send(args);
}
};
return JSONRequest;
});
// Included from: js/tinymce/classes/util/JSONP.js
/**
* JSONP.js
*
* Copyright, Moxiecode Systems AB
* Released under LGPL License.
*
* License: http://www.tinymce.com/license
* Contributing: http://www.tinymce.com/contributing
*/
define("tinymce/util/JSONP", [
"tinymce/dom/DOMUtils"
], function(DOMUtils) {
return {
callbacks: {},
count: 0,
send: function(settings) {
var self = this, dom = DOMUtils.DOM, count = settings.count !== undefined ? settings.count : self.count;
var id = 'tinymce_jsonp_' + count;
self.callbacks[count] = function(json) {
dom.remove(id);
delete self.callbacks[count];
settings.callback(json);
};
dom.add(dom.doc.body, 'script', {
id: id,
src: settings.url,
type: 'text/javascript'
});
self.count++;
}
};
});
// Included from: js/tinymce/classes/util/LocalStorage.js
/**
* LocalStorage.js
*
* Copyright, Moxiecode Systems AB
* Released under LGPL License.
*
* License: http://www.tinymce.com/license
* Contributing: http://www.tinymce.com/contributing
*/
/**
* This class will simulate LocalStorage on IE 7 and return the native version on modern browsers.
* Storage is done using userData on IE 7 and a special serialization format. The format is designed
* to be as small as possible by making sure that the keys and values doesn't need to be encoded. This
* makes it possible to store for example HTML data.
*
* Storage format for userData:
* ,, ,,...
*
* For example this data key1=value1,key2=value2 would be:
* 4,key1,6,value1,4,key2,6,value2
*
* @class tinymce.util.LocalStorage
* @static
* @version 4.0
* @example
* tinymce.util.LocalStorage.setItem('key', 'value');
* var value = tinymce.util.LocalStorage.getItem('key');
*/
define("tinymce/util/LocalStorage", [], function() {
var LocalStorage, storageElm, items, keys, userDataKey, hasOldIEDataSupport;
// Check for native support
try {
if (window.localStorage) {
return localStorage;
}
} catch (ex) {
// Ignore
}
userDataKey = "tinymce";
storageElm = document.documentElement;
hasOldIEDataSupport = !!storageElm.addBehavior;
if (hasOldIEDataSupport) {
storageElm.addBehavior('#default#userData');
}
/**
* Gets the keys names and updates LocalStorage.length property. Since IE7 doesn't have any getters/setters.
*/
function updateKeys() {
keys = [];
for (var key in items) {
keys.push(key);
}
LocalStorage.length = keys.length;
}
/**
* Loads the userData string and parses it into the items structure.
*/
function load() {
var key, data, value, pos = 0;
items = {};
// localStorage can be disabled on WebKit/Gecko so make a dummy storage
if (!hasOldIEDataSupport) {
return;
}
function next(end) {
var value, nextPos;
nextPos = end !== undefined ? pos + end : data.indexOf(',', pos);
if (nextPos === -1 || nextPos > data.length) {
return null;
}
value = data.substring(pos, nextPos);
pos = nextPos + 1;
return value;
}
storageElm.load(userDataKey);
data = storageElm.getAttribute(userDataKey) || '';
do {
var offset = next();
if (offset === null) {
break;
}
key = next(parseInt(offset, 32) || 0);
if (key !== null) {
offset = next();
if (offset === null) {
break;
}
value = next(parseInt(offset, 32) || 0);
if (key) {
items[key] = value;
}
}
} while (key !== null);
updateKeys();
}
/**
* Saves the items structure into a the userData format.
*/
function save() {
var value, data = '';
// localStorage can be disabled on WebKit/Gecko so make a dummy storage
if (!hasOldIEDataSupport) {
return;
}
for (var key in items) {
value = items[key];
data += (data ? ',' : '') + key.length.toString(32) + ',' + key + ',' + value.length.toString(32) + ',' + value;
}
storageElm.setAttribute(userDataKey, data);
try {
storageElm.save(userDataKey);
} catch (ex) {
// Ignore disk full
}
updateKeys();
}
LocalStorage = {
/**
* Length of the number of items in storage.
*
* @property length
* @type Number
* @return {Number} Number of items in storage.
*/
//length:0,
/**
* Returns the key name by index.
*
* @method key
* @param {Number} index Index of key to return.
* @return {String} Key value or null if it wasn't found.
*/
key: function(index) {
return keys[index];
},
/**
* Returns the value if the specified key or null if it wasn't found.
*
* @method getItem
* @param {String} key Key of item to retrive.
* @return {String} Value of the specified item or null if it wasn't found.
*/
getItem: function(key) {
return key in items ? items[key] : null;
},
/**
* Sets the value of the specified item by it's key.
*
* @method setItem
* @param {String} key Key of the item to set.
* @param {String} value Value of the item to set.
*/
setItem: function(key, value) {
items[key] = "" + value;
save();
},
/**
* Removes the specified item by key.
*
* @method removeItem
* @param {String} key Key of item to remove.
*/
removeItem: function(key) {
delete items[key];
save();
},
/**
* Removes all items.
*
* @method clear
*/
clear: function() {
items = {};
save();
}
};
load();
return LocalStorage;
});
// Included from: js/tinymce/classes/Compat.js
/**
* Compat.js
*
* Copyright, Moxiecode Systems AB
* Released under LGPL License.
*
* License: http://www.tinymce.com/license
* Contributing: http://www.tinymce.com/contributing
*/
/**
* TinyMCE core class.
*
* @static
* @class tinymce
* @borrow-members tinymce.EditorManager
* @borrow-members tinymce.util.Tools
*/
define("tinymce/Compat", [
"tinymce/dom/DOMUtils",
"tinymce/dom/EventUtils",
"tinymce/dom/ScriptLoader",
"tinymce/AddOnManager",
"tinymce/util/Tools",
"tinymce/Env"
], function(DOMUtils, EventUtils, ScriptLoader, AddOnManager, Tools, Env) {
var tinymce = window.tinymce;
/**
* @property {tinymce.dom.DOMUtils} DOM Global DOM instance.
* @property {tinymce.dom.ScriptLoader} ScriptLoader Global ScriptLoader instance.
* @property {tinymce.AddOnManager} PluginManager Global PluginManager instance.
* @property {tinymce.AddOnManager} ThemeManager Global ThemeManager instance.
*/
tinymce.DOM = DOMUtils.DOM;
tinymce.ScriptLoader = ScriptLoader.ScriptLoader;
tinymce.PluginManager = AddOnManager.PluginManager;
tinymce.ThemeManager = AddOnManager.ThemeManager;
tinymce.dom = tinymce.dom || {};
tinymce.dom.Event = EventUtils.Event;
Tools.each(Tools, function(func, key) {
tinymce[key] = func;
});
Tools.each('isOpera isWebKit isIE isGecko isMac'.split(' '), function(name) {
tinymce[name] = Env[name.substr(2).toLowerCase()];
});
return {};
});
// Describe the different namespaces
/**
* Root level namespace this contains classes directly releated to the TinyMCE editor.
*
* @namespace tinymce
*/
/**
* Contains classes for handling the browsers DOM.
*
* @namespace tinymce.dom
*/
/**
* Contains html parser and serializer logic.
*
* @namespace tinymce.html
*/
/**
* Contains the different UI types such as buttons, listboxes etc.
*
* @namespace tinymce.ui
*/
/**
* Contains various utility classes such as json parser, cookies etc.
*
* @namespace tinymce.util
*/
// Included from: js/tinymce/classes/ui/Layout.js
/**
* Layout.js
*
* Copyright, Moxiecode Systems AB
* Released under LGPL License.
*
* License: http://www.tinymce.com/license
* Contributing: http://www.tinymce.com/contributing
*/
/**
* Base layout manager class.
*
* @class tinymce.ui.Layout
*/
define("tinymce/ui/Layout", [
"tinymce/util/Class",
"tinymce/util/Tools"
], function(Class, Tools) {
"use strict";
return Class.extend({
Defaults: {
firstControlClass: 'first',
lastControlClass: 'last'
},
/**
* Constructs a layout instance with the specified settings.
*
* @constructor
* @param {Object} settings Name/value object with settings.
*/
init: function(settings) {
this.settings = Tools.extend({}, this.Defaults, settings);
},
/**
* This method gets invoked before the layout renders the controls.
*
* @method preRender
* @param {tinymce.ui.Container} container Container instance to preRender.
*/
preRender: function(container) {
container.addClass(this.settings.containerClass, 'body');
},
/**
* Applies layout classes to the container.
*
* @private
*/
applyClasses: function(container) {
var self = this, settings = self.settings, items, firstClass, lastClass;
items = container.items().filter(':visible');
firstClass = settings.firstControlClass;
lastClass = settings.lastControlClass;
items.each(function(item) {
item.removeClass(firstClass).removeClass(lastClass);
if (settings.controlClass) {
item.addClass(settings.controlClass);
}
});
items.eq(0).addClass(firstClass);
items.eq(-1).addClass(lastClass);
},
/**
* Renders the specified container and any layout specific HTML.
*
* @method renderHtml
* @param {tinymce.ui.Container} container Container to render HTML for.
*/
renderHtml: function(container) {
var self = this, settings = self.settings, items, html = '';
items = container.items();
items.eq(0).addClass(settings.firstControlClass);
items.eq(-1).addClass(settings.lastControlClass);
items.each(function(item) {
if (settings.controlClass) {
item.addClass(settings.controlClass);
}
html += item.renderHtml();
});
return html;
},
/**
* Recalculates the positions of the controls in the specified container.
*
* @method recalc
* @param {tinymce.ui.Container} container Container instance to recalc.
*/
recalc: function() {
},
/**
* This method gets invoked after the layout renders the controls.
*
* @method postRender
* @param {tinymce.ui.Container} container Container instance to postRender.
*/
postRender: function() {
}
});
});
// Included from: js/tinymce/classes/ui/AbsoluteLayout.js
/**
* AbsoluteLayout.js
*
* Copyright, Moxiecode Systems AB
* Released under LGPL License.
*
* License: http://www.tinymce.com/license
* Contributing: http://www.tinymce.com/contributing
*/
/**
* LayoutManager for absolute positioning. This layout manager is more of
* a base class for other layouts but can be created and used directly.
*
* @-x-less AbsoluteLayout.less
* @class tinymce.ui.AbsoluteLayout
* @extends tinymce.ui.Layout
*/
define("tinymce/ui/AbsoluteLayout", [
"tinymce/ui/Layout"
], function(Layout) {
"use strict";
return Layout.extend({
Defaults: {
containerClass: 'abs-layout',
controlClass: 'abs-layout-item'
},
/**
* Recalculates the positions of the controls in the specified container.
*
* @method recalc
* @param {tinymce.ui.Container} container Container instance to recalc.
*/
recalc: function(container) {
container.items().filter(':visible').each(function(ctrl) {
var settings = ctrl.settings;
ctrl.layoutRect({
x: settings.x,
y: settings.y,
w: settings.w,
h: settings.h
});
if (ctrl.recalc) {
ctrl.recalc();
}
});
},
/**
* Renders the specified container and any layout specific HTML.
*
* @method renderHtml
* @param {tinymce.ui.Container} container Container to render HTML for.
*/
renderHtml: function(container) {
return '' + this._super(container);
}
});
});
// Included from: js/tinymce/classes/ui/Tooltip.js
/**
* Tooltip.js
*
* Copyright, Moxiecode Systems AB
* Released under LGPL License.
*
* License: http://www.tinymce.com/license
* Contributing: http://www.tinymce.com/contributing
*/
/**
* Creates a tooltip instance.
*
* @-x-less ToolTip.less
* @class tinymce.ui.ToolTip
* @extends tinymce.ui.Control
* @mixes tinymce.ui.Movable
*/
define("tinymce/ui/Tooltip", [
"tinymce/ui/Control",
"tinymce/ui/Movable"
], function(Control, Movable) {
return Control.extend({
Mixins: [Movable],
Defaults: {
classes: 'widget tooltip tooltip-n'
},
/**
* Sets/gets the current label text.
*
* @method text
* @param {String} [text] New label text.
* @return {String|tinymce.ui.Tooltip} Current text or current label instance.
*/
text: function(value) {
var self = this;
if (typeof value != "undefined") {
self._value = value;
if (self._rendered) {
self.getEl().lastChild.innerHTML = self.encode(value);
}
return self;
}
return self._value;
},
/**
* Renders the control as a HTML string.
*
* @method renderHtml
* @return {String} HTML representing the control.
*/
renderHtml: function() {
var self = this, prefix = self.classPrefix;
return (
'' +
'' +
'' + self.encode(self._text) + '' +
''
);
},
/**
* Repaints the control after a layout operation.
*
* @method repaint
*/
repaint: function() {
var self = this, style, rect;
style = self.getEl().style;
rect = self._layoutRect;
style.left = rect.x + 'px';
style.top = rect.y + 'px';
style.zIndex = 0xFFFF + 0xFFFF;
}
});
});
// Included from: js/tinymce/classes/ui/Widget.js
/**
* Widget.js
*
* Copyright, Moxiecode Systems AB
* Released under LGPL License.
*
* License: http://www.tinymce.com/license
* Contributing: http://www.tinymce.com/contributing
*/
/**
* Widget base class a widget is a control that has a tooltip and some basic states.
*
* @class tinymce.ui.Widget
* @extends tinymce.ui.Control
*/
define("tinymce/ui/Widget", [
"tinymce/ui/Control",
"tinymce/ui/Tooltip"
], function(Control, Tooltip) {
"use strict";
var tooltip;
var Widget = Control.extend({
/**
* Constructs a instance with the specified settings.
*
* @constructor
* @param {Object} settings Name/value object with settings.
* @setting {String} tooltip Tooltip text to display when hovering.
* @setting {Boolean} autofocus True if the control should be focused when rendered.
* @setting {String} text Text to display inside widget.
*/
init: function(settings) {
var self = this;
self._super(settings);
settings = self.settings;
self.canFocus = true;
if (settings.tooltip && Widget.tooltips !== false) {
self.on('mouseenter', function(e) {
var tooltip = self.tooltip().moveTo(-0xFFFF);
if (e.control == self) {
var rel = tooltip.text(settings.tooltip).show().testMoveRel(self.getEl(), ['bc-tc', 'bc-tl', 'bc-tr']);
tooltip.toggleClass('tooltip-n', rel == 'bc-tc');
tooltip.toggleClass('tooltip-nw', rel == 'bc-tl');
tooltip.toggleClass('tooltip-ne', rel == 'bc-tr');
tooltip.moveRel(self.getEl(), rel);
} else {
tooltip.hide();
}
});
self.on('mouseleave mousedown click', function() {
self.tooltip().hide();
});
}
self.aria('label', settings.ariaLabel || settings.tooltip);
},
/**
* Returns the current tooltip instance.
*
* @method tooltip
* @return {tinymce.ui.Tooltip} Tooltip instance.
*/
tooltip: function() {
if (!tooltip) {
tooltip = new Tooltip({type: 'tooltip'});
tooltip.renderTo();
}
return tooltip;
},
/**
* Sets/gets the active state of the widget.
*
* @method active
* @param {Boolean} [state] State if the control is active.
* @return {Boolean|tinymce.ui.Widget} True/false or current widget instance.
*/
active: function(state) {
var self = this, undef;
if (state !== undef) {
self.aria('pressed', state);
self.toggleClass('active', state);
}
return self._super(state);
},
/**
* Sets/gets the disabled state of the widget.
*
* @method disabled
* @param {Boolean} [state] State if the control is disabled.
* @return {Boolean|tinymce.ui.Widget} True/false or current widget instance.
*/
disabled: function(state) {
var self = this, undef;
if (state !== undef) {
self.aria('disabled', state);
self.toggleClass('disabled', state);
}
return self._super(state);
},
/**
* Called after the control has been rendered.
*
* @method postRender
*/
postRender: function() {
var self = this, settings = self.settings;
self._rendered = true;
self._super();
if (!self.parent() && (settings.width || settings.height)) {
self.initLayoutRect();
self.repaint();
}
if (settings.autofocus) {
self.focus();
}
},
/**
* Removes the current control from DOM and from UI collections.
*
* @method remove
* @return {tinymce.ui.Control} Current control instance.
*/
remove: function() {
this._super();
if (tooltip) {
tooltip.remove();
tooltip = null;
}
}
});
return Widget;
});
// Included from: js/tinymce/classes/ui/Button.js
/**
* Button.js
*
* Copyright, Moxiecode Systems AB
* Released under LGPL License.
*
* License: http://www.tinymce.com/license
* Contributing: http://www.tinymce.com/contributing
*/
/**
* This class is used to create buttons. You can create them directly or through the Factory.
*
* @example
* // Create and render a button to the body element
* tinymce.ui.Factory.create({
* type: 'button',
* text: 'My button'
* }).renderTo(document.body);
*
* @-x-less Button.less
* @class tinymce.ui.Button
* @extends tinymce.ui.Widget
*/
define("tinymce/ui/Button", [
"tinymce/ui/Widget"
], function(Widget) {
"use strict";
return Widget.extend({
Defaults: {
classes: "widget btn",
role: "button"
},
/**
* Constructs a new button instance with the specified settings.
*
* @constructor
* @param {Object} settings Name/value object with settings.
* @setting {String} size Size of the button small|medium|large.
* @setting {String} image Image to use for icon.
* @setting {String} icon Icon to use for button.
*/
init: function(settings) {
var self = this, size;
self.on('click mousedown', function(e) {
e.preventDefault();
});
self._super(settings);
size = settings.size;
if (settings.subtype) {
self.addClass(settings.subtype);
}
if (size) {
self.addClass('btn-' + size);
}
},
/**
* Sets/gets the current button icon.
*
* @method icon
* @param {String} [icon] New icon identifier.
* @return {String|tinymce.ui.MenuButton} Current icon or current MenuButton instance.
*/
icon: function(icon) {
var self = this, prefix = self.classPrefix;
if (typeof icon == 'undefined') {
return self.settings.icon;
}
self.settings.icon = icon;
icon = icon ? prefix + 'ico ' + prefix + 'i-' + self.settings.icon : '';
if (self._rendered) {
var btnElm = self.getEl().firstChild, iconElm = btnElm.getElementsByTagName('i')[0];
if (icon) {
if (!iconElm || iconElm != btnElm.firstChild) {
iconElm = document.createElement('i');
btnElm.insertBefore(iconElm, btnElm.firstChild);
}
iconElm.className = icon;
} else if (iconElm) {
btnElm.removeChild(iconElm);
}
self.text(self._text); // Set text again to fix whitespace between icon + text
}
return self;
},
/**
* Repaints the button for example after it's been resizes by a layout engine.
*
* @method repaint
*/
repaint: function() {
var btnStyle = this.getEl().firstChild.style;
btnStyle.width = btnStyle.height = "100%";
this._super();
},
/**
* Sets/gets the current button text.
*
* @method text
* @param {String} [text] New button text.
* @return {String|tinymce.ui.Button} Current text or current Button instance.
*/
text: function(text) {
var self = this;
if (self._rendered) {
var textNode = self.getEl().lastChild.lastChild;
if (textNode) {
textNode.data = self.translate(text);
}
}
return self._super(text);
},
/**
* Renders the control as a HTML string.
*
* @method renderHtml
* @return {String} HTML representing the control.
*/
renderHtml: function() {
var self = this, id = self._id, prefix = self.classPrefix;
var icon = self.settings.icon, image;
image = self.settings.image;
if (image) {
icon = 'none';
// Support for [high dpi, low dpi] image sources
if (typeof image != "string") {
image = window.getSelection ? image[0] : image[1];
}
image = ' style="background-image: url(\'' + image + '\')"';
} else {
image = '';
}
icon = self.settings.icon ? prefix + 'ico ' + prefix + 'i-' + icon : '';
return (
'' +
'' +
''
);
}
});
});
// Included from: js/tinymce/classes/ui/ButtonGroup.js
/**
* ButtonGroup.js
*
* Copyright, Moxiecode Systems AB
* Released under LGPL License.
*
* License: http://www.tinymce.com/license
* Contributing: http://www.tinymce.com/contributing
*/
/**
* This control enables you to put multiple buttons into a group. This is
* useful when you want to combine similar toolbar buttons into a group.
*
* @example
* // Create and render a buttongroup with two buttons to the body element
* tinymce.ui.Factory.create({
* type: 'buttongroup',
* items: [
* {text: 'Button A'},
* {text: 'Button B'}
* ]
* }).renderTo(document.body);
*
* @-x-less ButtonGroup.less
* @class tinymce.ui.ButtonGroup
* @extends tinymce.ui.Container
*/
define("tinymce/ui/ButtonGroup", [
"tinymce/ui/Container"
], function(Container) {
"use strict";
return Container.extend({
Defaults: {
defaultType: 'button',
role: 'group'
},
/**
* Renders the control as a HTML string.
*
* @method renderHtml
* @return {String} HTML representing the control.
*/
renderHtml: function() {
var self = this, layout = self._layout;
self.addClass('btn-group');
self.preRender();
layout.preRender(self);
return (
'' +
'' +
(self.settings.html || '') + layout.renderHtml(self) +
'' +
''
);
}
});
});
// Included from: js/tinymce/classes/ui/Checkbox.js
/**
* Checkbox.js
*
* Copyright, Moxiecode Systems AB
* Released under LGPL License.
*
* License: http://www.tinymce.com/license
* Contributing: http://www.tinymce.com/contributing
*/
/**
* This control creates a custom checkbox.
*
* @example
* // Create and render a checkbox to the body element
* tinymce.ui.Factory.create({
* type: 'checkbox',
* checked: true,
* text: 'My checkbox'
* }).renderTo(document.body);
*
* @-x-less Checkbox.less
* @class tinymce.ui.Checkbox
* @extends tinymce.ui.Widget
*/
define("tinymce/ui/Checkbox", [
"tinymce/ui/Widget"
], function(Widget) {
"use strict";
return Widget.extend({
Defaults: {
classes: "checkbox",
role: "checkbox",
checked: false
},
/**
* Constructs a new Checkbox instance with the specified settings.
*
* @constructor
* @param {Object} settings Name/value object with settings.
* @setting {Boolean} checked True if the checkbox should be checked by default.
*/
init: function(settings) {
var self = this;
self._super(settings);
self.on('click mousedown', function(e) {
e.preventDefault();
});
self.on('click', function(e) {
e.preventDefault();
if (!self.disabled()) {
self.checked(!self.checked());
}
});
self.checked(self.settings.checked);
},
/**
* Getter/setter function for the checked state.
*
* @method checked
* @param {Boolean} [state] State to be set.
* @return {Boolean|tinymce.ui.Checkbox} True/false or checkbox if it's a set operation.
*/
checked: function(state) {
var self = this;
if (typeof state != "undefined") {
if (state) {
self.addClass('checked');
} else {
self.removeClass('checked');
}
self._checked = state;
self.aria('checked', state);
return self;
}
return self._checked;
},
/**
* Getter/setter function for the value state.
*
* @method value
* @param {Boolean} [state] State to be set.
* @return {Boolean|tinymce.ui.Checkbox} True/false or checkbox if it's a set operation.
*/
value: function(state) {
return this.checked(state);
},
/**
* Renders the control as a HTML string.
*
* @method renderHtml
* @return {String} HTML representing the control.
*/
renderHtml: function() {
var self = this, id = self._id, prefix = self.classPrefix;
return (
'' +
'' +
'' + self.encode(self._text) + '' +
''
);
}
});
});
// Included from: js/tinymce/classes/ui/ComboBox.js
/**
* ComboBox.js
*
* Copyright, Moxiecode Systems AB
* Released under LGPL License.
*
* License: http://www.tinymce.com/license
* Contributing: http://www.tinymce.com/contributing
*/
/**
* This class creates a combobox control. Select box that you select a value from or
* type a value into.
*
* @-x-less ComboBox.less
* @class tinymce.ui.ComboBox
* @extends tinymce.ui.Widget
*/
define("tinymce/ui/ComboBox", [
"tinymce/ui/Widget",
"tinymce/ui/Factory",
"tinymce/ui/DomUtils"
], function(Widget, Factory, DomUtils) {
"use strict";
return Widget.extend({
/**
* Constructs a new control instance with the specified settings.
*
* @constructor
* @param {Object} settings Name/value object with settings.
* @setting {String} placeholder Placeholder text to display.
*/
init: function(settings) {
var self = this;
self._super(settings);
self.addClass('combobox');
self.subinput = true;
self.ariaTarget = 'inp'; // TODO: Figure out a better way
settings = self.settings;
settings.menu = settings.menu || settings.values;
if (settings.menu) {
settings.icon = 'caret';
}
self.on('click', function(e) {
var elm = e.target, root = self.getEl();
while (elm && elm != root) {
if (elm.id && elm.id.indexOf('-open') != -1) {
self.fire('action');
if (settings.menu) {
self.showMenu();
if (e.aria) {
self.menu.items()[0].focus();
}
}
}
elm = elm.parentNode;
}
});
// TODO: Rework this
self.on('keydown', function(e) {
if (e.target.nodeName == "INPUT" && e.keyCode == 13) {
self.parents().reverse().each(function(ctrl) {
e.preventDefault();
self.fire('change');
if (ctrl.hasEventListeners('submit') && ctrl.toJSON) {
ctrl.fire('submit', {data: ctrl.toJSON()});
return false;
}
});
}
});
if (settings.placeholder) {
self.addClass('placeholder');
self.on('focusin', function() {
if (!self._hasOnChange) {
DomUtils.on(self.getEl('inp'), 'change', function() {
self.fire('change');
});
self._hasOnChange = true;
}
if (self.hasClass('placeholder')) {
self.getEl('inp').value = '';
self.removeClass('placeholder');
}
});
self.on('focusout', function() {
if (self.value().length === 0) {
self.getEl('inp').value = settings.placeholder;
self.addClass('placeholder');
}
});
}
},
showMenu: function() {
var self = this, settings = self.settings, menu;
if (!self.menu) {
menu = settings.menu || [];
// Is menu array then auto constuct menu control
if (menu.length) {
menu = {
type: 'menu',
items: menu
};
} else {
menu.type = menu.type || 'menu';
}
self.menu = Factory.create(menu).parent(self).renderTo(self.getContainerElm());
self.fire('createmenu');
self.menu.reflow();
self.menu.on('cancel', function(e) {
if (e.control === self.menu) {
self.focus();
}
});
self.menu.on('show hide', function(e) {
e.control.items().each(function(ctrl) {
ctrl.active(ctrl.value() == self.value());
});
}).fire('show');
self.menu.on('select', function(e) {
self.value(e.control.value());
});
self.on('focusin', function(e) {
if (e.target.tagName.toUpperCase() == 'INPUT') {
self.menu.hide();
}
});
self.aria('expanded', true);
}
self.menu.show();
self.menu.layoutRect({w: self.layoutRect().w});
self.menu.moveRel(self.getEl(), self.isRtl() ? ['br-tr', 'tr-br'] : ['bl-tl', 'tl-bl']);
},
/**
* Getter/setter function for the control value.
*
* @method value
* @param {String} [value] Value to be set.
* @return {String|tinymce.ui.ComboBox} Value or self if it's a set operation.
*/
value: function(value) {
var self = this;
if (typeof value != "undefined") {
self._value = value;
self.removeClass('placeholder');
if (self._rendered) {
self.getEl('inp').value = value;
}
return self;
}
if (self._rendered) {
value = self.getEl('inp').value;
if (value != self.settings.placeholder) {
return value;
}
return '';
}
return self._value;
},
/**
* Getter/setter function for the disabled state.
*
* @method value
* @param {Boolean} [state] State to be set.
* @return {Boolean|tinymce.ui.ComboBox} True/false or self if it's a set operation.
*/
disabled: function(state) {
var self = this;
if (self._rendered && typeof state != 'undefined') {
self.getEl('inp').disabled = state;
}
return self._super(state);
},
/**
* Focuses the input area of the control.
*
* @method focus
*/
focus: function() {
this.getEl('inp').focus();
},
/**
* Repaints the control after a layout operation.
*
* @method repaint
*/
repaint: function() {
var self = this, elm = self.getEl(), openElm = self.getEl('open'), rect = self.layoutRect();
var width, lineHeight;
if (openElm) {
width = rect.w - DomUtils.getSize(openElm).width - 10;
} else {
width = rect.w - 10;
}
// Detect old IE 7+8 add lineHeight to align caret vertically in the middle
var doc = document;
if (doc.all && (!doc.documentMode || doc.documentMode <= 8)) {
lineHeight = (self.layoutRect().h - 2) + 'px';
}
DomUtils.css(elm.firstChild, {
width: width,
lineHeight: lineHeight
});
self._super();
return self;
},
/**
* Post render method. Called after the control has been rendered to the target.
*
* @method postRender
* @return {tinymce.ui.ComboBox} Current combobox instance.
*/
postRender: function() {
var self = this;
DomUtils.on(this.getEl('inp'), 'change', function() {
self.fire('change');
});
return self._super();
},
remove: function() {
DomUtils.off(this.getEl('inp'));
this._super();
},
/**
* Renders the control as a HTML string.
*
* @method renderHtml
* @return {String} HTML representing the control.
*/
renderHtml: function() {
var self = this, id = self._id, settings = self.settings, prefix = self.classPrefix;
var value = settings.value || settings.placeholder || '';
var icon, text, openBtnHtml = '', extraAttrs = '';
if ("spellcheck" in settings) {
extraAttrs += ' spellcheck="' + settings.spellcheck + '"';
}
if (settings.maxLength) {
extraAttrs += ' maxlength="' + settings.maxLength + '"';
}
if (settings.size) {
extraAttrs += ' size="' + settings.size + '"';
}
if (settings.subtype) {
extraAttrs += ' type="' + settings.subtype + '"';
}
if (self.disabled()) {
extraAttrs += ' disabled="disabled"';
}
icon = settings.icon;
if (icon && icon != 'caret') {
icon = prefix + 'ico ' + prefix + 'i-' + settings.icon;
}
text = self._text;
if (icon || text) {
openBtnHtml = (
'' +
'' +
''
);
self.addClass('has-open');
}
return (
'' +
'' +
openBtnHtml +
''
);
}
});
});
// Included from: js/tinymce/classes/ui/ColorBox.js
/**
* ColorBox.js
*
* Copyright, Moxiecode Systems AB
* Released under LGPL License.
*
* License: http://www.tinymce.com/license
* Contributing: http://www.tinymce.com/contributing
*/
/**
* This widget lets you enter colors and browse for colors by pressing the color button. It also displays
* a preview of the current color.
*
* @-x-less ColorBox.less
* @class tinymce.ui.ColorBox
* @extends tinymce.ui.ComboBox
*/
define("tinymce/ui/ColorBox", [
"tinymce/ui/ComboBox"
], function(ComboBox) {
"use strict";
return ComboBox.extend({
/**
* Constructs a new control instance with the specified settings.
*
* @constructor
* @param {Object} settings Name/value object with settings.
*/
init: function(settings) {
var self = this;
settings.spellcheck = false;
if (settings.onaction) {
settings.icon = 'none';
}
self._super(settings);
self.addClass('colorbox');
self.on('change keyup postrender', function() {
self.repaintColor(self.value());
});
},
repaintColor: function(value) {
var elm = this.getEl().getElementsByTagName('i')[0];
if (elm) {
try {
elm.style.background = value;
} catch (ex) {
// Ignore
}
}
},
value: function(value) {
var self = this;
if (typeof value != "undefined") {
if (self._rendered) {
self.repaintColor(value);
}
}
return self._super(value);
}
});
});
// Included from: js/tinymce/classes/ui/PanelButton.js
/**
* PanelButton.js
*
* Copyright, Moxiecode Systems AB
* Released under LGPL License.
*
* License: http://www.tinymce.com/license
* Contributing: http://www.tinymce.com/contributing
*/
/**
* Creates a new panel button.
*
* @class tinymce.ui.PanelButton
* @extends tinymce.ui.Button
*/
define("tinymce/ui/PanelButton", [
"tinymce/ui/Button",
"tinymce/ui/FloatPanel"
], function(Button, FloatPanel) {
"use strict";
return Button.extend({
/**
* Shows the panel for the button.
*
* @method showPanel
*/
showPanel: function() {
var self = this, settings = self.settings;
self.active(true);
if (!self.panel) {
var panelSettings = settings.panel;
// Wrap panel in grid layout if type if specified
// This makes it possible to add forms or other containers directly in the panel option
if (panelSettings.type) {
panelSettings = {
layout: 'grid',
items: panelSettings
};
}
panelSettings.role = panelSettings.role || 'dialog';
panelSettings.popover = true;
panelSettings.autohide = true;
panelSettings.ariaRoot = true;
self.panel = new FloatPanel(panelSettings).on('hide', function() {
self.active(false);
}).on('cancel', function(e) {
e.stopPropagation();
self.focus();
self.hidePanel();
}).parent(self).renderTo(self.getContainerElm());
self.panel.fire('show');
self.panel.reflow();
} else {
self.panel.show();
}
self.panel.moveRel(self.getEl(), settings.popoverAlign || (self.isRtl() ? ['bc-tr', 'bc-tc'] : ['bc-tl', 'bc-tc']));
},
/**
* Hides the panel for the button.
*
* @method hidePanel
*/
hidePanel: function() {
var self = this;
if (self.panel) {
self.panel.hide();
}
},
/**
* Called after the control has been rendered.
*
* @method postRender
*/
postRender: function() {
var self = this;
self.aria('haspopup', true);
self.on('click', function(e) {
if (e.control === self) {
if (self.panel && self.panel.visible()) {
self.hidePanel();
} else {
self.showPanel();
self.panel.focus(!!e.aria);
}
}
});
return self._super();
},
remove: function() {
if (this.panel) {
this.panel.remove();
this.panel = null;
}
return this._super();
}
});
});
// Included from: js/tinymce/classes/ui/ColorButton.js
/**
* ColorButton.js
*
* Copyright, Moxiecode Systems AB
* Released under LGPL License.
*
* License: http://www.tinymce.com/license
* Contributing: http://www.tinymce.com/contributing
*/
/**
* This class creates a color button control. This is a split button in which the main
* button has a visual representation of the currently selected color. When clicked
* the caret button displays a color picker, allowing the user to select a new color.
*
* @-x-less ColorButton.less
* @class tinymce.ui.ColorButton
* @extends tinymce.ui.PanelButton
*/
define("tinymce/ui/ColorButton", [
"tinymce/ui/PanelButton",
"tinymce/dom/DOMUtils"
], function(PanelButton, DomUtils) {
"use strict";
var DOM = DomUtils.DOM;
return PanelButton.extend({
/**
* Constructs a new ColorButton instance with the specified settings.
*
* @constructor
* @param {Object} settings Name/value object with settings.
*/
init: function(settings) {
this._super(settings);
this.addClass('colorbutton');
},
/**
* Getter/setter for the current color.
*
* @method color
* @param {String} [color] Color to set.
* @return {String|tinymce.ui.ColorButton} Current color or current instance.
*/
color: function(color) {
if (color) {
this._color = color;
this.getEl('preview').style.backgroundColor = color;
return this;
}
return this._color;
},
/**
* Resets the current color.
*
* @method resetColor
* @return {tinymce.ui.ColorButton} Current instance.
*/
resetColor: function() {
this._color = null;
this.getEl('preview').style.backgroundColor = null;
return this;
},
/**
* Renders the control as a HTML string.
*
* @method renderHtml
* @return {String} HTML representing the control.
*/
renderHtml: function() {
var self = this, id = self._id, prefix = self.classPrefix;
var icon = self.settings.icon ? prefix + 'ico ' + prefix + 'i-' + self.settings.icon : '';
var image = self.settings.image ? ' style="background-image: url(\'' + self.settings.image + '\')"' : '';
return (
'' +
'' +
'' +
''
);
},
/**
* Called after the control has been rendered.
*
* @method postRender
*/
postRender: function() {
var self = this, onClickHandler = self.settings.onclick;
self.on('click', function(e) {
if (e.aria && e.aria.key == 'down') {
return;
}
if (e.control == self && !DOM.getParent(e.target, '.' + self.classPrefix + 'open')) {
e.stopImmediatePropagation();
onClickHandler.call(self, e);
}
});
delete self.settings.onclick;
return self._super();
}
});
});
// Included from: js/tinymce/classes/util/Color.js
/**
* Color.js
*
* Copyright, Moxiecode Systems AB
* Released under LGPL License.
*
* License: http://www.tinymce.com/license
* Contributing: http://www.tinymce.com/contributing
*/
/**
* This class lets you parse/serialize colors and convert rgb/hsb.
*
* @class tinymce.util.Color
* @example
* var white = new tinymce.util.Color({r: 255, g: 255, b: 255});
* var red = new tinymce.util.Color('#FF0000');
*
* console.log(white.toHex(), red.toHsv());
*/
define("tinymce/util/Color", [], function() {
var min = Math.min, max = Math.max, round = Math.round;
/**
* Constructs a new color instance.
*
* @constructor
* @method Color
* @param {String} value Optional initial value to parse.
*/
function Color(value) {
var self = this, r = 0, g = 0, b = 0;
function rgb2hsv(r, g, b) {
var h, s, v, d, minRGB, maxRGB;
h = 0;
s = 0;
v = 0;
r = r / 255;
g = g / 255;
b = b / 255;
minRGB = min(r, min(g, b));
maxRGB = max(r, max(g, b));
if (minRGB == maxRGB) {
v = minRGB;
return {
h: 0,
s: 0,
v: v * 100
};
}
/*eslint no-nested-ternary:0 */
d = (r == minRGB) ? g - b : ((b == minRGB) ? r - g : b - r);
h = (r == minRGB) ? 3 : ((b == minRGB) ? 1 : 5);
h = 60 * (h - d / (maxRGB - minRGB));
s = (maxRGB - minRGB) / maxRGB;
v = maxRGB;
return {
h: round(h),
s: round(s * 100),
v: round(v * 100)
};
}
function hsvToRgb(hue, saturation, brightness) {
var side, chroma, x, match;
hue = (parseInt(hue, 10) || 0) % 360;
saturation = parseInt(saturation, 10) / 100;
brightness = parseInt(brightness, 10) / 100;
saturation = max(0, min(saturation, 1));
brightness = max(0, min(brightness, 1));
if (saturation === 0) {
r = g = b = round(255 * brightness);
return;
}
side = hue / 60;
chroma = brightness * saturation;
x = chroma * (1 - Math.abs(side % 2 - 1));
match = brightness - chroma;
switch (Math.floor(side)) {
case 0:
r = chroma;
g = x;
b = 0;
break;
case 1:
r = x;
g = chroma;
b = 0;
break;
case 2:
r = 0;
g = chroma;
b = x;
break;
case 3:
r = 0;
g = x;
b = chroma;
break;
case 4:
r = x;
g = 0;
b = chroma;
break;
case 5:
r = chroma;
g = 0;
b = x;
break;
default:
r = g = b = 0;
}
r = round(255 * (r + match));
g = round(255 * (g + match));
b = round(255 * (b + match));
}
/**
* Returns the hex string of the current color. For example: #ff00ff
*
* @method toHex
* @return {String} Hex string of current color.
*/
function toHex() {
function hex(val) {
val = parseInt(val, 10).toString(16);
return val.length > 1 ? val : '0' + val;
}
return '#' + hex(r) + hex(g) + hex(b);
}
/**
* Returns the r, g, b values of the color. Each channel has a range from 0-255.
*
* @method toRgb
* @return {Object} Object with r, g, b fields.
*/
function toRgb() {
return {
r: r,
g: g,
b: b
};
}
/**
* Returns the h, s, v values of the color. Ranges: h=0-360, s=0-100, v=0-100.
*
* @method toHsv
* @return {Object} Object with h, s, v fields.
*/
function toHsv() {
return rgb2hsv(r, g, b);
}
/**
* Parses the specified value and populates the color instance.
*
* Supported format examples:
* * rbg(255,0,0)
* * #ff0000
* * #fff
* * {r: 255, g: 0, b: 0}
* * {h: 360, s: 100, v: 100}
*
* @method parse
* @param {Object/String} value Color value to parse.
* @return {tinymce.util.Color} Current color instance.
*/
function parse(value) {
var matches;
if (typeof value == 'object') {
if ("r" in value) {
r = value.r;
g = value.g;
b = value.b;
} else if ("v" in value) {
hsvToRgb(value.h, value.s, value.v);
}
} else {
if ((matches = /rgb\s*\(\s*([0-9]+)\s*,\s*([0-9]+)\s*,\s*([0-9]+)[^\)]*\)/gi.exec(value))) {
r = parseInt(matches[1], 10);
g = parseInt(matches[2], 10);
b = parseInt(matches[3], 10);
} else if ((matches = /#([0-F]{2})([0-F]{2})([0-F]{2})/gi.exec(value))) {
r = parseInt(matches[1], 16);
g = parseInt(matches[2], 16);
b = parseInt(matches[3], 16);
} else if ((matches = /#([0-F])([0-F])([0-F])/gi.exec(value))) {
r = parseInt(matches[1] + matches[1], 16);
g = parseInt(matches[2] + matches[2], 16);
b = parseInt(matches[3] + matches[3], 16);
}
}
r = r < 0 ? 0 : (r > 255 ? 255 : r);
g = g < 0 ? 0 : (g > 255 ? 255 : g);
b = b < 0 ? 0 : (b > 255 ? 255 : b);
return self;
}
if (value) {
parse(value);
}
self.toRgb = toRgb;
self.toHsv = toHsv;
self.toHex = toHex;
self.parse = parse;
}
return Color;
});
// Included from: js/tinymce/classes/ui/ColorPicker.js
/**
* ColorPicker.js
*
* Copyright, Moxiecode Systems AB
* Released under LGPL License.
*
* License: http://www.tinymce.com/license
* Contributing: http://www.tinymce.com/contributing
*/
/**
* Color picker widget lets you select colors.
*
* @-x-less ColorPicker.less
* @class tinymce.ui.ColorPicker
* @extends tinymce.ui.Widget
*/
define("tinymce/ui/ColorPicker", [
"tinymce/ui/Widget",
"tinymce/ui/DragHelper",
"tinymce/ui/DomUtils",
"tinymce/util/Color"
], function(Widget, DragHelper, DomUtils, Color) {
"use strict";
return Widget.extend({
Defaults: {
classes: "widget colorpicker"
},
/**
* Constructs a new colorpicker instance with the specified settings.
*
* @constructor
* @param {Object} settings Name/value object with settings.
* @setting {String} color Initial color value.
*/
init: function(settings) {
this._super(settings);
},
postRender: function() {
var self = this, color = self.color(), hsv, hueRootElm, huePointElm, svRootElm, svPointElm;
hueRootElm = self.getEl('h');
huePointElm = self.getEl('hp');
svRootElm = self.getEl('sv');
svPointElm = self.getEl('svp');
function getPos(elm, event) {
var pos = DomUtils.getPos(elm), x, y;
x = event.pageX - pos.x;
y = event.pageY - pos.y;
x = Math.max(0, Math.min(x / elm.clientWidth, 1));
y = Math.max(0, Math.min(y / elm.clientHeight, 1));
return {
x: x,
y: y
};
}
function updateColor(hsv, hueUpdate) {
var hue = (360 - hsv.h) / 360;
DomUtils.css(huePointElm, {
top: (hue * 100) + '%'
});
if (!hueUpdate) {
DomUtils.css(svPointElm, {
left: hsv.s + '%',
top: (100 - hsv.v) + '%'
});
}
svRootElm.style.background = new Color({s: 100, v: 100, h: hsv.h}).toHex();
self.color().parse({s: hsv.s, v: hsv.v, h: hsv.h});
}
function updateSaturationAndValue(e) {
var pos;
pos = getPos(svRootElm, e);
hsv.s = pos.x * 100;
hsv.v = (1 - pos.y) * 100;
updateColor(hsv);
self.fire('change');
}
function updateHue(e) {
var pos;
pos = getPos(hueRootElm, e);
hsv = color.toHsv();
hsv.h = (1 - pos.y) * 360;
updateColor(hsv, true);
self.fire('change');
}
self._repaint = function() {
hsv = color.toHsv();
updateColor(hsv);
};
self._super();
self._svdraghelper = new DragHelper(self._id + '-sv', {
start: updateSaturationAndValue,
drag: updateSaturationAndValue
});
self._hdraghelper = new DragHelper(self._id + '-h', {
start: updateHue,
drag: updateHue
});
self._repaint();
},
rgb: function() {
return this.color().toRgb();
},
value: function(value) {
var self = this;
if (arguments.length) {
self.color().parse(value);
if (self._rendered) {
self._repaint();
}
} else {
return self.color().toHex();
}
},
color: function() {
if (!this._color) {
this._color = new Color();
}
return this._color;
},
/**
* Renders the control as a HTML string.
*
* @method renderHtml
* @return {String} HTML representing the control.
*/
renderHtml: function() {
var self = this, id = self._id, prefix = self.classPrefix, hueHtml;
var stops = '#ff0000,#ff0080,#ff00ff,#8000ff,#0000ff,#0080ff,#00ffff,#00ff80,#00ff00,#80ff00,#ffff00,#ff8000,#ff0000';
function getOldIeFallbackHtml() {
var i, l, html = '', gradientPrefix, stopsList;
gradientPrefix = 'filter:progid:DXImageTransform.Microsoft.gradient(GradientType=0,startColorstr=';
stopsList = stops.split(',');
for (i = 0, l = stopsList.length - 1; i < l; i++) {
html += (
''
);
}
return html;
}
var gradientCssText = (
'background: -ms-linear-gradient(top,' + stops + ');' +
'background: linear-gradient(to bottom,' + stops + ');'
);
hueHtml = (
'' +
getOldIeFallbackHtml() +
'' +
''
);
return (
'' +
'' +
'' +
hueHtml +
' ' +
''
);
}
});
});
// Included from: js/tinymce/classes/ui/Path.js
/**
* Path.js
*
* Copyright, Moxiecode Systems AB
* Released under LGPL License.
*
* License: http://www.tinymce.com/license
* Contributing: http://www.tinymce.com/contributing
*/
/**
* Creates a new path control.
*
* @-x-less Path.less
* @class tinymce.ui.Path
* @extends tinymce.ui.Widget
*/
define("tinymce/ui/Path", [
"tinymce/ui/Widget"
], function(Widget) {
"use strict";
return Widget.extend({
/**
* Constructs a instance with the specified settings.
*
* @constructor
* @param {Object} settings Name/value object with settings.
* @setting {String} delimiter Delimiter to display between items in path.
*/
init: function(settings) {
var self = this;
if (!settings.delimiter) {
settings.delimiter = '\u00BB';
}
self._super(settings);
self.addClass('path');
self.canFocus = true;
self.on('click', function(e) {
var index, target = e.target;
if ((index = target.getAttribute('data-index'))) {
self.fire('select', {value: self.data()[index], index: index});
}
});
},
/**
* Focuses the current control.
*
* @method focus
* @return {tinymce.ui.Control} Current control instance.
*/
focus: function() {
var self = this;
self.getEl().firstChild.focus();
return self;
},
/**
* Sets/gets the data to be used for the path.
*
* @method data
* @param {Array} data Array with items name is rendered to path.
*/
data: function(data) {
var self = this;
if (typeof data !== "undefined") {
self._data = data;
self.update();
return self;
}
return self._data;
},
/**
* Updated the path.
*
* @private
*/
update: function() {
this.innerHtml(this._getPathHtml());
},
/**
* Called after the control has been rendered.
*
* @method postRender
*/
postRender: function() {
var self = this;
self._super();
self.data(self.settings.data);
},
/**
* Renders the control as a HTML string.
*
* @method renderHtml
* @return {String} HTML representing the control.
*/
renderHtml: function() {
var self = this;
return (
'' +
self._getPathHtml() +
''
);
},
_getPathHtml: function() {
var self = this, parts = self._data || [], i, l, html = '', prefix = self.classPrefix;
for (i = 0, l = parts.length; i < l; i++) {
html += (
(i > 0 ? ' ' : '') +
'' + parts[i].name + ''
);
}
if (!html) {
html = '\u00a0';
}
return html;
}
});
});
// Included from: js/tinymce/classes/ui/ElementPath.js
/**
* ElementPath.js
*
* Copyright, Moxiecode Systems AB
* Released under LGPL License.
*
* License: http://www.tinymce.com/license
* Contributing: http://www.tinymce.com/contributing
*/
/**
* This control creates an path for the current selections parent elements in TinyMCE.
*
* @class tinymce.ui.ElementPath
* @extends tinymce.ui.Path
*/
define("tinymce/ui/ElementPath", [
"tinymce/ui/Path",
"tinymce/EditorManager"
], function(Path, EditorManager) {
return Path.extend({
/**
* Post render method. Called after the control has been rendered to the target.
*
* @method postRender
* @return {tinymce.ui.ElementPath} Current combobox instance.
*/
postRender: function() {
var self = this, editor = EditorManager.activeEditor;
function isHidden(elm) {
if (elm.nodeType === 1) {
if (elm.nodeName == "BR" || !!elm.getAttribute('data-mce-bogus')) {
return true;
}
if (elm.getAttribute('data-mce-type') === 'bookmark') {
return true;
}
}
return false;
}
if (editor.settings.elementpath !== false) {
self.on('select', function(e) {
editor.focus();
editor.selection.select(this.data()[e.index].element);
editor.nodeChanged();
});
editor.on('nodeChange', function(e) {
var outParents = [], parents = e.parents, i = parents.length;
while (i--) {
if (parents[i].nodeType == 1 && !isHidden(parents[i])) {
var args = editor.fire('ResolveName', {
name: parents[i].nodeName.toLowerCase(),
target: parents[i]
});
if (!args.isDefaultPrevented()) {
outParents.push({name: args.name, element: parents[i]});
}
if (args.isPropagationStopped()) {
break;
}
}
}
self.data(outParents);
});
}
return self._super();
}
});
});
// Included from: js/tinymce/classes/ui/FormItem.js
/**
* FormItem.js
*
* Copyright, Moxiecode Systems AB
* Released under LGPL License.
*
* License: http://www.tinymce.com/license
* Contributing: http://www.tinymce.com/contributing
*/
/**
* This class is a container created by the form element with
* a label and control item.
*
* @class tinymce.ui.FormItem
* @extends tinymce.ui.Container
* @setting {String} label Label to display for the form item.
*/
define("tinymce/ui/FormItem", [
"tinymce/ui/Container"
], function(Container) {
"use strict";
return Container.extend({
Defaults: {
layout: 'flex',
align: 'center',
defaults: {
flex: 1
}
},
/**
* Renders the control as a HTML string.
*
* @method renderHtml
* @return {String} HTML representing the control.
*/
renderHtml: function() {
var self = this, layout = self._layout, prefix = self.classPrefix;
self.addClass('formitem');
layout.preRender(self);
return (
'' +
(self.settings.title ? ('' +
self.settings.title + '') : '') +
'' +
(self.settings.html || '') + layout.renderHtml(self) +
'' +
''
);
}
});
});
// Included from: js/tinymce/classes/ui/Form.js
/**
* Form.js
*
* Copyright, Moxiecode Systems AB
* Released under LGPL License.
*
* License: http://www.tinymce.com/license
* Contributing: http://www.tinymce.com/contributing
*/
/**
* This class creates a form container. A form container has the ability
* to automatically wrap items in tinymce.ui.FormItem instances.
*
* Each FormItem instance is a container for the label and the item.
*
* @example
* tinymce.ui.Factory.create({
* type: 'form',
* items: [
* {type: 'textbox', label: 'My text box'}
* ]
* }).renderTo(document.body);
*
* @class tinymce.ui.Form
* @extends tinymce.ui.Container
*/
define("tinymce/ui/Form", [
"tinymce/ui/Container",
"tinymce/ui/FormItem",
"tinymce/util/Tools"
], function(Container, FormItem, Tools) {
"use strict";
return Container.extend({
Defaults: {
containerCls: 'form',
layout: 'flex',
direction: 'column',
align: 'stretch',
flex: 1,
padding: 20,
labelGap: 30,
spacing: 10,
callbacks: {
submit: function() {
this.submit();
}
}
},
/**
* This method gets invoked before the control is rendered.
*
* @method preRender
*/
preRender: function() {
var self = this, items = self.items();
if (!self.settings.formItemDefaults) {
self.settings.formItemDefaults = {
layout: 'flex',
autoResize: "overflow",
defaults: {flex: 1}
};
}
// Wrap any labeled items in FormItems
items.each(function(ctrl) {
var formItem, label = ctrl.settings.label;
if (label) {
formItem = new FormItem(Tools.extend({
items: {
type: 'label',
id: ctrl._id + '-l',
text: label,
flex: 0,
forId: ctrl._id,
disabled: ctrl.disabled()
}
}, self.settings.formItemDefaults));
formItem.type = 'formitem';
ctrl.aria('labelledby', ctrl._id + '-l');
if (typeof ctrl.settings.flex == "undefined") {
ctrl.settings.flex = 1;
}
self.replace(ctrl, formItem);
formItem.add(ctrl);
}
});
},
/**
* Recalcs label widths.
*
* @private
*/
recalcLabels: function() {
var self = this, maxLabelWidth = 0, labels = [], i, labelGap, items;
if (self.settings.labelGapCalc === false) {
return;
}
if (self.settings.labelGapCalc == "children") {
items = self.find('formitem');
} else {
items = self.items();
}
items.filter('formitem').each(function(item) {
var labelCtrl = item.items()[0], labelWidth = labelCtrl.getEl().clientWidth;
maxLabelWidth = labelWidth > maxLabelWidth ? labelWidth : maxLabelWidth;
labels.push(labelCtrl);
});
labelGap = self.settings.labelGap || 0;
i = labels.length;
while (i--) {
labels[i].settings.minWidth = maxLabelWidth + labelGap;
}
},
/**
* Getter/setter for the visibility state.
*
* @method visible
* @param {Boolean} [state] True/false state to show/hide.
* @return {tinymce.ui.Form|Boolean} True/false state or current control.
*/
visible: function(state) {
var val = this._super(state);
if (state === true && this._rendered) {
this.recalcLabels();
}
return val;
},
/**
* Fires a submit event with the serialized form.
*
* @method submit
* @return {Object} Event arguments object.
*/
submit: function() {
return this.fire('submit', {data: this.toJSON()});
},
/**
* Post render method. Called after the control has been rendered to the target.
*
* @method postRender
* @return {tinymce.ui.ComboBox} Current combobox instance.
*/
postRender: function() {
var self = this;
self._super();
self.recalcLabels();
self.fromJSON(self.settings.data);
}
});
});
// Included from: js/tinymce/classes/ui/FieldSet.js
/**
* FieldSet.js
*
* Copyright, Moxiecode Systems AB
* Released under LGPL License.
*
* License: http://www.tinymce.com/license
* Contributing: http://www.tinymce.com/contributing
*/
/**
* This class creates fieldset containers.
*
* @-x-less FieldSet.less
* @class tinymce.ui.FieldSet
* @extends tinymce.ui.Form
*/
define("tinymce/ui/FieldSet", [
"tinymce/ui/Form"
], function(Form) {
"use strict";
return Form.extend({
Defaults: {
containerCls: 'fieldset',
layout: 'flex',
direction: 'column',
align: 'stretch',
flex: 1,
padding: "25 15 5 15",
labelGap: 30,
spacing: 10,
border: 1
},
/**
* Renders the control as a HTML string.
*
* @method renderHtml
* @return {String} HTML representing the control.
*/
renderHtml: function() {
var self = this, layout = self._layout, prefix = self.classPrefix;
self.preRender();
layout.preRender(self);
return (
''
);
}
});
});
// Included from: js/tinymce/classes/ui/FilePicker.js
/**
* FilePicker.js
*
* Copyright, Moxiecode Systems AB
* Released under LGPL License.
*
* License: http://www.tinymce.com/license
* Contributing: http://www.tinymce.com/contributing
*/
/*global tinymce:true */
/**
* This class creates a file picker control.
*
* @class tinymce.ui.FilePicker
* @extends tinymce.ui.ComboBox
*/
define("tinymce/ui/FilePicker", [
"tinymce/ui/ComboBox",
"tinymce/util/Tools"
], function(ComboBox, Tools) {
"use strict";
return ComboBox.extend({
/**
* Constructs a new control instance with the specified settings.
*
* @constructor
* @param {Object} settings Name/value object with settings.
*/
init: function(settings) {
var self = this, editor = tinymce.activeEditor, editorSettings = editor.settings;
var actionCallback, fileBrowserCallback, fileBrowserCallbackTypes;
settings.spellcheck = false;
fileBrowserCallbackTypes = editorSettings.file_picker_types || editorSettings.file_browser_callback_types;
if (fileBrowserCallbackTypes) {
fileBrowserCallbackTypes = Tools.makeMap(fileBrowserCallbackTypes, /[, ]/);
}
if (!fileBrowserCallbackTypes || fileBrowserCallbackTypes[settings.filetype]) {
fileBrowserCallback = editorSettings.file_picker_callback;
if (fileBrowserCallback && (!fileBrowserCallbackTypes || fileBrowserCallbackTypes[settings.filetype])) {
actionCallback = function() {
var meta = self.fire('beforecall').meta;
meta = Tools.extend({filetype: settings.filetype}, meta);
// file_picker_callback(callback, currentValue, metaData)
fileBrowserCallback.call(
editor,
function(value, meta) {
self.value(value).fire('change', {meta: meta});
},
self.value(),
meta
);
};
} else {
// Legacy callback: file_picker_callback(id, currentValue, filetype, window)
fileBrowserCallback = editorSettings.file_browser_callback;
if (fileBrowserCallback && (!fileBrowserCallbackTypes || fileBrowserCallbackTypes[settings.filetype])) {
actionCallback = function() {
fileBrowserCallback(
self.getEl('inp').id,
self.value(),
settings.filetype,
window
);
};
}
}
}
if (actionCallback) {
settings.icon = 'browse';
settings.onaction = actionCallback;
}
self._super(settings);
}
});
});
// Included from: js/tinymce/classes/ui/FitLayout.js
/**
* FitLayout.js
*
* Copyright, Moxiecode Systems AB
* Released under LGPL License.
*
* License: http://www.tinymce.com/license
* Contributing: http://www.tinymce.com/contributing
*/
/**
* This layout manager will resize the control to be the size of it's parent container.
* In other words width: 100% and height: 100%.
*
* @-x-less FitLayout.less
* @class tinymce.ui.FitLayout
* @extends tinymce.ui.AbsoluteLayout
*/
define("tinymce/ui/FitLayout", [
"tinymce/ui/AbsoluteLayout"
], function(AbsoluteLayout) {
"use strict";
return AbsoluteLayout.extend({
/**
* Recalculates the positions of the controls in the specified container.
*
* @method recalc
* @param {tinymce.ui.Container} container Container instance to recalc.
*/
recalc: function(container) {
var contLayoutRect = container.layoutRect(), paddingBox = container.paddingBox();
container.items().filter(':visible').each(function(ctrl) {
ctrl.layoutRect({
x: paddingBox.left,
y: paddingBox.top,
w: contLayoutRect.innerW - paddingBox.right - paddingBox.left,
h: contLayoutRect.innerH - paddingBox.top - paddingBox.bottom
});
if (ctrl.recalc) {
ctrl.recalc();
}
});
}
});
});
// Included from: js/tinymce/classes/ui/FlexLayout.js
/**
* FlexLayout.js
*
* Copyright, Moxiecode Systems AB
* Released under LGPL License.
*
* License: http://www.tinymce.com/license
* Contributing: http://www.tinymce.com/contributing
*/
/**
* This layout manager works similar to the CSS flex box.
*
* @setting {String} direction row|row-reverse|column|column-reverse
* @setting {Number} flex A positive-number to flex by.
* @setting {String} align start|end|center|stretch
* @setting {String} pack start|end|justify
*
* @class tinymce.ui.FlexLayout
* @extends tinymce.ui.AbsoluteLayout
*/
define("tinymce/ui/FlexLayout", [
"tinymce/ui/AbsoluteLayout"
], function(AbsoluteLayout) {
"use strict";
return AbsoluteLayout.extend({
/**
* Recalculates the positions of the controls in the specified container.
*
* @method recalc
* @param {tinymce.ui.Container} container Container instance to recalc.
*/
recalc: function(container) {
// A ton of variables, needs to be in the same scope for performance
var i, l, items, contLayoutRect, contPaddingBox, contSettings, align, pack, spacing, totalFlex, availableSpace, direction;
var ctrl, ctrlLayoutRect, ctrlSettings, flex, maxSizeItems = [], size, maxSize, ratio, rect, pos, maxAlignEndPos;
var sizeName, minSizeName, posName, maxSizeName, beforeName, innerSizeName, deltaSizeName, contentSizeName;
var alignAxisName, alignInnerSizeName, alignSizeName, alignMinSizeName, alignBeforeName, alignAfterName;
var alignDeltaSizeName, alignContentSizeName;
var max = Math.max, min = Math.min;
// Get container items, properties and settings
items = container.items().filter(':visible');
contLayoutRect = container.layoutRect();
contPaddingBox = container._paddingBox;
contSettings = container.settings;
direction = container.isRtl() ? (contSettings.direction || 'row-reversed') : contSettings.direction;
align = contSettings.align;
pack = container.isRtl() ? (contSettings.pack || 'end') : contSettings.pack;
spacing = contSettings.spacing || 0;
if (direction == "row-reversed" || direction == "column-reverse") {
items = items.set(items.toArray().reverse());
direction = direction.split('-')[0];
}
// Setup axis variable name for row/column direction since the calculations is the same
if (direction == "column") {
posName = "y";
sizeName = "h";
minSizeName = "minH";
maxSizeName = "maxH";
innerSizeName = "innerH";
beforeName = 'top';
deltaSizeName = "deltaH";
contentSizeName = "contentH";
alignBeforeName = "left";
alignSizeName = "w";
alignAxisName = "x";
alignInnerSizeName = "innerW";
alignMinSizeName = "minW";
alignAfterName = "right";
alignDeltaSizeName = "deltaW";
alignContentSizeName = "contentW";
} else {
posName = "x";
sizeName = "w";
minSizeName = "minW";
maxSizeName = "maxW";
innerSizeName = "innerW";
beforeName = 'left';
deltaSizeName = "deltaW";
contentSizeName = "contentW";
alignBeforeName = "top";
alignSizeName = "h";
alignAxisName = "y";
alignInnerSizeName = "innerH";
alignMinSizeName = "minH";
alignAfterName = "bottom";
alignDeltaSizeName = "deltaH";
alignContentSizeName = "contentH";
}
// Figure out total flex, availableSpace and collect any max size elements
availableSpace = contLayoutRect[innerSizeName] - contPaddingBox[beforeName] - contPaddingBox[beforeName];
maxAlignEndPos = totalFlex = 0;
for (i = 0, l = items.length; i < l; i++) {
ctrl = items[i];
ctrlLayoutRect = ctrl.layoutRect();
ctrlSettings = ctrl.settings;
flex = ctrlSettings.flex;
availableSpace -= (i < l - 1 ? spacing : 0);
if (flex > 0) {
totalFlex += flex;
// Flexed item has a max size then we need to check if we will hit that size
if (ctrlLayoutRect[maxSizeName]) {
maxSizeItems.push(ctrl);
}
ctrlLayoutRect.flex = flex;
}
availableSpace -= ctrlLayoutRect[minSizeName];
// Calculate the align end position to be used to check for overflow/underflow
size = contPaddingBox[alignBeforeName] + ctrlLayoutRect[alignMinSizeName] + contPaddingBox[alignAfterName];
if (size > maxAlignEndPos) {
maxAlignEndPos = size;
}
}
// Calculate minW/minH
rect = {};
if (availableSpace < 0) {
rect[minSizeName] = contLayoutRect[minSizeName] - availableSpace + contLayoutRect[deltaSizeName];
} else {
rect[minSizeName] = contLayoutRect[innerSizeName] - availableSpace + contLayoutRect[deltaSizeName];
}
rect[alignMinSizeName] = maxAlignEndPos + contLayoutRect[alignDeltaSizeName];
rect[contentSizeName] = contLayoutRect[innerSizeName] - availableSpace;
rect[alignContentSizeName] = maxAlignEndPos;
rect.minW = min(rect.minW, contLayoutRect.maxW);
rect.minH = min(rect.minH, contLayoutRect.maxH);
rect.minW = max(rect.minW, contLayoutRect.startMinWidth);
rect.minH = max(rect.minH, contLayoutRect.startMinHeight);
// Resize container container if minSize was changed
if (contLayoutRect.autoResize && (rect.minW != contLayoutRect.minW || rect.minH != contLayoutRect.minH)) {
rect.w = rect.minW;
rect.h = rect.minH;
container.layoutRect(rect);
this.recalc(container);
// Forced recalc for example if items are hidden/shown
if (container._lastRect === null) {
var parentCtrl = container.parent();
if (parentCtrl) {
parentCtrl._lastRect = null;
parentCtrl.recalc();
}
}
return;
}
// Handle max size elements, check if they will become to wide with current options
ratio = availableSpace / totalFlex;
for (i = 0, l = maxSizeItems.length; i < l; i++) {
ctrl = maxSizeItems[i];
ctrlLayoutRect = ctrl.layoutRect();
maxSize = ctrlLayoutRect[maxSizeName];
size = ctrlLayoutRect[minSizeName] + ctrlLayoutRect.flex * ratio;
if (size > maxSize) {
availableSpace -= (ctrlLayoutRect[maxSizeName] - ctrlLayoutRect[minSizeName]);
totalFlex -= ctrlLayoutRect.flex;
ctrlLayoutRect.flex = 0;
ctrlLayoutRect.maxFlexSize = maxSize;
} else {
ctrlLayoutRect.maxFlexSize = 0;
}
}
// Setup new ratio, target layout rect, start position
ratio = availableSpace / totalFlex;
pos = contPaddingBox[beforeName];
rect = {};
// Handle pack setting moves the start position to end, center
if (totalFlex === 0) {
if (pack == "end") {
pos = availableSpace + contPaddingBox[beforeName];
} else if (pack == "center") {
pos = Math.round(
(contLayoutRect[innerSizeName] / 2) - ((contLayoutRect[innerSizeName] - availableSpace) / 2)
) + contPaddingBox[beforeName];
if (pos < 0) {
pos = contPaddingBox[beforeName];
}
} else if (pack == "justify") {
pos = contPaddingBox[beforeName];
spacing = Math.floor(availableSpace / (items.length - 1));
}
}
// Default aligning (start) the other ones needs to be calculated while doing the layout
rect[alignAxisName] = contPaddingBox[alignBeforeName];
// Start laying out controls
for (i = 0, l = items.length; i < l; i++) {
ctrl = items[i];
ctrlLayoutRect = ctrl.layoutRect();
size = ctrlLayoutRect.maxFlexSize || ctrlLayoutRect[minSizeName];
// Align the control on the other axis
if (align === "center") {
rect[alignAxisName] = Math.round((contLayoutRect[alignInnerSizeName] / 2) - (ctrlLayoutRect[alignSizeName] / 2));
} else if (align === "stretch") {
rect[alignSizeName] = max(
ctrlLayoutRect[alignMinSizeName] || 0,
contLayoutRect[alignInnerSizeName] - contPaddingBox[alignBeforeName] - contPaddingBox[alignAfterName]
);
rect[alignAxisName] = contPaddingBox[alignBeforeName];
} else if (align === "end") {
rect[alignAxisName] = contLayoutRect[alignInnerSizeName] - ctrlLayoutRect[alignSizeName] - contPaddingBox.top;
}
// Calculate new size based on flex
if (ctrlLayoutRect.flex > 0) {
size += ctrlLayoutRect.flex * ratio;
}
rect[sizeName] = size;
rect[posName] = pos;
ctrl.layoutRect(rect);
// Recalculate containers
if (ctrl.recalc) {
ctrl.recalc();
}
// Move x/y position
pos += size + spacing;
}
}
});
});
// Included from: js/tinymce/classes/ui/FlowLayout.js
/**
* FlowLayout.js
*
* Copyright, Moxiecode Systems AB
* Released under LGPL License.
*
* License: http://www.tinymce.com/license
* Contributing: http://www.tinymce.com/contributing
*/
/**
* This layout manager will place the controls by using the browsers native layout.
*
* @-x-less FlowLayout.less
* @class tinymce.ui.FlowLayout
* @extends tinymce.ui.Layout
*/
define("tinymce/ui/FlowLayout", [
"tinymce/ui/Layout"
], function(Layout) {
return Layout.extend({
Defaults: {
containerClass: 'flow-layout',
controlClass: 'flow-layout-item',
endClass: 'break'
},
/**
* Recalculates the positions of the controls in the specified container.
*
* @method recalc
* @param {tinymce.ui.Container} container Container instance to recalc.
*/
recalc: function(container) {
container.items().filter(':visible').each(function(ctrl) {
if (ctrl.recalc) {
ctrl.recalc();
}
});
}
});
});
// Included from: js/tinymce/classes/ui/FormatControls.js
/**
* FormatControls.js
*
* Copyright, Moxiecode Systems AB
* Released under LGPL License.
*
* License: http://www.tinymce.com/license
* Contributing: http://www.tinymce.com/contributing
*/
/**
* Internal class containing all TinyMCE specific control types such as
* format listboxes, fontlist boxes, toolbar buttons etc.
*
* @class tinymce.ui.FormatControls
*/
define("tinymce/ui/FormatControls", [
"tinymce/ui/Control",
"tinymce/ui/Widget",
"tinymce/ui/FloatPanel",
"tinymce/util/Tools",
"tinymce/EditorManager",
"tinymce/Env"
], function(Control, Widget, FloatPanel, Tools, EditorManager, Env) {
var each = Tools.each;
EditorManager.on('AddEditor', function(e) {
if (e.editor.rtl) {
Control.rtl = true;
}
registerControls(e.editor);
});
Control.translate = function(text) {
return EditorManager.translate(text);
};
Widget.tooltips = !Env.iOS;
function registerControls(editor) {
var formatMenu;
function createListBoxChangeHandler(items, formatName) {
return function() {
var self = this;
editor.on('nodeChange', function(e) {
var formatter = editor.formatter;
var value = null;
each(e.parents, function(node) {
each(items, function(item) {
if (formatName) {
if (formatter.matchNode(node, formatName, {value: item.value})) {
value = item.value;
}
} else {
if (formatter.matchNode(node, item.value)) {
value = item.value;
}
}
if (value) {
return false;
}
});
if (value) {
return false;
}
});
self.value(value);
});
};
}
function createFormats(formats) {
formats = formats.replace(/;$/, '').split(';');
var i = formats.length;
while (i--) {
formats[i] = formats[i].split('=');
}
return formats;
}
function createFormatMenu() {
var count = 0, newFormats = [];
var defaultStyleFormats = [
{title: 'Headings', items: [
{title: 'Heading 1', format: 'h1'},
{title: 'Heading 2', format: 'h2'},
{title: 'Heading 3', format: 'h3'},
{title: 'Heading 4', format: 'h4'},
{title: 'Heading 5', format: 'h5'},
{title: 'Heading 6', format: 'h6'}
]},
{title: 'Inline', items: [
{title: 'Bold', icon: 'bold', format: 'bold'},
{title: 'Italic', icon: 'italic', format: 'italic'},
{title: 'Underline', icon: 'underline', format: 'underline'},
{title: 'Strikethrough', icon: 'strikethrough', format: 'strikethrough'},
{title: 'Superscript', icon: 'superscript', format: 'superscript'},
{title: 'Subscript', icon: 'subscript', format: 'subscript'},
{title: 'Code', icon: 'code', format: 'code'}
]},
{title: 'Blocks', items: [
{title: 'Paragraph', format: 'p'},
{title: 'Blockquote', format: 'blockquote'},
{title: 'Div', format: 'div'},
{title: 'Pre', format: 'pre'}
]},
{title: 'Alignment', items: [
{title: 'Left', icon: 'alignleft', format: 'alignleft'},
{title: 'Center', icon: 'aligncenter', format: 'aligncenter'},
{title: 'Right', icon: 'alignright', format: 'alignright'},
{title: 'Justify', icon: 'alignjustify', format: 'alignjustify'}
]}
];
function createMenu(formats) {
var menu = [];
if (!formats) {
return;
}
each(formats, function(format) {
var menuItem = {
text: format.title,
icon: format.icon
};
if (format.items) {
menuItem.menu = createMenu(format.items);
} else {
var formatName = format.format || "custom" + count++;
if (!format.format) {
format.name = formatName;
newFormats.push(format);
}
menuItem.format = formatName;
menuItem.cmd = format.cmd;
}
menu.push(menuItem);
});
return menu;
}
function createStylesMenu() {
var menu;
if (editor.settings.style_formats_merge) {
if (editor.settings.style_formats) {
menu = createMenu(defaultStyleFormats.concat(editor.settings.style_formats));
} else {
menu = createMenu(defaultStyleFormats);
}
} else {
menu = createMenu(editor.settings.style_formats || defaultStyleFormats);
}
return menu;
}
editor.on('init', function() {
each(newFormats, function(format) {
editor.formatter.register(format.name, format);
});
});
return {
type: 'menu',
items: createStylesMenu(),
onPostRender: function(e) {
editor.fire('renderFormatsMenu', {control: e.control});
},
itemDefaults: {
preview: true,
textStyle: function() {
if (this.settings.format) {
return editor.formatter.getCssText(this.settings.format);
}
},
onPostRender: function() {
var self = this;
self.parent().on('show', function() {
var formatName, command;
formatName = self.settings.format;
if (formatName) {
self.disabled(!editor.formatter.canApply(formatName));
self.active(editor.formatter.match(formatName));
}
command = self.settings.cmd;
if (command) {
self.active(editor.queryCommandState(command));
}
});
},
onclick: function() {
if (this.settings.format) {
toggleFormat(this.settings.format);
}
if (this.settings.cmd) {
editor.execCommand(this.settings.cmd);
}
}
}
};
}
formatMenu = createFormatMenu();
// Simple format controls :
each({
bold: 'Bold',
italic: 'Italic',
underline: 'Underline',
strikethrough: 'Strikethrough',
subscript: 'Subscript',
superscript: 'Superscript'
}, function(text, name) {
editor.addButton(name, {
tooltip: text,
onPostRender: function() {
var self = this;
// TODO: Fix this
if (editor.formatter) {
editor.formatter.formatChanged(name, function(state) {
self.active(state);
});
} else {
editor.on('init', function() {
editor.formatter.formatChanged(name, function(state) {
self.active(state);
});
});
}
},
onclick: function() {
toggleFormat(name);
}
});
});
// Simple command controls :[,]
each({
outdent: ['Decrease indent', 'Outdent'],
indent: ['Increase indent', 'Indent'],
cut: ['Cut', 'Cut'],
copy: ['Copy', 'Copy'],
paste: ['Paste', 'Paste'],
help: ['Help', 'mceHelp'],
selectall: ['Select all', 'SelectAll'],
removeformat: ['Clear formatting', 'RemoveFormat'],
visualaid: ['Visual aids', 'mceToggleVisualAid'],
newdocument: ['New document', 'mceNewDocument']
}, function(item, name) {
editor.addButton(name, {
tooltip: item[0],
cmd: item[1]
});
});
// Simple command controls with format state
each({
blockquote: ['Blockquote', 'mceBlockQuote'],
numlist: ['Numbered list', 'InsertOrderedList'],
bullist: ['Bullet list', 'InsertUnorderedList'],
subscript: ['Subscript', 'Subscript'],
superscript: ['Superscript', 'Superscript'],
alignleft: ['Align left', 'JustifyLeft'],
aligncenter: ['Align center', 'JustifyCenter'],
alignright: ['Align right', 'JustifyRight'],
alignjustify: ['Justify', 'JustifyFull']
}, function(item, name) {
editor.addButton(name, {
tooltip: item[0],
cmd: item[1],
onPostRender: function() {
var self = this;
// TODO: Fix this
if (editor.formatter) {
editor.formatter.formatChanged(name, function(state) {
self.active(state);
});
} else {
editor.on('init', function() {
editor.formatter.formatChanged(name, function(state) {
self.active(state);
});
});
}
}
});
});
function toggleUndoRedoState(type) {
return function() {
var self = this;
type = type == 'redo' ? 'hasRedo' : 'hasUndo';
function checkState() {
return editor.undoManager ? editor.undoManager[type]() : false;
}
self.disabled(!checkState());
editor.on('Undo Redo AddUndo TypingUndo ClearUndos', function() {
self.disabled(!checkState());
});
};
}
function toggleVisualAidState() {
var self = this;
editor.on('VisualAid', function(e) {
self.active(e.hasVisual);
});
self.active(editor.hasVisual);
}
editor.addButton('undo', {
tooltip: 'Undo',
onPostRender: toggleUndoRedoState('undo'),
cmd: 'undo'
});
editor.addButton('redo', {
tooltip: 'Redo',
onPostRender: toggleUndoRedoState('redo'),
cmd: 'redo'
});
editor.addMenuItem('newdocument', {
text: 'New document',
icon: 'newdocument',
cmd: 'mceNewDocument'
});
editor.addMenuItem('undo', {
text: 'Undo',
icon: 'undo',
shortcut: 'Meta+Z',
onPostRender: toggleUndoRedoState('undo'),
cmd: 'undo'
});
editor.addMenuItem('redo', {
text: 'Redo',
icon: 'redo',
shortcut: 'Meta+Y',
onPostRender: toggleUndoRedoState('redo'),
cmd: 'redo'
});
editor.addMenuItem('visualaid', {
text: 'Visual aids',
selectable: true,
onPostRender: toggleVisualAidState,
cmd: 'mceToggleVisualAid'
});
each({
cut: ['Cut', 'Cut', 'Meta+X'],
copy: ['Copy', 'Copy', 'Meta+C'],
paste: ['Paste', 'Paste', 'Meta+V'],
selectall: ['Select all', 'SelectAll', 'Meta+A'],
bold: ['Bold', 'Bold', 'Meta+B'],
italic: ['Italic', 'Italic', 'Meta+I'],
underline: ['Underline', 'Underline'],
strikethrough: ['Strikethrough', 'Strikethrough'],
subscript: ['Subscript', 'Subscript'],
superscript: ['Superscript', 'Superscript'],
removeformat: ['Clear formatting', 'RemoveFormat']
}, function(item, name) {
editor.addMenuItem(name, {
text: item[0],
icon: name,
shortcut: item[2],
cmd: item[1]
});
});
editor.on('mousedown', function() {
FloatPanel.hideAll();
});
function toggleFormat(fmt) {
if (fmt.control) {
fmt = fmt.control.value();
}
if (fmt) {
editor.execCommand('mceToggleFormat', false, fmt);
}
}
editor.addButton('styleselect', {
type: 'menubutton',
text: 'Formats',
menu: formatMenu
});
editor.addButton('formatselect', function() {
var items = [], blocks = createFormats(editor.settings.block_formats ||
'Paragraph=p;' +
'Heading 1=h1;' +
'Heading 2=h2;' +
'Heading 3=h3;' +
'Heading 4=h4;' +
'Heading 5=h5;' +
'Heading 6=h6;' +
'Preformatted=pre'
);
each(blocks, function(block) {
items.push({
text: block[0],
value: block[1],
textStyle: function() {
return editor.formatter.getCssText(block[1]);
}
});
});
return {
type: 'listbox',
text: blocks[0][0],
values: items,
fixedWidth: true,
onselect: toggleFormat,
onPostRender: createListBoxChangeHandler(items)
};
});
editor.addButton('fontselect', function() {
var defaultFontsFormats =
'Andale Mono=andale mono,monospace;' +
'Arial=arial,helvetica,sans-serif;' +
'Arial Black=arial black,sans-serif;' +
'Book Antiqua=book antiqua,palatino,serif;' +
'Comic Sans MS=comic sans ms,sans-serif;' +
'Courier New=courier new,courier,monospace;' +
'Georgia=georgia,palatino,serif;' +
'Helvetica=helvetica,arial,sans-serif;' +
'Impact=impact,sans-serif;' +
'Symbol=symbol;' +
'Tahoma=tahoma,arial,helvetica,sans-serif;' +
'Terminal=terminal,monaco,monospace;' +
'Times New Roman=times new roman,times,serif;' +
'Trebuchet MS=trebuchet ms,geneva,sans-serif;' +
'Verdana=verdana,geneva,sans-serif;' +
'Webdings=webdings;' +
'Wingdings=wingdings,zapf dingbats';
var items = [], fonts = createFormats(editor.settings.font_formats || defaultFontsFormats);
each(fonts, function(font) {
items.push({
text: {raw: font[0]},
value: font[1],
textStyle: font[1].indexOf('dings') == -1 ? 'font-family:' + font[1] : ''
});
});
return {
type: 'listbox',
text: 'Font Family',
tooltip: 'Font Family',
values: items,
fixedWidth: true,
onPostRender: createListBoxChangeHandler(items, 'fontname'),
onselect: function(e) {
if (e.control.settings.value) {
editor.execCommand('FontName', false, e.control.settings.value);
}
}
};
});
editor.addButton('fontsizeselect', function() {
var items = [], defaultFontsizeFormats = '8pt 10pt 12pt 14pt 18pt 24pt 36pt';
var fontsize_formats = editor.settings.fontsize_formats || defaultFontsizeFormats;
each(fontsize_formats.split(' '), function(item) {
var text = item, value = item;
// Allow text=value font sizes.
var values = item.split('=');
if (values.length > 1) {
text = values[0];
value = values[1];
}
items.push({text: text, value: value});
});
return {
type: 'listbox',
text: 'Font Sizes',
tooltip: 'Font Sizes',
values: items,
fixedWidth: true,
onPostRender: createListBoxChangeHandler(items, 'fontsize'),
onclick: function(e) {
if (e.control.settings.value) {
editor.execCommand('FontSize', false, e.control.settings.value);
}
}
};
});
editor.addMenuItem('formats', {
text: 'Formats',
menu: formatMenu
});
}
});
// Included from: js/tinymce/classes/ui/GridLayout.js
/**
* GridLayout.js
*
* Copyright, Moxiecode Systems AB
* Released under LGPL License.
*
* License: http://www.tinymce.com/license
* Contributing: http://www.tinymce.com/contributing
*/
/**
* This layout manager places controls in a grid.
*
* @setting {Number} spacing Spacing between controls.
* @setting {Number} spacingH Horizontal spacing between controls.
* @setting {Number} spacingV Vertical spacing between controls.
* @setting {Number} columns Number of columns to use.
* @setting {String/Array} alignH start|end|center|stretch or array of values for each column.
* @setting {String/Array} alignV start|end|center|stretch or array of values for each column.
* @setting {String} pack start|end
*
* @class tinymce.ui.GridLayout
* @extends tinymce.ui.AbsoluteLayout
*/
define("tinymce/ui/GridLayout", [
"tinymce/ui/AbsoluteLayout"
], function(AbsoluteLayout) {
"use strict";
return AbsoluteLayout.extend({
/**
* Recalculates the positions of the controls in the specified container.
*
* @method recalc
* @param {tinymce.ui.Container} container Container instance to recalc.
*/
recalc: function(container) {
var settings = container.settings, rows, cols, items, contLayoutRect, width, height, rect,
ctrlLayoutRect, ctrl, x, y, posX, posY, ctrlSettings, contPaddingBox, align, spacingH, spacingV, alignH, alignV, maxX, maxY,
colWidths = [], rowHeights = [], ctrlMinWidth, ctrlMinHeight, availableWidth, availableHeight, reverseRows, idx;
// Get layout settings
settings = container.settings;
items = container.items().filter(':visible');
contLayoutRect = container.layoutRect();
cols = settings.columns || Math.ceil(Math.sqrt(items.length));
rows = Math.ceil(items.length / cols);
spacingH = settings.spacingH || settings.spacing || 0;
spacingV = settings.spacingV || settings.spacing || 0;
alignH = settings.alignH || settings.align;
alignV = settings.alignV || settings.align;
contPaddingBox = container._paddingBox;
reverseRows = 'reverseRows' in settings ? settings.reverseRows : container.isRtl();
if (alignH && typeof alignH == "string") {
alignH = [alignH];
}
if (alignV && typeof alignV == "string") {
alignV = [alignV];
}
// Zero padd columnWidths
for (x = 0; x < cols; x++) {
colWidths.push(0);
}
// Zero padd rowHeights
for (y = 0; y < rows; y++) {
rowHeights.push(0);
}
// Calculate columnWidths and rowHeights
for (y = 0; y < rows; y++) {
for (x = 0; x < cols; x++) {
ctrl = items[y * cols + x];
// Out of bounds
if (!ctrl) {
break;
}
ctrlLayoutRect = ctrl.layoutRect();
ctrlMinWidth = ctrlLayoutRect.minW;
ctrlMinHeight = ctrlLayoutRect.minH;
colWidths[x] = ctrlMinWidth > colWidths[x] ? ctrlMinWidth : colWidths[x];
rowHeights[y] = ctrlMinHeight > rowHeights[y] ? ctrlMinHeight : rowHeights[y];
}
}
// Calculate maxX
availableWidth = contLayoutRect.innerW - contPaddingBox.left - contPaddingBox.right;
for (maxX = 0, x = 0; x < cols; x++) {
maxX += colWidths[x] + (x > 0 ? spacingH : 0);
availableWidth -= (x > 0 ? spacingH : 0) + colWidths[x];
}
// Calculate maxY
availableHeight = contLayoutRect.innerH - contPaddingBox.top - contPaddingBox.bottom;
for (maxY = 0, y = 0; y < rows; y++) {
maxY += rowHeights[y] + (y > 0 ? spacingV : 0);
availableHeight -= (y > 0 ? spacingV : 0) + rowHeights[y];
}
maxX += contPaddingBox.left + contPaddingBox.right;
maxY += contPaddingBox.top + contPaddingBox.bottom;
// Calculate minW/minH
rect = {};
rect.minW = maxX + (contLayoutRect.w - contLayoutRect.innerW);
rect.minH = maxY + (contLayoutRect.h - contLayoutRect.innerH);
rect.contentW = rect.minW - contLayoutRect.deltaW;
rect.contentH = rect.minH - contLayoutRect.deltaH;
rect.minW = Math.min(rect.minW, contLayoutRect.maxW);
rect.minH = Math.min(rect.minH, contLayoutRect.maxH);
rect.minW = Math.max(rect.minW, contLayoutRect.startMinWidth);
rect.minH = Math.max(rect.minH, contLayoutRect.startMinHeight);
// Resize container container if minSize was changed
if (contLayoutRect.autoResize && (rect.minW != contLayoutRect.minW || rect.minH != contLayoutRect.minH)) {
rect.w = rect.minW;
rect.h = rect.minH;
container.layoutRect(rect);
this.recalc(container);
// Forced recalc for example if items are hidden/shown
if (container._lastRect === null) {
var parentCtrl = container.parent();
if (parentCtrl) {
parentCtrl._lastRect = null;
parentCtrl.recalc();
}
}
return;
}
// Update contentW/contentH so absEnd moves correctly
if (contLayoutRect.autoResize) {
rect = container.layoutRect(rect);
rect.contentW = rect.minW - contLayoutRect.deltaW;
rect.contentH = rect.minH - contLayoutRect.deltaH;
}
var flexV;
if (settings.packV == 'start') {
flexV = 0;
} else {
flexV = availableHeight > 0 ? Math.floor(availableHeight / rows) : 0;
}
// Calculate totalFlex
var totalFlex = 0;
var flexWidths = settings.flexWidths;
if (flexWidths) {
for (x = 0; x < flexWidths.length; x++) {
totalFlex += flexWidths[x];
}
} else {
totalFlex = cols;
}
// Calculate new column widths based on flex values
var ratio = availableWidth / totalFlex;
for (x = 0; x < cols; x++) {
colWidths[x] += flexWidths ? flexWidths[x] * ratio : ratio;
}
// Move/resize controls
posY = contPaddingBox.top;
for (y = 0; y < rows; y++) {
posX = contPaddingBox.left;
height = rowHeights[y] + flexV;
for (x = 0; x < cols; x++) {
if (reverseRows) {
idx = y * cols + cols - 1 - x;
} else {
idx = y * cols + x;
}
ctrl = items[idx];
// No more controls to render then break
if (!ctrl) {
break;
}
// Get control settings and calculate x, y
ctrlSettings = ctrl.settings;
ctrlLayoutRect = ctrl.layoutRect();
width = Math.max(colWidths[x], ctrlLayoutRect.startMinWidth);
ctrlLayoutRect.x = posX;
ctrlLayoutRect.y = posY;
// Align control horizontal
align = ctrlSettings.alignH || (alignH ? (alignH[x] || alignH[0]) : null);
if (align == "center") {
ctrlLayoutRect.x = posX + (width / 2) - (ctrlLayoutRect.w / 2);
} else if (align == "right") {
ctrlLayoutRect.x = posX + width - ctrlLayoutRect.w;
} else if (align == "stretch") {
ctrlLayoutRect.w = width;
}
// Align control vertical
align = ctrlSettings.alignV || (alignV ? (alignV[x] || alignV[0]) : null);
if (align == "center") {
ctrlLayoutRect.y = posY + (height / 2) - (ctrlLayoutRect.h / 2);
} else if (align == "bottom") {
ctrlLayoutRect.y = posY + height - ctrlLayoutRect.h;
} else if (align == "stretch") {
ctrlLayoutRect.h = height;
}
ctrl.layoutRect(ctrlLayoutRect);
posX += width + spacingH;
if (ctrl.recalc) {
ctrl.recalc();
}
}
posY += height + spacingV;
}
}
});
});
// Included from: js/tinymce/classes/ui/Iframe.js
/**
* Iframe.js
*
* Copyright, Moxiecode Systems AB
* Released under LGPL License.
*
* License: http://www.tinymce.com/license
* Contributing: http://www.tinymce.com/contributing
*/
/*jshint scripturl:true */
/**
* This class creates an iframe.
*
* @setting {String} url Url to open in the iframe.
*
* @-x-less Iframe.less
* @class tinymce.ui.Iframe
* @extends tinymce.ui.Widget
*/
define("tinymce/ui/Iframe", [
"tinymce/ui/Widget"
], function(Widget) {
"use strict";
return Widget.extend({
/**
* Renders the control as a HTML string.
*
* @method renderHtml
* @return {String} HTML representing the control.
*/
renderHtml: function() {
var self = this;
self.addClass('iframe');
self.canFocus = false;
/*eslint no-script-url:0 */
return (
''
);
},
/**
* Setter for the iframe source.
*
* @method src
* @param {String} src Source URL for iframe.
*/
src: function(src) {
this.getEl().src = src;
},
/**
* Inner HTML for the iframe.
*
* @method html
* @param {String} html HTML string to set as HTML inside the iframe.
* @param {function} callback Optional callback to execute when the iframe body is filled with contents.
* @return {tinymce.ui.Iframe} Current iframe control.
*/
html: function(html, callback) {
var self = this, body = this.getEl().contentWindow.document.body;
// Wait for iframe to initialize IE 10 takes time
if (!body) {
setTimeout(function() {
self.html(html);
}, 0);
} else {
body.innerHTML = html;
if (callback) {
callback();
}
}
return this;
}
});
});
// Included from: js/tinymce/classes/ui/Label.js
/**
* Label.js
*
* Copyright, Moxiecode Systems AB
* Released under LGPL License.
*
* License: http://www.tinymce.com/license
* Contributing: http://www.tinymce.com/contributing
*/
/**
* This class creates a label element. A label is a simple text control
* that can be bound to other controls.
*
* @-x-less Label.less
* @class tinymce.ui.Label
* @extends tinymce.ui.Widget
*/
define("tinymce/ui/Label", [
"tinymce/ui/Widget",
"tinymce/ui/DomUtils"
], function(Widget, DomUtils) {
"use strict";
return Widget.extend({
/**
* Constructs a instance with the specified settings.
*
* @constructor
* @param {Object} settings Name/value object with settings.
* @param {Boolean} multiline Multiline label.
*/
init: function(settings) {
var self = this;
self._super(settings);
self.addClass('widget');
self.addClass('label');
self.canFocus = false;
if (settings.multiline) {
self.addClass('autoscroll');
}
if (settings.strong) {
self.addClass('strong');
}
},
/**
* Initializes the current controls layout rect.
* This will be executed by the layout managers to determine the
* default minWidth/minHeight etc.
*
* @method initLayoutRect
* @return {Object} Layout rect instance.
*/
initLayoutRect: function() {
var self = this, layoutRect = self._super();
if (self.settings.multiline) {
var size = DomUtils.getSize(self.getEl());
// Check if the text fits within maxW if not then try word wrapping it
if (size.width > layoutRect.maxW) {
layoutRect.minW = layoutRect.maxW;
self.addClass('multiline');
}
self.getEl().style.width = layoutRect.minW + 'px';
layoutRect.startMinH = layoutRect.h = layoutRect.minH = Math.min(layoutRect.maxH, DomUtils.getSize(self.getEl()).height);
}
return layoutRect;
},
/**
* Repaints the control after a layout operation.
*
* @method repaint
*/
repaint: function() {
var self = this;
if (!self.settings.multiline) {
self.getEl().style.lineHeight = self.layoutRect().h + 'px';
}
return self._super();
},
/**
* Sets/gets the current label text.
*
* @method text
* @param {String} [text] New label text.
* @return {String|tinymce.ui.Label} Current text or current label instance.
*/
text: function(text) {
var self = this;
if (self._rendered && text) {
this.innerHtml(self.encode(text));
}
return self._super(text);
},
/**
* Renders the control as a HTML string.
*
* @method renderHtml
* @return {String} HTML representing the control.
*/
renderHtml: function() {
var self = this, forId = self.settings.forId;
return (
''
);
}
});
});
// Included from: js/tinymce/classes/ui/Toolbar.js
/**
* Toolbar.js
*
* Copyright, Moxiecode Systems AB
* Released under LGPL License.
*
* License: http://www.tinymce.com/license
* Contributing: http://www.tinymce.com/contributing
*/
/**
* Creates a new toolbar.
*
* @class tinymce.ui.Toolbar
* @extends tinymce.ui.Container
*/
define("tinymce/ui/Toolbar", [
"tinymce/ui/Container"
], function(Container) {
"use strict";
return Container.extend({
Defaults: {
role: 'toolbar',
layout: 'flow'
},
/**
* Constructs a instance with the specified settings.
*
* @constructor
* @param {Object} settings Name/value object with settings.
*/
init: function(settings) {
var self = this;
self._super(settings);
self.addClass('toolbar');
},
/**
* Called after the control has been rendered.
*
* @method postRender
*/
postRender: function() {
var self = this;
self.items().addClass('toolbar-item');
return self._super();
}
});
});
// Included from: js/tinymce/classes/ui/MenuBar.js
/**
* MenuBar.js
*
* Copyright, Moxiecode Systems AB
* Released under LGPL License.
*
* License: http://www.tinymce.com/license
* Contributing: http://www.tinymce.com/contributing
*/
/**
* Creates a new menubar.
*
* @-x-less MenuBar.less
* @class tinymce.ui.MenuBar
* @extends tinymce.ui.Toolbar
*/
define("tinymce/ui/MenuBar", [
"tinymce/ui/Toolbar"
], function(Toolbar) {
"use strict";
return Toolbar.extend({
Defaults: {
role: 'menubar',
containerCls: 'menubar',
ariaRoot: true,
defaults: {
type: 'menubutton'
}
}
});
});
// Included from: js/tinymce/classes/ui/MenuButton.js
/**
* MenuButton.js
*
* Copyright, Moxiecode Systems AB
* Released under LGPL License.
*
* License: http://www.tinymce.com/license
* Contributing: http://www.tinymce.com/contributing
*/
/**
* Creates a new menu button.
*
* @-x-less MenuButton.less
* @class tinymce.ui.MenuButton
* @extends tinymce.ui.Button
*/
define("tinymce/ui/MenuButton", [
"tinymce/ui/Button",
"tinymce/ui/Factory",
"tinymce/ui/MenuBar"
], function(Button, Factory, MenuBar) {
"use strict";
// TODO: Maybe add as some global function
function isChildOf(node, parent) {
while (node) {
if (parent === node) {
return true;
}
node = node.parentNode;
}
return false;
}
var MenuButton = Button.extend({
/**
* Constructs a instance with the specified settings.
*
* @constructor
* @param {Object} settings Name/value object with settings.
*/
init: function(settings) {
var self = this;
self._renderOpen = true;
self._super(settings);
self.addClass('menubtn');
if (settings.fixedWidth) {
self.addClass('fixed-width');
}
self.aria('haspopup', true);
self.hasPopup = true;
},
/**
* Shows the menu for the button.
*
* @method showMenu
*/
showMenu: function() {
var self = this, settings = self.settings, menu;
if (self.menu && self.menu.visible()) {
return self.hideMenu();
}
if (!self.menu) {
menu = settings.menu || [];
// Is menu array then auto constuct menu control
if (menu.length) {
menu = {
type: 'menu',
items: menu
};
} else {
menu.type = menu.type || 'menu';
}
self.menu = Factory.create(menu).parent(self).renderTo();
self.fire('createmenu');
self.menu.reflow();
self.menu.on('cancel', function(e) {
if (e.control.parent() === self.menu) {
e.stopPropagation();
self.focus();
self.hideMenu();
}
});
// Move focus to button when a menu item is selected/clicked
self.menu.on('select', function() {
self.focus();
});
self.menu.on('show hide', function(e) {
if (e.control == self.menu) {
self.activeMenu(e.type == 'show');
}
self.aria('expanded', e.type == 'show');
}).fire('show');
}
self.menu.show();
self.menu.layoutRect({w: self.layoutRect().w});
self.menu.moveRel(self.getEl(), self.isRtl() ? ['br-tr', 'tr-br'] : ['bl-tl', 'tl-bl']);
},
/**
* Hides the menu for the button.
*
* @method hideMenu
*/
hideMenu: function() {
var self = this;
if (self.menu) {
self.menu.items().each(function(item) {
if (item.hideMenu) {
item.hideMenu();
}
});
self.menu.hide();
}
},
/**
* Sets the active menu state.
*
* @private
*/
activeMenu: function(state) {
this.toggleClass('active', state);
},
/**
* Renders the control as a HTML string.
*
* @method renderHtml
* @return {String} HTML representing the control.
*/
renderHtml: function() {
var self = this, id = self._id, prefix = self.classPrefix;
var icon = self.settings.icon, image;
image = self.settings.image;
if (image) {
icon = 'none';
// Support for [high dpi, low dpi] image sources
if (typeof image != "string") {
image = window.getSelection ? image[0] : image[1];
}
image = ' style="background-image: url(\'' + image + '\')"';
} else {
image = '';
}
icon = self.settings.icon ? prefix + 'ico ' + prefix + 'i-' + icon : '';
self.aria('role', self.parent() instanceof MenuBar ? 'menuitem' : 'button');
return (
'' +
'' +
''
);
},
/**
* Gets invoked after the control has been rendered.
*
* @method postRender
*/
postRender: function() {
var self = this;
self.on('click', function(e) {
if (e.control === self && isChildOf(e.target, self.getEl())) {
self.showMenu();
if (e.aria) {
self.menu.items()[0].focus();
}
}
});
self.on('mouseenter', function(e) {
var overCtrl = e.control, parent = self.parent(), hasVisibleSiblingMenu;
if (overCtrl && parent && overCtrl instanceof MenuButton && overCtrl.parent() == parent) {
parent.items().filter('MenuButton').each(function(ctrl) {
if (ctrl.hideMenu && ctrl != overCtrl) {
if (ctrl.menu && ctrl.menu.visible()) {
hasVisibleSiblingMenu = true;
}
ctrl.hideMenu();
}
});
if (hasVisibleSiblingMenu) {
overCtrl.focus(); // Fix for: #5887
overCtrl.showMenu();
}
}
});
return self._super();
},
/**
* Sets/gets the current button text.
*
* @method text
* @param {String} [text] New button text.
* @return {String|tinymce.ui.MenuButton} Current text or current MenuButton instance.
*/
text: function(text) {
var self = this, i, children;
if (self._rendered) {
children = self.getEl('open').getElementsByTagName('span');
for (i = 0; i < children.length; i++) {
children[i].innerHTML = (self.settings.icon && text ? '\u00a0' : '') + self.encode(text);
}
}
return this._super(text);
},
/**
* Removes the control and it's menus.
*
* @method remove
*/
remove: function() {
this._super();
if (this.menu) {
this.menu.remove();
}
}
});
return MenuButton;
});
// Included from: js/tinymce/classes/ui/ListBox.js
/**
* ListBox.js
*
* Copyright, Moxiecode Systems AB
* Released under LGPL License.
*
* License: http://www.tinymce.com/license
* Contributing: http://www.tinymce.com/contributing
*/
/**
* Creates a new list box control.
*
* @-x-less ListBox.less
* @class tinymce.ui.ListBox
* @extends tinymce.ui.MenuButton
*/
define("tinymce/ui/ListBox", [
"tinymce/ui/MenuButton"
], function(MenuButton) {
"use strict";
return MenuButton.extend({
/**
* Constructs a instance with the specified settings.
*
* @constructor
* @param {Object} settings Name/value object with settings.
* @setting {Array} values Array with values to add to list box.
*/
init: function(settings) {
var self = this, values, selected, selectedText, lastItemCtrl;
function setSelected(menuValues) {
// Try to find a selected value
for (var i = 0; i < menuValues.length; i++) {
selected = menuValues[i].selected || settings.value === menuValues[i].value;
if (selected) {
selectedText = selectedText || menuValues[i].text;
self._value = menuValues[i].value;
break;
}
// If the value has a submenu, try to find the selected values in that menu
if (menuValues[i].menu) {
setSelected(menuValues[i].menu);
}
}
}
self._values = values = settings.values;
if (values) {
setSelected(values);
// Default with first item
if (!selected && values.length > 0) {
selectedText = values[0].text;
self._value = values[0].value;
}
settings.menu = values;
}
settings.text = settings.text || selectedText || values[0].text;
self._super(settings);
self.addClass('listbox');
self.on('select', function(e) {
var ctrl = e.control;
if (lastItemCtrl) {
e.lastControl = lastItemCtrl;
}
if (settings.multiple) {
ctrl.active(!ctrl.active());
} else {
self.value(e.control.settings.value);
}
lastItemCtrl = ctrl;
});
},
/**
* disable/enable 某一list的item
* leanote ace life ace
* value = convert, state = true | false
*/
diableValue: function(value, state) {
var self = this;
var menu = self.settings.menu;
if (self.menu) {
self.menu.items().each(function(ctrl) {// menuitem
if(ctrl.value() === value) {
ctrl.disabled(state);
return;
}
});
} else {
for (var i = 0; i < menu.length; i++) {
if(menu[i].value == value) {
menu[i].disabled = state;
return;
}
}
}
},
/**
* Getter/setter function for the control value.
*
* @method value
* @param {String} [value] Value to be set.
* @return {Boolean/tinymce.ui.ListBox} Value or self if it's a set operation.
*/
value: function(value) {
var self = this, active, selectedText, menu;
function activateByValue(menu, value) {
menu.items().each(function(ctrl) {
active = ctrl.value() === value;
if (active) {
selectedText = selectedText || ctrl.text();
}
ctrl.active(active);
if (ctrl.menu) {
activateByValue(ctrl.menu, value);
}
});
}
function setActiveValues(menuValues) {
for (var i = 0; i < menuValues.length; i++) {
active = menuValues[i].value == value;
if (active) {
selectedText = selectedText || menuValues[i].text;
}
menuValues[i].active = active;
if (menuValues[i].menu) {
setActiveValues(menuValues[i].menu);
}
}
}
if (typeof value != "undefined") {
if (self.menu) {
activateByValue(self.menu, value);
} else {
menu = self.settings.menu;
setActiveValues(menu);
}
self.text(selectedText || this.settings.text);
}
return self._super(value);
}
});
});
// Included from: js/tinymce/classes/ui/MenuItem.js
/**
* MenuItem.js
*
* Copyright, Moxiecode Systems AB
* Released under LGPL License.
*
* License: http://www.tinymce.com/license
* Contributing: http://www.tinymce.com/contributing
*/
/**
* Creates a new menu item.
*
* @-x-less MenuItem.less
* @class tinymce.ui.MenuItem
* @extends tinymce.ui.Widget
*/
define("tinymce/ui/MenuItem", [
"tinymce/ui/Widget",
"tinymce/ui/Factory",
"tinymce/Env"
], function(Widget, Factory, Env) {
"use strict";
return Widget.extend({
Defaults: {
border: 0,
role: 'menuitem'
},
/**
* Constructs a instance with the specified settings.
*
* @constructor
* @param {Object} settings Name/value object with settings.
* @setting {Boolean} selectable Selectable menu.
* @setting {Array} menu Submenu array with items.
* @setting {String} shortcut Shortcut to display for menu item. Example: Ctrl+X
*/
init: function(settings) {
var self = this;
self.hasPopup = true;
self._super(settings);
settings = self.settings;
self.addClass('menu-item');
if (settings.menu) {
self.addClass('menu-item-expand');
}
if (settings.preview) {
self.addClass('menu-item-preview');
}
if (self._text === '-' || self._text === '|') {
self.addClass('menu-item-sep');
self.aria('role', 'separator');
self._text = '-';
}
if (settings.selectable) {
self.aria('role', 'menuitemcheckbox');
self.addClass('menu-item-checkbox');
settings.icon = 'selected';
}
if (!settings.preview && !settings.selectable) {
self.addClass('menu-item-normal');
}
self.on('mousedown', function(e) {
e.preventDefault();
});
if (settings.menu && !settings.ariaHideMenu) {
self.aria('haspopup', true);
}
},
/**
* Returns true/false if the menuitem has sub menu.
*
* @method hasMenus
* @return {Boolean} True/false state if it has submenu.
*/
hasMenus: function() {
return !!this.settings.menu;
},
/**
* Shows the menu for the menu item.
*
* @method showMenu
*/
showMenu: function() {
var self = this, settings = self.settings, menu, parent = self.parent();
parent.items().each(function(ctrl) {
if (ctrl !== self) {
ctrl.hideMenu();
}
});
if (settings.menu) {
menu = self.menu;
if (!menu) {
menu = settings.menu;
// Is menu array then auto constuct menu control
if (menu.length) {
menu = {
type: 'menu',
items: menu
};
} else {
menu.type = menu.type || 'menu';
}
if (parent.settings.itemDefaults) {
menu.itemDefaults = parent.settings.itemDefaults;
}
menu = self.menu = Factory.create(menu).parent(self).renderTo();
menu.reflow();
menu.on('cancel', function(e) {
e.stopPropagation();
self.focus();
menu.hide();
});
menu.on('show hide', function(e) {
e.control.items().each(function(ctrl) {
ctrl.active(ctrl.settings.selected);
});
}).fire('show');
menu.on('hide', function(e) {
if (e.control === menu) {
self.removeClass('selected');
}
});
menu.submenu = true;
} else {
menu.show();
}
menu._parentMenu = parent;
menu.addClass('menu-sub');
var rel = menu.testMoveRel(
self.getEl(),
self.isRtl() ? ['tl-tr', 'bl-br', 'tr-tl', 'br-bl'] : ['tr-tl', 'br-bl', 'tl-tr', 'bl-br']
);
menu.moveRel(self.getEl(), rel);
menu.rel = rel;
rel = 'menu-sub-' + rel;
menu.removeClass(menu._lastRel);
menu.addClass(rel);
menu._lastRel = rel;
self.addClass('selected');
self.aria('expanded', true);
}
},
/**
* Hides the menu for the menu item.
*
* @method hideMenu
*/
hideMenu: function() {
var self = this;
if (self.menu) {
self.menu.items().each(function(item) {
if (item.hideMenu) {
item.hideMenu();
}
});
self.menu.hide();
self.aria('expanded', false);
}
return self;
},
/**
* Renders the control as a HTML string.
*
* @method renderHtml
* @return {String} HTML representing the control.
*/
renderHtml: function() {
var self = this, id = self._id, settings = self.settings, prefix = self.classPrefix, text = self.encode(self._text);
var icon = self.settings.icon, image = '', shortcut = settings.shortcut;
// Converts shortcut format to Mac/PC variants
function convertShortcut(shortcut) {
var i, value, replace = {};
if (Env.mac) {
replace = {
alt: '⌥',
ctrl: '⌘',
shift: '⇧',
meta: '⌘'
};
} else {
replace = {
meta: 'Ctrl'
};
}
shortcut = shortcut.split('+');
for (i = 0; i < shortcut.length; i++) {
value = replace[shortcut[i].toLowerCase()];
if (value) {
shortcut[i] = value;
}
}
return shortcut.join('+');
}
if (icon) {
self.parent().addClass('menu-has-icons');
}
if (settings.image) {
icon = 'none';
image = ' style="background-image: url(\'' + settings.image + '\')"';
}
if (shortcut) {
shortcut = convertShortcut(shortcut);
}
icon = prefix + 'ico ' + prefix + 'i-' + (self.settings.icon || 'none');
return (
'' +
(text !== '-' ? '\u00a0' : '') +
(text !== '-' ? '' + text + '' : '') +
(shortcut ? ' ' : '') +
(settings.menu ? '' : '') +
''
);
},
/**
* Gets invoked after the control has been rendered.
*
* @method postRender
*/
postRender: function() {
var self = this, settings = self.settings;
var textStyle = settings.textStyle;
if (typeof textStyle == "function") {
textStyle = textStyle.call(this);
}
if (textStyle) {
var textElm = self.getEl('text');
if (textElm) {
textElm.setAttribute('style', textStyle);
}
}
self.on('mouseenter click', function(e) {
if (e.control === self) {
if (!settings.menu && e.type === 'click') {
self.fire('select');
self.parent().hideAll();
} else {
self.showMenu();
if (e.aria) {
self.menu.focus(true);
}
}
}
});
self._super();
return self;
},
active: function(state) {
if (typeof state != "undefined") {
this.aria('checked', state);
}
return this._super(state);
},
/**
* Removes the control and it's menus.
*
* @method remove
*/
remove: function() {
this._super();
if (this.menu) {
this.menu.remove();
}
}
});
});
// Included from: js/tinymce/classes/ui/Menu.js
/**
* Menu.js
*
* Copyright, Moxiecode Systems AB
* Released under LGPL License.
*
* License: http://www.tinymce.com/license
* Contributing: http://www.tinymce.com/contributing
*/
/**
* Creates a new menu.
*
* @-x-less Menu.less
* @class tinymce.ui.Menu
* @extends tinymce.ui.FloatPanel
*/
define("tinymce/ui/Menu", [
"tinymce/ui/FloatPanel",
"tinymce/ui/MenuItem",
"tinymce/util/Tools"
], function(FloatPanel, MenuItem, Tools) {
"use strict";
var Menu = FloatPanel.extend({
Defaults: {
defaultType: 'menuitem',
border: 1,
layout: 'stack',
role: 'application',
bodyRole: 'menu',
ariaRoot: true
},
/**
* Constructs a instance with the specified settings.
*
* @constructor
* @param {Object} settings Name/value object with settings.
*/
init: function(settings) {
var self = this;
settings.autohide = true;
settings.constrainToViewport = true;
if (settings.itemDefaults) {
var items = settings.items, i = items.length;
while (i--) {
items[i] = Tools.extend({}, settings.itemDefaults, items[i]);
}
}
self._super(settings);
self.addClass('menu');
},
/**
* Repaints the control after a layout operation.
*
* @method repaint
*/
repaint: function() {
this.toggleClass('menu-align', true);
this._super();
this.getEl().style.height = '';
this.getEl('body').style.height = '';
return this;
},
/**
* Hides/closes the menu.
*
* @method cancel
*/
cancel: function() {
var self = this;
self.hideAll();
self.fire('select');
},
/**
* Hide menu and all sub menus.
*
* @method hideAll
*/
hideAll: function() {
var self = this;
this.find('menuitem').exec('hideMenu');
return self._super();
},
/*
getContainerElm: function() {
var doc = document, id = this.classPrefix + 'menucontainer';
var elm = doc.getElementById(id);
if (!elm) {
elm = doc.createElement('div');
elm.id = id;
elm.setAttribute('role', 'application');
elm.className = this.classPrefix + '-reset';
elm.style.position = 'absolute';
elm.style.top = elm.style.left = '0';
elm.style.overflow = 'visible';
doc.body.appendChild(elm);
}
return elm;
},
*/
/**
* Invoked before the menu is rendered.
*
* @method preRender
*/
preRender: function() {
var self = this;
self.items().each(function(ctrl) {
var settings = ctrl.settings;
if (settings.icon || settings.selectable) {
self._hasIcons = true;
return false;
}
});
return self._super();
}
});
return Menu;
});
// Included from: js/tinymce/classes/ui/Radio.js
/**
* Radio.js
*
* Copyright, Moxiecode Systems AB
* Released under LGPL License.
*
* License: http://www.tinymce.com/license
* Contributing: http://www.tinymce.com/contributing
*/
/**
* Creates a new radio button.
*
* @-x-less Radio.less
* @class tinymce.ui.Radio
* @extends tinymce.ui.Checkbox
*/
define("tinymce/ui/Radio", [
"tinymce/ui/Checkbox"
], function(Checkbox) {
"use strict";
return Checkbox.extend({
Defaults: {
classes: "radio",
role: "radio"
}
});
});
// Included from: js/tinymce/classes/ui/ResizeHandle.js
/**
* ResizeHandle.js
*
* Copyright, Moxiecode Systems AB
* Released under LGPL License.
*
* License: http://www.tinymce.com/license
* Contributing: http://www.tinymce.com/contributing
*/
/**
* Renders a resize handle that fires ResizeStart, Resize and ResizeEnd events.
*
* @-x-less ResizeHandle.less
* @class tinymce.ui.ResizeHandle
* @extends tinymce.ui.Widget
*/
define("tinymce/ui/ResizeHandle", [
"tinymce/ui/Widget",
"tinymce/ui/DragHelper"
], function(Widget, DragHelper) {
"use strict";
return Widget.extend({
/**
* Renders the control as a HTML string.
*
* @method renderHtml
* @return {String} HTML representing the control.
*/
renderHtml: function() {
var self = this, prefix = self.classPrefix;
self.addClass('resizehandle');
if (self.settings.direction == "both") {
self.addClass('resizehandle-both');
}
self.canFocus = false;
return (
'' +
'' +
''
);
},
/**
* Called after the control has been rendered.
*
* @method postRender
*/
postRender: function() {
var self = this;
self._super();
self.resizeDragHelper = new DragHelper(this._id, {
start: function() {
self.fire('ResizeStart');
},
drag: function(e) {
if (self.settings.direction != "both") {
e.deltaX = 0;
}
self.fire('Resize', e);
},
stop: function() {
self.fire('ResizeEnd');
}
});
},
remove: function() {
if (this.resizeDragHelper) {
this.resizeDragHelper.destroy();
}
return this._super();
}
});
});
// Included from: js/tinymce/classes/ui/Spacer.js
/**
* Spacer.js
*
* Copyright, Moxiecode Systems AB
* Released under LGPL License.
*
* License: http://www.tinymce.com/license
* Contributing: http://www.tinymce.com/contributing
*/
/**
* Creates a spacer. This control is used in flex layouts for example.
*
* @-x-less Spacer.less
* @class tinymce.ui.Spacer
* @extends tinymce.ui.Widget
*/
define("tinymce/ui/Spacer", [
"tinymce/ui/Widget"
], function(Widget) {
"use strict";
return Widget.extend({
/**
* Renders the control as a HTML string.
*
* @method renderHtml
* @return {String} HTML representing the control.
*/
renderHtml: function() {
var self = this;
self.addClass('spacer');
self.canFocus = false;
return '';
}
});
});
// Included from: js/tinymce/classes/ui/SplitButton.js
/**
* SplitButton.js
*
* Copyright, Moxiecode Systems AB
* Released under LGPL License.
*
* License: http://www.tinymce.com/license
* Contributing: http://www.tinymce.com/contributing
*/
/**
* Creates a split button.
*
* @-x-less SplitButton.less
* @class tinymce.ui.SplitButton
* @extends tinymce.ui.MenuButton
*/
define("tinymce/ui/SplitButton", [
"tinymce/ui/MenuButton",
"tinymce/ui/DomUtils"
], function(MenuButton, DomUtils) {
return MenuButton.extend({
Defaults: {
classes: "widget btn splitbtn",
role: "button"
},
/**
* Repaints the control after a layout operation.
*
* @method repaint
*/
repaint: function() {
var self = this, elm = self.getEl(), rect = self.layoutRect(), mainButtonElm, menuButtonElm;
self._super();
mainButtonElm = elm.firstChild;
menuButtonElm = elm.lastChild;
DomUtils.css(mainButtonElm, {
width: rect.w - DomUtils.getSize(menuButtonElm).width,
height: rect.h - 2
});
DomUtils.css(menuButtonElm, {
height: rect.h - 2
});
return self;
},
/**
* Sets the active menu state.
*
* @private
*/
activeMenu: function(state) {
var self = this;
DomUtils.toggleClass(self.getEl().lastChild, self.classPrefix + 'active', state);
},
/**
* Renders the control as a HTML string.
*
* @method renderHtml
* @return {String} HTML representing the control.
*/
renderHtml: function() {
var self = this, id = self._id, prefix = self.classPrefix, image;
var icon = self.settings.icon;
image = self.settings.image;
if (image) {
icon = 'none';
// Support for [high dpi, low dpi] image sources
if (typeof image != "string") {
image = window.getSelection ? image[0] : image[1];
}
image = ' style="background-image: url(\'' + image + '\')"';
} else {
image = '';
}
icon = self.settings.icon ? prefix + 'ico ' + prefix + 'i-' + icon : '';
return (
'' +
'' +
'' +
''
);
},
/**
* Called after the control has been rendered.
*
* @method postRender
*/
postRender: function() {
var self = this, onClickHandler = self.settings.onclick;
self.on('click', function(e) {
var node = e.target;
if (e.control == this) {
// Find clicks that is on the main button
while (node) {
if ((e.aria && e.aria.key != 'down') || (node.nodeName == 'BUTTON' && node.className.indexOf('open') == -1)) {
e.stopImmediatePropagation();
onClickHandler.call(this, e);
return;
}
node = node.parentNode;
}
}
});
delete self.settings.onclick;
return self._super();
}
});
});
// Included from: js/tinymce/classes/ui/StackLayout.js
/**
* StackLayout.js
*
* Copyright, Moxiecode Systems AB
* Released under LGPL License.
*
* License: http://www.tinymce.com/license
* Contributing: http://www.tinymce.com/contributing
*/
/**
* This layout uses the browsers layout when the items are blocks.
*
* @-x-less StackLayout.less
* @class tinymce.ui.StackLayout
* @extends tinymce.ui.FlowLayout
*/
define("tinymce/ui/StackLayout", [
"tinymce/ui/FlowLayout"
], function(FlowLayout) {
"use strict";
return FlowLayout.extend({
Defaults: {
containerClass: 'stack-layout',
controlClass: 'stack-layout-item',
endClass: 'break'
}
});
});
// Included from: js/tinymce/classes/ui/TabPanel.js
/**
* TabPanel.js
*
* Copyright, Moxiecode Systems AB
* Released under LGPL License.
*
* License: http://www.tinymce.com/license
* Contributing: http://www.tinymce.com/contributing
*/
/**
* Creates a tab panel control.
*
* @-x-less TabPanel.less
* @class tinymce.ui.TabPanel
* @extends tinymce.ui.Panel
*
* @setting {Number} activeTab Active tab index.
*/
define("tinymce/ui/TabPanel", [
"tinymce/ui/Panel",
"tinymce/ui/DomUtils"
], function(Panel, DomUtils) {
"use strict";
return Panel.extend({
Defaults: {
layout: 'absolute',
defaults: {
type: 'panel'
}
},
/**
* Activates the specified tab by index.
*
* @method activateTab
* @param {Number} idx Index of the tab to activate.
*/
activateTab: function(idx) {
var activeTabElm;
if (this.activeTabId) {
activeTabElm = this.getEl(this.activeTabId);
DomUtils.removeClass(activeTabElm, this.classPrefix + 'active');
activeTabElm.setAttribute('aria-selected', "false");
}
this.activeTabId = 't' + idx;
activeTabElm = this.getEl('t' + idx);
activeTabElm.setAttribute('aria-selected', "true");
DomUtils.addClass(activeTabElm, this.classPrefix + 'active');
this.items()[idx].show().fire('showtab');
this.reflow();
this.items().each(function(item, i) {
if (idx != i) {
item.hide();
}
});
},
/**
* Renders the control as a HTML string.
*
* @method renderHtml
* @return {String} HTML representing the control.
*/
renderHtml: function() {
var self = this, layout = self._layout, tabsHtml = '', prefix = self.classPrefix;
self.preRender();
layout.preRender(self);
self.items().each(function(ctrl, i) {
var id = self._id + '-t' + i;
ctrl.aria('role', 'tabpanel');
ctrl.aria('labelledby', id);
tabsHtml += (
'' +
self.encode(ctrl.settings.title) +
''
);
});
return (
'' +
'' +
tabsHtml +
'' +
'' +
layout.renderHtml(self) +
'' +
''
);
},
/**
* Called after the control has been rendered.
*
* @method postRender
*/
postRender: function() {
var self = this;
self._super();
self.settings.activeTab = self.settings.activeTab || 0;
self.activateTab(self.settings.activeTab);
this.on('click', function(e) {
var targetParent = e.target.parentNode;
if (e.target.parentNode.id == self._id + '-head') {
var i = targetParent.childNodes.length;
while (i--) {
if (targetParent.childNodes[i] == e.target) {
self.activateTab(i);
}
}
}
});
},
/**
* Initializes the current controls layout rect.
* This will be executed by the layout managers to determine the
* default minWidth/minHeight etc.
*
* @method initLayoutRect
* @return {Object} Layout rect instance.
*/
initLayoutRect: function() {
var self = this, rect, minW, minH;
minW = DomUtils.getSize(self.getEl('head')).width;
minW = minW < 0 ? 0 : minW;
minH = 0;
self.items().each(function(item) {
minW = Math.max(minW, item.layoutRect().minW);
minH = Math.max(minH, item.layoutRect().minH);
});
self.items().each(function(ctrl) {
ctrl.settings.x = 0;
ctrl.settings.y = 0;
ctrl.settings.w = minW;
ctrl.settings.h = minH;
ctrl.layoutRect({
x: 0,
y: 0,
w: minW,
h: minH
});
});
var headH = DomUtils.getSize(self.getEl('head')).height;
self.settings.minWidth = minW;
self.settings.minHeight = minH + headH;
rect = self._super();
rect.deltaH += headH;
rect.innerH = rect.h - rect.deltaH;
return rect;
}
});
});
// Included from: js/tinymce/classes/ui/TextBox.js
/**
* TextBox.js
*
* Copyright, Moxiecode Systems AB
* Released under LGPL License.
*
* License: http://www.tinymce.com/license
* Contributing: http://www.tinymce.com/contributing
*/
/**
* Creates a new textbox.
*
* @-x-less TextBox.less
* @class tinymce.ui.TextBox
* @extends tinymce.ui.Widget
*/
define("tinymce/ui/TextBox", [
"tinymce/ui/Widget",
"tinymce/ui/DomUtils"
], function(Widget, DomUtils) {
"use strict";
return Widget.extend({
/**
* Constructs a instance with the specified settings.
*
* @constructor
* @param {Object} settings Name/value object with settings.
* @setting {Boolean} multiline True if the textbox is a multiline control.
* @setting {Number} maxLength Max length for the textbox.
* @setting {Number} size Size of the textbox in characters.
*/
init: function(settings) {
var self = this;
self._super(settings);
self._value = settings.value || '';
self.addClass('textbox');
if (settings.multiline) {
self.addClass('multiline');
} else {
// TODO: Rework this
self.on('keydown', function(e) {
if (e.keyCode == 13) {
self.parents().reverse().each(function(ctrl) {
e.preventDefault();
if (ctrl.hasEventListeners('submit') && ctrl.toJSON) {
ctrl.fire('submit', {data: ctrl.toJSON()});
return false;
}
});
}
});
}
},
/**
* Getter/setter function for the disabled state.
*
* @method value
* @param {Boolean} [state] State to be set.
* @return {Boolean|tinymce.ui.ComboBox} True/false or self if it's a set operation.
*/
disabled: function(state) {
var self = this;
if (self._rendered && typeof state != 'undefined') {
self.getEl().disabled = state;
}
return self._super(state);
},
/**
* Getter/setter function for the control value.
*
* @method value
* @param {String} [value] Value to be set.
* @return {String|tinymce.ui.ComboBox} Value or self if it's a set operation.
*/
value: function(value) {
var self = this;
if (typeof value != "undefined") {
self._value = value;
if (self._rendered) {
self.getEl().value = value;
}
return self;
}
if (self._rendered) {
return self.getEl().value;
}
return self._value;
},
/**
* Repaints the control after a layout operation.
*
* @method repaint
*/
repaint: function() {
var self = this, style, rect, borderBox, borderW = 0, borderH = 0, lastRepaintRect;
style = self.getEl().style;
rect = self._layoutRect;
lastRepaintRect = self._lastRepaintRect || {};
// Detect old IE 7+8 add lineHeight to align caret vertically in the middle
var doc = document;
if (!self.settings.multiline && doc.all && (!doc.documentMode || doc.documentMode <= 8)) {
style.lineHeight = (rect.h - borderH) + 'px';
}
borderBox = self._borderBox;
borderW = borderBox.left + borderBox.right + 8;
borderH = borderBox.top + borderBox.bottom + (self.settings.multiline ? 8 : 0);
if (rect.x !== lastRepaintRect.x) {
style.left = rect.x + 'px';
lastRepaintRect.x = rect.x;
}
if (rect.y !== lastRepaintRect.y) {
style.top = rect.y + 'px';
lastRepaintRect.y = rect.y;
}
if (rect.w !== lastRepaintRect.w) {
style.width = (rect.w - borderW) + 'px';
lastRepaintRect.w = rect.w;
}
if (rect.h !== lastRepaintRect.h) {
style.height = (rect.h - borderH) + 'px';
lastRepaintRect.h = rect.h;
}
self._lastRepaintRect = lastRepaintRect;
self.fire('repaint', {}, false);
return self;
},
/**
* Renders the control as a HTML string.
*
* @method renderHtml
* @return {String} HTML representing the control.
*/
renderHtml: function() {
var self = this, id = self._id, settings = self.settings, value = self.encode(self._value, false), extraAttrs = '';
if ("spellcheck" in settings) {
extraAttrs += ' spellcheck="' + settings.spellcheck + '"';
}
if (settings.maxLength) {
extraAttrs += ' maxlength="' + settings.maxLength + '"';
}
if (settings.size) {
extraAttrs += ' size="' + settings.size + '"';
}
if (settings.subtype) {
extraAttrs += ' type="' + settings.subtype + '"';
}
if (self.disabled()) {
extraAttrs += ' disabled="disabled"';
}
if (settings.multiline) {
return (
''
);
}
return '';
},
/**
* Called after the control has been rendered.
*
* @method postRender
*/
postRender: function() {
var self = this;
DomUtils.on(self.getEl(), 'change', function(e) {
self.fire('change', e);
});
return self._super();
},
remove: function() {
DomUtils.off(this.getEl());
this._super();
}
});
});
// Included from: js/tinymce/classes/ui/Throbber.js
/**
* Throbber.js
*
* Copyright, Moxiecode Systems AB
* Released under LGPL License.
*
* License: http://www.tinymce.com/license
* Contributing: http://www.tinymce.com/contributing
*/
/**
* This class enables you to display a Throbber for any element.
*
* @-x-less Throbber.less
* @class tinymce.ui.Throbber
*/
define("tinymce/ui/Throbber", [
"tinymce/ui/DomUtils",
"tinymce/ui/Control"
], function(DomUtils, Control) {
"use strict";
/**
* Constructs a new throbber.
*
* @constructor
* @param {Element} elm DOM Html element to display throbber in.
* @param {Boolean} inline Optional true/false state if the throbber should be appended to end of element for infinite scroll.
*/
return function(elm, inline) {
var self = this, state, classPrefix = Control.classPrefix;
/**
* Shows the throbber.
*
* @method show
* @param {Number} [time] Time to wait before showing.
* @return {tinymce.ui.Throbber} Current throbber instance.
*/
self.show = function(time) {
self.hide();
state = true;
window.setTimeout(function() {
if (state) {
elm.appendChild(DomUtils.createFragment(
''
));
}
}, time || 0);
return self;
};
/**
* Hides the throbber.
*
* @method hide
* @return {tinymce.ui.Throbber} Current throbber instance.
*/
self.hide = function() {
var child = elm.lastChild;
if (child && child.className.indexOf('throbber') != -1) {
child.parentNode.removeChild(child);
}
state = false;
return self;
};
};
});
expose(["tinymce/dom/EventUtils","tinymce/dom/Sizzle","tinymce/Env","tinymce/util/Tools","tinymce/dom/DomQuery","tinymce/html/Styles","tinymce/dom/TreeWalker","tinymce/dom/Range","tinymce/html/Entities","tinymce/dom/DOMUtils","tinymce/dom/ScriptLoader","tinymce/AddOnManager","tinymce/dom/RangeUtils","tinymce/html/Node","tinymce/html/Schema","tinymce/html/SaxParser","tinymce/html/DomParser","tinymce/html/Writer","tinymce/html/Serializer","tinymce/dom/Serializer","tinymce/dom/TridentSelection","tinymce/util/VK","tinymce/dom/ControlSelection","tinymce/dom/BookmarkManager","tinymce/dom/Selection","tinymce/dom/ElementUtils","tinymce/Formatter","tinymce/UndoManager","tinymce/EnterKey","tinymce/ForceBlocks","tinymce/EditorCommands","tinymce/util/URI","tinymce/util/Class","tinymce/util/EventDispatcher","tinymce/ui/Selector","tinymce/ui/Collection","tinymce/ui/DomUtils","tinymce/ui/Control","tinymce/ui/Factory","tinymce/ui/KeyboardNavigation","tinymce/ui/Container","tinymce/ui/DragHelper","tinymce/ui/Scrollable","tinymce/ui/Panel","tinymce/ui/Movable","tinymce/ui/Resizable","tinymce/ui/FloatPanel","tinymce/ui/Window","tinymce/ui/MessageBox","tinymce/WindowManager","tinymce/util/Quirks","tinymce/util/Observable","tinymce/EditorObservable","tinymce/Shortcuts","tinymce/Editor","tinymce/util/I18n","tinymce/FocusManager","tinymce/EditorManager","tinymce/LegacyInput","tinymce/util/XHR","tinymce/util/JSON","tinymce/util/JSONRequest","tinymce/util/JSONP","tinymce/util/LocalStorage","tinymce/Compat","tinymce/ui/Layout","tinymce/ui/AbsoluteLayout","tinymce/ui/Tooltip","tinymce/ui/Widget","tinymce/ui/Button","tinymce/ui/ButtonGroup","tinymce/ui/Checkbox","tinymce/ui/ComboBox","tinymce/ui/ColorBox","tinymce/ui/PanelButton","tinymce/ui/ColorButton","tinymce/util/Color","tinymce/ui/ColorPicker","tinymce/ui/Path","tinymce/ui/ElementPath","tinymce/ui/FormItem","tinymce/ui/Form","tinymce/ui/FieldSet","tinymce/ui/FilePicker","tinymce/ui/FitLayout","tinymce/ui/FlexLayout","tinymce/ui/FlowLayout","tinymce/ui/FormatControls","tinymce/ui/GridLayout","tinymce/ui/Iframe","tinymce/ui/Label","tinymce/ui/Toolbar","tinymce/ui/MenuBar","tinymce/ui/MenuButton","tinymce/ui/ListBox","tinymce/ui/MenuItem","tinymce/ui/Menu","tinymce/ui/Radio","tinymce/ui/ResizeHandle","tinymce/ui/Spacer","tinymce/ui/SplitButton","tinymce/ui/StackLayout","tinymce/ui/TabPanel","tinymce/ui/TextBox","tinymce/ui/Throbber"]);
})(this);