/** * $Id$ * * @author Moxiecode * @copyright Copyright © 2004-2007, Moxiecode Systems AB, All rights reserved. */ /** * Core engine class for TinyMCE, a instance of this class is available as a global called tinyMCE. * * @constructor */ function TinyMCE_Engine() { var ua; this.majorVersion = "2"; this.minorVersion = "1.2"; this.releaseDate = "2007-08-21"; this.instances = []; this.switchClassCache = []; this.windowArgs = []; this.loadedFiles = []; this.pendingFiles = []; this.loadingIndex = 0; this.configs = []; this.currentConfig = 0; this.eventHandlers = []; this.log = []; this.undoLevels = []; this.undoIndex = 0; this.typingUndoIndex = -1; this.settings = []; // Browser check ua = navigator.userAgent; this.isMSIE = (navigator.appName == "Microsoft Internet Explorer"); this.isMSIE5 = this.isMSIE && (ua.indexOf('MSIE 5') != -1); this.isMSIE5_0 = this.isMSIE && (ua.indexOf('MSIE 5.0') != -1); this.isMSIE7 = this.isMSIE && (ua.indexOf('MSIE 7') != -1); this.isGecko = ua.indexOf('Gecko') != -1; // Will also be true on Safari this.isSafari = ua.indexOf('Safari') != -1; this.isOpera = window['opera'] && opera.buildNumber ? true : false; this.isMac = ua.indexOf('Mac') != -1; this.isNS7 = ua.indexOf('Netscape/7') != -1; this.isNS71 = ua.indexOf('Netscape/7.1') != -1; this.dialogCounter = 0; this.plugins = []; this.themes = []; this.menus = []; this.loadedPlugins = []; this.buttonMap = []; this.isLoaded = false; // Fake MSIE on Opera and if Opera fakes IE, Gecko or Safari cancel those if (this.isOpera) { this.isMSIE = true; this.isGecko = false; this.isSafari = false; } this.isIE = this.isMSIE; this.isRealIE = this.isMSIE && !this.isOpera; // TinyMCE editor id instance counter this.idCounter = 0; }; /**#@+ * @member TinyMCE_Engine */ TinyMCE_Engine.prototype = { /**#@+ * @method */ /** * Initializes TinyMCE with the specific configuration settings. This method * may be called multiple times when multiple instances with diffrent settings is to be created. * * @param {Array} Name/Value array of initialization settings. */ init : function(settings) { var theme, nl, baseHREF = "", i, cssPath, entities, h, p, src, elements = [], head; // IE 5.0x is no longer supported since 5.5, 6.0 and 7.0 now exists. We can't support old browsers forever, sorry. if (this.isMSIE5_0) return; this.settings = settings; // Check if valid browser has execcommand support if (typeof(document.execCommand) == 'undefined') return; // Get script base path if (!tinyMCE.baseURL) { // Search through head head = document.getElementsByTagName('head')[0]; if (head) { for (i=0, nl = head.getElementsByTagName('script'); i'); this._def("font_size_classes", ''); this._def("font_size_style_values", 'xx-small,x-small,small,medium,large,x-large,xx-large', true); this._def("event_elements", 'a,img', true); this._def("convert_urls", true); this._def("table_inline_editing", false); this._def("object_resizing", true); this._def("custom_shortcuts", true); this._def("convert_on_click", false); this._def("content_css", ''); this._def("fix_list_elements", true); this._def("fix_table_elements", false); this._def("strict_loading_mode", document.contentType == 'application/xhtml+xml'); this._def("hidden_tab_class", ''); this._def("display_tab_class", ''); this._def("gecko_spellcheck", false); this._def("hide_selects_on_submit", true); this._def("forced_root_block", false); this._def("remove_trailing_nbsp", false); this._def("save_on_tinymce_forms", false); // Force strict loading mode to false on non Gecko browsers if (this.isMSIE && !this.isOpera) this.settings.strict_loading_mode = false; // Browser check IE if (this.isMSIE && this.settings.browsers.indexOf('msie') == -1) return; // Browser check Gecko if (this.isGecko && this.settings.browsers.indexOf('gecko') == -1) return; // Browser check Safari if (this.isSafari && this.settings.browsers.indexOf('safari') == -1) return; // Browser check Opera if (this.isOpera && this.settings.browsers.indexOf('opera') == -1) return; // If not super absolute make it so baseHREF = tinyMCE.settings.document_base_url; h = document.location.href; p = h.indexOf('://'); if (p > 0 && document.location.protocol != "file:") { p = h.indexOf('/', p + 3); h = h.substring(0, p); if (baseHREF.indexOf('://') == -1) baseHREF = h + baseHREF; tinyMCE.settings.document_base_url = baseHREF; tinyMCE.settings.document_base_prefix = h; } // Trim away query part if (baseHREF.indexOf('?') != -1) baseHREF = baseHREF.substring(0, baseHREF.indexOf('?')); this.settings.base_href = baseHREF.substring(0, baseHREF.lastIndexOf('/')) + "/"; theme = this.settings.theme; this.inlineStrict = 'A|BR|SPAN|BDO|MAP|OBJECT|IMG|TT|I|B|BIG|SMALL|EM|STRONG|DFN|CODE|Q|SAMP|KBD|VAR|CITE|ABBR|ACRONYM|SUB|SUP|#text|#comment'; this.inlineTransitional = 'A|BR|SPAN|BDO|OBJECT|APPLET|IMG|MAP|IFRAME|TT|I|B|U|S|STRIKE|BIG|SMALL|FONT|BASEFONT|EM|STRONG|DFN|CODE|Q|SAMP|KBD|VAR|CITE|ABBR|ACRONYM|SUB|SUP|INPUT|SELECT|TEXTAREA|LABEL|BUTTON|#text|#comment'; this.blockElms = 'H[1-6]|P|DIV|ADDRESS|PRE|FORM|TABLE|LI|OL|UL|TD|CAPTION|BLOCKQUOTE|CENTER|DL|DT|DD|DIR|FIELDSET|FORM|NOSCRIPT|NOFRAMES|MENU|ISINDEX|SAMP'; this.blockRegExp = new RegExp("^(" + this.blockElms + ")$", "i"); this.posKeyCodes = [13,45,36,35,33,34,37,38,39,40]; this.uniqueURL = 'javascript:void(091039730);'; // Make unique URL non real URL this.uniqueTag = ''; this.callbacks = ['onInit', 'getInfo', 'getEditorTemplate', 'setupContent', 'onChange', 'onPageLoad', 'handleNodeChange', 'initInstance', 'execCommand', 'getControlHTML', 'handleEvent', 'cleanup', 'removeInstance']; // Theme url this.settings.theme_href = tinyMCE.baseURL + "/themes/" + theme; if (!tinyMCE.isIE || tinyMCE.isOpera) this.settings.force_br_newlines = false; if (tinyMCE.getParam("popups_css", false)) { cssPath = tinyMCE.getParam("popups_css", ""); // Is relative if (cssPath.indexOf('://') == -1 && cssPath.charAt(0) != '/') this.settings.popups_css = this.documentBasePath + "/" + cssPath; else this.settings.popups_css = cssPath; } else this.settings.popups_css = tinyMCE.baseURL + "/themes/" + theme + "/css/editor_popup.css"; if (tinyMCE.getParam("editor_css", false)) { cssPath = tinyMCE.getParam("editor_css", ""); // Is relative if (cssPath.indexOf('://') == -1 && cssPath.charAt(0) != '/') this.settings.editor_css = this.documentBasePath + "/" + cssPath; else this.settings.editor_css = cssPath; } else { if (this.settings.editor_css !== '') this.settings.editor_css = tinyMCE.baseURL + "/themes/" + theme + "/css/editor_ui.css"; } // Only do this once if (this.configs.length == 0) { if (typeof(TinyMCECompressed) == "undefined") { tinyMCE.addEvent(window, "DOMContentLoaded", TinyMCE_Engine.prototype.onLoad); if (tinyMCE.isRealIE) { if (document.body) tinyMCE.addEvent(document.body, "readystatechange", TinyMCE_Engine.prototype.onLoad); else tinyMCE.addEvent(document, "readystatechange", TinyMCE_Engine.prototype.onLoad); } tinyMCE.addEvent(window, "load", TinyMCE_Engine.prototype.onLoad); tinyMCE._addUnloadEvents(); } } this.loadScript(tinyMCE.baseURL + '/themes/' + this.settings.theme + '/editor_template' + tinyMCE.srcMode + '.js'); this.loadScript(tinyMCE.baseURL + '/langs/' + this.settings.language + '.js'); this.loadCSS(this.settings.editor_css); // Add plugins p = tinyMCE.getParam('plugins', '', true, ','); if (p.length > 0) { for (i=0; i&"]', 'g'); }, /** * Adds unload event handles to execute triggerSave. * * @private */ _addUnloadEvents : function() { var st = tinyMCE.settings.add_unload_trigger; if (tinyMCE.isIE) { if (st) { tinyMCE.addEvent(window, "unload", TinyMCE_Engine.prototype.unloadHandler); tinyMCE.addEvent(window.document, "beforeunload", TinyMCE_Engine.prototype.unloadHandler); } } else { if (st) tinyMCE.addEvent(window, "unload", function () {tinyMCE.triggerSave(true, true);}); } }, /** * Assigns a default value for a specific config parameter. * * @param {string} key Settings key to add default value to. * @param {object} def_val Default value to assign if the settings option isn't defined. * @param {boolean} t Trim all white space, if true all whitespace will be removed from option value. * @private */ _def : function(key, def_val, t) { var v = tinyMCE.getParam(key, def_val); v = t ? v.replace(/\s+/g, "") : v; this.settings[key] = v; }, /** * Returns true/false if the specified plugin is loaded or not. * * @param {string} n Plugin name to look for. * @return true/false if the specified plugin is loaded or not. * @type boolean */ hasPlugin : function(n) { return typeof(this.plugins[n]) != "undefined" && this.plugins[n] != null; }, /** * Adds the specified plugin to the list of loaded plugins, this will also setup the baseURL * property of the plugin. * * @param {string} Plugin name/id. * @param {TinyMCE_Plugin} p Plugin instance to add. */ addPlugin : function(n, p) { var op = this.plugins[n]; // Use the previous plugin object base URL used when loading external plugins p.baseURL = op ? op.baseURL : tinyMCE.baseURL + "/plugins/" + n; this.plugins[n] = p; this.loadNextScript(); }, /** * Sets the baseURL of the specified plugin, this is useful if the plugin is loaded from * a external location. * * @param {string} n Plugin name/id to set base URL on. This have to be added before. * @param {string} u Base URL of plugin, this string should be the URL prefix for the plugin without a trailing slash. */ setPluginBaseURL : function(n, u) { var op = this.plugins[n]; if (op) op.baseURL = u; else this.plugins[n] = {baseURL : u}; }, /** * Load plugin from external URL. * * @param {string} n Plugin name for example \"emotions\". * @param {string} u URL of plugin directory to load. */ loadPlugin : function(n, u) { u = u.indexOf('.js') != -1 ? u.substring(0, u.lastIndexOf('/')) : u; u = u.charAt(u.length-1) == '/' ? u.substring(0, u.length-1) : u; this.plugins[n] = {baseURL : u}; this.loadScript(u + "/editor_plugin" + (tinyMCE.srcMode ? '_src' : '') + ".js"); }, /** * Returns true/false if the specified theme is loaded or not. * * @param {string} n Theme name/id to check for. * @return true/false if the specified theme is loaded or not. * @type boolean */ hasTheme : function(n) { return typeof(this.themes[n]) != "undefined" && this.themes[n] != null; }, /** * Adds the specified theme in to the list of loaded themes. * * @param {string} n Theme name/id to add the object reference to. * @param {TinyMCE_Theme} t Theme instance to add to the loaded list. */ addTheme : function(n, t) { this.themes[n] = t; this.loadNextScript(); }, /** * Adds a floating menu instance to TinyMCE. * * @param {string} n TinyMCE menu id. * @param {TinyMCE_Menu} m TinyMCE menu instance. */ addMenu : function(n, m) { this.menus[n] = m; }, /** * Checks if the specified menu by name is added to TinyMCE. * * @param {string} n TinyMCE menu id. * @return true/false if it exists or not. * @type boolean */ hasMenu : function(n) { return typeof(this.plugins[n]) != "undefined" && this.plugins[n] != null; }, /** * Loads the specified script by writing the a script tag to the current page. * This will also check if the file has been loaded before. This function should only be used * when the page is loading. * * @param {string} url Script URL to load. */ loadScript : function(url) { var i; for (i=0; i'); this.loadedFiles[this.loadedFiles.length] = url; }, /** * Loads the next script in chain. */ loadNextScript : function() { var d = document, se; if (!tinyMCE.settings.strict_loading_mode) return; if (this.loadingIndex < this.pendingFiles.length) { se = d.createElementNS('http://www.w3.org/1999/xhtml', 'script'); se.setAttribute('language', 'javascript'); se.setAttribute('type', 'text/javascript'); se.setAttribute('src', this.pendingFiles[this.loadingIndex++]); d.getElementsByTagName("head")[0].appendChild(se); } else this.loadingIndex = -1; // Done with loading }, /** * Loads the specified CSS by writing the a link tag to the current page. * This will also check if the file has been loaded before. This function should only be used * when the page is loading. * * @param {string} url CSS file URL to load or comma separated list of files. */ loadCSS : function(url) { var ar = url.replace(/\s+/, '').split(','); var lflen = 0, csslen = 0, skip = false; var x = 0, i = 0, nl, le; for (x = 0,csslen = ar.length; x 0) { /* Make sure it doesn't exist. */ for (i=0, lflen=this.loadedFiles.length; i'); this.loadedFiles[this.loadedFiles.length] = ar[x]; } } } }, /** * Imports a CSS file into a allready loaded document. This will add a link element * to the head element of the document. * * @param {DOMDocument} doc DOM Document to load CSS into. * @param {string} css CSS File URL to load or comma separated list of files. */ importCSS : function(doc, css) { var css_ary = css.replace(/\s+/, '').split(','); var csslen, elm, headArr, x, css_file; for (x = 0, csslen = css_ary.length; x 0) { // Is relative, make absolute if (css_file.indexOf('://') == -1 && css_file.charAt(0) != '/') css_file = this.documentBasePath + "/" + css_file; if (typeof(doc.createStyleSheet) == "undefined") { elm = doc.createElement("link"); elm.rel = "stylesheet"; elm.href = css_file; if ((headArr = doc.getElementsByTagName("head")) != null && headArr.length > 0) headArr[0].appendChild(elm); } else doc.createStyleSheet(css_file); } } }, /** * Displays a confirm dialog when a user clicks/focus a textarea that is to be converted into * a TinyMCE instance. * * @param {DOMEvent} e DOM event instance. * @param {Array} settings Name/Value array of initialization settings. */ confirmAdd : function(e, settings) { var elm = tinyMCE.isIE ? event.srcElement : e.target; var elementId = elm.name ? elm.name : elm.id; tinyMCE.settings = settings; if (tinyMCE.settings.convert_on_click || (!elm.getAttribute('mce_noask') && confirm(tinyMCELang.lang_edit_confirm))) tinyMCE.addMCEControl(elm, elementId); elm.setAttribute('mce_noask', 'true'); }, /** * Moves the contents from the hidden textarea to the editor that gets inserted. * * @param {string} form_element_name Form element name to move contents from. * @deprecated */ updateContent : function(form_element_name) { var formElement, n, inst, doc; // Find MCE instance linked to given form element and copy it's value formElement = document.getElementById(form_element_name); for (n in tinyMCE.instances) { inst = tinyMCE.instances[n]; if (!tinyMCE.isInstance(inst)) continue; inst.switchSettings(); if (inst.formElement == formElement) { doc = inst.getDoc(); tinyMCE._setHTML(doc, inst.formElement.value); if (!tinyMCE.isIE) doc.body.innerHTML = tinyMCE._cleanupHTML(inst, doc, this.settings, doc.body, inst.visualAid); } } }, /** * Adds a TinyMCE editor control instance to a specific form element. * * @param {HTMLElement} replace_element HTML element object to replace. * @param {string} form_element_name HTML form element name, * @param {DOMDocument} target_document Target document that holds the element. */ addMCEControl : function(replace_element, form_element_name, target_document) { var id = "mce_editor_" + tinyMCE.idCounter++; var inst = new TinyMCE_Control(tinyMCE.settings); inst.editorId = id; this.instances[id] = inst; inst._onAdd(replace_element, form_element_name, target_document); }, /** * Removes the specified instance from TinyMCE Engine. * * @param {MCEControl} ti Target instance to remove from TinyMCE. * @return Removed MCEControl instance. * @type MCEControl */ removeInstance : function(ti) { var t = [], n, i; // Remove from instances for (n in tinyMCE.instances) { i = tinyMCE.instances[n]; if (tinyMCE.isInstance(i) && ti != i) t[n] = i; } tinyMCE.instances = t; // Remove from global undo/redo n = []; t = tinyMCE.undoLevels; for (i=0; i