diff --git a/phpgwapi/inc/class.common.inc.php b/phpgwapi/inc/class.common.inc.php index 1a88743bcd..c628cc8d4f 100644 --- a/phpgwapi/inc/class.common.inc.php +++ b/phpgwapi/inc/class.common.inc.php @@ -1233,6 +1233,14 @@ function get_java_script() { $java_script = ''; + + /* this flag is for all javascript code that has to be put before other jscode. + Think of conf vars etc... (pim@lingewoud.nl) */ + if (isset($GLOBALS['phpgw_info']['flags']['java_script_thirst'])) + { + $java_script .= $GLOBALS['phpgw_info']['flags']['java_script_thirst'] . "\n"; + } + if(@is_object($GLOBALS['phpgw']->js)) { $java_script .= $GLOBALS['phpgw']->js->get_script_links(); diff --git a/phpgwapi/inc/class.html.inc.php b/phpgwapi/inc/class.html.inc.php index eb29c43dec..d8a30503ef 100644 --- a/phpgwapi/inc/class.html.inc.php +++ b/phpgwapi/inc/class.html.inc.php @@ -145,6 +145,8 @@ } if (!strstr($GLOBALS['phpgw_info']['flags']['java_script'],'htmlarea')) { + + $GLOBALS['phpgw']->js->validate_file('htmlarea','htmlarea'); $GLOBALS['phpgw']->js->validate_file('htmlarea','dialog'); $lang = $GLOBALS['phpgw_info']['user']['preferences']['common']['lang']; @@ -157,13 +159,18 @@ $GLOBALS['phpgw_info']['flags']['java_script'] .= ''."\n"; } - $GLOBALS['phpgw_info']['flags']['java_script'] .= -' -\n"; + + $GLOBALS['phpgw_info']['flags']['java_script_thirst'] .= + ' + \n"; + } +// echo $GLOBALS['phpgw_info']['flags']['java_script']; +// die('test'); $id = str_replace(array('[',']'),array('_',''),$name); // no brakets in the id allowed by js $GLOBALS['phpgw']->js->set_onload("HTMLArea.replace('$id',htmlareaConfig);"); diff --git a/phpgwapi/js/htmlarea/htmlarea.js b/phpgwapi/js/htmlarea/htmlarea.js index 0299e5c5ab..5417cd5e3c 100644 --- a/phpgwapi/js/htmlarea/htmlarea.js +++ b/phpgwapi/js/htmlarea/htmlarea.js @@ -1,15 +1,31 @@ -// -// htmlArea v3.0 - Copyright (c) 2002 interactivetools.com, inc. +// htmlArea v3.0 - Copyright (c) 2002-2004 interactivetools.com, inc. // This copyright notice MUST stay intact for use (see license.txt). // +// Portions (c) dynarch.com, 2003-2004 +// // A free WYSIWYG editor replacement for fields. // For full source code and docs, visit http://www.interactivetools.com/ // -// Version 3.0 developed by Mihai Bazon for InteractiveTools. -// http://students.infoiasi.ro/~mishoo +// Version 3.0 developed by Mihai Bazon. +// http://dynarch.com/mishoo // // $Id$ +if (typeof _editor_url == "string") { + // Leave exactly one backslash at the end of _editor_url + _editor_url = _editor_url.replace(/\x2f*$/, '/'); +} else { + alert("WARNING: _editor_url is not set! You should set this variable to the editor files path; it should preferably be an absolute path, like in '/htmlarea', but it can be relative if you prefer. Further we will try to load the editor files correctly but we'll probably fail."); + _editor_url = ''; +} + +// make sure we have a language +if (typeof _editor_lang == "string") { + _editor_lang = _editor_lang.toLowerCase(); +} else { + _editor_lang = "en"; +} + // Creates a new HTMLArea object. Tries to replace the textarea with the given // ID with it. function HTMLArea(textarea, config) { @@ -24,10 +40,36 @@ function HTMLArea(textarea, config) { this._editMode = "wysiwyg"; this.plugins = {}; this._timerToolbar = null; + this._timerUndo = null; + this._undoQueue = new Array(this.config.undoSteps); + this._undoPos = -1; + this._customUndo = false; this._mdoc = document; // cache the document, we need it in plugins + this.doctype = ''; } }; +// load some scripts +(function() { + var scripts = HTMLArea._scripts = [ _editor_url + "htmlarea.js", + _editor_url + "dialog.js", + _editor_url + "popupwin.js", + _editor_url + "lang/" + _editor_lang + ".js" ]; + var head = document.getElementsByTagName("head")[0]; + // start from 1, htmlarea.js is already loaded + for (var i = 1; i < scripts.length; ++i) { + var script = document.createElement("script"); + script.src = scripts[i]; + head.appendChild(script); + } + })(); + +// cache some regexps +HTMLArea.RE_tagName = /(<\/|<)\s*([^ \t\n>]+)/ig; +HTMLArea.RE_doctype = /()\n?/i; +HTMLArea.RE_head = /((.|\n)*?)<\/head>/i; +HTMLArea.RE_body = /((.|\n)*?)<\/body>/i; + HTMLArea.Config = function () { this.version = "3.0"; @@ -37,25 +79,35 @@ HTMLArea.Config = function () { // enable creation of a status bar? this.statusBar = true; + // maximum size of the undo queue + this.undoSteps = 20; + + // the time interval at which undo samples are taken + this.undoTimeout = 500; // 1/2 sec. + // the next parameter specifies whether the toolbar should be included // in the size or not. this.sizeIncludesToolbar = true; + // if true then HTMLArea will retrieve the full HTML, starting with the + // tag. + this.fullPage = false; + // style included in the iframe document - this.pageStyle = "body { background-color: #fff; font-family: verdana,sans-serif; }"; - if (typeof _editor_url != "undefined") { - this.editorURL = _editor_url; - } else { - this.editorURL = ""; - } + this.pageStyle = ""; + + // set to true if you want Word code to be cleaned upon Paste + this.killWordOnPaste = false; + + // BaseURL included in the iframe document + this.baseURL = document.baseURI || document.URL; + if (this.baseURL && this.baseURL.match(/(.*)\/([^\/]+)/)) + this.baseURL = RegExp.$1 + "/"; // URL-s this.imgURL = "images/"; this.popupURL = "popups/"; - // configuration for plugins - this.plugins = {}; - /** CUSTOMIZING THE TOOLBAR * ------------------------- * @@ -67,17 +119,18 @@ HTMLArea.Config = function () { */ this.toolbar = [ [ "fontname", "space", - "fontsize", "space", - "formatblock", "space", - "bold", "italic", "underline", "separator", - "strikethrough", "subscript", "superscript", "separator", - "copy", "cut", "paste", "space", "undo", "redo" ], - - [ "justifyleft", "justifycenter", "justifyright", "justifyfull", "separator", - "insertorderedlist", "insertunorderedlist", "outdent", "indent", "separator", - "forecolor", "hilitecolor", "textindicator", "separator", - "inserthorizontalrule", "createlink", "insertimage", "inserttable", "htmlmode", "separator", - /*"popupeditor", "separator", "showhelp",*/ "about" ] + "fontsize", "space", + "formatblock", "space", + "bold", "italic", "underline", "strikethrough", "separator", + "subscript", "superscript", "separator", + "copy", "cut", "paste", "space", "undo", "redo" ], + + [ "justifyleft", "justifycenter", "justifyright", "justifyfull", "separator", + "lefttoright", "righttoleft", "separator", + "insertorderedlist", "insertunorderedlist", "outdent", "indent", "separator", + "forecolor", "hilitecolor", "separator", + "inserthorizontalrule", "createlink", "insertimage", "inserttable", "htmlmode", "separator", + "popupeditor", "separator", "showhelp", "about" ] ]; this.fontname = { @@ -116,17 +169,7 @@ HTMLArea.Config = function () { this.customSelects = {}; function cut_copy_paste(e, cmd, obj) { - try { - e.execCommand(cmd); - } catch (e) { - if (HTMLArea.is_gecko) { - alert("Some revisions of Mozilla/Gecko do not support programatic " + - "access to cut/copy/paste functions, for security reasons. " + - "Your browser is one of them. Please use the standard key combinations:\n" + - "CTRL-X for cut, CTRL-C for copy, CTRL-V for paste."); - obj.element.style.display = "none"; - } - } + e.execCommand(cmd); }; // ADDING CUSTOM BUTTONS: please read below! @@ -144,35 +187,37 @@ HTMLArea.Config = function () { // - Icon: path to an icon image file for the button (TODO: use one image for all buttons!) // - Enabled in text mode: if false the button gets disabled for text-only mode; otherwise enabled all the time. this.btnList = { - bold: [ "Bold", "images/ed_format_bold.gif", false, function(e) {e.execCommand("bold");} ], - italic: [ "Italic", "images/ed_format_italic.gif", false, function(e) {e.execCommand("italic");} ], - underline: [ "Underline", "images/ed_format_underline.gif", false, function(e) {e.execCommand("underline");} ], - strikethrough: [ "Strikethrough", "images/ed_format_strike.gif", false, function(e) {e.execCommand("strikethrough");} ], - subscript: [ "Subscript", "images/ed_format_sub.gif", false, function(e) {e.execCommand("subscript");} ], - superscript: [ "Superscript", "images/ed_format_sup.gif", false, function(e) {e.execCommand("superscript");} ], - justifyleft: [ "Justify Left", "images/ed_align_left.gif", false, function(e) {e.execCommand("justifyleft");} ], - justifycenter: [ "Justify Center", "images/ed_align_center.gif", false, function(e) {e.execCommand("justifycenter");} ], - justifyright: [ "Justify Right", "images/ed_align_right.gif", false, function(e) {e.execCommand("justifyright");} ], - justifyfull: [ "Justify Full", "images/ed_align_justify.gif", false, function(e) {e.execCommand("justifyfull");} ], - insertorderedlist: [ "Ordered List", "images/ed_list_num.gif", false, function(e) {e.execCommand("insertorderedlist");} ], - insertunorderedlist: [ "Bulleted List", "images/ed_list_bullet.gif", false, function(e) {e.execCommand("insertunorderedlist");} ], - outdent: [ "Decrease Indent", "images/ed_indent_less.gif", false, function(e) {e.execCommand("outdent");} ], - indent: [ "Increase Indent", "images/ed_indent_more.gif", false, function(e) {e.execCommand("indent");} ], - forecolor: [ "Font Color", "images/ed_color_fg.gif", false, function(e) {e.execCommand("forecolor");} ], - hilitecolor: [ "Background Color", "images/ed_color_bg.gif", false, function(e) {e.execCommand("hilitecolor");} ], - inserthorizontalrule: [ "Horizontal Rule", "images/ed_hr.gif", false, function(e) {e.execCommand("inserthorizontalrule");} ], - createlink: [ "Insert Web Link", "images/ed_link.gif", false, function(e) {e.execCommand("createlink", true);} ], - insertimage: [ "Insert Image", "images/ed_image.gif", false, function(e) {e.execCommand("insertimage");} ], - inserttable: [ "Insert Table", "images/insert_table.gif", false, function(e) {e.execCommand("inserttable");} ], - htmlmode: [ "Toggle HTML Source", "images/ed_html.gif", true, function(e) {e.execCommand("htmlmode");} ], - popupeditor: [ "Enlarge Editor", "images/fullscreen_maximize.gif", true, function(e) {e.execCommand("popupeditor");} ], - about: [ "About this editor", "images/ed_about.gif", true, function(e) {e.execCommand("about");} ], - showhelp: [ "Help using editor", "images/ed_help.gif", true, function(e) {e.execCommand("showhelp");} ], - undo: [ "Undoes your last action", "images/ed_undo.gif", false, function(e) {e.execCommand("undo");} ], - redo: [ "Redoes your last action", "images/ed_redo.gif", false, function(e) {e.execCommand("redo");} ], - cut: [ "Cut selection", "images/ed_cut.gif", false, cut_copy_paste ], - copy: [ "Copy selection", "images/ed_copy.gif", false, cut_copy_paste ], - paste: [ "Paste from clipboard", "images/ed_paste.gif", false, cut_copy_paste ] +bold: [ "Bold", "ed_format_bold.gif", false, function(e) {e.execCommand("bold");} ], +italic: [ "Italic", "ed_format_italic.gif", false, function(e) {e.execCommand("italic");} ], +underline: [ "Underline", "ed_format_underline.gif", false, function(e) {e.execCommand("underline");} ], +strikethrough: [ "Strikethrough", "ed_format_strike.gif", false, function(e) {e.execCommand("strikethrough");} ], +subscript: [ "Subscript", "ed_format_sub.gif", false, function(e) {e.execCommand("subscript");} ], +superscript: [ "Superscript", "ed_format_sup.gif", false, function(e) {e.execCommand("superscript");} ], +justifyleft: [ "Justify Left", "ed_align_left.gif", false, function(e) {e.execCommand("justifyleft");} ], +justifycenter: [ "Justify Center", "ed_align_center.gif", false, function(e) {e.execCommand("justifycenter");} ], +justifyright: [ "Justify Right", "ed_align_right.gif", false, function(e) {e.execCommand("justifyright");} ], +justifyfull: [ "Justify Full", "ed_align_justify.gif", false, function(e) {e.execCommand("justifyfull");} ], +insertorderedlist: [ "Ordered List", "ed_list_num.gif", false, function(e) {e.execCommand("insertorderedlist");} ], +insertunorderedlist: [ "Bulleted List", "ed_list_bullet.gif", false, function(e) {e.execCommand("insertunorderedlist");} ], +outdent: [ "Decrease Indent", "ed_indent_less.gif", false, function(e) {e.execCommand("outdent");} ], +indent: [ "Increase Indent", "ed_indent_more.gif", false, function(e) {e.execCommand("indent");} ], +forecolor: [ "Font Color", "ed_color_fg.gif", false, function(e) {e.execCommand("forecolor");} ], +hilitecolor: [ "Background Color", "ed_color_bg.gif", false, function(e) {e.execCommand("hilitecolor");} ], +inserthorizontalrule: [ "Horizontal Rule", "ed_hr.gif", false, function(e) {e.execCommand("inserthorizontalrule");} ], +createlink: [ "Insert Web Link", "ed_link.gif", false, function(e) {e.execCommand("createlink", true);} ], +insertimage: [ "Insert/Modify Image", "ed_image.gif", false, function(e) {e.execCommand("insertimage");} ], +inserttable: [ "Insert Table", "insert_table.gif", false, function(e) {e.execCommand("inserttable");} ], +htmlmode: [ "Toggle HTML Source", "ed_html.gif", true, function(e) {e.execCommand("htmlmode");} ], +popupeditor: [ "Enlarge Editor", "fullscreen_maximize.gif", true, function(e) {e.execCommand("popupeditor");} ], +about: [ "About this editor", "ed_about.gif", true, function(e) {e.execCommand("about");} ], +showhelp: [ "Help using editor", "ed_help.gif", true, function(e) {e.execCommand("showhelp");} ], +undo: [ "Undoes your last action", "ed_undo.gif", false, function(e) {e.execCommand("undo");} ], +redo: [ "Redoes your last action", "ed_redo.gif", false, function(e) {e.execCommand("redo");} ], +cut: [ "Cut selection", "ed_cut.gif", false, cut_copy_paste ], +copy: [ "Copy selection", "ed_copy.gif", false, cut_copy_paste ], +paste: [ "Paste from clipboard", "ed_paste.gif", false, cut_copy_paste ], +lefttoright: [ "Direction left to right", "ed_left_to_right.gif", false, function(e) {e.execCommand("lefttoright");} ], +righttoleft: [ "Direction right to left", "ed_right_to_left.gif", false, function(e) {e.execCommand("righttoleft");} ] }; /* ADDING CUSTOM BUTTONS * --------------------- @@ -197,9 +242,10 @@ HTMLArea.Config = function () { * An alternate (also more convenient and recommended) way to * accomplish this is to use the registerButton function below. */ - // initialize tooltips from the I18N module + // initialize tooltips from the I18N module and generate correct image path for (var i in this.btnList) { var btn = this.btnList[i]; + btn[1] = _editor_url + this.imgURL + btn[1]; if (typeof HTMLArea.I18N.tooltips[i] != "undefined") { btn[0] = HTMLArea.I18N.tooltips[i]; } @@ -235,14 +281,14 @@ HTMLArea.Config.prototype.registerButton = function(id, tooltip, image, textMode } // check for existing id if (typeof this.customSelects[the_id] != "undefined") { - alert("WARNING [HTMLArea.Config::registerDropdown]:\nA dropdown with the same ID already exists."); + // alert("WARNING [HTMLArea.Config::registerDropdown]:\nA dropdown with the same ID already exists."); } if (typeof this.btnList[the_id] != "undefined") { - alert("WARNING [HTMLArea.Config::registerDropdown]:\nA button with the same ID already exists."); + // alert("WARNING [HTMLArea.Config::registerDropdown]:\nA button with the same ID already exists."); } switch (typeof id) { - case "string": this.btnList[id] = [ tooltip, image, textMode, action, context ]; break; - case "object": this.btnList[id.id] = [ id.tooltip, id.image, id.textMode, id.action, id.context ]; break; + case "string": this.btnList[id] = [ tooltip, image, textMode, action, context ]; break; + case "object": this.btnList[id.id] = [ id.tooltip, id.image, id.textMode, id.action, id.context ]; break; } }; @@ -255,14 +301,40 @@ HTMLArea.Config.prototype.registerButton = function(id, tooltip, image, textMode HTMLArea.Config.prototype.registerDropdown = function(object) { // check for existing id if (typeof this.customSelects[object.id] != "undefined") { - alert("WARNING [HTMLArea.Config::registerDropdown]:\nA dropdown with the same ID already exists."); + // alert("WARNING [HTMLArea.Config::registerDropdown]:\nA dropdown with the same ID already exists."); } if (typeof this.btnList[object.id] != "undefined") { - alert("WARNING [HTMLArea.Config::registerDropdown]:\nA button with the same ID already exists."); + // alert("WARNING [HTMLArea.Config::registerDropdown]:\nA button with the same ID already exists."); } this.customSelects[object.id] = object; }; +/** Call this function to remove some buttons/drop-down boxes from the toolbar. + * Pass as the only parameter a string containing button/drop-down names + * delimited by spaces. Note that the string should also begin with a space + * and end with a space. Example: + * + * config.hideSomeButtons(" fontname fontsize textindicator "); + * + * It's useful because it's easier to remove stuff from the defaul toolbar than + * create a brand new toolbar ;-) + */ +HTMLArea.Config.prototype.hideSomeButtons = function(remove) { + var toolbar = this.toolbar; + for (var i in toolbar) { + var line = toolbar[i]; + for (var j = line.length; --j >= 0; ) { + if (remove.indexOf(" " + line[j] + " ") >= 0) { + var len = 1; + if (/separator|space/.test(line[j + 1])) { + len = 2; + } + line.splice(j, len); + } + } + } +}; + /** Helper function: replace all TEXTAREA-s in the document with HTMLArea-s. */ HTMLArea.replaceAll = function(config) { var tas = document.getElementsByTagName("textarea"); @@ -271,7 +343,7 @@ HTMLArea.replaceAll = function(config) { /** Helper function: replaces the TEXTAREA with the given ID with HTMLArea. */ HTMLArea.replace = function(id, config) { - var ta = document.getElementById(id); + var ta = HTMLArea.getElementById("textarea", id); return ta ? (new HTMLArea(ta, config)).generate() : null; }; @@ -312,21 +384,21 @@ HTMLArea.prototype._createToolbar = function () { var el = this.element; if (oldval != newval) { switch (id) { - case "enabled": - if (newval) { - HTMLArea._removeClass(el, "buttonDisabled"); - el.disabled = false; - } else { - HTMLArea._addClass(el, "buttonDisabled"); - el.disabled = true; - } + case "enabled": + if (newval) { + HTMLArea._removeClass(el, "buttonDisabled"); + el.disabled = false; + } else { + HTMLArea._addClass(el, "buttonDisabled"); + el.disabled = true; + } break; - case "active": - if (newval) { - HTMLArea._addClass(el, "buttonPressed"); - } else { - HTMLArea._removeClass(el, "buttonPressed"); - } + case "active": + if (newval) { + HTMLArea._addClass(el, "buttonPressed"); + } else { + HTMLArea._removeClass(el, "buttonPressed"); + } break; } this[id] = newval; @@ -344,20 +416,20 @@ HTMLArea.prototype._createToolbar = function () { var customSelects = editor.config.customSelects; var context = null; switch (txt) { - case "fontsize": - case "fontname": - case "formatblock": - // the following line retrieves the correct - // configuration option because the variable name - // inside the Config object is named the same as the - // button/select in the toolbar. For instance, if txt - // == "formatblock" we retrieve config.formatblock (or - // a different way to write it in JS is - // config["formatblock"]. - options = editor.config[txt]; + case "fontsize": + case "fontname": + case "formatblock": + // the following line retrieves the correct + // configuration option because the variable name + // inside the Config object is named the same as the + // button/select in the toolbar. For instance, if txt + // == "formatblock" we retrieve config.formatblock (or + // a different way to write it in JS is + // config["formatblock"]. + options = editor.config[txt]; cmd = txt; break; - default: + default: // try to fetch it from the list of registered selects cmd = txt; var dropdown = customSelects[cmd]; @@ -372,13 +444,13 @@ HTMLArea.prototype._createToolbar = function () { if (options) { el = document.createElement("select"); var obj = { - name : txt, // field name - element : el, // the UI element (SELECT) - enabled : true, // is it enabled? - text : false, // enabled in text mode? - cmd : cmd, // command ID - state : setButtonStatus, // for changing state - context : context +name : txt, // field name +element : el, // the UI element (SELECT) +enabled : true, // is it enabled? +text : false, // enabled in text mode? +cmd : cmd, // command ID +state : setButtonStatus, // for changing state +context : context }; tb_objects[txt] = obj; for (var i in options) { @@ -388,8 +460,8 @@ HTMLArea.prototype._createToolbar = function () { el.appendChild(op); } HTMLArea._addEvent(el, "change", function () { - editor._comboSelected(el, txt); - }); + editor._comboSelected(el, txt); + }); } return el; }; // END of function: createSelect @@ -400,34 +472,34 @@ HTMLArea.prototype._createToolbar = function () { var el = null; var btn = null; switch (txt) { - case "separator": - el = document.createElement("div"); + case "separator": + el = document.createElement("div"); el.className = "separator"; break; - case "space": - el = document.createElement("div"); + case "space": + el = document.createElement("div"); el.className = "space"; break; - case "linebreak": - newLine(); + case "linebreak": + newLine(); return false; - case "textindicator": - el = document.createElement("div"); + case "textindicator": + el = document.createElement("div"); el.appendChild(document.createTextNode("A")); el.className = "indicator"; el.title = HTMLArea.I18N.tooltips.textindicator; var obj = { - name : txt, // the button name (i.e. 'bold') - element : el, // the UI element (DIV) - enabled : true, // is it enabled? - active : false, // is it pressed? - text : false, // enabled in text mode? - cmd : "textindicator", // the command ID - state : setButtonStatus // for changing state +name : txt, // the button name (i.e. 'bold') +element : el, // the UI element (DIV) +enabled : true, // is it enabled? +active : false, // is it pressed? +text : false, // enabled in text mode? +cmd : "textindicator", // the command ID +state : setButtonStatus // for changing state }; tb_objects[txt] = obj; break; - default: + default: btn = editor.config.btnList[txt]; } if (!el && btn) { @@ -437,47 +509,47 @@ HTMLArea.prototype._createToolbar = function () { // let's just pretend we have a button object, and // assign all the needed information to it. var obj = { - name : txt, // the button name (i.e. 'bold') - element : el, // the UI element (DIV) - enabled : true, // is it enabled? - active : false, // is it pressed? - text : btn[2], // enabled in text mode? - cmd : btn[3], // the command ID - state : setButtonStatus, // for changing state - context : btn[4] || null // enabled in a certain context? +name : txt, // the button name (i.e. 'bold') +element : el, // the UI element (DIV) +enabled : true, // is it enabled? +active : false, // is it pressed? +text : btn[2], // enabled in text mode? +cmd : btn[3], // the command ID +state : setButtonStatus, // for changing state +context : btn[4] || null // enabled in a certain context? }; tb_objects[txt] = obj; // handlers to emulate nice flat toolbar buttons HTMLArea._addEvent(el, "mouseover", function () { - if (obj.enabled) { + if (obj.enabled) { HTMLArea._addClass(el, "buttonHover"); - } - }); + } + }); HTMLArea._addEvent(el, "mouseout", function () { - if (obj.enabled) with (HTMLArea) { + if (obj.enabled) with (HTMLArea) { _removeClass(el, "buttonHover"); _removeClass(el, "buttonActive"); (obj.active) && _addClass(el, "buttonPressed"); - } - }); + } + }); HTMLArea._addEvent(el, "mousedown", function (ev) { - if (obj.enabled) with (HTMLArea) { + if (obj.enabled) with (HTMLArea) { _addClass(el, "buttonActive"); _removeClass(el, "buttonPressed"); _stopEvent(is_ie ? window.event : ev); - } - }); + } + }); // when clicked, do the following: HTMLArea._addEvent(el, "click", function (ev) { - if (obj.enabled) with (HTMLArea) { + if (obj.enabled) with (HTMLArea) { _removeClass(el, "buttonActive"); _removeClass(el, "buttonHover"); obj.cmd(editor, obj.name, obj); _stopEvent(is_ie ? window.event : ev); - } - }); + } + }); var img = document.createElement("img"); - img.src = editor.imgURL(btn[1]); + img.src = btn[1]; img.style.width = "18px"; img.style.height = "18px"; el.appendChild(img); @@ -525,19 +597,20 @@ HTMLArea.prototype._createToolbar = function () { }; HTMLArea.prototype._createStatusBar = function() { - var div = document.createElement("div"); - div.className = "statusBar"; - this._htmlArea.appendChild(div); - this._statusBar = div; - div.appendChild(document.createTextNode(HTMLArea.I18N.msg["Path"] + ": ")); + var statusbar = document.createElement("div"); + statusbar.className = "statusBar"; + this._htmlArea.appendChild(statusbar); + this._statusBar = statusbar; + // statusbar.appendChild(document.createTextNode(HTMLArea.I18N.msg["Path"] + ": ")); // creates a holder for the path view div = document.createElement("span"); div.className = "statusBarTree"; + div.innerHTML = HTMLArea.I18N.msg["Path"] + ": "; this._statusBarTree = div; this._statusBar.appendChild(div); if (!this.config.statusBar) { // disable it... - div.style.display = "none"; + statusbar.style.display = "none"; } }; @@ -548,11 +621,11 @@ HTMLArea.prototype.generate = function () { var textarea = this._textArea; if (typeof textarea == "string") { // it's not element but ID - this._textArea = textarea = document.getElementById(textarea); + this._textArea = textarea = HTMLArea.getElementById("textarea", textarea); } this._ta_size = { - w: textarea.offsetWidth, - h: textarea.offsetHeight +w: textarea.offsetWidth, +h: textarea.offsetHeight }; textarea.style.display = "none"; @@ -567,8 +640,23 @@ HTMLArea.prototype.generate = function () { if (textarea.form) { // we have a form, on submit get the HTMLArea content and // update original textarea. - textarea.form.onsubmit = function() { + var f = textarea.form; + if (typeof f.onsubmit == "function") { + var funcref = f.onsubmit; + if (typeof f.__msh_prevOnSubmit == "undefined") { + f.__msh_prevOnSubmit = []; + } + f.__msh_prevOnSubmit.push(funcref); + } + f.onsubmit = function() { editor._textArea.value = editor.getHTML(); + var a = this.__msh_prevOnSubmit; + // call previous submit methods if they were there. + if (typeof a != "undefined") { + for (var i in a) { + a[i](); + } + } }; } @@ -595,9 +683,9 @@ HTMLArea.prototype.generate = function () { if (!HTMLArea.is_ie) { iframe.style.borderWidth = "1px"; - // iframe.frameBorder = "1"; - // iframe.marginHeight = "0"; - // iframe.marginWidth = "0"; + // iframe.frameBorder = "1"; + // iframe.marginHeight = "0"; + // iframe.marginWidth = "0"; } // size the IFRAME according to user's prefs or initial textarea @@ -625,7 +713,7 @@ HTMLArea.prototype.generate = function () { // the editor including the toolbar now have the same size as the // original textarea.. which means that we need to reduce that a bit. textarea.style.width = iframe.style.width; - textarea.style.height = iframe.style.height; + textarea.style.height = iframe.style.height; // IMPORTANT: we have to allow Mozilla a short time to recognize the // new frame. Otherwise we get a stupid exception. @@ -636,7 +724,7 @@ HTMLArea.prototype.generate = function () { // FIXME: don't know what else to do here. Normally // we'll never reach this point. if (HTMLArea.is_gecko) { - setTimeout(initIframe, 10); + setTimeout(initIframe, 100); return false; } else { alert("ERROR: IFRAME can't be initialized."); @@ -647,17 +735,31 @@ HTMLArea.prototype.generate = function () { doc.designMode = "on"; } editor._doc = doc; - doc.open(); - var html = "\n"; - html += "\n"; - html += "\n"; - html += "\n"; - html += "\n"; - html += editor._textArea.value; - html += "\n"; - html += ""; - doc.write(html); - doc.close(); + if (!editor.config.fullPage) { + doc.open(); + var html = "\n"; + html += "\n"; + if (editor.config.baseURL) + html += ''; + html += "\n"; + html += "\n"; + html += "\n"; + html += editor._textArea.value; + html += "\n"; + html += ""; + doc.write(html); + doc.close(); + } else { + var html = editor._textArea.value; + if (html.match(HTMLArea.RE_doctype)) { + editor.setDoctype(RegExp.$1); + html = html.replace(HTMLArea.RE_doctype, ""); + } + doc.open(); + doc.write(html); + doc.close(); + } if (HTMLArea.is_ie) { // enable editable mode for IE. For some reason this @@ -671,11 +773,24 @@ HTMLArea.prototype.generate = function () { HTMLArea._addEvents (doc, ["keydown", "keypress", "mousedown", "mouseup", "drag"], function (event) { - return editor._editorEvent(HTMLArea.is_ie ? editor._iframe.contentWindow.event : event); + return editor._editorEvent(HTMLArea.is_ie ? editor._iframe.contentWindow.event : event); }); - editor.updateToolbar(); + + // check if any plugins have registered refresh handlers + for (var i in editor.plugins) { + var plugin = editor.plugins[i].instance; + if (typeof plugin.onGenerate == "function") + plugin.onGenerate(); + } + + setTimeout(function() { + editor.updateToolbar(); + }, 250); + + if (typeof editor.onGenerate == "function") + editor.onGenerate(); }; - setTimeout(initIframe, HTMLArea.is_gecko ? 10 : 0); + setTimeout(initIframe, 100); }; // Switches editor mode; parameter can be "textmode" or "wysiwyg". If no @@ -685,25 +800,32 @@ HTMLArea.prototype.setMode = function(mode) { mode = ((this._editMode == "textmode") ? "wysiwyg" : "textmode"); } switch (mode) { - case "textmode": - this._textArea.value = this.getHTML(); + case "textmode": + this._textArea.value = this.getHTML(); this._iframe.style.display = "none"; this._textArea.style.display = "block"; if (this.config.statusBar) { this._statusBar.innerHTML = HTMLArea.I18N.msg["TEXT_MODE"]; } break; - case "wysiwyg": - if (HTMLArea.is_gecko) { - // disable design mode before changing innerHTML - this._doc.designMode = "off"; - } - this._doc.body.innerHTML = this.getHTML(); + case "wysiwyg": + if (HTMLArea.is_gecko) { + // disable design mode before changing innerHTML + try { + this._doc.designMode = "off"; + } catch(e) {}; + } + if (!this.config.fullPage) + this._doc.body.innerHTML = this.getHTML(); + else + this.setFullHTML(this.getHTML()); this._iframe.style.display = "block"; this._textArea.style.display = "none"; if (HTMLArea.is_gecko) { // we need to refresh that info for Moz-1.3a - this._doc.designMode = "on"; + try { + this._doc.designMode = "on"; + } catch(e) {}; } if (this.config.statusBar) { this._statusBar.innerHTML = ''; @@ -711,7 +833,7 @@ HTMLArea.prototype.setMode = function(mode) { this._statusBar.appendChild(this._statusBarTree); } break; - default: + default: alert("Mode <" + mode + "> not defined!"); return false; } @@ -719,36 +841,159 @@ HTMLArea.prototype.setMode = function(mode) { this.focusEditor(); }; +HTMLArea.prototype.setFullHTML = function(html) { + var save_multiline = RegExp.multiline; + RegExp.multiline = true; + if (html.match(HTMLArea.RE_doctype)) { + this.setDoctype(RegExp.$1); + html = html.replace(HTMLArea.RE_doctype, ""); + } + RegExp.multiline = save_multiline; + if (!HTMLArea.is_ie) { + if (html.match(HTMLArea.RE_head)) + this._doc.getElementsByTagName("head")[0].innerHTML = RegExp.$1; + if (html.match(HTMLArea.RE_body)) + this._doc.getElementsByTagName("body")[0].innerHTML = RegExp.$1; + } else { + var html_re = /((.|\n)*?)<\/html>/i; + html = html.replace(html_re, "$1"); + this._doc.open(); + this._doc.write(html); + this._doc.close(); + this._doc.body.contentEditable = true; + return true; + } +}; + /*************************************************** * Category: PLUGINS ***************************************************/ +// this is the variant of the function above where the plugin arguments are +// already packed in an array. Externally, it should be only used in the +// full-screen editor code, in order to initialize plugins with the same +// parameters as in the opener window. +HTMLArea.prototype.registerPlugin2 = function(plugin, args) { + if (typeof plugin == "string") + plugin = eval(plugin); + var obj = new plugin(this, args); + if (obj) { + var clone = {}; + var info = plugin._pluginInfo; + for (var i in info) + clone[i] = info[i]; + clone.instance = obj; + clone.args = args; + this.plugins[plugin._pluginInfo.name] = clone; + } else + alert("Can't register plugin " + plugin.toString() + "."); +}; + // Create the specified plugin and register it with this HTMLArea -HTMLArea.prototype.registerPlugin = function(pluginName) { - this.plugins[pluginName] = eval("new " + pluginName + "(this);"); +HTMLArea.prototype.registerPlugin = function() { + var plugin = arguments[0]; + var args = []; + for (var i = 1; i < arguments.length; ++i) + args.push(arguments[i]); + this.registerPlugin2(plugin, args); }; // static function that loads the required plugin and lang file, based on the // language loaded already for HTMLArea. You better make sure that the plugin // _has_ that language, otherwise shit might happen ;-) HTMLArea.loadPlugin = function(pluginName) { - var editorurl = ''; - if (typeof _editor_url != "undefined") { - editorurl = _editor_url + "/"; - } - var dir = editorurl + "plugins/" + pluginName; + var dir = _editor_url + "plugins/" + pluginName; var plugin = pluginName.replace(/([a-z])([A-Z])([a-z])/g, - function (str, l1, l2, l3) { - return l1 + "-" + l2.toLowerCase() + l3; - }).toLowerCase() + ".js"; - document.write(""); - document.write(""); + function (str, l1, l2, l3) { + return l1 + "-" + l2.toLowerCase() + l3; + }).toLowerCase() + ".js"; + var plugin_file = dir + "/" + plugin; + var plugin_lang = dir + "/lang/" + HTMLArea.I18N.lang + ".js"; + HTMLArea._scripts.push(plugin_file, plugin_lang); + document.write(""); + document.write(""); }; +HTMLArea.loadStyle = function(style, plugin) { + var url = _editor_url || ''; + if (typeof plugin != "undefined") { + url += "plugins/" + plugin + "/"; + } + url += style; + document.write(""); +}; +HTMLArea.loadStyle("htmlarea.css"); + /*************************************************** * Category: EDITOR UTILITIES ***************************************************/ +// The following function is a slight variation of the word cleaner code posted +// by Weeezl (user @ InteractiveTools forums). +HTMLArea.prototype._wordClean = function() { + var D = this.getInnerHTML(); + if (D.indexOf('class=Mso') >= 0) { + + // make one line + D = D.replace(/\r\n/g, ' '). + replace(/\n/g, ' '). + replace(/\r/g, ' '). + replace(/\ \;/g,' '); + + // keep tags, strip attributes + D = D.replace(/ class=[^\s|>]*/gi,''). + //replace(/]*TEXT-ALIGN: justify[^>]*>/gi,''). + replace(/ style=\"[^>]*\"/gi,''). + replace(/ align=[^\s|>]*/gi,''); + + //clean up tags + D = D.replace(/]*>/gi,''). + replace(/]*>/gi,''). + replace(/]*>/gi,''). + replace(/]*>/gi,''); + + // replace outdated tags + D = D.replace(//gi,''). + replace(/<\/b>/gi,''); + + // mozilla doesn't like tags + D = D.replace(//gi,''). + replace(/<\/em>/gi,''); + + // kill unwanted tags + D = D.replace(/<\?xml:[^>]*>/g, ''). // Word xml + replace(/<\/?st1:[^>]*>/g,''). // Word SmartTags + replace(/<\/?[a-z]\:[^>]*>/g,''). // All other funny Word non-HTML stuff + replace(/<\/?font[^>]*>/gi,''). // Disable if you want to keep font formatting + replace(/<\/?span[^>]*>/gi,' '). + replace(/<\/?div[^>]*>/gi,' '). + replace(/<\/?pre[^>]*>/gi,' '). + replace(/<\/?h[1-6][^>]*>/gi,' '); + + //remove empty tags + //D = D.replace(/<\/strong>/gi,''). + //replace(/<\/i>/gi,''). + //replace(/]*><\/P>/gi,''); + + // nuke double tags + oldlen = D.length + 1; + while(oldlen > D.length) { + oldlen = D.length; + // join us now and free the tags, we'll be free hackers, we'll be free... ;-) + D = D.replace(/<([a-z][a-z]*)> *<\/\1>/gi,' '). + replace(/<([a-z][a-z]*)> *<([a-z][^>]*)> *<\/\1>/gi,'<$2>'); + } + D = D.replace(/<([a-z][a-z]*)><\1>/gi,'<$1>'). + replace(/<\/([a-z][a-z]*)><\/\1>/gi,'<\/$1>'); + + // nuke double spaces + D = D.replace(/ */gi,' '); + + this.setHTML(D); + this.updateToolbar(); + } +}; + HTMLArea.prototype.forceRedraw = function() { this._doc.body.style.visibility = "hidden"; this._doc.body.style.visibility = "visible"; @@ -758,13 +1003,49 @@ HTMLArea.prototype.forceRedraw = function() { // focuses the iframe window. returns a reference to the editor document. HTMLArea.prototype.focusEditor = function() { switch (this._editMode) { - case "wysiwyg" : this._iframe.contentWindow.focus(); break; - case "textmode": this._textArea.focus(); break; - default : alert("ERROR: mode " + this._editMode + " is not defined"); + case "wysiwyg" : this._iframe.contentWindow.focus(); break; + case "textmode": this._textArea.focus(); break; + default : alert("ERROR: mode " + this._editMode + " is not defined"); } return this._doc; }; +// takes a snapshot of the current text (for undo) +HTMLArea.prototype._undoTakeSnapshot = function() { + ++this._undoPos; + if (this._undoPos >= this.config.undoSteps) { + // remove the first element + this._undoQueue.shift(); + --this._undoPos; + } + // use the fasted method (getInnerHTML); + var take = true; + var txt = this.getInnerHTML(); + if (this._undoPos > 0) + take = (this._undoQueue[this._undoPos - 1] != txt); + if (take) { + this._undoQueue[this._undoPos] = txt; + } else { + this._undoPos--; + } +}; + +HTMLArea.prototype.undo = function() { + if (this._undoPos > 0) { + var txt = this._undoQueue[--this._undoPos]; + if (txt) this.setHTML(txt); + else ++this._undoPos; + } +}; + +HTMLArea.prototype.redo = function() { + if (this._undoPos < this._undoQueue.length - 1) { + var txt = this._undoQueue[++this._undoPos]; + if (txt) this.setHTML(txt); + else --this._undoPos; + } +}; + // updates enabled/disable/active state of the toolbar elements HTMLArea.prototype.updateToolbar = function(noStatus) { var doc = this._doc; @@ -773,7 +1054,7 @@ HTMLArea.prototype.updateToolbar = function(noStatus) { if (!text) { ancestors = this.getAllAncestors(); if (this.config.statusBar && !noStatus) { - this._statusBarTree.innerHTML = ''; // clear + this._statusBarTree.innerHTML = HTMLArea.I18N.msg["Path"] + ": "; // clear for (var i = ancestors.length; --i >= 0;) { var el = ancestors[i]; if (!el) { @@ -861,58 +1142,80 @@ HTMLArea.prototype.updateToolbar = function(noStatus) { continue; } switch (cmd) { - case "fontname": - case "fontsize": - case "formatblock": - if (!text) { - var value = ("" + doc.queryCommandValue(cmd)).toLowerCase(); - if (!value) { - // FIXME: what do we do here? - break; - } - // HACK -- retrieve the config option for this - // combo box. We rely on the fact that the - // variable in config has the same name as - // button name in the toolbar. - var options = this.config[cmd]; - var k = 0; - // btn.element.selectedIndex = 0; - for (var j in options) { - // FIXME: the following line is scary. - if ((j.toLowerCase() == value) || - (options[j].substr(0, value.length).toLowerCase() == value)) { - btn.element.selectedIndex = k; + case "fontname": + case "fontsize": + case "formatblock": + if (!text) try { + var value = ("" + doc.queryCommandValue(cmd)).toLowerCase(); + if (!value) { + // FIXME: what do we do here? break; } - ++k; - } - } - break; - case "textindicator": - if (!text) { - try {with (btn.element.style) { - backgroundColor = HTMLArea._makeColor( - doc.queryCommandValue(HTMLArea.is_ie ? "backcolor" : "hilitecolor")); - if (/transparent/i.test(backgroundColor)) { - // Mozilla - backgroundColor = HTMLArea._makeColor(doc.queryCommandValue("backcolor")); + // HACK -- retrieve the config option for this + // combo box. We rely on the fact that the + // variable in config has the same name as + // button name in the toolbar. + var options = this.config[cmd]; + var k = 0; + // btn.element.selectedIndex = 0; + for (var j in options) { + // FIXME: the following line is scary. + if ((j.toLowerCase() == value) || + (options[j].substr(0, value.length).toLowerCase() == value)) { + btn.element.selectedIndex = k; + break; + } + ++k; } - color = HTMLArea._makeColor(doc.queryCommandValue("forecolor")); - fontFamily = doc.queryCommandValue("fontname"); - fontWeight = doc.queryCommandState("bold") ? "bold" : "normal"; - fontStyle = doc.queryCommandState("italic") ? "italic" : "normal"; - }} catch (e) { - // alert(e + "\n\n" + cmd); - } - } + } catch(e) {}; break; - case "htmlmode": btn.state("active", text); break; - default: + case "textindicator": + if (!text) { + try {with (btn.element.style) { + backgroundColor = HTMLArea._makeColor( + doc.queryCommandValue(HTMLArea.is_ie ? "backcolor" : "hilitecolor")); + if (/transparent/i.test(backgroundColor)) { + // Mozilla + backgroundColor = HTMLArea._makeColor(doc.queryCommandValue("backcolor")); + } + color = HTMLArea._makeColor(doc.queryCommandValue("forecolor")); + fontFamily = doc.queryCommandValue("fontname"); + fontWeight = doc.queryCommandState("bold") ? "bold" : "normal"; + fontStyle = doc.queryCommandState("italic") ? "italic" : "normal"; + }} catch (e) { + // alert(e + "\n\n" + cmd); + } + } + break; + case "htmlmode": btn.state("active", text); break; + case "lefttoright": + case "righttoleft": + var el = this.getParentElement(); + while (el && !HTMLArea.isBlockElement(el)) + el = el.parentNode; + if (el) + btn.state("active", (el.style.direction == ((cmd == "righttoleft") ? "rtl" : "ltr"))); + break; + default: try { btn.state("active", (!text && doc.queryCommandState(cmd))); } catch (e) {} } } + // take undo snapshots + if (this._customUndo && !this._timerUndo) { + this._undoTakeSnapshot(); + var editor = this; + this._timerUndo = setTimeout(function() { + editor._timerUndo = null; + }, this.config.undoTimeout); + } + // check if any plugins have registered refresh handlers + for (var i in this.plugins) { + var plugin = this.plugins[i].instance; + if (typeof plugin.onUpdateToolbar == "function") + plugin.onUpdateToolbar(); + } }; /** Returns a node after which we can insert other nodes, in the current @@ -928,35 +1231,35 @@ HTMLArea.prototype.insertNodeAtSelection = function(toBeInserted) { var node = range.startContainer; var pos = range.startOffset; switch (node.nodeType) { - case 3: // Node.TEXT_NODE - // we have to split it at the caret position. - if (toBeInserted.nodeType == 3) { - // do optimized insertion - node.insertData(pos, toBeInserted.data); - range = this._createRange(); - range.setEnd(node, pos + toBeInserted.length); - range.setStart(node, pos + toBeInserted.length); - sel.addRange(range); - } else { - node = node.splitText(pos); + case 3: // Node.TEXT_NODE + // we have to split it at the caret position. + if (toBeInserted.nodeType == 3) { + // do optimized insertion + node.insertData(pos, toBeInserted.data); + range = this._createRange(); + range.setEnd(node, pos + toBeInserted.length); + range.setStart(node, pos + toBeInserted.length); + sel.addRange(range); + } else { + node = node.splitText(pos); + var selnode = toBeInserted; + if (toBeInserted.nodeType == 11 /* Node.DOCUMENT_FRAGMENT_NODE */) { + selnode = selnode.firstChild; + } + node.parentNode.insertBefore(toBeInserted, node); + this.selectNodeContents(selnode); + this.updateToolbar(); + } + break; + case 1: // Node.ELEMENT_NODE var selnode = toBeInserted; if (toBeInserted.nodeType == 11 /* Node.DOCUMENT_FRAGMENT_NODE */) { selnode = selnode.firstChild; } - node.parentNode.insertBefore(toBeInserted, node); + node.insertBefore(toBeInserted, node.childNodes[pos]); this.selectNodeContents(selnode); this.updateToolbar(); - } - break; - case 1: // Node.ELEMENT_NODE - var selnode = toBeInserted; - if (toBeInserted.nodeType == 11 /* Node.DOCUMENT_FRAGMENT_NODE */) { - selnode = selnode.firstChild; - } - node.insertBefore(toBeInserted, node.childNodes[pos]); - this.selectNodeContents(selnode); - this.updateToolbar(); - break; + break; } } else { return null; // this function not yet used for IE @@ -968,13 +1271,35 @@ HTMLArea.prototype.getParentElement = function() { var sel = this._getSelection(); var range = this._createRange(sel); if (HTMLArea.is_ie) { - return range.parentElement ? range.parentElement() : this._doc.body; - } else { + switch (sel.type) { + case "Text": + case "None": + // It seems that even for selection of type "None", + // there _is_ a parent element and it's value is not + // only correct, but very important to us. MSIE is + // certainly the buggiest browser in the world and I + // wonder, God, how can Earth stand it? + return range.parentElement(); + case "Control": + return range.item(0); + default: + return this._doc.body; + } + } else try { var p = range.commonAncestorContainer; + if (!range.collapsed && range.startContainer == range.endContainer && + range.startOffset - range.endOffset <= 1 && range.startContainer.hasChildNodes()) + p = range.startContainer.childNodes[range.startOffset]; + /* + alert(range.startContainer + ":" + range.startOffset + "\n" + + range.endContainer + ":" + range.endOffset); + */ while (p.nodeType == 3) { p = p.parentNode; } return p; + } catch (e) { + return null; } }; @@ -1051,45 +1376,104 @@ HTMLArea.prototype.getSelectedHTML = function() { if (HTMLArea.is_ie) { existing = range.htmlText; } else { - existing = HTMLArea.getHTML(range.cloneContents(), false); + existing = HTMLArea.getHTML(range.cloneContents(), false, this); } return existing; }; -// Called when the user clicks on "InsertImage" button -HTMLArea.prototype._insertImage = function() { - var editor = this; // for nested functions - this._popupDialog("insert_image.html", function(param) { - if (!param) { // user must have pressed Cancel +/// Return true if we have some selection +HTMLArea.prototype.hasSelectedText = function() { + // FIXME: come _on_ mishoo, you can do better than this ;-) + return this.getSelectedHTML() != ''; +}; + +HTMLArea.prototype._createLink = function(link) { + var editor = this; + var outparam = null; + if (typeof link == "undefined") { + link = this.getParentElement(); + if (link && !/^a$/i.test(link.tagName)) + link = null; + } + if (link) outparam = { +f_href : HTMLArea.is_ie ? editor.stripBaseURL(link.href) : link.getAttribute("href"), +f_title : link.title, +f_target : link.target + }; + this._popupDialog("link.html", function(param) { + if (!param) return false; - } - var sel = editor._getSelection(); - var range = editor._createRange(sel); - editor._doc.execCommand("insertimage", false, param["f_url"]); - var img = null; - if (HTMLArea.is_ie) { + var a = link; + if (!a) { + editor._doc.execCommand("createlink", false, param.f_href); + a = editor.getParentElement(); + var sel = editor._getSelection(); + var range = editor._createRange(sel); + if (!HTMLArea.is_ie) { + a = range.startContainer; + if (!/^a$/i.test(a.tagName)) + a = a.nextSibling; + } + } else a.href = param.f_href.trim(); + if (!/^a$/i.test(a.tagName)) + return false; + a.target = param.f_target.trim(); + a.title = param.f_title.trim(); + editor.selectNodeContents(a); + editor.updateToolbar(); + }, outparam); +}; + +// Called when the user clicks on "InsertImage" button. If an image is already +// there, it will just modify it's properties. +HTMLArea.prototype._insertImage = function(image) { + var editor = this; // for nested functions + var outparam = null; + if (typeof image == "undefined") { + image = this.getParentElement(); + if (image && !/^img$/i.test(image.tagName)) + image = null; + } + if (image) outparam = { +f_url : HTMLArea.is_ie ? editor.stripBaseURL(image.src) : image.getAttribute("src"), +f_alt : image.alt, +f_border : image.border, +f_align : image.align, +f_vert : image.vspace, +f_horiz : image.hspace + }; + this._popupDialog("insert_image.html", function(param) { + if (!param) { // user must have pressed Cancel + return false; + } + var img = image; + if (!img) { + var sel = editor._getSelection(); + var range = editor._createRange(sel); + editor._doc.execCommand("insertimage", false, param.f_url); + if (HTMLArea.is_ie) { img = range.parentElement(); // wonder if this works... if (img.tagName.toLowerCase() != "img") { - img = img.previousSibling; + img = img.previousSibling; } - } else { + } else { img = range.startContainer.previousSibling; - } - for (field in param) { - var value = param[field]; - if (!value) { - continue; } - switch (field) { - case "f_alt" : img.alt = value; break; - case "f_border" : img.border = parseInt(value); break; - case "f_align" : img.align = value; break; - case "f_vert" : img.vspace = parseInt(value); break; - case "f_horiz" : img.hspace = parseInt(value); break; + } else { + img.src = param.f_url; } - } - }, null); + for (field in param) { + var value = param[field]; + switch (field) { + case "f_alt" : img.alt = value; break; + case "f_border" : img.border = parseInt(value || "0"); break; + case "f_align" : img.align = value; break; + case "f_vert" : img.vspace = parseInt(value || "0"); break; + case "f_horiz" : img.hspace = parseInt(value || "0"); break; + } + } + }, outparam); }; // Called when the user clicks the Insert Table button @@ -1098,45 +1482,45 @@ HTMLArea.prototype._insertTable = function() { var range = this._createRange(sel); var editor = this; // for nested functions this._popupDialog("insert_table.html", function(param) { - if (!param) { // user must have pressed Cancel + if (!param) { // user must have pressed Cancel return false; - } - var doc = editor._doc; - // create the table element - var table = doc.createElement("table"); - // assign the given arguments - for (var field in param) { + } + var doc = editor._doc; + // create the table element + var table = doc.createElement("table"); + // assign the given arguments + for (var field in param) { var value = param[field]; if (!value) { - continue; + continue; } switch (field) { - case "f_width" : table.style.width = value + param["f_unit"]; break; - case "f_align" : table.align = value; break; - case "f_border" : table.border = parseInt(value); break; - case "f_spacing" : table.cellspacing = parseInt(value); break; - case "f_padding" : table.cellpadding = parseInt(value); break; + case "f_width" : table.style.width = value + param["f_unit"]; break; + case "f_align" : table.align = value; break; + case "f_border" : table.border = parseInt(value); break; + case "f_spacing" : table.cellspacing = parseInt(value); break; + case "f_padding" : table.cellpadding = parseInt(value); break; } - } - var tbody = doc.createElement("tbody"); - table.appendChild(tbody); - for (var i = 0; i < param["f_rows"]; ++i) { - var tr = doc.createElement("tr"); - tbody.appendChild(tr); - for (var j = 0; j < param["f_cols"]; ++j) { - var td = doc.createElement("td"); - tr.appendChild(td); - // Mozilla likes to see something inside the cell. - (HTMLArea.is_gecko) && td.appendChild(doc.createElement("br")); } - } - if (HTMLArea.is_ie) { - range.pasteHTML(table.outerHTML); - } else { - // insert the table - editor.insertNodeAtSelection(table); - } - return true; + var tbody = doc.createElement("tbody"); + table.appendChild(tbody); + for (var i = 0; i < param["f_rows"]; ++i) { + var tr = doc.createElement("tr"); + tbody.appendChild(tr); + for (var j = 0; j < param["f_cols"]; ++j) { + var td = doc.createElement("td"); + tr.appendChild(td); + // Mozilla likes to see something inside the cell. + (HTMLArea.is_gecko) && td.appendChild(doc.createElement("br")); + } + } + if (HTMLArea.is_ie) { + range.pasteHTML(table.outerHTML); + } else { + // insert the table + editor.insertNodeAtSelection(table); + } + return true; }, null); }; @@ -1150,13 +1534,13 @@ HTMLArea.prototype._comboSelected = function(el, txt) { this.focusEditor(); var value = el.options[el.selectedIndex].value; switch (txt) { - case "fontname": - case "fontsize": this.execCommand(txt, false, value); break; - case "formatblock": - (HTMLArea.is_ie) && (value = "<" + value + ">"); + case "fontname": + case "fontsize": this.execCommand(txt, false, value); break; + case "formatblock": + (HTMLArea.is_ie) && (value = "<" + value + ">"); this.execCommand(txt, false, value); break; - default: + default: // try to look it up in the registered dropdowns var dropdown = this.config.customSelects[txt]; if (typeof dropdown != "undefined") { @@ -1172,46 +1556,81 @@ HTMLArea.prototype._comboSelected = function(el, txt) { HTMLArea.prototype.execCommand = function(cmdID, UI, param) { var editor = this; // for nested functions this.focusEditor(); - switch (cmdID.toLowerCase()) { - case "htmlmode" : this.setMode(); break; - case "hilitecolor": - (HTMLArea.is_ie) && (cmdID = "backcolor"); - case "forecolor": - this._popupDialog("select_color.html", function(color) { - if (color) { // selection not canceled - editor._doc.execCommand(cmdID, false, "#" + color); - } - }, HTMLArea._colorToRgb(this._doc.queryCommandValue(cmdID))); + cmdID = cmdID.toLowerCase(); + switch (cmdID) { + case "htmlmode" : this.setMode(); break; + case "hilitecolor": + (HTMLArea.is_ie) && (cmdID = "backcolor"); + case "forecolor": + this._popupDialog("select_color.html", function(color) { + if (color) { // selection not canceled + editor._doc.execCommand(cmdID, false, "#" + color); + } + }, HTMLArea._colorToRgb(this._doc.queryCommandValue(cmdID))); break; - case "createlink": - if (HTMLArea.is_ie || !UI) { - this._doc.execCommand(cmdID, UI, param); - } else { - // browser is Mozilla & wants UI - var param; - if ((param = prompt("Enter URL"))) { - this._doc.execCommand(cmdID, false, param); - } - } + case "createlink": + this._createLink(); break; - case "popupeditor": - if (HTMLArea.is_ie) { - window.open(this.popupURL("fullscreen.html"), "ha_fullscreen", - "toolbar=no,location=no,directories=no,status=no,menubar=no," + - "scrollbars=no,resizable=yes,width=640,height=480"); - } else { - window.open(this.popupURL("fullscreen.html"), "ha_fullscreen", - "toolbar=no,menubar=no,personalbar=no,width=640,height=480," + - "scrollbars=no,resizable=yes"); - } + case "popupeditor": + if (HTMLArea.is_ie) { + //if (confirm(HTMLArea.I18N.msg["IE-sucks-full-screen"])) + { + window.open(this.popupURL("fullscreen.html"), "ha_fullscreen", + "toolbar=no,location=no,directories=no,status=no,menubar=no," + + "scrollbars=no,resizable=yes,width=640,height=480"); + } + } else { + window.open(this.popupURL("fullscreen.html"), "ha_fullscreen", + "toolbar=no,menubar=no,personalbar=no,width=640,height=480," + + "scrollbars=no,resizable=yes"); + } // pass this object to the newly opened window HTMLArea._object = this; break; - case "inserttable": this._insertTable(); break; - case "insertimage": this._insertImage(); break; - case "about" : this._popupDialog("about.html", null, null); break; - case "showhelp" : window.open("reference.html", "ha_help"); break; - default: this._doc.execCommand(cmdID, UI, param); + case "undo": + case "redo": + if (this._customUndo) + this[cmdID](); + else + this._doc.execCommand(cmdID, UI, param); + break; + case "inserttable": this._insertTable(); break; + case "insertimage": this._insertImage(); break; + case "about" : this._popupDialog("about.html", null, this); break; + case "showhelp" : window.open(_editor_url + "reference.html", "ha_help"); break; + + case "killword": this._wordClean(); break; + + case "cut": + case "copy": + case "paste": + try { + if (this.config.killWordOnPaste) + this._wordClean(); + this._doc.execCommand(cmdID, UI, param); + } catch (e) { + if (HTMLArea.is_gecko) { + if (confirm("Unprivileged scripts cannot access Cut/Copy/Paste programatically " + + "for security reasons. Click OK to see a technical note at mozilla.org " + + "which shows you how to allow a script to access the clipboard.")) + window.open("http://mozilla.org/editor/midasdemo/securityprefs.html"); + } + } + break; + case "lefttoright": + case "righttoleft": + var dir = (cmdID == "righttoleft") ? "rtl" : "ltr"; + var el = this.getParentElement(); + while (el && !HTMLArea.isBlockElement(el)) + el = el.parentNode; + if (el) { + if (el.style.direction == dir) + el.style.direction = ""; + else + el.style.direction = dir; + } + break; + default: this._doc.execCommand(cmdID, UI, param); } this.updateToolbar(); return false; @@ -1229,42 +1648,47 @@ HTMLArea.prototype._editorEvent = function(ev) { var cmd = null; var value = null; switch (key) { - case 'a': - if (!HTMLArea.is_ie) { - // KEY select all - sel = this._getSelection(); - sel.removeAllRanges(); - range = this._createRange(); - range.selectNodeContents(this._doc.body); - sel.addRange(range); - HTMLArea._stopEvent(ev); - } - break; + case 'a': + if (!HTMLArea.is_ie) { + // KEY select all + sel = this._getSelection(); + sel.removeAllRanges(); + range = this._createRange(); + range.selectNodeContents(this._doc.body); + sel.addRange(range); + HTMLArea._stopEvent(ev); + } + break; - // simple key commands follow + // simple key commands follow - case 'b': cmd = "bold"; break; - case 'i': cmd = "italic"; break; - case 'u': cmd = "underline"; break; - case 's': cmd = "strikethrough"; break; - case 'l': cmd = "justifyleft"; break; - case 'e': cmd = "justifycenter"; break; - case 'r': cmd = "justifyright"; break; - case 'j': cmd = "justifyfull"; break; + case 'b': cmd = "bold"; break; + case 'i': cmd = "italic"; break; + case 'u': cmd = "underline"; break; + case 's': cmd = "strikethrough"; break; + case 'l': cmd = "justifyleft"; break; + case 'e': cmd = "justifycenter"; break; + case 'r': cmd = "justifyright"; break; + case 'j': cmd = "justifyfull"; break; + case 'z': cmd = "undo"; break; + case 'y': cmd = "redo"; break; + case 'v': cmd = "paste"; break; - // headings - case '1': - case '2': - case '3': - case '4': - case '5': - case '6': - cmd = "formatblock"; - value = "h" + key; - if (HTMLArea.is_ie) { - value = "<" + value + ">"; - } - break; + case '0': cmd = "killword"; break; + + // headings + case '1': + case '2': + case '3': + case '4': + case '5': + case '6': + cmd = "formatblock"; + value = "h" + key; + if (HTMLArea.is_ie) { + value = "<" + value + ">"; + } + break; } if (cmd) { // execute simple command @@ -1273,34 +1697,38 @@ HTMLArea.prototype._editorEvent = function(ev) { } } /* - else if (keyEvent) { - // other keys here - switch (ev.keyCode) { - case 13: // KEY enter - // if (HTMLArea.is_ie) { - this.insertHTML(""); - HTMLArea._stopEvent(ev); - // } - break; - } + else if (keyEvent) { + // other keys here + switch (ev.keyCode) { + case 13: // KEY enter + // if (HTMLArea.is_ie) { + this.insertHTML(""); + HTMLArea._stopEvent(ev); + // } + break; } - */ + } + */ // update the toolbar state after some time if (editor._timerToolbar) { clearTimeout(editor._timerToolbar); } editor._timerToolbar = setTimeout(function() { - editor.updateToolbar(); - editor._timerToolbar = null; - }, 50); + editor.updateToolbar(); + editor._timerToolbar = null; + }, 50); }; // retrieve the HTML HTMLArea.prototype.getHTML = function() { switch (this._editMode) { - case "wysiwyg" : return HTMLArea.getHTML(this._doc.body, false); - case "textmode" : return this._textArea.value; - default : alert("Mode <" + mode + "> not defined!"); + case "wysiwyg" : + if (!this.config.fullPage) { + return HTMLArea.getHTML(this._doc.body, false, this); + } else + return this.doctype + "\n" + HTMLArea.getHTML(this._doc.documentElement, true, this); + case "textmode" : return this._textArea.value; + default : alert("Mode <" + mode + "> not defined!"); } return false; }; @@ -1308,9 +1736,13 @@ HTMLArea.prototype.getHTML = function() { // retrieve the HTML (fastest version, but uses innerHTML) HTMLArea.prototype.getInnerHTML = function() { switch (this._editMode) { - case "wysiwyg" : return this._doc.body.innerHTML; - case "textmode" : return this._textArea.value; - default : alert("Mode <" + mode + "> not defined!"); + case "wysiwyg" : + if (!this.config.fullPage) + return this._doc.body.innerHTML; + else + return this.doctype + "\n" + this._doc.documentElement.innerHTML; + case "textmode" : return this._textArea.value; + default : alert("Mode <" + mode + "> not defined!"); } return false; }; @@ -1318,13 +1750,24 @@ HTMLArea.prototype.getInnerHTML = function() { // completely change the HTML inside HTMLArea.prototype.setHTML = function(html) { switch (this._editMode) { - case "wysiwyg" : this._doc.body.innerHTML = html; break; - case "textmode" : this._textArea.value = html; break; - default : alert("Mode <" + mode + "> not defined!"); + case "wysiwyg" : + if (!this.config.fullPage) + this._doc.body.innerHTML = html; + else + // this._doc.documentElement.innerHTML = html; + this._doc.body.innerHTML = html; + break; + case "textmode" : this._textArea.value = html; break; + default : alert("Mode <" + mode + "> not defined!"); } return false; }; +// sets the given doctype (useful when config.fullPage is true) +HTMLArea.prototype.setDoctype = function(doctype) { + this.doctype = doctype; +}; + /*************************************************** * Category: UTILITY FUNCTIONS ***************************************************/ @@ -1342,17 +1785,38 @@ HTMLArea.is_gecko = (navigator.product == "Gecko"); // variable used to pass the object to the popup editor window. HTMLArea._object = null; +// function that returns a clone of the given object +HTMLArea.cloneObject = function(obj) { + var newObj = new Object; + + // check for array objects + if (obj.constructor.toString().indexOf("function Array(") == 1) { + newObj = obj.constructor(); + } + + // check for function objects (as usual, IE is fucked up) + if (obj.constructor.toString().indexOf("function Function(") == 1) { + newObj = obj; // just copy reference to it + } else for (var n in obj) { + var node = obj[n]; + if (typeof node == 'object') { newObj[n] = HTMLArea.cloneObject(node); } + else { newObj[n] = node; } + } + + return newObj; +}; + // FIXME!!! this should return false for IE < 5.5 HTMLArea.checkSupportedBrowser = function() { if (HTMLArea.is_gecko) { if (navigator.productSub < 20021201) { alert("You need at least Mozilla-1.3 Alpha.\n" + - "Sorry, your Gecko is not supported."); + "Sorry, your Gecko is not supported."); return false; } if (navigator.productSub < 20030210) { alert("Mozilla < 1.3 Beta is not supported!\n" + - "I'll try, though, but it might not work."); + "I'll try, though, but it might not work."); } } return HTMLArea.is_gecko || HTMLArea.is_ie; @@ -1376,7 +1840,11 @@ HTMLArea.prototype._createRange = function(sel) { } else { this.focusEditor(); if (typeof sel != "undefined") { - return sel.getRangeAt(0); + try { + return sel.getRangeAt(0); + } catch(e) { + return this._doc.createRange(); + } } else { return this._doc.createRange(); } @@ -1464,7 +1932,7 @@ HTMLArea.isBlockElement = function(el) { }; HTMLArea.needsClosingTag = function(el) { - var closingTags = " script style div span tr td tbody table em strong font a "; + var closingTags = " head script style div span tr td tbody table em strong font a title "; return (closingTags.indexOf(" " + el.tagName.toLowerCase() + " ") != -1); }; @@ -1482,72 +1950,114 @@ HTMLArea.htmlEncode = function(str) { // Retrieves the HTML code from the given node. This is a replacement for // getting innerHTML, using standard DOM calls. -HTMLArea.getHTML = function(root, outputRoot) { +HTMLArea.getHTML = function(root, outputRoot, editor) { var html = ""; switch (root.nodeType) { - case 1: // Node.ELEMENT_NODE - case 11: // Node.DOCUMENT_FRAGMENT_NODE - var closed; - var i; - if (outputRoot) { - closed = (!(root.hasChildNodes() || HTMLArea.needsClosingTag(root))); - html = "<" + root.tagName.toLowerCase(); - var attrs = root.attributes; - for (i = 0; i < attrs.length; ++i) { - var a = attrs.item(i); - if (!a.specified) { - continue; - } - var name = a.nodeName.toLowerCase(); - if (/_moz/.test(name)) { - // Mozilla reports some special tags - // here; we don't need them. - continue; - } - var value; - if (name != "style") { - // IE5.5 reports 25 when cellSpacing is - // 1; other values might be doomed too. - // For this reason we extract the - // values directly from the root node. - // I'm starting to HATE JavaScript - // development. Browser differences - // suck. - if (typeof root[a.nodeName] != "undefined") { - value = root[a.nodeName]; - } else { - value = a.nodeValue; + case 1: // Node.ELEMENT_NODE + case 11: // Node.DOCUMENT_FRAGMENT_NODE + var closed; + var i; + var root_tag = (root.nodeType == 1) ? root.tagName.toLowerCase() : ''; + if (HTMLArea.is_ie && root_tag == "head") { + if (outputRoot) + html += ""; + // lowercasize + var save_multiline = RegExp.multiline; + RegExp.multiline = true; + var txt = root.innerHTML.replace(HTMLArea.RE_tagName, function(str, p1, p2) { + return p1 + p2.toLowerCase(); + }); + RegExp.multiline = save_multiline; + html += txt; + if (outputRoot) + html += ""; + break; + } else if (outputRoot) { + closed = (!(root.hasChildNodes() || HTMLArea.needsClosingTag(root))); + html = "<" + root.tagName.toLowerCase(); + var attrs = root.attributes; + for (i = 0; i < attrs.length; ++i) { + var a = attrs.item(i); + if (!a.specified) { + continue; } - } else { // IE fails to put style in attributes list - // FIXME: cssText reported by IE is UPPERCASE - value = root.style.cssText; + var name = a.nodeName.toLowerCase(); + if (/_moz|contenteditable/.test(name)) { + // avoid certain attributes + continue; + } + var value; + if (name != "style") { + // IE5.5 reports 25 when cellSpacing is + // 1; other values might be doomed too. + // For this reason we extract the + // values directly from the root node. + // I'm starting to HATE JavaScript + // development. Browser differences + // suck. + // + // Using Gecko the values of href and src are converted to absolute links + // unless we get them using nodeValue() + if (typeof root[a.nodeName] != "undefined" && name != "href" && name != "src") { + value = root[a.nodeName]; + } else { + value = a.nodeValue; + // IE seems not willing to return the original values - it converts to absolute + // links using a.nodeValue, a.value, a.stringValue, root.getAttribute("href") + // So we have to strip the baseurl manually -/ + if (HTMLArea.is_ie && (name == "href" || name == "src")) { + value = editor.stripBaseURL(value); + } + } + } else { // IE fails to put style in attributes list + // FIXME: cssText reported by IE is UPPERCASE + value = root.style.cssText; + } + if (/(_moz|^$)/.test(value)) { + // Mozilla reports some special tags + // here; we don't need them. + continue; + } + html += " " + name + '="' + value + '"'; } - if (/_moz/.test(value)) { - // Mozilla reports some special tags - // here; we don't need them. - continue; - } - html += " " + name + '="' + value + '"'; + html += closed ? " />" : ">"; } - html += closed ? " />" : ">"; - } - for (i = root.firstChild; i; i = i.nextSibling) { - html += HTMLArea.getHTML(i, true); - } - if (outputRoot && !closed) { - html += "" + root.tagName.toLowerCase() + ">"; - } - break; - case 3: // Node.TEXT_NODE - html = HTMLArea.htmlEncode(root.data); - break; - case 8: // Node.COMMENT_NODE - html = ""; - break; // skip comments, for now. + for (i = root.firstChild; i; i = i.nextSibling) { + html += HTMLArea.getHTML(i, true, editor); + } + if (outputRoot && !closed) { + html += "" + root.tagName.toLowerCase() + ">"; + } + break; + case 3: // Node.TEXT_NODE + html = HTMLArea.htmlEncode(root.data); + break; + case 8: // Node.COMMENT_NODE + html = ""; + break; // skip comments, for now. } return html; }; +HTMLArea.prototype.stripBaseURL = function(string) { + var baseurl = this.config.baseURL; + + // strip to last directory in case baseurl points to a file + baseurl = baseurl.replace(/[^\/]+$/, ''); + var basere = new RegExp(baseurl); + string = string.replace(basere, ""); + + // strip host-part of URL which is added by MSIE to links relative to server root + baseurl = baseurl.replace(/^(https?:\/\/[^\/]+)(.*)$/, '$1'); + basere = new RegExp(baseurl); + return string.replace(basere, ""); +}; + +String.prototype.trim = function() { + a = this.replace(/^\s+/, ''); + return a.replace(/\s+$/, ''); +}; + // creates a rgb-style color from a number HTMLArea._makeColor = function(v) { if (typeof v != "number") { @@ -1562,42 +2072,45 @@ HTMLArea._makeColor = function(v) { }; // returns hexadecimal color representation from a number or a rgb-style color. -HTMLArea._colorToRgb = function(v) { - // returns the hex representation of one byte (2 digits) - function hex(d) { - return (d < 16) ? ("0" + d.toString(16)) : d.toString(16); - }; + HTMLArea._colorToRgb = function(v) { + if (!v) + return ''; - if (typeof v == "number") { - // we're talking to IE here - var r = v & 0xFF; - var g = (v >> 8) & 0xFF; - var b = (v >> 16) & 0xFF; - return "#" + hex(r) + hex(g) + hex(b); - } + // returns the hex representation of one byte (2 digits) + function hex(d) { + return (d < 16) ? ("0" + d.toString(16)) : d.toString(16); + }; - if (v.substr(0, 3) == "rgb") { - // in rgb(...) form -- Mozilla - var re = /rgb\s*\(\s*([0-9]+)\s*,\s*([0-9]+)\s*,\s*([0-9]+)\s*\)/; - if (v.match(re)) { - var r = parseInt(RegExp.$1); - var g = parseInt(RegExp.$2); - var b = parseInt(RegExp.$3); + if (typeof v == "number") { + // we're talking to IE here + var r = v & 0xFF; + var g = (v >> 8) & 0xFF; + var b = (v >> 16) & 0xFF; return "#" + hex(r) + hex(g) + hex(b); } - // doesn't match RE?! maybe uses percentages or float numbers - // -- FIXME: not yet implemented. + + if (v.substr(0, 3) == "rgb") { + // in rgb(...) form -- Mozilla + var re = /rgb\s*\(\s*([0-9]+)\s*,\s*([0-9]+)\s*,\s*([0-9]+)\s*\)/; + if (v.match(re)) { + var r = parseInt(RegExp.$1); + var g = parseInt(RegExp.$2); + var b = parseInt(RegExp.$3); + return "#" + hex(r) + hex(g) + hex(b); + } + // doesn't match RE?! maybe uses percentages or float numbers + // -- FIXME: not yet implemented. + return null; + } + + if (v.substr(0, 1) == "#") { + // already hex rgb (hopefully :D ) + return v; + } + + // if everything else fails ;) return null; - } - - if (v[0] == "#") { - // already hex rgb (hopefully :D ) - return v; - } - - // if everything else fails ;) - return null; -}; + }; // modal dialogs for Mozilla (for IE we're using the showModalDialog() call). @@ -1611,19 +2124,44 @@ HTMLArea.prototype._popupDialog = function(url, action, init) { // paths HTMLArea.prototype.imgURL = function(file, plugin) { - if (typeof plugin == "undefined") { - return this.config.editorURL + file; - } else { - return this.config.editorURL + "plugins/" + plugin + "/img/" + file; - } + if (typeof plugin == "undefined") + return _editor_url + file; + else + return _editor_url + "plugins/" + plugin + "/img/" + file; }; HTMLArea.prototype.popupURL = function(file) { - return this.config.editorURL + this.config.popupURL + file; + var url = ""; + if (file.match(/^plugin:\/\/(.*?)\/(.*)/)) { + var plugin = RegExp.$1; + var popup = RegExp.$2; + if (!/\.html$/.test(popup)) + popup += ".html"; + url = _editor_url + "plugins/" + plugin + "/popups/" + popup; + } else + url = _editor_url + this.config.popupURL + file; + return url; }; +/** + * FIX: Internet Explorer returns an item having the _name_ equal to the given + * id, even if it's not having any id. This way it can return a different form + * field even if it's not a textarea. This workarounds the problem by + * specifically looking to search only elements having a certain tag name. + */ +HTMLArea.getElementById = function(tag, id) { + var el, i, objs = document.getElementsByTagName(tag); + for (i = objs.length; --i >= 0 && (el = objs[i]);) + if (el.id == id) + return el; + return null; +}; + + + // EOF // Local variables: // // c-basic-offset:8 // // indent-tabs-mode:t // // End: // +
]*TEXT-ALIGN: justify[^>]*>/gi,'
'). + replace(/ style=\"[^>]*\"/gi,''). + replace(/ align=[^\s|>]*/gi,''); + + //clean up tags + D = D.replace(/]*>/gi,''). + replace(/]*>/gi,''). + replace(/]*>/gi,''). + replace(/]*>/gi,''); + + // replace outdated tags + D = D.replace(//gi,''). + replace(/<\/b>/gi,''); + + // mozilla doesn't like tags + D = D.replace(//gi,''). + replace(/<\/em>/gi,''); + + // kill unwanted tags + D = D.replace(/<\?xml:[^>]*>/g, ''). // Word xml + replace(/<\/?st1:[^>]*>/g,''). // Word SmartTags + replace(/<\/?[a-z]\:[^>]*>/g,''). // All other funny Word non-HTML stuff + replace(/<\/?font[^>]*>/gi,''). // Disable if you want to keep font formatting + replace(/<\/?span[^>]*>/gi,' '). + replace(/<\/?div[^>]*>/gi,' '). + replace(/<\/?pre[^>]*>/gi,' '). + replace(/<\/?h[1-6][^>]*>/gi,' '); + + //remove empty tags + //D = D.replace(/<\/strong>/gi,''). + //replace(/<\/i>/gi,''). + //replace(/]*><\/P>/gi,''); + + // nuke double tags + oldlen = D.length + 1; + while(oldlen > D.length) { + oldlen = D.length; + // join us now and free the tags, we'll be free hackers, we'll be free... ;-) + D = D.replace(/<([a-z][a-z]*)> *<\/\1>/gi,' '). + replace(/<([a-z][a-z]*)> *<([a-z][^>]*)> *<\/\1>/gi,'<$2>'); + } + D = D.replace(/<([a-z][a-z]*)><\1>/gi,'<$1>'). + replace(/<\/([a-z][a-z]*)><\/\1>/gi,'<\/$1>'); + + // nuke double spaces + D = D.replace(/ */gi,' '); + + this.setHTML(D); + this.updateToolbar(); + } +}; + HTMLArea.prototype.forceRedraw = function() { this._doc.body.style.visibility = "hidden"; this._doc.body.style.visibility = "visible"; @@ -758,13 +1003,49 @@ HTMLArea.prototype.forceRedraw = function() { // focuses the iframe window. returns a reference to the editor document. HTMLArea.prototype.focusEditor = function() { switch (this._editMode) { - case "wysiwyg" : this._iframe.contentWindow.focus(); break; - case "textmode": this._textArea.focus(); break; - default : alert("ERROR: mode " + this._editMode + " is not defined"); + case "wysiwyg" : this._iframe.contentWindow.focus(); break; + case "textmode": this._textArea.focus(); break; + default : alert("ERROR: mode " + this._editMode + " is not defined"); } return this._doc; }; +// takes a snapshot of the current text (for undo) +HTMLArea.prototype._undoTakeSnapshot = function() { + ++this._undoPos; + if (this._undoPos >= this.config.undoSteps) { + // remove the first element + this._undoQueue.shift(); + --this._undoPos; + } + // use the fasted method (getInnerHTML); + var take = true; + var txt = this.getInnerHTML(); + if (this._undoPos > 0) + take = (this._undoQueue[this._undoPos - 1] != txt); + if (take) { + this._undoQueue[this._undoPos] = txt; + } else { + this._undoPos--; + } +}; + +HTMLArea.prototype.undo = function() { + if (this._undoPos > 0) { + var txt = this._undoQueue[--this._undoPos]; + if (txt) this.setHTML(txt); + else ++this._undoPos; + } +}; + +HTMLArea.prototype.redo = function() { + if (this._undoPos < this._undoQueue.length - 1) { + var txt = this._undoQueue[++this._undoPos]; + if (txt) this.setHTML(txt); + else --this._undoPos; + } +}; + // updates enabled/disable/active state of the toolbar elements HTMLArea.prototype.updateToolbar = function(noStatus) { var doc = this._doc; @@ -773,7 +1054,7 @@ HTMLArea.prototype.updateToolbar = function(noStatus) { if (!text) { ancestors = this.getAllAncestors(); if (this.config.statusBar && !noStatus) { - this._statusBarTree.innerHTML = ''; // clear + this._statusBarTree.innerHTML = HTMLArea.I18N.msg["Path"] + ": "; // clear for (var i = ancestors.length; --i >= 0;) { var el = ancestors[i]; if (!el) { @@ -861,58 +1142,80 @@ HTMLArea.prototype.updateToolbar = function(noStatus) { continue; } switch (cmd) { - case "fontname": - case "fontsize": - case "formatblock": - if (!text) { - var value = ("" + doc.queryCommandValue(cmd)).toLowerCase(); - if (!value) { - // FIXME: what do we do here? - break; - } - // HACK -- retrieve the config option for this - // combo box. We rely on the fact that the - // variable in config has the same name as - // button name in the toolbar. - var options = this.config[cmd]; - var k = 0; - // btn.element.selectedIndex = 0; - for (var j in options) { - // FIXME: the following line is scary. - if ((j.toLowerCase() == value) || - (options[j].substr(0, value.length).toLowerCase() == value)) { - btn.element.selectedIndex = k; + case "fontname": + case "fontsize": + case "formatblock": + if (!text) try { + var value = ("" + doc.queryCommandValue(cmd)).toLowerCase(); + if (!value) { + // FIXME: what do we do here? break; } - ++k; - } - } - break; - case "textindicator": - if (!text) { - try {with (btn.element.style) { - backgroundColor = HTMLArea._makeColor( - doc.queryCommandValue(HTMLArea.is_ie ? "backcolor" : "hilitecolor")); - if (/transparent/i.test(backgroundColor)) { - // Mozilla - backgroundColor = HTMLArea._makeColor(doc.queryCommandValue("backcolor")); + // HACK -- retrieve the config option for this + // combo box. We rely on the fact that the + // variable in config has the same name as + // button name in the toolbar. + var options = this.config[cmd]; + var k = 0; + // btn.element.selectedIndex = 0; + for (var j in options) { + // FIXME: the following line is scary. + if ((j.toLowerCase() == value) || + (options[j].substr(0, value.length).toLowerCase() == value)) { + btn.element.selectedIndex = k; + break; + } + ++k; } - color = HTMLArea._makeColor(doc.queryCommandValue("forecolor")); - fontFamily = doc.queryCommandValue("fontname"); - fontWeight = doc.queryCommandState("bold") ? "bold" : "normal"; - fontStyle = doc.queryCommandState("italic") ? "italic" : "normal"; - }} catch (e) { - // alert(e + "\n\n" + cmd); - } - } + } catch(e) {}; break; - case "htmlmode": btn.state("active", text); break; - default: + case "textindicator": + if (!text) { + try {with (btn.element.style) { + backgroundColor = HTMLArea._makeColor( + doc.queryCommandValue(HTMLArea.is_ie ? "backcolor" : "hilitecolor")); + if (/transparent/i.test(backgroundColor)) { + // Mozilla + backgroundColor = HTMLArea._makeColor(doc.queryCommandValue("backcolor")); + } + color = HTMLArea._makeColor(doc.queryCommandValue("forecolor")); + fontFamily = doc.queryCommandValue("fontname"); + fontWeight = doc.queryCommandState("bold") ? "bold" : "normal"; + fontStyle = doc.queryCommandState("italic") ? "italic" : "normal"; + }} catch (e) { + // alert(e + "\n\n" + cmd); + } + } + break; + case "htmlmode": btn.state("active", text); break; + case "lefttoright": + case "righttoleft": + var el = this.getParentElement(); + while (el && !HTMLArea.isBlockElement(el)) + el = el.parentNode; + if (el) + btn.state("active", (el.style.direction == ((cmd == "righttoleft") ? "rtl" : "ltr"))); + break; + default: try { btn.state("active", (!text && doc.queryCommandState(cmd))); } catch (e) {} } } + // take undo snapshots + if (this._customUndo && !this._timerUndo) { + this._undoTakeSnapshot(); + var editor = this; + this._timerUndo = setTimeout(function() { + editor._timerUndo = null; + }, this.config.undoTimeout); + } + // check if any plugins have registered refresh handlers + for (var i in this.plugins) { + var plugin = this.plugins[i].instance; + if (typeof plugin.onUpdateToolbar == "function") + plugin.onUpdateToolbar(); + } }; /** Returns a node after which we can insert other nodes, in the current @@ -928,35 +1231,35 @@ HTMLArea.prototype.insertNodeAtSelection = function(toBeInserted) { var node = range.startContainer; var pos = range.startOffset; switch (node.nodeType) { - case 3: // Node.TEXT_NODE - // we have to split it at the caret position. - if (toBeInserted.nodeType == 3) { - // do optimized insertion - node.insertData(pos, toBeInserted.data); - range = this._createRange(); - range.setEnd(node, pos + toBeInserted.length); - range.setStart(node, pos + toBeInserted.length); - sel.addRange(range); - } else { - node = node.splitText(pos); + case 3: // Node.TEXT_NODE + // we have to split it at the caret position. + if (toBeInserted.nodeType == 3) { + // do optimized insertion + node.insertData(pos, toBeInserted.data); + range = this._createRange(); + range.setEnd(node, pos + toBeInserted.length); + range.setStart(node, pos + toBeInserted.length); + sel.addRange(range); + } else { + node = node.splitText(pos); + var selnode = toBeInserted; + if (toBeInserted.nodeType == 11 /* Node.DOCUMENT_FRAGMENT_NODE */) { + selnode = selnode.firstChild; + } + node.parentNode.insertBefore(toBeInserted, node); + this.selectNodeContents(selnode); + this.updateToolbar(); + } + break; + case 1: // Node.ELEMENT_NODE var selnode = toBeInserted; if (toBeInserted.nodeType == 11 /* Node.DOCUMENT_FRAGMENT_NODE */) { selnode = selnode.firstChild; } - node.parentNode.insertBefore(toBeInserted, node); + node.insertBefore(toBeInserted, node.childNodes[pos]); this.selectNodeContents(selnode); this.updateToolbar(); - } - break; - case 1: // Node.ELEMENT_NODE - var selnode = toBeInserted; - if (toBeInserted.nodeType == 11 /* Node.DOCUMENT_FRAGMENT_NODE */) { - selnode = selnode.firstChild; - } - node.insertBefore(toBeInserted, node.childNodes[pos]); - this.selectNodeContents(selnode); - this.updateToolbar(); - break; + break; } } else { return null; // this function not yet used for IE @@ -968,13 +1271,35 @@ HTMLArea.prototype.getParentElement = function() { var sel = this._getSelection(); var range = this._createRange(sel); if (HTMLArea.is_ie) { - return range.parentElement ? range.parentElement() : this._doc.body; - } else { + switch (sel.type) { + case "Text": + case "None": + // It seems that even for selection of type "None", + // there _is_ a parent element and it's value is not + // only correct, but very important to us. MSIE is + // certainly the buggiest browser in the world and I + // wonder, God, how can Earth stand it? + return range.parentElement(); + case "Control": + return range.item(0); + default: + return this._doc.body; + } + } else try { var p = range.commonAncestorContainer; + if (!range.collapsed && range.startContainer == range.endContainer && + range.startOffset - range.endOffset <= 1 && range.startContainer.hasChildNodes()) + p = range.startContainer.childNodes[range.startOffset]; + /* + alert(range.startContainer + ":" + range.startOffset + "\n" + + range.endContainer + ":" + range.endOffset); + */ while (p.nodeType == 3) { p = p.parentNode; } return p; + } catch (e) { + return null; } }; @@ -1051,45 +1376,104 @@ HTMLArea.prototype.getSelectedHTML = function() { if (HTMLArea.is_ie) { existing = range.htmlText; } else { - existing = HTMLArea.getHTML(range.cloneContents(), false); + existing = HTMLArea.getHTML(range.cloneContents(), false, this); } return existing; }; -// Called when the user clicks on "InsertImage" button -HTMLArea.prototype._insertImage = function() { - var editor = this; // for nested functions - this._popupDialog("insert_image.html", function(param) { - if (!param) { // user must have pressed Cancel +/// Return true if we have some selection +HTMLArea.prototype.hasSelectedText = function() { + // FIXME: come _on_ mishoo, you can do better than this ;-) + return this.getSelectedHTML() != ''; +}; + +HTMLArea.prototype._createLink = function(link) { + var editor = this; + var outparam = null; + if (typeof link == "undefined") { + link = this.getParentElement(); + if (link && !/^a$/i.test(link.tagName)) + link = null; + } + if (link) outparam = { +f_href : HTMLArea.is_ie ? editor.stripBaseURL(link.href) : link.getAttribute("href"), +f_title : link.title, +f_target : link.target + }; + this._popupDialog("link.html", function(param) { + if (!param) return false; - } - var sel = editor._getSelection(); - var range = editor._createRange(sel); - editor._doc.execCommand("insertimage", false, param["f_url"]); - var img = null; - if (HTMLArea.is_ie) { + var a = link; + if (!a) { + editor._doc.execCommand("createlink", false, param.f_href); + a = editor.getParentElement(); + var sel = editor._getSelection(); + var range = editor._createRange(sel); + if (!HTMLArea.is_ie) { + a = range.startContainer; + if (!/^a$/i.test(a.tagName)) + a = a.nextSibling; + } + } else a.href = param.f_href.trim(); + if (!/^a$/i.test(a.tagName)) + return false; + a.target = param.f_target.trim(); + a.title = param.f_title.trim(); + editor.selectNodeContents(a); + editor.updateToolbar(); + }, outparam); +}; + +// Called when the user clicks on "InsertImage" button. If an image is already +// there, it will just modify it's properties. +HTMLArea.prototype._insertImage = function(image) { + var editor = this; // for nested functions + var outparam = null; + if (typeof image == "undefined") { + image = this.getParentElement(); + if (image && !/^img$/i.test(image.tagName)) + image = null; + } + if (image) outparam = { +f_url : HTMLArea.is_ie ? editor.stripBaseURL(image.src) : image.getAttribute("src"), +f_alt : image.alt, +f_border : image.border, +f_align : image.align, +f_vert : image.vspace, +f_horiz : image.hspace + }; + this._popupDialog("insert_image.html", function(param) { + if (!param) { // user must have pressed Cancel + return false; + } + var img = image; + if (!img) { + var sel = editor._getSelection(); + var range = editor._createRange(sel); + editor._doc.execCommand("insertimage", false, param.f_url); + if (HTMLArea.is_ie) { img = range.parentElement(); // wonder if this works... if (img.tagName.toLowerCase() != "img") { - img = img.previousSibling; + img = img.previousSibling; } - } else { + } else { img = range.startContainer.previousSibling; - } - for (field in param) { - var value = param[field]; - if (!value) { - continue; } - switch (field) { - case "f_alt" : img.alt = value; break; - case "f_border" : img.border = parseInt(value); break; - case "f_align" : img.align = value; break; - case "f_vert" : img.vspace = parseInt(value); break; - case "f_horiz" : img.hspace = parseInt(value); break; + } else { + img.src = param.f_url; } - } - }, null); + for (field in param) { + var value = param[field]; + switch (field) { + case "f_alt" : img.alt = value; break; + case "f_border" : img.border = parseInt(value || "0"); break; + case "f_align" : img.align = value; break; + case "f_vert" : img.vspace = parseInt(value || "0"); break; + case "f_horiz" : img.hspace = parseInt(value || "0"); break; + } + } + }, outparam); }; // Called when the user clicks the Insert Table button @@ -1098,45 +1482,45 @@ HTMLArea.prototype._insertTable = function() { var range = this._createRange(sel); var editor = this; // for nested functions this._popupDialog("insert_table.html", function(param) { - if (!param) { // user must have pressed Cancel + if (!param) { // user must have pressed Cancel return false; - } - var doc = editor._doc; - // create the table element - var table = doc.createElement("table"); - // assign the given arguments - for (var field in param) { + } + var doc = editor._doc; + // create the table element + var table = doc.createElement("table"); + // assign the given arguments + for (var field in param) { var value = param[field]; if (!value) { - continue; + continue; } switch (field) { - case "f_width" : table.style.width = value + param["f_unit"]; break; - case "f_align" : table.align = value; break; - case "f_border" : table.border = parseInt(value); break; - case "f_spacing" : table.cellspacing = parseInt(value); break; - case "f_padding" : table.cellpadding = parseInt(value); break; + case "f_width" : table.style.width = value + param["f_unit"]; break; + case "f_align" : table.align = value; break; + case "f_border" : table.border = parseInt(value); break; + case "f_spacing" : table.cellspacing = parseInt(value); break; + case "f_padding" : table.cellpadding = parseInt(value); break; } - } - var tbody = doc.createElement("tbody"); - table.appendChild(tbody); - for (var i = 0; i < param["f_rows"]; ++i) { - var tr = doc.createElement("tr"); - tbody.appendChild(tr); - for (var j = 0; j < param["f_cols"]; ++j) { - var td = doc.createElement("td"); - tr.appendChild(td); - // Mozilla likes to see something inside the cell. - (HTMLArea.is_gecko) && td.appendChild(doc.createElement("br")); } - } - if (HTMLArea.is_ie) { - range.pasteHTML(table.outerHTML); - } else { - // insert the table - editor.insertNodeAtSelection(table); - } - return true; + var tbody = doc.createElement("tbody"); + table.appendChild(tbody); + for (var i = 0; i < param["f_rows"]; ++i) { + var tr = doc.createElement("tr"); + tbody.appendChild(tr); + for (var j = 0; j < param["f_cols"]; ++j) { + var td = doc.createElement("td"); + tr.appendChild(td); + // Mozilla likes to see something inside the cell. + (HTMLArea.is_gecko) && td.appendChild(doc.createElement("br")); + } + } + if (HTMLArea.is_ie) { + range.pasteHTML(table.outerHTML); + } else { + // insert the table + editor.insertNodeAtSelection(table); + } + return true; }, null); }; @@ -1150,13 +1534,13 @@ HTMLArea.prototype._comboSelected = function(el, txt) { this.focusEditor(); var value = el.options[el.selectedIndex].value; switch (txt) { - case "fontname": - case "fontsize": this.execCommand(txt, false, value); break; - case "formatblock": - (HTMLArea.is_ie) && (value = "<" + value + ">"); + case "fontname": + case "fontsize": this.execCommand(txt, false, value); break; + case "formatblock": + (HTMLArea.is_ie) && (value = "<" + value + ">"); this.execCommand(txt, false, value); break; - default: + default: // try to look it up in the registered dropdowns var dropdown = this.config.customSelects[txt]; if (typeof dropdown != "undefined") { @@ -1172,46 +1556,81 @@ HTMLArea.prototype._comboSelected = function(el, txt) { HTMLArea.prototype.execCommand = function(cmdID, UI, param) { var editor = this; // for nested functions this.focusEditor(); - switch (cmdID.toLowerCase()) { - case "htmlmode" : this.setMode(); break; - case "hilitecolor": - (HTMLArea.is_ie) && (cmdID = "backcolor"); - case "forecolor": - this._popupDialog("select_color.html", function(color) { - if (color) { // selection not canceled - editor._doc.execCommand(cmdID, false, "#" + color); - } - }, HTMLArea._colorToRgb(this._doc.queryCommandValue(cmdID))); + cmdID = cmdID.toLowerCase(); + switch (cmdID) { + case "htmlmode" : this.setMode(); break; + case "hilitecolor": + (HTMLArea.is_ie) && (cmdID = "backcolor"); + case "forecolor": + this._popupDialog("select_color.html", function(color) { + if (color) { // selection not canceled + editor._doc.execCommand(cmdID, false, "#" + color); + } + }, HTMLArea._colorToRgb(this._doc.queryCommandValue(cmdID))); break; - case "createlink": - if (HTMLArea.is_ie || !UI) { - this._doc.execCommand(cmdID, UI, param); - } else { - // browser is Mozilla & wants UI - var param; - if ((param = prompt("Enter URL"))) { - this._doc.execCommand(cmdID, false, param); - } - } + case "createlink": + this._createLink(); break; - case "popupeditor": - if (HTMLArea.is_ie) { - window.open(this.popupURL("fullscreen.html"), "ha_fullscreen", - "toolbar=no,location=no,directories=no,status=no,menubar=no," + - "scrollbars=no,resizable=yes,width=640,height=480"); - } else { - window.open(this.popupURL("fullscreen.html"), "ha_fullscreen", - "toolbar=no,menubar=no,personalbar=no,width=640,height=480," + - "scrollbars=no,resizable=yes"); - } + case "popupeditor": + if (HTMLArea.is_ie) { + //if (confirm(HTMLArea.I18N.msg["IE-sucks-full-screen"])) + { + window.open(this.popupURL("fullscreen.html"), "ha_fullscreen", + "toolbar=no,location=no,directories=no,status=no,menubar=no," + + "scrollbars=no,resizable=yes,width=640,height=480"); + } + } else { + window.open(this.popupURL("fullscreen.html"), "ha_fullscreen", + "toolbar=no,menubar=no,personalbar=no,width=640,height=480," + + "scrollbars=no,resizable=yes"); + } // pass this object to the newly opened window HTMLArea._object = this; break; - case "inserttable": this._insertTable(); break; - case "insertimage": this._insertImage(); break; - case "about" : this._popupDialog("about.html", null, null); break; - case "showhelp" : window.open("reference.html", "ha_help"); break; - default: this._doc.execCommand(cmdID, UI, param); + case "undo": + case "redo": + if (this._customUndo) + this[cmdID](); + else + this._doc.execCommand(cmdID, UI, param); + break; + case "inserttable": this._insertTable(); break; + case "insertimage": this._insertImage(); break; + case "about" : this._popupDialog("about.html", null, this); break; + case "showhelp" : window.open(_editor_url + "reference.html", "ha_help"); break; + + case "killword": this._wordClean(); break; + + case "cut": + case "copy": + case "paste": + try { + if (this.config.killWordOnPaste) + this._wordClean(); + this._doc.execCommand(cmdID, UI, param); + } catch (e) { + if (HTMLArea.is_gecko) { + if (confirm("Unprivileged scripts cannot access Cut/Copy/Paste programatically " + + "for security reasons. Click OK to see a technical note at mozilla.org " + + "which shows you how to allow a script to access the clipboard.")) + window.open("http://mozilla.org/editor/midasdemo/securityprefs.html"); + } + } + break; + case "lefttoright": + case "righttoleft": + var dir = (cmdID == "righttoleft") ? "rtl" : "ltr"; + var el = this.getParentElement(); + while (el && !HTMLArea.isBlockElement(el)) + el = el.parentNode; + if (el) { + if (el.style.direction == dir) + el.style.direction = ""; + else + el.style.direction = dir; + } + break; + default: this._doc.execCommand(cmdID, UI, param); } this.updateToolbar(); return false; @@ -1229,42 +1648,47 @@ HTMLArea.prototype._editorEvent = function(ev) { var cmd = null; var value = null; switch (key) { - case 'a': - if (!HTMLArea.is_ie) { - // KEY select all - sel = this._getSelection(); - sel.removeAllRanges(); - range = this._createRange(); - range.selectNodeContents(this._doc.body); - sel.addRange(range); - HTMLArea._stopEvent(ev); - } - break; + case 'a': + if (!HTMLArea.is_ie) { + // KEY select all + sel = this._getSelection(); + sel.removeAllRanges(); + range = this._createRange(); + range.selectNodeContents(this._doc.body); + sel.addRange(range); + HTMLArea._stopEvent(ev); + } + break; - // simple key commands follow + // simple key commands follow - case 'b': cmd = "bold"; break; - case 'i': cmd = "italic"; break; - case 'u': cmd = "underline"; break; - case 's': cmd = "strikethrough"; break; - case 'l': cmd = "justifyleft"; break; - case 'e': cmd = "justifycenter"; break; - case 'r': cmd = "justifyright"; break; - case 'j': cmd = "justifyfull"; break; + case 'b': cmd = "bold"; break; + case 'i': cmd = "italic"; break; + case 'u': cmd = "underline"; break; + case 's': cmd = "strikethrough"; break; + case 'l': cmd = "justifyleft"; break; + case 'e': cmd = "justifycenter"; break; + case 'r': cmd = "justifyright"; break; + case 'j': cmd = "justifyfull"; break; + case 'z': cmd = "undo"; break; + case 'y': cmd = "redo"; break; + case 'v': cmd = "paste"; break; - // headings - case '1': - case '2': - case '3': - case '4': - case '5': - case '6': - cmd = "formatblock"; - value = "h" + key; - if (HTMLArea.is_ie) { - value = "<" + value + ">"; - } - break; + case '0': cmd = "killword"; break; + + // headings + case '1': + case '2': + case '3': + case '4': + case '5': + case '6': + cmd = "formatblock"; + value = "h" + key; + if (HTMLArea.is_ie) { + value = "<" + value + ">"; + } + break; } if (cmd) { // execute simple command @@ -1273,34 +1697,38 @@ HTMLArea.prototype._editorEvent = function(ev) { } } /* - else if (keyEvent) { - // other keys here - switch (ev.keyCode) { - case 13: // KEY enter - // if (HTMLArea.is_ie) { - this.insertHTML(""); - HTMLArea._stopEvent(ev); - // } - break; - } + else if (keyEvent) { + // other keys here + switch (ev.keyCode) { + case 13: // KEY enter + // if (HTMLArea.is_ie) { + this.insertHTML(""); + HTMLArea._stopEvent(ev); + // } + break; } - */ + } + */ // update the toolbar state after some time if (editor._timerToolbar) { clearTimeout(editor._timerToolbar); } editor._timerToolbar = setTimeout(function() { - editor.updateToolbar(); - editor._timerToolbar = null; - }, 50); + editor.updateToolbar(); + editor._timerToolbar = null; + }, 50); }; // retrieve the HTML HTMLArea.prototype.getHTML = function() { switch (this._editMode) { - case "wysiwyg" : return HTMLArea.getHTML(this._doc.body, false); - case "textmode" : return this._textArea.value; - default : alert("Mode <" + mode + "> not defined!"); + case "wysiwyg" : + if (!this.config.fullPage) { + return HTMLArea.getHTML(this._doc.body, false, this); + } else + return this.doctype + "\n" + HTMLArea.getHTML(this._doc.documentElement, true, this); + case "textmode" : return this._textArea.value; + default : alert("Mode <" + mode + "> not defined!"); } return false; }; @@ -1308,9 +1736,13 @@ HTMLArea.prototype.getHTML = function() { // retrieve the HTML (fastest version, but uses innerHTML) HTMLArea.prototype.getInnerHTML = function() { switch (this._editMode) { - case "wysiwyg" : return this._doc.body.innerHTML; - case "textmode" : return this._textArea.value; - default : alert("Mode <" + mode + "> not defined!"); + case "wysiwyg" : + if (!this.config.fullPage) + return this._doc.body.innerHTML; + else + return this.doctype + "\n" + this._doc.documentElement.innerHTML; + case "textmode" : return this._textArea.value; + default : alert("Mode <" + mode + "> not defined!"); } return false; }; @@ -1318,13 +1750,24 @@ HTMLArea.prototype.getInnerHTML = function() { // completely change the HTML inside HTMLArea.prototype.setHTML = function(html) { switch (this._editMode) { - case "wysiwyg" : this._doc.body.innerHTML = html; break; - case "textmode" : this._textArea.value = html; break; - default : alert("Mode <" + mode + "> not defined!"); + case "wysiwyg" : + if (!this.config.fullPage) + this._doc.body.innerHTML = html; + else + // this._doc.documentElement.innerHTML = html; + this._doc.body.innerHTML = html; + break; + case "textmode" : this._textArea.value = html; break; + default : alert("Mode <" + mode + "> not defined!"); } return false; }; +// sets the given doctype (useful when config.fullPage is true) +HTMLArea.prototype.setDoctype = function(doctype) { + this.doctype = doctype; +}; + /*************************************************** * Category: UTILITY FUNCTIONS ***************************************************/ @@ -1342,17 +1785,38 @@ HTMLArea.is_gecko = (navigator.product == "Gecko"); // variable used to pass the object to the popup editor window. HTMLArea._object = null; +// function that returns a clone of the given object +HTMLArea.cloneObject = function(obj) { + var newObj = new Object; + + // check for array objects + if (obj.constructor.toString().indexOf("function Array(") == 1) { + newObj = obj.constructor(); + } + + // check for function objects (as usual, IE is fucked up) + if (obj.constructor.toString().indexOf("function Function(") == 1) { + newObj = obj; // just copy reference to it + } else for (var n in obj) { + var node = obj[n]; + if (typeof node == 'object') { newObj[n] = HTMLArea.cloneObject(node); } + else { newObj[n] = node; } + } + + return newObj; +}; + // FIXME!!! this should return false for IE < 5.5 HTMLArea.checkSupportedBrowser = function() { if (HTMLArea.is_gecko) { if (navigator.productSub < 20021201) { alert("You need at least Mozilla-1.3 Alpha.\n" + - "Sorry, your Gecko is not supported."); + "Sorry, your Gecko is not supported."); return false; } if (navigator.productSub < 20030210) { alert("Mozilla < 1.3 Beta is not supported!\n" + - "I'll try, though, but it might not work."); + "I'll try, though, but it might not work."); } } return HTMLArea.is_gecko || HTMLArea.is_ie; @@ -1376,7 +1840,11 @@ HTMLArea.prototype._createRange = function(sel) { } else { this.focusEditor(); if (typeof sel != "undefined") { - return sel.getRangeAt(0); + try { + return sel.getRangeAt(0); + } catch(e) { + return this._doc.createRange(); + } } else { return this._doc.createRange(); } @@ -1464,7 +1932,7 @@ HTMLArea.isBlockElement = function(el) { }; HTMLArea.needsClosingTag = function(el) { - var closingTags = " script style div span tr td tbody table em strong font a "; + var closingTags = " head script style div span tr td tbody table em strong font a title "; return (closingTags.indexOf(" " + el.tagName.toLowerCase() + " ") != -1); }; @@ -1482,72 +1950,114 @@ HTMLArea.htmlEncode = function(str) { // Retrieves the HTML code from the given node. This is a replacement for // getting innerHTML, using standard DOM calls. -HTMLArea.getHTML = function(root, outputRoot) { +HTMLArea.getHTML = function(root, outputRoot, editor) { var html = ""; switch (root.nodeType) { - case 1: // Node.ELEMENT_NODE - case 11: // Node.DOCUMENT_FRAGMENT_NODE - var closed; - var i; - if (outputRoot) { - closed = (!(root.hasChildNodes() || HTMLArea.needsClosingTag(root))); - html = "<" + root.tagName.toLowerCase(); - var attrs = root.attributes; - for (i = 0; i < attrs.length; ++i) { - var a = attrs.item(i); - if (!a.specified) { - continue; - } - var name = a.nodeName.toLowerCase(); - if (/_moz/.test(name)) { - // Mozilla reports some special tags - // here; we don't need them. - continue; - } - var value; - if (name != "style") { - // IE5.5 reports 25 when cellSpacing is - // 1; other values might be doomed too. - // For this reason we extract the - // values directly from the root node. - // I'm starting to HATE JavaScript - // development. Browser differences - // suck. - if (typeof root[a.nodeName] != "undefined") { - value = root[a.nodeName]; - } else { - value = a.nodeValue; + case 1: // Node.ELEMENT_NODE + case 11: // Node.DOCUMENT_FRAGMENT_NODE + var closed; + var i; + var root_tag = (root.nodeType == 1) ? root.tagName.toLowerCase() : ''; + if (HTMLArea.is_ie && root_tag == "head") { + if (outputRoot) + html += ""; + // lowercasize + var save_multiline = RegExp.multiline; + RegExp.multiline = true; + var txt = root.innerHTML.replace(HTMLArea.RE_tagName, function(str, p1, p2) { + return p1 + p2.toLowerCase(); + }); + RegExp.multiline = save_multiline; + html += txt; + if (outputRoot) + html += ""; + break; + } else if (outputRoot) { + closed = (!(root.hasChildNodes() || HTMLArea.needsClosingTag(root))); + html = "<" + root.tagName.toLowerCase(); + var attrs = root.attributes; + for (i = 0; i < attrs.length; ++i) { + var a = attrs.item(i); + if (!a.specified) { + continue; } - } else { // IE fails to put style in attributes list - // FIXME: cssText reported by IE is UPPERCASE - value = root.style.cssText; + var name = a.nodeName.toLowerCase(); + if (/_moz|contenteditable/.test(name)) { + // avoid certain attributes + continue; + } + var value; + if (name != "style") { + // IE5.5 reports 25 when cellSpacing is + // 1; other values might be doomed too. + // For this reason we extract the + // values directly from the root node. + // I'm starting to HATE JavaScript + // development. Browser differences + // suck. + // + // Using Gecko the values of href and src are converted to absolute links + // unless we get them using nodeValue() + if (typeof root[a.nodeName] != "undefined" && name != "href" && name != "src") { + value = root[a.nodeName]; + } else { + value = a.nodeValue; + // IE seems not willing to return the original values - it converts to absolute + // links using a.nodeValue, a.value, a.stringValue, root.getAttribute("href") + // So we have to strip the baseurl manually -/ + if (HTMLArea.is_ie && (name == "href" || name == "src")) { + value = editor.stripBaseURL(value); + } + } + } else { // IE fails to put style in attributes list + // FIXME: cssText reported by IE is UPPERCASE + value = root.style.cssText; + } + if (/(_moz|^$)/.test(value)) { + // Mozilla reports some special tags + // here; we don't need them. + continue; + } + html += " " + name + '="' + value + '"'; } - if (/_moz/.test(value)) { - // Mozilla reports some special tags - // here; we don't need them. - continue; - } - html += " " + name + '="' + value + '"'; + html += closed ? " />" : ">"; } - html += closed ? " />" : ">"; - } - for (i = root.firstChild; i; i = i.nextSibling) { - html += HTMLArea.getHTML(i, true); - } - if (outputRoot && !closed) { - html += "" + root.tagName.toLowerCase() + ">"; - } - break; - case 3: // Node.TEXT_NODE - html = HTMLArea.htmlEncode(root.data); - break; - case 8: // Node.COMMENT_NODE - html = ""; - break; // skip comments, for now. + for (i = root.firstChild; i; i = i.nextSibling) { + html += HTMLArea.getHTML(i, true, editor); + } + if (outputRoot && !closed) { + html += "" + root.tagName.toLowerCase() + ">"; + } + break; + case 3: // Node.TEXT_NODE + html = HTMLArea.htmlEncode(root.data); + break; + case 8: // Node.COMMENT_NODE + html = ""; + break; // skip comments, for now. } return html; }; +HTMLArea.prototype.stripBaseURL = function(string) { + var baseurl = this.config.baseURL; + + // strip to last directory in case baseurl points to a file + baseurl = baseurl.replace(/[^\/]+$/, ''); + var basere = new RegExp(baseurl); + string = string.replace(basere, ""); + + // strip host-part of URL which is added by MSIE to links relative to server root + baseurl = baseurl.replace(/^(https?:\/\/[^\/]+)(.*)$/, '$1'); + basere = new RegExp(baseurl); + return string.replace(basere, ""); +}; + +String.prototype.trim = function() { + a = this.replace(/^\s+/, ''); + return a.replace(/\s+$/, ''); +}; + // creates a rgb-style color from a number HTMLArea._makeColor = function(v) { if (typeof v != "number") { @@ -1562,42 +2072,45 @@ HTMLArea._makeColor = function(v) { }; // returns hexadecimal color representation from a number or a rgb-style color. -HTMLArea._colorToRgb = function(v) { - // returns the hex representation of one byte (2 digits) - function hex(d) { - return (d < 16) ? ("0" + d.toString(16)) : d.toString(16); - }; + HTMLArea._colorToRgb = function(v) { + if (!v) + return ''; - if (typeof v == "number") { - // we're talking to IE here - var r = v & 0xFF; - var g = (v >> 8) & 0xFF; - var b = (v >> 16) & 0xFF; - return "#" + hex(r) + hex(g) + hex(b); - } + // returns the hex representation of one byte (2 digits) + function hex(d) { + return (d < 16) ? ("0" + d.toString(16)) : d.toString(16); + }; - if (v.substr(0, 3) == "rgb") { - // in rgb(...) form -- Mozilla - var re = /rgb\s*\(\s*([0-9]+)\s*,\s*([0-9]+)\s*,\s*([0-9]+)\s*\)/; - if (v.match(re)) { - var r = parseInt(RegExp.$1); - var g = parseInt(RegExp.$2); - var b = parseInt(RegExp.$3); + if (typeof v == "number") { + // we're talking to IE here + var r = v & 0xFF; + var g = (v >> 8) & 0xFF; + var b = (v >> 16) & 0xFF; return "#" + hex(r) + hex(g) + hex(b); } - // doesn't match RE?! maybe uses percentages or float numbers - // -- FIXME: not yet implemented. + + if (v.substr(0, 3) == "rgb") { + // in rgb(...) form -- Mozilla + var re = /rgb\s*\(\s*([0-9]+)\s*,\s*([0-9]+)\s*,\s*([0-9]+)\s*\)/; + if (v.match(re)) { + var r = parseInt(RegExp.$1); + var g = parseInt(RegExp.$2); + var b = parseInt(RegExp.$3); + return "#" + hex(r) + hex(g) + hex(b); + } + // doesn't match RE?! maybe uses percentages or float numbers + // -- FIXME: not yet implemented. + return null; + } + + if (v.substr(0, 1) == "#") { + // already hex rgb (hopefully :D ) + return v; + } + + // if everything else fails ;) return null; - } - - if (v[0] == "#") { - // already hex rgb (hopefully :D ) - return v; - } - - // if everything else fails ;) - return null; -}; + }; // modal dialogs for Mozilla (for IE we're using the showModalDialog() call). @@ -1611,19 +2124,44 @@ HTMLArea.prototype._popupDialog = function(url, action, init) { // paths HTMLArea.prototype.imgURL = function(file, plugin) { - if (typeof plugin == "undefined") { - return this.config.editorURL + file; - } else { - return this.config.editorURL + "plugins/" + plugin + "/img/" + file; - } + if (typeof plugin == "undefined") + return _editor_url + file; + else + return _editor_url + "plugins/" + plugin + "/img/" + file; }; HTMLArea.prototype.popupURL = function(file) { - return this.config.editorURL + this.config.popupURL + file; + var url = ""; + if (file.match(/^plugin:\/\/(.*?)\/(.*)/)) { + var plugin = RegExp.$1; + var popup = RegExp.$2; + if (!/\.html$/.test(popup)) + popup += ".html"; + url = _editor_url + "plugins/" + plugin + "/popups/" + popup; + } else + url = _editor_url + this.config.popupURL + file; + return url; }; +/** + * FIX: Internet Explorer returns an item having the _name_ equal to the given + * id, even if it's not having any id. This way it can return a different form + * field even if it's not a textarea. This workarounds the problem by + * specifically looking to search only elements having a certain tag name. + */ +HTMLArea.getElementById = function(tag, id) { + var el, i, objs = document.getElementsByTagName(tag); + for (i = objs.length; --i >= 0 && (el = objs[i]);) + if (el.id == id) + return el; + return null; +}; + + + // EOF // Local variables: // // c-basic-offset:8 // // indent-tabs-mode:t // // End: // +
]*><\/P>/gi,''); + + // nuke double tags + oldlen = D.length + 1; + while(oldlen > D.length) { + oldlen = D.length; + // join us now and free the tags, we'll be free hackers, we'll be free... ;-) + D = D.replace(/<([a-z][a-z]*)> *<\/\1>/gi,' '). + replace(/<([a-z][a-z]*)> *<([a-z][^>]*)> *<\/\1>/gi,'<$2>'); + } + D = D.replace(/<([a-z][a-z]*)><\1>/gi,'<$1>'). + replace(/<\/([a-z][a-z]*)><\/\1>/gi,'<\/$1>'); + + // nuke double spaces + D = D.replace(/ */gi,' '); + + this.setHTML(D); + this.updateToolbar(); + } +}; + HTMLArea.prototype.forceRedraw = function() { this._doc.body.style.visibility = "hidden"; this._doc.body.style.visibility = "visible"; @@ -758,13 +1003,49 @@ HTMLArea.prototype.forceRedraw = function() { // focuses the iframe window. returns a reference to the editor document. HTMLArea.prototype.focusEditor = function() { switch (this._editMode) { - case "wysiwyg" : this._iframe.contentWindow.focus(); break; - case "textmode": this._textArea.focus(); break; - default : alert("ERROR: mode " + this._editMode + " is not defined"); + case "wysiwyg" : this._iframe.contentWindow.focus(); break; + case "textmode": this._textArea.focus(); break; + default : alert("ERROR: mode " + this._editMode + " is not defined"); } return this._doc; }; +// takes a snapshot of the current text (for undo) +HTMLArea.prototype._undoTakeSnapshot = function() { + ++this._undoPos; + if (this._undoPos >= this.config.undoSteps) { + // remove the first element + this._undoQueue.shift(); + --this._undoPos; + } + // use the fasted method (getInnerHTML); + var take = true; + var txt = this.getInnerHTML(); + if (this._undoPos > 0) + take = (this._undoQueue[this._undoPos - 1] != txt); + if (take) { + this._undoQueue[this._undoPos] = txt; + } else { + this._undoPos--; + } +}; + +HTMLArea.prototype.undo = function() { + if (this._undoPos > 0) { + var txt = this._undoQueue[--this._undoPos]; + if (txt) this.setHTML(txt); + else ++this._undoPos; + } +}; + +HTMLArea.prototype.redo = function() { + if (this._undoPos < this._undoQueue.length - 1) { + var txt = this._undoQueue[++this._undoPos]; + if (txt) this.setHTML(txt); + else --this._undoPos; + } +}; + // updates enabled/disable/active state of the toolbar elements HTMLArea.prototype.updateToolbar = function(noStatus) { var doc = this._doc; @@ -773,7 +1054,7 @@ HTMLArea.prototype.updateToolbar = function(noStatus) { if (!text) { ancestors = this.getAllAncestors(); if (this.config.statusBar && !noStatus) { - this._statusBarTree.innerHTML = ''; // clear + this._statusBarTree.innerHTML = HTMLArea.I18N.msg["Path"] + ": "; // clear for (var i = ancestors.length; --i >= 0;) { var el = ancestors[i]; if (!el) { @@ -861,58 +1142,80 @@ HTMLArea.prototype.updateToolbar = function(noStatus) { continue; } switch (cmd) { - case "fontname": - case "fontsize": - case "formatblock": - if (!text) { - var value = ("" + doc.queryCommandValue(cmd)).toLowerCase(); - if (!value) { - // FIXME: what do we do here? - break; - } - // HACK -- retrieve the config option for this - // combo box. We rely on the fact that the - // variable in config has the same name as - // button name in the toolbar. - var options = this.config[cmd]; - var k = 0; - // btn.element.selectedIndex = 0; - for (var j in options) { - // FIXME: the following line is scary. - if ((j.toLowerCase() == value) || - (options[j].substr(0, value.length).toLowerCase() == value)) { - btn.element.selectedIndex = k; + case "fontname": + case "fontsize": + case "formatblock": + if (!text) try { + var value = ("" + doc.queryCommandValue(cmd)).toLowerCase(); + if (!value) { + // FIXME: what do we do here? break; } - ++k; - } - } - break; - case "textindicator": - if (!text) { - try {with (btn.element.style) { - backgroundColor = HTMLArea._makeColor( - doc.queryCommandValue(HTMLArea.is_ie ? "backcolor" : "hilitecolor")); - if (/transparent/i.test(backgroundColor)) { - // Mozilla - backgroundColor = HTMLArea._makeColor(doc.queryCommandValue("backcolor")); + // HACK -- retrieve the config option for this + // combo box. We rely on the fact that the + // variable in config has the same name as + // button name in the toolbar. + var options = this.config[cmd]; + var k = 0; + // btn.element.selectedIndex = 0; + for (var j in options) { + // FIXME: the following line is scary. + if ((j.toLowerCase() == value) || + (options[j].substr(0, value.length).toLowerCase() == value)) { + btn.element.selectedIndex = k; + break; + } + ++k; } - color = HTMLArea._makeColor(doc.queryCommandValue("forecolor")); - fontFamily = doc.queryCommandValue("fontname"); - fontWeight = doc.queryCommandState("bold") ? "bold" : "normal"; - fontStyle = doc.queryCommandState("italic") ? "italic" : "normal"; - }} catch (e) { - // alert(e + "\n\n" + cmd); - } - } + } catch(e) {}; break; - case "htmlmode": btn.state("active", text); break; - default: + case "textindicator": + if (!text) { + try {with (btn.element.style) { + backgroundColor = HTMLArea._makeColor( + doc.queryCommandValue(HTMLArea.is_ie ? "backcolor" : "hilitecolor")); + if (/transparent/i.test(backgroundColor)) { + // Mozilla + backgroundColor = HTMLArea._makeColor(doc.queryCommandValue("backcolor")); + } + color = HTMLArea._makeColor(doc.queryCommandValue("forecolor")); + fontFamily = doc.queryCommandValue("fontname"); + fontWeight = doc.queryCommandState("bold") ? "bold" : "normal"; + fontStyle = doc.queryCommandState("italic") ? "italic" : "normal"; + }} catch (e) { + // alert(e + "\n\n" + cmd); + } + } + break; + case "htmlmode": btn.state("active", text); break; + case "lefttoright": + case "righttoleft": + var el = this.getParentElement(); + while (el && !HTMLArea.isBlockElement(el)) + el = el.parentNode; + if (el) + btn.state("active", (el.style.direction == ((cmd == "righttoleft") ? "rtl" : "ltr"))); + break; + default: try { btn.state("active", (!text && doc.queryCommandState(cmd))); } catch (e) {} } } + // take undo snapshots + if (this._customUndo && !this._timerUndo) { + this._undoTakeSnapshot(); + var editor = this; + this._timerUndo = setTimeout(function() { + editor._timerUndo = null; + }, this.config.undoTimeout); + } + // check if any plugins have registered refresh handlers + for (var i in this.plugins) { + var plugin = this.plugins[i].instance; + if (typeof plugin.onUpdateToolbar == "function") + plugin.onUpdateToolbar(); + } }; /** Returns a node after which we can insert other nodes, in the current @@ -928,35 +1231,35 @@ HTMLArea.prototype.insertNodeAtSelection = function(toBeInserted) { var node = range.startContainer; var pos = range.startOffset; switch (node.nodeType) { - case 3: // Node.TEXT_NODE - // we have to split it at the caret position. - if (toBeInserted.nodeType == 3) { - // do optimized insertion - node.insertData(pos, toBeInserted.data); - range = this._createRange(); - range.setEnd(node, pos + toBeInserted.length); - range.setStart(node, pos + toBeInserted.length); - sel.addRange(range); - } else { - node = node.splitText(pos); + case 3: // Node.TEXT_NODE + // we have to split it at the caret position. + if (toBeInserted.nodeType == 3) { + // do optimized insertion + node.insertData(pos, toBeInserted.data); + range = this._createRange(); + range.setEnd(node, pos + toBeInserted.length); + range.setStart(node, pos + toBeInserted.length); + sel.addRange(range); + } else { + node = node.splitText(pos); + var selnode = toBeInserted; + if (toBeInserted.nodeType == 11 /* Node.DOCUMENT_FRAGMENT_NODE */) { + selnode = selnode.firstChild; + } + node.parentNode.insertBefore(toBeInserted, node); + this.selectNodeContents(selnode); + this.updateToolbar(); + } + break; + case 1: // Node.ELEMENT_NODE var selnode = toBeInserted; if (toBeInserted.nodeType == 11 /* Node.DOCUMENT_FRAGMENT_NODE */) { selnode = selnode.firstChild; } - node.parentNode.insertBefore(toBeInserted, node); + node.insertBefore(toBeInserted, node.childNodes[pos]); this.selectNodeContents(selnode); this.updateToolbar(); - } - break; - case 1: // Node.ELEMENT_NODE - var selnode = toBeInserted; - if (toBeInserted.nodeType == 11 /* Node.DOCUMENT_FRAGMENT_NODE */) { - selnode = selnode.firstChild; - } - node.insertBefore(toBeInserted, node.childNodes[pos]); - this.selectNodeContents(selnode); - this.updateToolbar(); - break; + break; } } else { return null; // this function not yet used for IE @@ -968,13 +1271,35 @@ HTMLArea.prototype.getParentElement = function() { var sel = this._getSelection(); var range = this._createRange(sel); if (HTMLArea.is_ie) { - return range.parentElement ? range.parentElement() : this._doc.body; - } else { + switch (sel.type) { + case "Text": + case "None": + // It seems that even for selection of type "None", + // there _is_ a parent element and it's value is not + // only correct, but very important to us. MSIE is + // certainly the buggiest browser in the world and I + // wonder, God, how can Earth stand it? + return range.parentElement(); + case "Control": + return range.item(0); + default: + return this._doc.body; + } + } else try { var p = range.commonAncestorContainer; + if (!range.collapsed && range.startContainer == range.endContainer && + range.startOffset - range.endOffset <= 1 && range.startContainer.hasChildNodes()) + p = range.startContainer.childNodes[range.startOffset]; + /* + alert(range.startContainer + ":" + range.startOffset + "\n" + + range.endContainer + ":" + range.endOffset); + */ while (p.nodeType == 3) { p = p.parentNode; } return p; + } catch (e) { + return null; } }; @@ -1051,45 +1376,104 @@ HTMLArea.prototype.getSelectedHTML = function() { if (HTMLArea.is_ie) { existing = range.htmlText; } else { - existing = HTMLArea.getHTML(range.cloneContents(), false); + existing = HTMLArea.getHTML(range.cloneContents(), false, this); } return existing; }; -// Called when the user clicks on "InsertImage" button -HTMLArea.prototype._insertImage = function() { - var editor = this; // for nested functions - this._popupDialog("insert_image.html", function(param) { - if (!param) { // user must have pressed Cancel +/// Return true if we have some selection +HTMLArea.prototype.hasSelectedText = function() { + // FIXME: come _on_ mishoo, you can do better than this ;-) + return this.getSelectedHTML() != ''; +}; + +HTMLArea.prototype._createLink = function(link) { + var editor = this; + var outparam = null; + if (typeof link == "undefined") { + link = this.getParentElement(); + if (link && !/^a$/i.test(link.tagName)) + link = null; + } + if (link) outparam = { +f_href : HTMLArea.is_ie ? editor.stripBaseURL(link.href) : link.getAttribute("href"), +f_title : link.title, +f_target : link.target + }; + this._popupDialog("link.html", function(param) { + if (!param) return false; - } - var sel = editor._getSelection(); - var range = editor._createRange(sel); - editor._doc.execCommand("insertimage", false, param["f_url"]); - var img = null; - if (HTMLArea.is_ie) { + var a = link; + if (!a) { + editor._doc.execCommand("createlink", false, param.f_href); + a = editor.getParentElement(); + var sel = editor._getSelection(); + var range = editor._createRange(sel); + if (!HTMLArea.is_ie) { + a = range.startContainer; + if (!/^a$/i.test(a.tagName)) + a = a.nextSibling; + } + } else a.href = param.f_href.trim(); + if (!/^a$/i.test(a.tagName)) + return false; + a.target = param.f_target.trim(); + a.title = param.f_title.trim(); + editor.selectNodeContents(a); + editor.updateToolbar(); + }, outparam); +}; + +// Called when the user clicks on "InsertImage" button. If an image is already +// there, it will just modify it's properties. +HTMLArea.prototype._insertImage = function(image) { + var editor = this; // for nested functions + var outparam = null; + if (typeof image == "undefined") { + image = this.getParentElement(); + if (image && !/^img$/i.test(image.tagName)) + image = null; + } + if (image) outparam = { +f_url : HTMLArea.is_ie ? editor.stripBaseURL(image.src) : image.getAttribute("src"), +f_alt : image.alt, +f_border : image.border, +f_align : image.align, +f_vert : image.vspace, +f_horiz : image.hspace + }; + this._popupDialog("insert_image.html", function(param) { + if (!param) { // user must have pressed Cancel + return false; + } + var img = image; + if (!img) { + var sel = editor._getSelection(); + var range = editor._createRange(sel); + editor._doc.execCommand("insertimage", false, param.f_url); + if (HTMLArea.is_ie) { img = range.parentElement(); // wonder if this works... if (img.tagName.toLowerCase() != "img") { - img = img.previousSibling; + img = img.previousSibling; } - } else { + } else { img = range.startContainer.previousSibling; - } - for (field in param) { - var value = param[field]; - if (!value) { - continue; } - switch (field) { - case "f_alt" : img.alt = value; break; - case "f_border" : img.border = parseInt(value); break; - case "f_align" : img.align = value; break; - case "f_vert" : img.vspace = parseInt(value); break; - case "f_horiz" : img.hspace = parseInt(value); break; + } else { + img.src = param.f_url; } - } - }, null); + for (field in param) { + var value = param[field]; + switch (field) { + case "f_alt" : img.alt = value; break; + case "f_border" : img.border = parseInt(value || "0"); break; + case "f_align" : img.align = value; break; + case "f_vert" : img.vspace = parseInt(value || "0"); break; + case "f_horiz" : img.hspace = parseInt(value || "0"); break; + } + } + }, outparam); }; // Called when the user clicks the Insert Table button @@ -1098,45 +1482,45 @@ HTMLArea.prototype._insertTable = function() { var range = this._createRange(sel); var editor = this; // for nested functions this._popupDialog("insert_table.html", function(param) { - if (!param) { // user must have pressed Cancel + if (!param) { // user must have pressed Cancel return false; - } - var doc = editor._doc; - // create the table element - var table = doc.createElement("table"); - // assign the given arguments - for (var field in param) { + } + var doc = editor._doc; + // create the table element + var table = doc.createElement("table"); + // assign the given arguments + for (var field in param) { var value = param[field]; if (!value) { - continue; + continue; } switch (field) { - case "f_width" : table.style.width = value + param["f_unit"]; break; - case "f_align" : table.align = value; break; - case "f_border" : table.border = parseInt(value); break; - case "f_spacing" : table.cellspacing = parseInt(value); break; - case "f_padding" : table.cellpadding = parseInt(value); break; + case "f_width" : table.style.width = value + param["f_unit"]; break; + case "f_align" : table.align = value; break; + case "f_border" : table.border = parseInt(value); break; + case "f_spacing" : table.cellspacing = parseInt(value); break; + case "f_padding" : table.cellpadding = parseInt(value); break; } - } - var tbody = doc.createElement("tbody"); - table.appendChild(tbody); - for (var i = 0; i < param["f_rows"]; ++i) { - var tr = doc.createElement("tr"); - tbody.appendChild(tr); - for (var j = 0; j < param["f_cols"]; ++j) { - var td = doc.createElement("td"); - tr.appendChild(td); - // Mozilla likes to see something inside the cell. - (HTMLArea.is_gecko) && td.appendChild(doc.createElement("br")); } - } - if (HTMLArea.is_ie) { - range.pasteHTML(table.outerHTML); - } else { - // insert the table - editor.insertNodeAtSelection(table); - } - return true; + var tbody = doc.createElement("tbody"); + table.appendChild(tbody); + for (var i = 0; i < param["f_rows"]; ++i) { + var tr = doc.createElement("tr"); + tbody.appendChild(tr); + for (var j = 0; j < param["f_cols"]; ++j) { + var td = doc.createElement("td"); + tr.appendChild(td); + // Mozilla likes to see something inside the cell. + (HTMLArea.is_gecko) && td.appendChild(doc.createElement("br")); + } + } + if (HTMLArea.is_ie) { + range.pasteHTML(table.outerHTML); + } else { + // insert the table + editor.insertNodeAtSelection(table); + } + return true; }, null); }; @@ -1150,13 +1534,13 @@ HTMLArea.prototype._comboSelected = function(el, txt) { this.focusEditor(); var value = el.options[el.selectedIndex].value; switch (txt) { - case "fontname": - case "fontsize": this.execCommand(txt, false, value); break; - case "formatblock": - (HTMLArea.is_ie) && (value = "<" + value + ">"); + case "fontname": + case "fontsize": this.execCommand(txt, false, value); break; + case "formatblock": + (HTMLArea.is_ie) && (value = "<" + value + ">"); this.execCommand(txt, false, value); break; - default: + default: // try to look it up in the registered dropdowns var dropdown = this.config.customSelects[txt]; if (typeof dropdown != "undefined") { @@ -1172,46 +1556,81 @@ HTMLArea.prototype._comboSelected = function(el, txt) { HTMLArea.prototype.execCommand = function(cmdID, UI, param) { var editor = this; // for nested functions this.focusEditor(); - switch (cmdID.toLowerCase()) { - case "htmlmode" : this.setMode(); break; - case "hilitecolor": - (HTMLArea.is_ie) && (cmdID = "backcolor"); - case "forecolor": - this._popupDialog("select_color.html", function(color) { - if (color) { // selection not canceled - editor._doc.execCommand(cmdID, false, "#" + color); - } - }, HTMLArea._colorToRgb(this._doc.queryCommandValue(cmdID))); + cmdID = cmdID.toLowerCase(); + switch (cmdID) { + case "htmlmode" : this.setMode(); break; + case "hilitecolor": + (HTMLArea.is_ie) && (cmdID = "backcolor"); + case "forecolor": + this._popupDialog("select_color.html", function(color) { + if (color) { // selection not canceled + editor._doc.execCommand(cmdID, false, "#" + color); + } + }, HTMLArea._colorToRgb(this._doc.queryCommandValue(cmdID))); break; - case "createlink": - if (HTMLArea.is_ie || !UI) { - this._doc.execCommand(cmdID, UI, param); - } else { - // browser is Mozilla & wants UI - var param; - if ((param = prompt("Enter URL"))) { - this._doc.execCommand(cmdID, false, param); - } - } + case "createlink": + this._createLink(); break; - case "popupeditor": - if (HTMLArea.is_ie) { - window.open(this.popupURL("fullscreen.html"), "ha_fullscreen", - "toolbar=no,location=no,directories=no,status=no,menubar=no," + - "scrollbars=no,resizable=yes,width=640,height=480"); - } else { - window.open(this.popupURL("fullscreen.html"), "ha_fullscreen", - "toolbar=no,menubar=no,personalbar=no,width=640,height=480," + - "scrollbars=no,resizable=yes"); - } + case "popupeditor": + if (HTMLArea.is_ie) { + //if (confirm(HTMLArea.I18N.msg["IE-sucks-full-screen"])) + { + window.open(this.popupURL("fullscreen.html"), "ha_fullscreen", + "toolbar=no,location=no,directories=no,status=no,menubar=no," + + "scrollbars=no,resizable=yes,width=640,height=480"); + } + } else { + window.open(this.popupURL("fullscreen.html"), "ha_fullscreen", + "toolbar=no,menubar=no,personalbar=no,width=640,height=480," + + "scrollbars=no,resizable=yes"); + } // pass this object to the newly opened window HTMLArea._object = this; break; - case "inserttable": this._insertTable(); break; - case "insertimage": this._insertImage(); break; - case "about" : this._popupDialog("about.html", null, null); break; - case "showhelp" : window.open("reference.html", "ha_help"); break; - default: this._doc.execCommand(cmdID, UI, param); + case "undo": + case "redo": + if (this._customUndo) + this[cmdID](); + else + this._doc.execCommand(cmdID, UI, param); + break; + case "inserttable": this._insertTable(); break; + case "insertimage": this._insertImage(); break; + case "about" : this._popupDialog("about.html", null, this); break; + case "showhelp" : window.open(_editor_url + "reference.html", "ha_help"); break; + + case "killword": this._wordClean(); break; + + case "cut": + case "copy": + case "paste": + try { + if (this.config.killWordOnPaste) + this._wordClean(); + this._doc.execCommand(cmdID, UI, param); + } catch (e) { + if (HTMLArea.is_gecko) { + if (confirm("Unprivileged scripts cannot access Cut/Copy/Paste programatically " + + "for security reasons. Click OK to see a technical note at mozilla.org " + + "which shows you how to allow a script to access the clipboard.")) + window.open("http://mozilla.org/editor/midasdemo/securityprefs.html"); + } + } + break; + case "lefttoright": + case "righttoleft": + var dir = (cmdID == "righttoleft") ? "rtl" : "ltr"; + var el = this.getParentElement(); + while (el && !HTMLArea.isBlockElement(el)) + el = el.parentNode; + if (el) { + if (el.style.direction == dir) + el.style.direction = ""; + else + el.style.direction = dir; + } + break; + default: this._doc.execCommand(cmdID, UI, param); } this.updateToolbar(); return false; @@ -1229,42 +1648,47 @@ HTMLArea.prototype._editorEvent = function(ev) { var cmd = null; var value = null; switch (key) { - case 'a': - if (!HTMLArea.is_ie) { - // KEY select all - sel = this._getSelection(); - sel.removeAllRanges(); - range = this._createRange(); - range.selectNodeContents(this._doc.body); - sel.addRange(range); - HTMLArea._stopEvent(ev); - } - break; + case 'a': + if (!HTMLArea.is_ie) { + // KEY select all + sel = this._getSelection(); + sel.removeAllRanges(); + range = this._createRange(); + range.selectNodeContents(this._doc.body); + sel.addRange(range); + HTMLArea._stopEvent(ev); + } + break; - // simple key commands follow + // simple key commands follow - case 'b': cmd = "bold"; break; - case 'i': cmd = "italic"; break; - case 'u': cmd = "underline"; break; - case 's': cmd = "strikethrough"; break; - case 'l': cmd = "justifyleft"; break; - case 'e': cmd = "justifycenter"; break; - case 'r': cmd = "justifyright"; break; - case 'j': cmd = "justifyfull"; break; + case 'b': cmd = "bold"; break; + case 'i': cmd = "italic"; break; + case 'u': cmd = "underline"; break; + case 's': cmd = "strikethrough"; break; + case 'l': cmd = "justifyleft"; break; + case 'e': cmd = "justifycenter"; break; + case 'r': cmd = "justifyright"; break; + case 'j': cmd = "justifyfull"; break; + case 'z': cmd = "undo"; break; + case 'y': cmd = "redo"; break; + case 'v': cmd = "paste"; break; - // headings - case '1': - case '2': - case '3': - case '4': - case '5': - case '6': - cmd = "formatblock"; - value = "h" + key; - if (HTMLArea.is_ie) { - value = "<" + value + ">"; - } - break; + case '0': cmd = "killword"; break; + + // headings + case '1': + case '2': + case '3': + case '4': + case '5': + case '6': + cmd = "formatblock"; + value = "h" + key; + if (HTMLArea.is_ie) { + value = "<" + value + ">"; + } + break; } if (cmd) { // execute simple command @@ -1273,34 +1697,38 @@ HTMLArea.prototype._editorEvent = function(ev) { } } /* - else if (keyEvent) { - // other keys here - switch (ev.keyCode) { - case 13: // KEY enter - // if (HTMLArea.is_ie) { - this.insertHTML(""); - HTMLArea._stopEvent(ev); - // } - break; - } + else if (keyEvent) { + // other keys here + switch (ev.keyCode) { + case 13: // KEY enter + // if (HTMLArea.is_ie) { + this.insertHTML(""); + HTMLArea._stopEvent(ev); + // } + break; } - */ + } + */ // update the toolbar state after some time if (editor._timerToolbar) { clearTimeout(editor._timerToolbar); } editor._timerToolbar = setTimeout(function() { - editor.updateToolbar(); - editor._timerToolbar = null; - }, 50); + editor.updateToolbar(); + editor._timerToolbar = null; + }, 50); }; // retrieve the HTML HTMLArea.prototype.getHTML = function() { switch (this._editMode) { - case "wysiwyg" : return HTMLArea.getHTML(this._doc.body, false); - case "textmode" : return this._textArea.value; - default : alert("Mode <" + mode + "> not defined!"); + case "wysiwyg" : + if (!this.config.fullPage) { + return HTMLArea.getHTML(this._doc.body, false, this); + } else + return this.doctype + "\n" + HTMLArea.getHTML(this._doc.documentElement, true, this); + case "textmode" : return this._textArea.value; + default : alert("Mode <" + mode + "> not defined!"); } return false; }; @@ -1308,9 +1736,13 @@ HTMLArea.prototype.getHTML = function() { // retrieve the HTML (fastest version, but uses innerHTML) HTMLArea.prototype.getInnerHTML = function() { switch (this._editMode) { - case "wysiwyg" : return this._doc.body.innerHTML; - case "textmode" : return this._textArea.value; - default : alert("Mode <" + mode + "> not defined!"); + case "wysiwyg" : + if (!this.config.fullPage) + return this._doc.body.innerHTML; + else + return this.doctype + "\n" + this._doc.documentElement.innerHTML; + case "textmode" : return this._textArea.value; + default : alert("Mode <" + mode + "> not defined!"); } return false; }; @@ -1318,13 +1750,24 @@ HTMLArea.prototype.getInnerHTML = function() { // completely change the HTML inside HTMLArea.prototype.setHTML = function(html) { switch (this._editMode) { - case "wysiwyg" : this._doc.body.innerHTML = html; break; - case "textmode" : this._textArea.value = html; break; - default : alert("Mode <" + mode + "> not defined!"); + case "wysiwyg" : + if (!this.config.fullPage) + this._doc.body.innerHTML = html; + else + // this._doc.documentElement.innerHTML = html; + this._doc.body.innerHTML = html; + break; + case "textmode" : this._textArea.value = html; break; + default : alert("Mode <" + mode + "> not defined!"); } return false; }; +// sets the given doctype (useful when config.fullPage is true) +HTMLArea.prototype.setDoctype = function(doctype) { + this.doctype = doctype; +}; + /*************************************************** * Category: UTILITY FUNCTIONS ***************************************************/ @@ -1342,17 +1785,38 @@ HTMLArea.is_gecko = (navigator.product == "Gecko"); // variable used to pass the object to the popup editor window. HTMLArea._object = null; +// function that returns a clone of the given object +HTMLArea.cloneObject = function(obj) { + var newObj = new Object; + + // check for array objects + if (obj.constructor.toString().indexOf("function Array(") == 1) { + newObj = obj.constructor(); + } + + // check for function objects (as usual, IE is fucked up) + if (obj.constructor.toString().indexOf("function Function(") == 1) { + newObj = obj; // just copy reference to it + } else for (var n in obj) { + var node = obj[n]; + if (typeof node == 'object') { newObj[n] = HTMLArea.cloneObject(node); } + else { newObj[n] = node; } + } + + return newObj; +}; + // FIXME!!! this should return false for IE < 5.5 HTMLArea.checkSupportedBrowser = function() { if (HTMLArea.is_gecko) { if (navigator.productSub < 20021201) { alert("You need at least Mozilla-1.3 Alpha.\n" + - "Sorry, your Gecko is not supported."); + "Sorry, your Gecko is not supported."); return false; } if (navigator.productSub < 20030210) { alert("Mozilla < 1.3 Beta is not supported!\n" + - "I'll try, though, but it might not work."); + "I'll try, though, but it might not work."); } } return HTMLArea.is_gecko || HTMLArea.is_ie; @@ -1376,7 +1840,11 @@ HTMLArea.prototype._createRange = function(sel) { } else { this.focusEditor(); if (typeof sel != "undefined") { - return sel.getRangeAt(0); + try { + return sel.getRangeAt(0); + } catch(e) { + return this._doc.createRange(); + } } else { return this._doc.createRange(); } @@ -1464,7 +1932,7 @@ HTMLArea.isBlockElement = function(el) { }; HTMLArea.needsClosingTag = function(el) { - var closingTags = " script style div span tr td tbody table em strong font a "; + var closingTags = " head script style div span tr td tbody table em strong font a title "; return (closingTags.indexOf(" " + el.tagName.toLowerCase() + " ") != -1); }; @@ -1482,72 +1950,114 @@ HTMLArea.htmlEncode = function(str) { // Retrieves the HTML code from the given node. This is a replacement for // getting innerHTML, using standard DOM calls. -HTMLArea.getHTML = function(root, outputRoot) { +HTMLArea.getHTML = function(root, outputRoot, editor) { var html = ""; switch (root.nodeType) { - case 1: // Node.ELEMENT_NODE - case 11: // Node.DOCUMENT_FRAGMENT_NODE - var closed; - var i; - if (outputRoot) { - closed = (!(root.hasChildNodes() || HTMLArea.needsClosingTag(root))); - html = "<" + root.tagName.toLowerCase(); - var attrs = root.attributes; - for (i = 0; i < attrs.length; ++i) { - var a = attrs.item(i); - if (!a.specified) { - continue; - } - var name = a.nodeName.toLowerCase(); - if (/_moz/.test(name)) { - // Mozilla reports some special tags - // here; we don't need them. - continue; - } - var value; - if (name != "style") { - // IE5.5 reports 25 when cellSpacing is - // 1; other values might be doomed too. - // For this reason we extract the - // values directly from the root node. - // I'm starting to HATE JavaScript - // development. Browser differences - // suck. - if (typeof root[a.nodeName] != "undefined") { - value = root[a.nodeName]; - } else { - value = a.nodeValue; + case 1: // Node.ELEMENT_NODE + case 11: // Node.DOCUMENT_FRAGMENT_NODE + var closed; + var i; + var root_tag = (root.nodeType == 1) ? root.tagName.toLowerCase() : ''; + if (HTMLArea.is_ie && root_tag == "head") { + if (outputRoot) + html += ""; + // lowercasize + var save_multiline = RegExp.multiline; + RegExp.multiline = true; + var txt = root.innerHTML.replace(HTMLArea.RE_tagName, function(str, p1, p2) { + return p1 + p2.toLowerCase(); + }); + RegExp.multiline = save_multiline; + html += txt; + if (outputRoot) + html += ""; + break; + } else if (outputRoot) { + closed = (!(root.hasChildNodes() || HTMLArea.needsClosingTag(root))); + html = "<" + root.tagName.toLowerCase(); + var attrs = root.attributes; + for (i = 0; i < attrs.length; ++i) { + var a = attrs.item(i); + if (!a.specified) { + continue; } - } else { // IE fails to put style in attributes list - // FIXME: cssText reported by IE is UPPERCASE - value = root.style.cssText; + var name = a.nodeName.toLowerCase(); + if (/_moz|contenteditable/.test(name)) { + // avoid certain attributes + continue; + } + var value; + if (name != "style") { + // IE5.5 reports 25 when cellSpacing is + // 1; other values might be doomed too. + // For this reason we extract the + // values directly from the root node. + // I'm starting to HATE JavaScript + // development. Browser differences + // suck. + // + // Using Gecko the values of href and src are converted to absolute links + // unless we get them using nodeValue() + if (typeof root[a.nodeName] != "undefined" && name != "href" && name != "src") { + value = root[a.nodeName]; + } else { + value = a.nodeValue; + // IE seems not willing to return the original values - it converts to absolute + // links using a.nodeValue, a.value, a.stringValue, root.getAttribute("href") + // So we have to strip the baseurl manually -/ + if (HTMLArea.is_ie && (name == "href" || name == "src")) { + value = editor.stripBaseURL(value); + } + } + } else { // IE fails to put style in attributes list + // FIXME: cssText reported by IE is UPPERCASE + value = root.style.cssText; + } + if (/(_moz|^$)/.test(value)) { + // Mozilla reports some special tags + // here; we don't need them. + continue; + } + html += " " + name + '="' + value + '"'; } - if (/_moz/.test(value)) { - // Mozilla reports some special tags - // here; we don't need them. - continue; - } - html += " " + name + '="' + value + '"'; + html += closed ? " />" : ">"; } - html += closed ? " />" : ">"; - } - for (i = root.firstChild; i; i = i.nextSibling) { - html += HTMLArea.getHTML(i, true); - } - if (outputRoot && !closed) { - html += "" + root.tagName.toLowerCase() + ">"; - } - break; - case 3: // Node.TEXT_NODE - html = HTMLArea.htmlEncode(root.data); - break; - case 8: // Node.COMMENT_NODE - html = ""; - break; // skip comments, for now. + for (i = root.firstChild; i; i = i.nextSibling) { + html += HTMLArea.getHTML(i, true, editor); + } + if (outputRoot && !closed) { + html += "" + root.tagName.toLowerCase() + ">"; + } + break; + case 3: // Node.TEXT_NODE + html = HTMLArea.htmlEncode(root.data); + break; + case 8: // Node.COMMENT_NODE + html = ""; + break; // skip comments, for now. } return html; }; +HTMLArea.prototype.stripBaseURL = function(string) { + var baseurl = this.config.baseURL; + + // strip to last directory in case baseurl points to a file + baseurl = baseurl.replace(/[^\/]+$/, ''); + var basere = new RegExp(baseurl); + string = string.replace(basere, ""); + + // strip host-part of URL which is added by MSIE to links relative to server root + baseurl = baseurl.replace(/^(https?:\/\/[^\/]+)(.*)$/, '$1'); + basere = new RegExp(baseurl); + return string.replace(basere, ""); +}; + +String.prototype.trim = function() { + a = this.replace(/^\s+/, ''); + return a.replace(/\s+$/, ''); +}; + // creates a rgb-style color from a number HTMLArea._makeColor = function(v) { if (typeof v != "number") { @@ -1562,42 +2072,45 @@ HTMLArea._makeColor = function(v) { }; // returns hexadecimal color representation from a number or a rgb-style color. -HTMLArea._colorToRgb = function(v) { - // returns the hex representation of one byte (2 digits) - function hex(d) { - return (d < 16) ? ("0" + d.toString(16)) : d.toString(16); - }; + HTMLArea._colorToRgb = function(v) { + if (!v) + return ''; - if (typeof v == "number") { - // we're talking to IE here - var r = v & 0xFF; - var g = (v >> 8) & 0xFF; - var b = (v >> 16) & 0xFF; - return "#" + hex(r) + hex(g) + hex(b); - } + // returns the hex representation of one byte (2 digits) + function hex(d) { + return (d < 16) ? ("0" + d.toString(16)) : d.toString(16); + }; - if (v.substr(0, 3) == "rgb") { - // in rgb(...) form -- Mozilla - var re = /rgb\s*\(\s*([0-9]+)\s*,\s*([0-9]+)\s*,\s*([0-9]+)\s*\)/; - if (v.match(re)) { - var r = parseInt(RegExp.$1); - var g = parseInt(RegExp.$2); - var b = parseInt(RegExp.$3); + if (typeof v == "number") { + // we're talking to IE here + var r = v & 0xFF; + var g = (v >> 8) & 0xFF; + var b = (v >> 16) & 0xFF; return "#" + hex(r) + hex(g) + hex(b); } - // doesn't match RE?! maybe uses percentages or float numbers - // -- FIXME: not yet implemented. + + if (v.substr(0, 3) == "rgb") { + // in rgb(...) form -- Mozilla + var re = /rgb\s*\(\s*([0-9]+)\s*,\s*([0-9]+)\s*,\s*([0-9]+)\s*\)/; + if (v.match(re)) { + var r = parseInt(RegExp.$1); + var g = parseInt(RegExp.$2); + var b = parseInt(RegExp.$3); + return "#" + hex(r) + hex(g) + hex(b); + } + // doesn't match RE?! maybe uses percentages or float numbers + // -- FIXME: not yet implemented. + return null; + } + + if (v.substr(0, 1) == "#") { + // already hex rgb (hopefully :D ) + return v; + } + + // if everything else fails ;) return null; - } - - if (v[0] == "#") { - // already hex rgb (hopefully :D ) - return v; - } - - // if everything else fails ;) - return null; -}; + }; // modal dialogs for Mozilla (for IE we're using the showModalDialog() call). @@ -1611,19 +2124,44 @@ HTMLArea.prototype._popupDialog = function(url, action, init) { // paths HTMLArea.prototype.imgURL = function(file, plugin) { - if (typeof plugin == "undefined") { - return this.config.editorURL + file; - } else { - return this.config.editorURL + "plugins/" + plugin + "/img/" + file; - } + if (typeof plugin == "undefined") + return _editor_url + file; + else + return _editor_url + "plugins/" + plugin + "/img/" + file; }; HTMLArea.prototype.popupURL = function(file) { - return this.config.editorURL + this.config.popupURL + file; + var url = ""; + if (file.match(/^plugin:\/\/(.*?)\/(.*)/)) { + var plugin = RegExp.$1; + var popup = RegExp.$2; + if (!/\.html$/.test(popup)) + popup += ".html"; + url = _editor_url + "plugins/" + plugin + "/popups/" + popup; + } else + url = _editor_url + this.config.popupURL + file; + return url; }; +/** + * FIX: Internet Explorer returns an item having the _name_ equal to the given + * id, even if it's not having any id. This way it can return a different form + * field even if it's not a textarea. This workarounds the problem by + * specifically looking to search only elements having a certain tag name. + */ +HTMLArea.getElementById = function(tag, id) { + var el, i, objs = document.getElementsByTagName(tag); + for (i = objs.length; --i >= 0 && (el = objs[i]);) + if (el.id == id) + return el; + return null; +}; + + + // EOF // Local variables: // // c-basic-offset:8 // // indent-tabs-mode:t // // End: // +