--- loncom/html/htmlarea/Attic/htmlarea.js 2004/02/18 08:07:15 1.1 +++ loncom/html/htmlarea/Attic/htmlarea.js 2004/06/02 00:55:16 1.4 @@ -1,14 +1,30 @@ -// -// 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: htmlarea.js,v 1.1 2004/02/18 08:07:15 www Exp $ +// $Id: htmlarea.js,v 1.4 2004/06/02 00:55:16 www Exp $ + +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. @@ -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 * ------------------------- * @@ -69,16 +121,17 @@ HTMLArea.Config = function () { [ "fontname", "space", "fontsize", "space", "formatblock", "space", - "bold", "italic", "underline", "separator", - "strikethrough", "subscript", "superscript", "separator", + "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", "textindicator", "separator", + "forecolor", "hilitecolor", "separator", "inserthorizontalrule", "createlink", "insertimage", "inserttable", "htmlmode", "separator", "popupeditor", "separator", "showhelp", "about" ] - ]; + ]; this.fontname = { "Arial": 'arial,helvetica,sans-serif', @@ -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,10 +281,10 @@ HTMLArea.Config.prototype.registerButton } // 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; @@ -255,14 +301,40 @@ HTMLArea.Config.prototype.registerButton 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; }; @@ -477,7 +549,7 @@ HTMLArea.prototype._createToolbar = func } }); 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 = func }; 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,7 +621,7 @@ 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, @@ -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](); + } + } }; } @@ -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 @@ -673,9 +775,22 @@ HTMLArea.prototype.generate = function ( function (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 @@ -696,14 +811,21 @@ HTMLArea.prototype.setMode = function(mo case "wysiwyg": if (HTMLArea.is_gecko) { // disable design mode before changing innerHTML - this._doc.designMode = "off"; + try { + this._doc.designMode = "off"; + } catch(e) {}; } - this._doc.body.innerHTML = this.getHTML(); + 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 = ''; @@ -719,36 +841,159 @@ HTMLArea.prototype.setMode = function(mo 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(""); + 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"; @@ -765,6 +1010,42 @@ HTMLArea.prototype.focusEditor = functio 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 = funct 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) { @@ -864,7 +1145,7 @@ HTMLArea.prototype.updateToolbar = funct case "fontname": case "fontsize": case "formatblock": - if (!text) { + if (!text) try { var value = ("" + doc.queryCommandValue(cmd)).toLowerCase(); if (!value) { // FIXME: what do we do here? @@ -886,7 +1167,7 @@ HTMLArea.prototype.updateToolbar = funct } ++k; } - } + } catch(e) {}; break; case "textindicator": if (!text) { @@ -907,12 +1188,34 @@ HTMLArea.prototype.updateToolbar = funct } 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 @@ -968,13 +1271,35 @@ HTMLArea.prototype.getParentElement = fu 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 = fun 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() { +/// 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 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 sel = editor._getSelection(); - var range = editor._createRange(sel); - editor._doc.execCommand("insertimage", false, param["f_url"]); - var img = null; - if (HTMLArea.is_ie) { - img = range.parentElement(); - // wonder if this works... - if (img.tagName.toLowerCase() != "img") { - img = img.previousSibling; + 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; + } + } else { + img = range.startContainer.previousSibling; } } else { - img = range.startContainer.previousSibling; + img.src = param.f_url; } 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_border" : img.border = parseInt(value || "0"); break; case "f_align" : img.align = value; break; - case "f_vert" : img.vspace = parseInt(value); break; - case "f_horiz" : img.hspace = parseInt(value); break; + case "f_vert" : img.vspace = parseInt(value || "0"); break; + case "f_horiz" : img.hspace = parseInt(value || "0"); break; } } - }, null); + }, outparam); }; // Called when the user clicks the Insert Table button @@ -1172,7 +1556,8 @@ HTMLArea.prototype._comboSelected = func HTMLArea.prototype.execCommand = function(cmdID, UI, param) { var editor = this; // for nested functions this.focusEditor(); - switch (cmdID.toLowerCase()) { + cmdID = cmdID.toLowerCase(); + switch (cmdID) { case "htmlmode" : this.setMode(); break; case "hilitecolor": (HTMLArea.is_ie) && (cmdID = "backcolor"); @@ -1184,33 +1569,67 @@ HTMLArea.prototype.execCommand = functio }, 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); - } - } + this._createLink(); break; case "popupeditor": + // this object will be passed to the newly opened window + HTMLArea._object = this; 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"); + //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 "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, null); break; - case "showhelp" : window.open("reference.html", "ha_help"); 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(); @@ -1222,6 +1641,12 @@ HTMLArea.prototype.execCommand = functio HTMLArea.prototype._editorEvent = function(ev) { var editor = this; var keyEvent = (HTMLArea.is_ie && ev.type == "keydown") || (ev.type == "keypress"); + if (keyEvent) { + for (var i in editor.plugins) { + var plugin = editor.plugins[i].instance; + if (typeof plugin.onKeyPress == "function") plugin.onKeyPress(ev); + } + } if (keyEvent && ev.ctrlKey) { var sel = null; var range = null; @@ -1251,6 +1676,11 @@ HTMLArea.prototype._editorEvent = functi 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; + + case '0': cmd = "killword"; break; // headings case '1': @@ -1298,7 +1728,11 @@ HTMLArea.prototype._editorEvent = functi // retrieve the HTML HTMLArea.prototype.getHTML = function() { switch (this._editMode) { - case "wysiwyg" : return HTMLArea.getHTML(this._doc.body, false); + 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!"); } @@ -1308,7 +1742,11 @@ 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 "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!"); } @@ -1318,13 +1756,24 @@ HTMLArea.prototype.getInnerHTML = functi // completely change the HTML inside HTMLArea.prototype.setHTML = function(html) { switch (this._editMode) { - case "wysiwyg" : this._doc.body.innerHTML = html; break; + 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 +1791,38 @@ HTMLArea.is_gecko = (navigator.product // 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."); + window.status="WYSIWYG Editor: You need at least Mozilla-1.3 Alpha. " + + "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."); + window.status="WYSIWYG Editor: Mozilla < 1.3 Beta is not supported! " + + "I'll try, though, but it might not work."; } } return HTMLArea.is_gecko || HTMLArea.is_ie; @@ -1376,7 +1846,11 @@ HTMLArea.prototype._createRange = functi } 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 +1938,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,14 +1956,29 @@ 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) { + 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; @@ -1499,9 +1988,8 @@ HTMLArea.getHTML = function(root, output continue; } var name = a.nodeName.toLowerCase(); - if (/_moz/.test(name)) { - // Mozilla reports some special tags - // here; we don't need them. + if (/_moz|contenteditable|_msh/.test(name)) { + // avoid certain attributes continue; } var value; @@ -1513,16 +2001,25 @@ HTMLArea.getHTML = function(root, output // I'm starting to HATE JavaScript // development. Browser differences // suck. - if (typeof root[a.nodeName] != "undefined") { + // + // 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)) { + if (/(_moz|^$)/.test(value)) { // Mozilla reports some special tags // here; we don't need them. continue; @@ -1532,14 +2029,17 @@ HTMLArea.getHTML = function(root, output html += closed ? " />" : ">"; } for (i = root.firstChild; i; i = i.nextSibling) { - html += HTMLArea.getHTML(i, true); + html += HTMLArea.getHTML(i, true, editor); } if (outputRoot && !closed) { html += "" + root.tagName.toLowerCase() + ">"; } break; case 3: // Node.TEXT_NODE - html = HTMLArea.htmlEncode(root.data); + // If a text node is alone in an element and all spaces, replace it with an non breaking one + // This partially undoes the damage done by moz, which translates ' 's into spaces in the data element + if ( !root.previousSibling && !root.nextSibling && root.data.match(/^\s*$/i) ) html = ' '; + else html = HTMLArea.htmlEncode(root.data); break; case 8: // Node.COMMENT_NODE html = ""; @@ -1548,6 +2048,25 @@ HTMLArea.getHTML = function(root, output 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") { @@ -1563,6 +2082,9 @@ HTMLArea._makeColor = function(v) { // returns hexadecimal color representation from a number or a rgb-style color. HTMLArea._colorToRgb = function(v) { + if (!v) + return ''; + // returns the hex representation of one byte (2 digits) function hex(d) { return (d < 16) ? ("0" + d.toString(16)) : d.toString(16); @@ -1590,7 +2112,7 @@ HTMLArea._colorToRgb = function(v) { return null; } - if (v[0] == "#") { + if (v.substr(0, 1) == "#") { // already hex rgb (hopefully :D ) return v; } @@ -1611,17 +2133,41 @@ HTMLArea.prototype._popupDialog = functi // 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 //
]*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"; @@ -765,6 +1010,42 @@ HTMLArea.prototype.focusEditor = functio 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 = funct 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) { @@ -864,7 +1145,7 @@ HTMLArea.prototype.updateToolbar = funct case "fontname": case "fontsize": case "formatblock": - if (!text) { + if (!text) try { var value = ("" + doc.queryCommandValue(cmd)).toLowerCase(); if (!value) { // FIXME: what do we do here? @@ -886,7 +1167,7 @@ HTMLArea.prototype.updateToolbar = funct } ++k; } - } + } catch(e) {}; break; case "textindicator": if (!text) { @@ -907,12 +1188,34 @@ HTMLArea.prototype.updateToolbar = funct } 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 @@ -968,13 +1271,35 @@ HTMLArea.prototype.getParentElement = fu 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 = fun 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() { +/// 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 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 sel = editor._getSelection(); - var range = editor._createRange(sel); - editor._doc.execCommand("insertimage", false, param["f_url"]); - var img = null; - if (HTMLArea.is_ie) { - img = range.parentElement(); - // wonder if this works... - if (img.tagName.toLowerCase() != "img") { - img = img.previousSibling; + 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; + } + } else { + img = range.startContainer.previousSibling; } } else { - img = range.startContainer.previousSibling; + img.src = param.f_url; } 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_border" : img.border = parseInt(value || "0"); break; case "f_align" : img.align = value; break; - case "f_vert" : img.vspace = parseInt(value); break; - case "f_horiz" : img.hspace = parseInt(value); break; + case "f_vert" : img.vspace = parseInt(value || "0"); break; + case "f_horiz" : img.hspace = parseInt(value || "0"); break; } } - }, null); + }, outparam); }; // Called when the user clicks the Insert Table button @@ -1172,7 +1556,8 @@ HTMLArea.prototype._comboSelected = func HTMLArea.prototype.execCommand = function(cmdID, UI, param) { var editor = this; // for nested functions this.focusEditor(); - switch (cmdID.toLowerCase()) { + cmdID = cmdID.toLowerCase(); + switch (cmdID) { case "htmlmode" : this.setMode(); break; case "hilitecolor": (HTMLArea.is_ie) && (cmdID = "backcolor"); @@ -1184,33 +1569,67 @@ HTMLArea.prototype.execCommand = functio }, 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); - } - } + this._createLink(); break; case "popupeditor": + // this object will be passed to the newly opened window + HTMLArea._object = this; 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"); + //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 "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, null); break; - case "showhelp" : window.open("reference.html", "ha_help"); 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(); @@ -1222,6 +1641,12 @@ HTMLArea.prototype.execCommand = functio HTMLArea.prototype._editorEvent = function(ev) { var editor = this; var keyEvent = (HTMLArea.is_ie && ev.type == "keydown") || (ev.type == "keypress"); + if (keyEvent) { + for (var i in editor.plugins) { + var plugin = editor.plugins[i].instance; + if (typeof plugin.onKeyPress == "function") plugin.onKeyPress(ev); + } + } if (keyEvent && ev.ctrlKey) { var sel = null; var range = null; @@ -1251,6 +1676,11 @@ HTMLArea.prototype._editorEvent = functi 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; + + case '0': cmd = "killword"; break; // headings case '1': @@ -1298,7 +1728,11 @@ HTMLArea.prototype._editorEvent = functi // retrieve the HTML HTMLArea.prototype.getHTML = function() { switch (this._editMode) { - case "wysiwyg" : return HTMLArea.getHTML(this._doc.body, false); + 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!"); } @@ -1308,7 +1742,11 @@ 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 "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!"); } @@ -1318,13 +1756,24 @@ HTMLArea.prototype.getInnerHTML = functi // completely change the HTML inside HTMLArea.prototype.setHTML = function(html) { switch (this._editMode) { - case "wysiwyg" : this._doc.body.innerHTML = html; break; + 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 +1791,38 @@ HTMLArea.is_gecko = (navigator.product // 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."); + window.status="WYSIWYG Editor: You need at least Mozilla-1.3 Alpha. " + + "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."); + window.status="WYSIWYG Editor: Mozilla < 1.3 Beta is not supported! " + + "I'll try, though, but it might not work."; } } return HTMLArea.is_gecko || HTMLArea.is_ie; @@ -1376,7 +1846,11 @@ HTMLArea.prototype._createRange = functi } 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 +1938,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,14 +1956,29 @@ 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) { + 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; @@ -1499,9 +1988,8 @@ HTMLArea.getHTML = function(root, output continue; } var name = a.nodeName.toLowerCase(); - if (/_moz/.test(name)) { - // Mozilla reports some special tags - // here; we don't need them. + if (/_moz|contenteditable|_msh/.test(name)) { + // avoid certain attributes continue; } var value; @@ -1513,16 +2001,25 @@ HTMLArea.getHTML = function(root, output // I'm starting to HATE JavaScript // development. Browser differences // suck. - if (typeof root[a.nodeName] != "undefined") { + // + // 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)) { + if (/(_moz|^$)/.test(value)) { // Mozilla reports some special tags // here; we don't need them. continue; @@ -1532,14 +2029,17 @@ HTMLArea.getHTML = function(root, output html += closed ? " />" : ">"; } for (i = root.firstChild; i; i = i.nextSibling) { - html += HTMLArea.getHTML(i, true); + html += HTMLArea.getHTML(i, true, editor); } if (outputRoot && !closed) { html += "" + root.tagName.toLowerCase() + ">"; } break; case 3: // Node.TEXT_NODE - html = HTMLArea.htmlEncode(root.data); + // If a text node is alone in an element and all spaces, replace it with an non breaking one + // This partially undoes the damage done by moz, which translates ' 's into spaces in the data element + if ( !root.previousSibling && !root.nextSibling && root.data.match(/^\s*$/i) ) html = ' '; + else html = HTMLArea.htmlEncode(root.data); break; case 8: // Node.COMMENT_NODE html = ""; @@ -1548,6 +2048,25 @@ HTMLArea.getHTML = function(root, output 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") { @@ -1563,6 +2082,9 @@ HTMLArea._makeColor = function(v) { // returns hexadecimal color representation from a number or a rgb-style color. HTMLArea._colorToRgb = function(v) { + if (!v) + return ''; + // returns the hex representation of one byte (2 digits) function hex(d) { return (d < 16) ? ("0" + d.toString(16)) : d.toString(16); @@ -1590,7 +2112,7 @@ HTMLArea._colorToRgb = function(v) { return null; } - if (v[0] == "#") { + if (v.substr(0, 1) == "#") { // already hex rgb (hopefully :D ) return v; } @@ -1611,17 +2133,41 @@ HTMLArea.prototype._popupDialog = functi // 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 //
]*><\/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"; @@ -765,6 +1010,42 @@ HTMLArea.prototype.focusEditor = functio 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 = funct 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) { @@ -864,7 +1145,7 @@ HTMLArea.prototype.updateToolbar = funct case "fontname": case "fontsize": case "formatblock": - if (!text) { + if (!text) try { var value = ("" + doc.queryCommandValue(cmd)).toLowerCase(); if (!value) { // FIXME: what do we do here? @@ -886,7 +1167,7 @@ HTMLArea.prototype.updateToolbar = funct } ++k; } - } + } catch(e) {}; break; case "textindicator": if (!text) { @@ -907,12 +1188,34 @@ HTMLArea.prototype.updateToolbar = funct } 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 @@ -968,13 +1271,35 @@ HTMLArea.prototype.getParentElement = fu 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 = fun 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() { +/// 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 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 sel = editor._getSelection(); - var range = editor._createRange(sel); - editor._doc.execCommand("insertimage", false, param["f_url"]); - var img = null; - if (HTMLArea.is_ie) { - img = range.parentElement(); - // wonder if this works... - if (img.tagName.toLowerCase() != "img") { - img = img.previousSibling; + 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; + } + } else { + img = range.startContainer.previousSibling; } } else { - img = range.startContainer.previousSibling; + img.src = param.f_url; } 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_border" : img.border = parseInt(value || "0"); break; case "f_align" : img.align = value; break; - case "f_vert" : img.vspace = parseInt(value); break; - case "f_horiz" : img.hspace = parseInt(value); break; + case "f_vert" : img.vspace = parseInt(value || "0"); break; + case "f_horiz" : img.hspace = parseInt(value || "0"); break; } } - }, null); + }, outparam); }; // Called when the user clicks the Insert Table button @@ -1172,7 +1556,8 @@ HTMLArea.prototype._comboSelected = func HTMLArea.prototype.execCommand = function(cmdID, UI, param) { var editor = this; // for nested functions this.focusEditor(); - switch (cmdID.toLowerCase()) { + cmdID = cmdID.toLowerCase(); + switch (cmdID) { case "htmlmode" : this.setMode(); break; case "hilitecolor": (HTMLArea.is_ie) && (cmdID = "backcolor"); @@ -1184,33 +1569,67 @@ HTMLArea.prototype.execCommand = functio }, 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); - } - } + this._createLink(); break; case "popupeditor": + // this object will be passed to the newly opened window + HTMLArea._object = this; 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"); + //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 "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, null); break; - case "showhelp" : window.open("reference.html", "ha_help"); 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(); @@ -1222,6 +1641,12 @@ HTMLArea.prototype.execCommand = functio HTMLArea.prototype._editorEvent = function(ev) { var editor = this; var keyEvent = (HTMLArea.is_ie && ev.type == "keydown") || (ev.type == "keypress"); + if (keyEvent) { + for (var i in editor.plugins) { + var plugin = editor.plugins[i].instance; + if (typeof plugin.onKeyPress == "function") plugin.onKeyPress(ev); + } + } if (keyEvent && ev.ctrlKey) { var sel = null; var range = null; @@ -1251,6 +1676,11 @@ HTMLArea.prototype._editorEvent = functi 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; + + case '0': cmd = "killword"; break; // headings case '1': @@ -1298,7 +1728,11 @@ HTMLArea.prototype._editorEvent = functi // retrieve the HTML HTMLArea.prototype.getHTML = function() { switch (this._editMode) { - case "wysiwyg" : return HTMLArea.getHTML(this._doc.body, false); + 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!"); } @@ -1308,7 +1742,11 @@ 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 "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!"); } @@ -1318,13 +1756,24 @@ HTMLArea.prototype.getInnerHTML = functi // completely change the HTML inside HTMLArea.prototype.setHTML = function(html) { switch (this._editMode) { - case "wysiwyg" : this._doc.body.innerHTML = html; break; + 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 +1791,38 @@ HTMLArea.is_gecko = (navigator.product // 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."); + window.status="WYSIWYG Editor: You need at least Mozilla-1.3 Alpha. " + + "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."); + window.status="WYSIWYG Editor: Mozilla < 1.3 Beta is not supported! " + + "I'll try, though, but it might not work."; } } return HTMLArea.is_gecko || HTMLArea.is_ie; @@ -1376,7 +1846,11 @@ HTMLArea.prototype._createRange = functi } 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 +1938,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,14 +1956,29 @@ 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) { + var root_tag = (root.nodeType == 1) ? root.tagName.toLowerCase() : ''; + if (HTMLArea.is_ie && root_tag == "head") { + if (outputRoot) + html += "