File:  [LON-CAPA] / loncom / html / htmlarea / Attic / htmlarea.js
Revision 1.4: download - view: text, annotated - select for diffs
Wed Jun 2 00:55:16 2004 UTC (20 years, 1 month ago) by www
Branches: MAIN
CVS tags: version_1_3_X, version_1_3_3, version_1_3_2, version_1_3_1, version_1_3_0, version_1_2_X, version_1_2_99_1, version_1_2_99_0, version_1_2_1, version_1_2_0, version_1_1_99_5, version_1_1_99_4, version_1_1_99_3, version_1_1_99_2, version_1_1_99_1, version_1_1_99_0, HEAD
Graceful downgrading - no whining if browser incompatible
New files from RC1

    1: // htmlArea v3.0 - Copyright (c) 2002-2004 interactivetools.com, inc.
    2: // This copyright notice MUST stay intact for use (see license.txt).
    3: //
    4: // Portions (c) dynarch.com, 2003-2004
    5: //
    6: // A free WYSIWYG editor replacement for <textarea> fields.
    7: // For full source code and docs, visit http://www.interactivetools.com/
    8: //
    9: // Version 3.0 developed by Mihai Bazon.
   10: //   http://dynarch.com/mishoo
   11: //
   12: // $Id: htmlarea.js,v 1.4 2004/06/02 00:55:16 www Exp $
   13: 
   14: if (typeof _editor_url == "string") {
   15: 	// Leave exactly one backslash at the end of _editor_url
   16: 	_editor_url = _editor_url.replace(/\x2f*$/, '/');
   17: } else {
   18: 	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.");
   19: 	_editor_url = '';
   20: }
   21: 
   22: // make sure we have a language
   23: if (typeof _editor_lang == "string") {
   24: 	_editor_lang = _editor_lang.toLowerCase();
   25: } else {
   26: 	_editor_lang = "en";
   27: }
   28: 
   29: // Creates a new HTMLArea object.  Tries to replace the textarea with the given
   30: // ID with it.
   31: function HTMLArea(textarea, config) {
   32: 	if (HTMLArea.checkSupportedBrowser()) {
   33: 		if (typeof config == "undefined") {
   34: 			this.config = new HTMLArea.Config();
   35: 		} else {
   36: 			this.config = config;
   37: 		}
   38: 		this._htmlArea = null;
   39: 		this._textArea = textarea;
   40: 		this._editMode = "wysiwyg";
   41: 		this.plugins = {};
   42: 		this._timerToolbar = null;
   43: 		this._timerUndo = null;
   44: 		this._undoQueue = new Array(this.config.undoSteps);
   45: 		this._undoPos = -1;
   46: 		this._customUndo = false;
   47: 		this._mdoc = document; // cache the document, we need it in plugins
   48: 		this.doctype = '';
   49: 	}
   50: };
   51: 
   52: // load some scripts
   53: (function() {
   54: 	var scripts = HTMLArea._scripts = [ _editor_url + "htmlarea.js",
   55: 					    _editor_url + "dialog.js",
   56: 					    _editor_url + "popupwin.js",
   57: 					    _editor_url + "lang/" + _editor_lang + ".js" ];
   58: 	var head = document.getElementsByTagName("head")[0];
   59: 	// start from 1, htmlarea.js is already loaded
   60: 	for (var i = 1; i < scripts.length; ++i) {
   61: 		var script = document.createElement("script");
   62: 		script.src = scripts[i];
   63: 		head.appendChild(script);
   64: 	}
   65: })();
   66: 
   67: // cache some regexps
   68: HTMLArea.RE_tagName = /(<\/|<)\s*([^ \t\n>]+)/ig;
   69: HTMLArea.RE_doctype = /(<!doctype((.|\n)*?)>)\n?/i;
   70: HTMLArea.RE_head    = /<head>((.|\n)*?)<\/head>/i;
   71: HTMLArea.RE_body    = /<body>((.|\n)*?)<\/body>/i;
   72: 
   73: HTMLArea.Config = function () {
   74: 	this.version = "3.0";
   75: 
   76: 	this.width = "auto";
   77: 	this.height = "auto";
   78: 
   79: 	// enable creation of a status bar?
   80: 	this.statusBar = true;
   81: 
   82: 	// maximum size of the undo queue
   83: 	this.undoSteps = 20;
   84: 
   85: 	// the time interval at which undo samples are taken
   86: 	this.undoTimeout = 500;	// 1/2 sec.
   87: 
   88: 	// the next parameter specifies whether the toolbar should be included
   89: 	// in the size or not.
   90: 	this.sizeIncludesToolbar = true;
   91: 
   92: 	// if true then HTMLArea will retrieve the full HTML, starting with the
   93: 	// <HTML> tag.
   94: 	this.fullPage = false;
   95: 
   96: 	// style included in the iframe document
   97: 	this.pageStyle = "";
   98: 
   99: 	// set to true if you want Word code to be cleaned upon Paste
  100: 	this.killWordOnPaste = false;
  101: 
  102: 	// BaseURL included in the iframe document
  103: 	this.baseURL = document.baseURI || document.URL;
  104: 	if (this.baseURL && this.baseURL.match(/(.*)\/([^\/]+)/))
  105: 		this.baseURL = RegExp.$1 + "/";
  106: 
  107: 	// URL-s
  108: 	this.imgURL = "images/";
  109: 	this.popupURL = "popups/";
  110: 
  111: 	/** CUSTOMIZING THE TOOLBAR
  112: 	 * -------------------------
  113: 	 *
  114: 	 * It is recommended that you customize the toolbar contents in an
  115: 	 * external file (i.e. the one calling HTMLArea) and leave this one
  116: 	 * unchanged.  That's because when we (InteractiveTools.com) release a
  117: 	 * new official version, it's less likely that you will have problems
  118: 	 * upgrading HTMLArea.
  119: 	 */
  120: 	this.toolbar = [
  121: 		[ "fontname", "space",
  122: 		  "fontsize", "space",
  123: 		  "formatblock", "space",
  124: 		  "bold", "italic", "underline", "strikethrough", "separator",
  125: 		  "subscript", "superscript", "separator",
  126: 		  "copy", "cut", "paste", "space", "undo", "redo" ],
  127: 
  128: 		[ "justifyleft", "justifycenter", "justifyright", "justifyfull", "separator",
  129: 		  "lefttoright", "righttoleft", "separator",
  130: 		  "insertorderedlist", "insertunorderedlist", "outdent", "indent", "separator",
  131: 		  "forecolor", "hilitecolor", "separator",
  132: 		  "inserthorizontalrule", "createlink", "insertimage", "inserttable", "htmlmode", "separator",
  133: 		  "popupeditor", "separator", "showhelp", "about" ]
  134: 	];
  135: 
  136: 	this.fontname = {
  137: 		"Arial":	   'arial,helvetica,sans-serif',
  138: 		"Courier New":	   'courier new,courier,monospace',
  139: 		"Georgia":	   'georgia,times new roman,times,serif',
  140: 		"Tahoma":	   'tahoma,arial,helvetica,sans-serif',
  141: 		"Times New Roman": 'times new roman,times,serif',
  142: 		"Verdana":	   'verdana,arial,helvetica,sans-serif',
  143: 		"impact":	   'impact',
  144: 		"WingDings":	   'wingdings'
  145: 	};
  146: 
  147: 	this.fontsize = {
  148: 		"1 (8 pt)":  "1",
  149: 		"2 (10 pt)": "2",
  150: 		"3 (12 pt)": "3",
  151: 		"4 (14 pt)": "4",
  152: 		"5 (18 pt)": "5",
  153: 		"6 (24 pt)": "6",
  154: 		"7 (36 pt)": "7"
  155: 	};
  156: 
  157: 	this.formatblock = {
  158: 		"Heading 1": "h1",
  159: 		"Heading 2": "h2",
  160: 		"Heading 3": "h3",
  161: 		"Heading 4": "h4",
  162: 		"Heading 5": "h5",
  163: 		"Heading 6": "h6",
  164: 		"Normal": "p",
  165: 		"Address": "address",
  166: 		"Formatted": "pre"
  167: 	};
  168: 
  169: 	this.customSelects = {};
  170: 
  171: 	function cut_copy_paste(e, cmd, obj) {
  172: 		e.execCommand(cmd);
  173: 	};
  174: 
  175: 	// ADDING CUSTOM BUTTONS: please read below!
  176: 	// format of the btnList elements is "ID: [ ToolTip, Icon, Enabled in text mode?, ACTION ]"
  177: 	//    - ID: unique ID for the button.  If the button calls document.execCommand
  178: 	//	    it's wise to give it the same name as the called command.
  179: 	//    - ACTION: function that gets called when the button is clicked.
  180: 	//              it has the following prototype:
  181: 	//                 function(editor, buttonName)
  182: 	//              - editor is the HTMLArea object that triggered the call
  183: 	//              - buttonName is the ID of the clicked button
  184: 	//              These 2 parameters makes it possible for you to use the same
  185: 	//              handler for more HTMLArea objects or for more different buttons.
  186: 	//    - ToolTip: default tooltip, for cases when it is not defined in the -lang- file (HTMLArea.I18N)
  187: 	//    - Icon: path to an icon image file for the button (TODO: use one image for all buttons!)
  188: 	//    - Enabled in text mode: if false the button gets disabled for text-only mode; otherwise enabled all the time.
  189: 	this.btnList = {
  190: 		bold: [ "Bold", "ed_format_bold.gif", false, function(e) {e.execCommand("bold");} ],
  191: 		italic: [ "Italic", "ed_format_italic.gif", false, function(e) {e.execCommand("italic");} ],
  192: 		underline: [ "Underline", "ed_format_underline.gif", false, function(e) {e.execCommand("underline");} ],
  193: 		strikethrough: [ "Strikethrough", "ed_format_strike.gif", false, function(e) {e.execCommand("strikethrough");} ],
  194: 		subscript: [ "Subscript", "ed_format_sub.gif", false, function(e) {e.execCommand("subscript");} ],
  195: 		superscript: [ "Superscript", "ed_format_sup.gif", false, function(e) {e.execCommand("superscript");} ],
  196: 		justifyleft: [ "Justify Left", "ed_align_left.gif", false, function(e) {e.execCommand("justifyleft");} ],
  197: 		justifycenter: [ "Justify Center", "ed_align_center.gif", false, function(e) {e.execCommand("justifycenter");} ],
  198: 		justifyright: [ "Justify Right", "ed_align_right.gif", false, function(e) {e.execCommand("justifyright");} ],
  199: 		justifyfull: [ "Justify Full", "ed_align_justify.gif", false, function(e) {e.execCommand("justifyfull");} ],
  200: 		insertorderedlist: [ "Ordered List", "ed_list_num.gif", false, function(e) {e.execCommand("insertorderedlist");} ],
  201: 		insertunorderedlist: [ "Bulleted List", "ed_list_bullet.gif", false, function(e) {e.execCommand("insertunorderedlist");} ],
  202: 		outdent: [ "Decrease Indent", "ed_indent_less.gif", false, function(e) {e.execCommand("outdent");} ],
  203: 		indent: [ "Increase Indent", "ed_indent_more.gif", false, function(e) {e.execCommand("indent");} ],
  204: 		forecolor: [ "Font Color", "ed_color_fg.gif", false, function(e) {e.execCommand("forecolor");} ],
  205: 		hilitecolor: [ "Background Color", "ed_color_bg.gif", false, function(e) {e.execCommand("hilitecolor");} ],
  206: 		inserthorizontalrule: [ "Horizontal Rule", "ed_hr.gif", false, function(e) {e.execCommand("inserthorizontalrule");} ],
  207: 		createlink: [ "Insert Web Link", "ed_link.gif", false, function(e) {e.execCommand("createlink", true);} ],
  208: 		insertimage: [ "Insert/Modify Image", "ed_image.gif", false, function(e) {e.execCommand("insertimage");} ],
  209: 		inserttable: [ "Insert Table", "insert_table.gif", false, function(e) {e.execCommand("inserttable");} ],
  210: 		htmlmode: [ "Toggle HTML Source", "ed_html.gif", true, function(e) {e.execCommand("htmlmode");} ],
  211: 		popupeditor: [ "Enlarge Editor", "fullscreen_maximize.gif", true, function(e) {e.execCommand("popupeditor");} ],
  212: 		about: [ "About this editor", "ed_about.gif", true, function(e) {e.execCommand("about");} ],
  213: 		showhelp: [ "Help using editor", "ed_help.gif", true, function(e) {e.execCommand("showhelp");} ],
  214: 		undo: [ "Undoes your last action", "ed_undo.gif", false, function(e) {e.execCommand("undo");} ],
  215: 		redo: [ "Redoes your last action", "ed_redo.gif", false, function(e) {e.execCommand("redo");} ],
  216: 		cut: [ "Cut selection", "ed_cut.gif", false, cut_copy_paste ],
  217: 		copy: [ "Copy selection", "ed_copy.gif", false, cut_copy_paste ],
  218: 		paste: [ "Paste from clipboard", "ed_paste.gif", false, cut_copy_paste ],
  219: 		lefttoright: [ "Direction left to right", "ed_left_to_right.gif", false, function(e) {e.execCommand("lefttoright");} ],
  220: 		righttoleft: [ "Direction right to left", "ed_right_to_left.gif", false, function(e) {e.execCommand("righttoleft");} ]
  221: 	};
  222: 	/* ADDING CUSTOM BUTTONS
  223: 	 * ---------------------
  224: 	 *
  225: 	 * It is recommended that you add the custom buttons in an external
  226: 	 * file and leave this one unchanged.  That's because when we
  227: 	 * (InteractiveTools.com) release a new official version, it's less
  228: 	 * likely that you will have problems upgrading HTMLArea.
  229: 	 *
  230: 	 * Example on how to add a custom button when you construct the HTMLArea:
  231: 	 *
  232: 	 *   var editor = new HTMLArea("your_text_area_id");
  233: 	 *   var cfg = editor.config; // this is the default configuration
  234: 	 *   cfg.btnList["my-hilite"] =
  235: 	 *	[ function(editor) { editor.surroundHTML('<span style="background:yellow">', '</span>'); }, // action
  236: 	 *	  "Highlight selection", // tooltip
  237: 	 *	  "my_hilite.gif", // image
  238: 	 *	  false // disabled in text mode
  239: 	 *	];
  240: 	 *   cfg.toolbar.push(["linebreak", "my-hilite"]); // add the new button to the toolbar
  241: 	 *
  242: 	 * An alternate (also more convenient and recommended) way to
  243: 	 * accomplish this is to use the registerButton function below.
  244: 	 */
  245: 	// initialize tooltips from the I18N module and generate correct image path
  246: 	for (var i in this.btnList) {
  247: 		var btn = this.btnList[i];
  248: 		btn[1] = _editor_url + this.imgURL + btn[1];
  249: 		if (typeof HTMLArea.I18N.tooltips[i] != "undefined") {
  250: 			btn[0] = HTMLArea.I18N.tooltips[i];
  251: 		}
  252: 	}
  253: };
  254: 
  255: /** Helper function: register a new button with the configuration.  It can be
  256:  * called with all 5 arguments, or with only one (first one).  When called with
  257:  * only one argument it must be an object with the following properties: id,
  258:  * tooltip, image, textMode, action.  Examples:
  259:  *
  260:  * 1. config.registerButton("my-hilite", "Hilite text", "my-hilite.gif", false, function(editor) {...});
  261:  * 2. config.registerButton({
  262:  *      id       : "my-hilite",      // the ID of your button
  263:  *      tooltip  : "Hilite text",    // the tooltip
  264:  *      image    : "my-hilite.gif",  // image to be displayed in the toolbar
  265:  *      textMode : false,            // disabled in text mode
  266:  *      action   : function(editor) { // called when the button is clicked
  267:  *                   editor.surroundHTML('<span class="hilite">', '</span>');
  268:  *                 },
  269:  *      context  : "p"               // will be disabled if outside a <p> element
  270:  *    });
  271:  */
  272: HTMLArea.Config.prototype.registerButton = function(id, tooltip, image, textMode, action, context) {
  273: 	var the_id;
  274: 	if (typeof id == "string") {
  275: 		the_id = id;
  276: 	} else if (typeof id == "object") {
  277: 		the_id = id.id;
  278: 	} else {
  279: 		alert("ERROR [HTMLArea.Config::registerButton]:\ninvalid arguments");
  280: 		return false;
  281: 	}
  282: 	// check for existing id
  283: 	if (typeof this.customSelects[the_id] != "undefined") {
  284: 		// alert("WARNING [HTMLArea.Config::registerDropdown]:\nA dropdown with the same ID already exists.");
  285: 	}
  286: 	if (typeof this.btnList[the_id] != "undefined") {
  287: 		// alert("WARNING [HTMLArea.Config::registerDropdown]:\nA button with the same ID already exists.");
  288: 	}
  289: 	switch (typeof id) {
  290: 	    case "string": this.btnList[id] = [ tooltip, image, textMode, action, context ]; break;
  291: 	    case "object": this.btnList[id.id] = [ id.tooltip, id.image, id.textMode, id.action, id.context ]; break;
  292: 	}
  293: };
  294: 
  295: /** The following helper function registers a dropdown box with the editor
  296:  * configuration.  You still have to add it to the toolbar, same as with the
  297:  * buttons.  Call it like this:
  298:  *
  299:  * FIXME: add example
  300:  */
  301: HTMLArea.Config.prototype.registerDropdown = function(object) {
  302: 	// check for existing id
  303: 	if (typeof this.customSelects[object.id] != "undefined") {
  304: 		// alert("WARNING [HTMLArea.Config::registerDropdown]:\nA dropdown with the same ID already exists.");
  305: 	}
  306: 	if (typeof this.btnList[object.id] != "undefined") {
  307: 		// alert("WARNING [HTMLArea.Config::registerDropdown]:\nA button with the same ID already exists.");
  308: 	}
  309: 	this.customSelects[object.id] = object;
  310: };
  311: 
  312: /** Call this function to remove some buttons/drop-down boxes from the toolbar.
  313:  * Pass as the only parameter a string containing button/drop-down names
  314:  * delimited by spaces.  Note that the string should also begin with a space
  315:  * and end with a space.  Example:
  316:  *
  317:  *   config.hideSomeButtons(" fontname fontsize textindicator ");
  318:  *
  319:  * It's useful because it's easier to remove stuff from the defaul toolbar than
  320:  * create a brand new toolbar ;-)
  321:  */
  322: HTMLArea.Config.prototype.hideSomeButtons = function(remove) {
  323: 	var toolbar = this.toolbar;
  324: 	for (var i in toolbar) {
  325: 		var line = toolbar[i];
  326: 		for (var j = line.length; --j >= 0; ) {
  327: 			if (remove.indexOf(" " + line[j] + " ") >= 0) {
  328: 				var len = 1;
  329: 				if (/separator|space/.test(line[j + 1])) {
  330: 					len = 2;
  331: 				}
  332: 				line.splice(j, len);
  333: 			}
  334: 		}
  335: 	}
  336: };
  337: 
  338: /** Helper function: replace all TEXTAREA-s in the document with HTMLArea-s. */
  339: HTMLArea.replaceAll = function(config) {
  340: 	var tas = document.getElementsByTagName("textarea");
  341: 	for (var i = tas.length; i > 0; (new HTMLArea(tas[--i], config)).generate());
  342: };
  343: 
  344: /** Helper function: replaces the TEXTAREA with the given ID with HTMLArea. */
  345: HTMLArea.replace = function(id, config) {
  346: 	var ta = HTMLArea.getElementById("textarea", id);
  347: 	return ta ? (new HTMLArea(ta, config)).generate() : null;
  348: };
  349: 
  350: // Creates the toolbar and appends it to the _htmlarea
  351: HTMLArea.prototype._createToolbar = function () {
  352: 	var editor = this;	// to access this in nested functions
  353: 
  354: 	var toolbar = document.createElement("div");
  355: 	this._toolbar = toolbar;
  356: 	toolbar.className = "toolbar";
  357: 	toolbar.unselectable = "1";
  358: 	var tb_row = null;
  359: 	var tb_objects = new Object();
  360: 	this._toolbarObjects = tb_objects;
  361: 
  362: 	// creates a new line in the toolbar
  363: 	function newLine() {
  364: 		var table = document.createElement("table");
  365: 		table.border = "0px";
  366: 		table.cellSpacing = "0px";
  367: 		table.cellPadding = "0px";
  368: 		toolbar.appendChild(table);
  369: 		// TBODY is required for IE, otherwise you don't see anything
  370: 		// in the TABLE.
  371: 		var tb_body = document.createElement("tbody");
  372: 		table.appendChild(tb_body);
  373: 		tb_row = document.createElement("tr");
  374: 		tb_body.appendChild(tb_row);
  375: 	}; // END of function: newLine
  376: 	// init first line
  377: 	newLine();
  378: 
  379: 	// updates the state of a toolbar element.  This function is member of
  380: 	// a toolbar element object (unnamed objects created by createButton or
  381: 	// createSelect functions below).
  382: 	function setButtonStatus(id, newval) {
  383: 		var oldval = this[id];
  384: 		var el = this.element;
  385: 		if (oldval != newval) {
  386: 			switch (id) {
  387: 			    case "enabled":
  388: 				if (newval) {
  389: 					HTMLArea._removeClass(el, "buttonDisabled");
  390: 					el.disabled = false;
  391: 				} else {
  392: 					HTMLArea._addClass(el, "buttonDisabled");
  393: 					el.disabled = true;
  394: 				}
  395: 				break;
  396: 			    case "active":
  397: 				if (newval) {
  398: 					HTMLArea._addClass(el, "buttonPressed");
  399: 				} else {
  400: 					HTMLArea._removeClass(el, "buttonPressed");
  401: 				}
  402: 				break;
  403: 			}
  404: 			this[id] = newval;
  405: 		}
  406: 	}; // END of function: setButtonStatus
  407: 
  408: 	// this function will handle creation of combo boxes.  Receives as
  409: 	// parameter the name of a button as defined in the toolBar config.
  410: 	// This function is called from createButton, above, if the given "txt"
  411: 	// doesn't match a button.
  412: 	function createSelect(txt) {
  413: 		var options = null;
  414: 		var el = null;
  415: 		var cmd = null;
  416: 		var customSelects = editor.config.customSelects;
  417: 		var context = null;
  418: 		switch (txt) {
  419: 		    case "fontsize":
  420: 		    case "fontname":
  421: 		    case "formatblock":
  422: 			// the following line retrieves the correct
  423: 			// configuration option because the variable name
  424: 			// inside the Config object is named the same as the
  425: 			// button/select in the toolbar.  For instance, if txt
  426: 			// == "formatblock" we retrieve config.formatblock (or
  427: 			// a different way to write it in JS is
  428: 			// config["formatblock"].
  429: 			options = editor.config[txt];
  430: 			cmd = txt;
  431: 			break;
  432: 		    default:
  433: 			// try to fetch it from the list of registered selects
  434: 			cmd = txt;
  435: 			var dropdown = customSelects[cmd];
  436: 			if (typeof dropdown != "undefined") {
  437: 				options = dropdown.options;
  438: 				context = dropdown.context;
  439: 			} else {
  440: 				alert("ERROR [createSelect]:\nCan't find the requested dropdown definition");
  441: 			}
  442: 			break;
  443: 		}
  444: 		if (options) {
  445: 			el = document.createElement("select");
  446: 			var obj = {
  447: 				name	: txt, // field name
  448: 				element : el,	// the UI element (SELECT)
  449: 				enabled : true, // is it enabled?
  450: 				text	: false, // enabled in text mode?
  451: 				cmd	: cmd, // command ID
  452: 				state	: setButtonStatus, // for changing state
  453: 				context : context
  454: 			};
  455: 			tb_objects[txt] = obj;
  456: 			for (var i in options) {
  457: 				var op = document.createElement("option");
  458: 				op.appendChild(document.createTextNode(i));
  459: 				op.value = options[i];
  460: 				el.appendChild(op);
  461: 			}
  462: 			HTMLArea._addEvent(el, "change", function () {
  463: 				editor._comboSelected(el, txt);
  464: 			});
  465: 		}
  466: 		return el;
  467: 	}; // END of function: createSelect
  468: 
  469: 	// appends a new button to toolbar
  470: 	function createButton(txt) {
  471: 		// the element that will be created
  472: 		var el = null;
  473: 		var btn = null;
  474: 		switch (txt) {
  475: 		    case "separator":
  476: 			el = document.createElement("div");
  477: 			el.className = "separator";
  478: 			break;
  479: 		    case "space":
  480: 			el = document.createElement("div");
  481: 			el.className = "space";
  482: 			break;
  483: 		    case "linebreak":
  484: 			newLine();
  485: 			return false;
  486: 		    case "textindicator":
  487: 			el = document.createElement("div");
  488: 			el.appendChild(document.createTextNode("A"));
  489: 			el.className = "indicator";
  490: 			el.title = HTMLArea.I18N.tooltips.textindicator;
  491: 			var obj = {
  492: 				name	: txt, // the button name (i.e. 'bold')
  493: 				element : el, // the UI element (DIV)
  494: 				enabled : true, // is it enabled?
  495: 				active	: false, // is it pressed?
  496: 				text	: false, // enabled in text mode?
  497: 				cmd	: "textindicator", // the command ID
  498: 				state	: setButtonStatus // for changing state
  499: 			};
  500: 			tb_objects[txt] = obj;
  501: 			break;
  502: 		    default:
  503: 			btn = editor.config.btnList[txt];
  504: 		}
  505: 		if (!el && btn) {
  506: 			el = document.createElement("div");
  507: 			el.title = btn[0];
  508: 			el.className = "button";
  509: 			// let's just pretend we have a button object, and
  510: 			// assign all the needed information to it.
  511: 			var obj = {
  512: 				name	: txt, // the button name (i.e. 'bold')
  513: 				element : el, // the UI element (DIV)
  514: 				enabled : true, // is it enabled?
  515: 				active	: false, // is it pressed?
  516: 				text	: btn[2], // enabled in text mode?
  517: 				cmd	: btn[3], // the command ID
  518: 				state	: setButtonStatus, // for changing state
  519: 				context : btn[4] || null // enabled in a certain context?
  520: 			};
  521: 			tb_objects[txt] = obj;
  522: 			// handlers to emulate nice flat toolbar buttons
  523: 			HTMLArea._addEvent(el, "mouseover", function () {
  524: 				if (obj.enabled) {
  525: 					HTMLArea._addClass(el, "buttonHover");
  526: 				}
  527: 			});
  528: 			HTMLArea._addEvent(el, "mouseout", function () {
  529: 				if (obj.enabled) with (HTMLArea) {
  530: 					_removeClass(el, "buttonHover");
  531: 					_removeClass(el, "buttonActive");
  532: 					(obj.active) && _addClass(el, "buttonPressed");
  533: 				}
  534: 			});
  535: 			HTMLArea._addEvent(el, "mousedown", function (ev) {
  536: 				if (obj.enabled) with (HTMLArea) {
  537: 					_addClass(el, "buttonActive");
  538: 					_removeClass(el, "buttonPressed");
  539: 					_stopEvent(is_ie ? window.event : ev);
  540: 				}
  541: 			});
  542: 			// when clicked, do the following:
  543: 			HTMLArea._addEvent(el, "click", function (ev) {
  544: 				if (obj.enabled) with (HTMLArea) {
  545: 					_removeClass(el, "buttonActive");
  546: 					_removeClass(el, "buttonHover");
  547: 					obj.cmd(editor, obj.name, obj);
  548: 					_stopEvent(is_ie ? window.event : ev);
  549: 				}
  550: 			});
  551: 			var img = document.createElement("img");
  552: 			img.src = btn[1];
  553: 			img.style.width = "18px";
  554: 			img.style.height = "18px";
  555: 			el.appendChild(img);
  556: 		} else if (!el) {
  557: 			el = createSelect(txt);
  558: 		}
  559: 		if (el) {
  560: 			var tb_cell = document.createElement("td");
  561: 			tb_row.appendChild(tb_cell);
  562: 			tb_cell.appendChild(el);
  563: 		} else {
  564: 			alert("FIXME: Unknown toolbar item: " + txt);
  565: 		}
  566: 		return el;
  567: 	};
  568: 
  569: 	var first = true;
  570: 	for (var i in this.config.toolbar) {
  571: 		if (!first) {
  572: 			createButton("linebreak");
  573: 		} else {
  574: 			first = false;
  575: 		}
  576: 		var group = this.config.toolbar[i];
  577: 		for (var j in group) {
  578: 			var code = group[j];
  579: 			if (/^([IT])\[(.*?)\]/.test(code)) {
  580: 				// special case, create text label
  581: 				var l7ed = RegExp.$1 == "I"; // localized?
  582: 				var label = RegExp.$2;
  583: 				if (l7ed) {
  584: 					label = HTMLArea.I18N.custom[label];
  585: 				}
  586: 				var tb_cell = document.createElement("td");
  587: 				tb_row.appendChild(tb_cell);
  588: 				tb_cell.className = "label";
  589: 				tb_cell.innerHTML = label;
  590: 			} else {
  591: 				createButton(code);
  592: 			}
  593: 		}
  594: 	}
  595: 
  596: 	this._htmlArea.appendChild(toolbar);
  597: };
  598: 
  599: HTMLArea.prototype._createStatusBar = function() {
  600: 	var statusbar = document.createElement("div");
  601: 	statusbar.className = "statusBar";
  602: 	this._htmlArea.appendChild(statusbar);
  603: 	this._statusBar = statusbar;
  604: 	// statusbar.appendChild(document.createTextNode(HTMLArea.I18N.msg["Path"] + ": "));
  605: 	// creates a holder for the path view
  606: 	div = document.createElement("span");
  607: 	div.className = "statusBarTree";
  608: 	div.innerHTML = HTMLArea.I18N.msg["Path"] + ": ";
  609: 	this._statusBarTree = div;
  610: 	this._statusBar.appendChild(div);
  611: 	if (!this.config.statusBar) {
  612: 		// disable it...
  613: 		statusbar.style.display = "none";
  614: 	}
  615: };
  616: 
  617: // Creates the HTMLArea object and replaces the textarea with it.
  618: HTMLArea.prototype.generate = function () {
  619: 	var editor = this;	// we'll need "this" in some nested functions
  620: 	// get the textarea
  621: 	var textarea = this._textArea;
  622: 	if (typeof textarea == "string") {
  623: 		// it's not element but ID
  624: 		this._textArea = textarea = HTMLArea.getElementById("textarea", textarea);
  625: 	}
  626: 	this._ta_size = {
  627: 		w: textarea.offsetWidth,
  628: 		h: textarea.offsetHeight
  629: 	};
  630: 	textarea.style.display = "none";
  631: 
  632: 	// create the editor framework
  633: 	var htmlarea = document.createElement("div");
  634: 	htmlarea.className = "htmlarea";
  635: 	this._htmlArea = htmlarea;
  636: 
  637: 	// insert the editor before the textarea.
  638: 	textarea.parentNode.insertBefore(htmlarea, textarea);
  639: 
  640: 	if (textarea.form) {
  641: 		// we have a form, on submit get the HTMLArea content and
  642: 		// update original textarea.
  643: 		var f = textarea.form;
  644: 		if (typeof f.onsubmit == "function") {
  645: 			var funcref = f.onsubmit;
  646: 			if (typeof f.__msh_prevOnSubmit == "undefined") {
  647: 				f.__msh_prevOnSubmit = [];
  648: 			}
  649: 			f.__msh_prevOnSubmit.push(funcref);
  650: 		}
  651: 		f.onsubmit = function() {
  652: 			editor._textArea.value = editor.getHTML();
  653: 			var a = this.__msh_prevOnSubmit;
  654: 			// call previous submit methods if they were there.
  655: 			if (typeof a != "undefined") {
  656: 				for (var i in a) {
  657: 					a[i]();
  658: 				}
  659: 			}
  660: 		};
  661: 	}
  662: 
  663: 	// add a handler for the "back/forward" case -- on body.unload we save
  664: 	// the HTML content into the original textarea.
  665: 	window.onunload = function() {
  666: 		editor._textArea.value = editor.getHTML();
  667: 	};
  668: 
  669: 	// creates & appends the toolbar
  670: 	this._createToolbar();
  671: 
  672: 	// create the IFRAME
  673: 	var iframe = document.createElement("iframe");
  674: 	htmlarea.appendChild(iframe);
  675: 
  676: 	this._iframe = iframe;
  677: 
  678: 	// creates & appends the status bar, if the case
  679: 	this._createStatusBar();
  680: 
  681: 	// remove the default border as it keeps us from computing correctly
  682: 	// the sizes.  (somebody tell me why doesn't this work in IE)
  683: 
  684: 	if (!HTMLArea.is_ie) {
  685: 		iframe.style.borderWidth = "1px";
  686: 	// iframe.frameBorder = "1";
  687: 	// iframe.marginHeight = "0";
  688: 	// iframe.marginWidth = "0";
  689: 	}
  690: 
  691: 	// size the IFRAME according to user's prefs or initial textarea
  692: 	var height = (this.config.height == "auto" ? (this._ta_size.h + "px") : this.config.height);
  693: 	height = parseInt(height);
  694: 	var width = (this.config.width == "auto" ? (this._ta_size.w + "px") : this.config.width);
  695: 	width = parseInt(width);
  696: 
  697: 	if (!HTMLArea.is_ie) {
  698: 		height -= 2;
  699: 		width -= 2;
  700: 	}
  701: 
  702: 	iframe.style.width = width + "px";
  703: 	if (this.config.sizeIncludesToolbar) {
  704: 		// substract toolbar height
  705: 		height -= this._toolbar.offsetHeight;
  706: 		height -= this._statusBar.offsetHeight;
  707: 	}
  708: 	if (height < 0) {
  709: 		height = 0;
  710: 	}
  711: 	iframe.style.height = height + "px";
  712: 
  713: 	// the editor including the toolbar now have the same size as the
  714: 	// original textarea.. which means that we need to reduce that a bit.
  715: 	textarea.style.width = iframe.style.width;
  716:  	textarea.style.height = iframe.style.height;
  717: 
  718: 	// IMPORTANT: we have to allow Mozilla a short time to recognize the
  719: 	// new frame.  Otherwise we get a stupid exception.
  720: 	function initIframe() {
  721: 		var doc = editor._iframe.contentWindow.document;
  722: 		if (!doc) {
  723: 			// Try again..
  724: 			// FIXME: don't know what else to do here.  Normally
  725: 			// we'll never reach this point.
  726: 			if (HTMLArea.is_gecko) {
  727: 				setTimeout(initIframe, 100);
  728: 				return false;
  729: 			} else {
  730: 				alert("ERROR: IFRAME can't be initialized.");
  731: 			}
  732: 		}
  733: 		if (HTMLArea.is_gecko) {
  734: 			// enable editable mode for Mozilla
  735: 			doc.designMode = "on";
  736: 		}
  737: 		editor._doc = doc;
  738: 		if (!editor.config.fullPage) {
  739: 			doc.open();
  740: 			var html = "<html>\n";
  741: 			html += "<head>\n";
  742: 			if (editor.config.baseURL)
  743: 				html += '<base href="' + editor.config.baseURL + '" />';
  744: 			html += "<style> html,body { border: 0px; } " +
  745: 				editor.config.pageStyle + "</style>\n";
  746: 			html += "</head>\n";
  747: 			html += "<body>\n";
  748: 			html += editor._textArea.value;
  749: 			html += "</body>\n";
  750: 			html += "</html>";
  751: 			doc.write(html);
  752: 			doc.close();
  753: 		} else {
  754: 			var html = editor._textArea.value;
  755: 			if (html.match(HTMLArea.RE_doctype)) {
  756: 				editor.setDoctype(RegExp.$1);
  757: 				html = html.replace(HTMLArea.RE_doctype, "");
  758: 			}
  759: 			doc.open();
  760: 			doc.write(html);
  761: 			doc.close();
  762: 		}
  763: 
  764: 		if (HTMLArea.is_ie) {
  765: 			// enable editable mode for IE.	 For some reason this
  766: 			// doesn't work if done in the same place as for Gecko
  767: 			// (above).
  768: 			doc.body.contentEditable = true;
  769: 		}
  770: 
  771: 		editor.focusEditor();
  772: 		// intercept some events; for updating the toolbar & keyboard handlers
  773: 		HTMLArea._addEvents
  774: 			(doc, ["keydown", "keypress", "mousedown", "mouseup", "drag"],
  775: 			 function (event) {
  776: 				 return editor._editorEvent(HTMLArea.is_ie ? editor._iframe.contentWindow.event : event);
  777: 			 });
  778: 
  779: 		// check if any plugins have registered refresh handlers
  780: 		for (var i in editor.plugins) {
  781: 			var plugin = editor.plugins[i].instance;
  782: 			if (typeof plugin.onGenerate == "function")
  783: 				plugin.onGenerate();
  784: 		}
  785: 
  786: 		setTimeout(function() {
  787: 			editor.updateToolbar();
  788: 		}, 250);
  789: 
  790: 		if (typeof editor.onGenerate == "function")
  791: 			editor.onGenerate();
  792: 	};
  793: 	setTimeout(initIframe, 100);
  794: };
  795: 
  796: // Switches editor mode; parameter can be "textmode" or "wysiwyg".  If no
  797: // parameter was passed this function toggles between modes.
  798: HTMLArea.prototype.setMode = function(mode) {
  799: 	if (typeof mode == "undefined") {
  800: 		mode = ((this._editMode == "textmode") ? "wysiwyg" : "textmode");
  801: 	}
  802: 	switch (mode) {
  803: 	    case "textmode":
  804: 		this._textArea.value = this.getHTML();
  805: 		this._iframe.style.display = "none";
  806: 		this._textArea.style.display = "block";
  807: 		if (this.config.statusBar) {
  808: 			this._statusBar.innerHTML = HTMLArea.I18N.msg["TEXT_MODE"];
  809: 		}
  810: 		break;
  811: 	    case "wysiwyg":
  812: 		if (HTMLArea.is_gecko) {
  813: 			// disable design mode before changing innerHTML
  814: 			try {
  815: 				this._doc.designMode = "off";
  816: 			} catch(e) {};
  817: 		}
  818: 		if (!this.config.fullPage)
  819: 			this._doc.body.innerHTML = this.getHTML();
  820: 		else
  821: 			this.setFullHTML(this.getHTML());
  822: 		this._iframe.style.display = "block";
  823: 		this._textArea.style.display = "none";
  824: 		if (HTMLArea.is_gecko) {
  825: 			// we need to refresh that info for Moz-1.3a
  826: 			try {
  827: 				this._doc.designMode = "on";
  828: 			} catch(e) {};
  829: 		}
  830: 		if (this.config.statusBar) {
  831: 			this._statusBar.innerHTML = '';
  832: 			this._statusBar.appendChild(document.createTextNode(HTMLArea.I18N.msg["Path"] + ": "));
  833: 			this._statusBar.appendChild(this._statusBarTree);
  834: 		}
  835: 		break;
  836: 	    default:
  837: 		alert("Mode <" + mode + "> not defined!");
  838: 		return false;
  839: 	}
  840: 	this._editMode = mode;
  841: 	this.focusEditor();
  842: };
  843: 
  844: HTMLArea.prototype.setFullHTML = function(html) {
  845: 	var save_multiline = RegExp.multiline;
  846: 	RegExp.multiline = true;
  847: 	if (html.match(HTMLArea.RE_doctype)) {
  848: 		this.setDoctype(RegExp.$1);
  849: 		html = html.replace(HTMLArea.RE_doctype, "");
  850: 	}
  851: 	RegExp.multiline = save_multiline;
  852: 	if (!HTMLArea.is_ie) {
  853: 		if (html.match(HTMLArea.RE_head))
  854: 			this._doc.getElementsByTagName("head")[0].innerHTML = RegExp.$1;
  855: 		if (html.match(HTMLArea.RE_body))
  856: 			this._doc.getElementsByTagName("body")[0].innerHTML = RegExp.$1;
  857: 	} else {
  858: 		var html_re = /<html>((.|\n)*?)<\/html>/i;
  859: 		html = html.replace(html_re, "$1");
  860: 		this._doc.open();
  861: 		this._doc.write(html);
  862: 		this._doc.close();
  863: 		this._doc.body.contentEditable = true;
  864: 		return true;
  865: 	}
  866: };
  867: 
  868: /***************************************************
  869:  *  Category: PLUGINS
  870:  ***************************************************/
  871: 
  872: // this is the variant of the function above where the plugin arguments are
  873: // already packed in an array.  Externally, it should be only used in the
  874: // full-screen editor code, in order to initialize plugins with the same
  875: // parameters as in the opener window.
  876: HTMLArea.prototype.registerPlugin2 = function(plugin, args) {
  877: 	if (typeof plugin == "string")
  878: 		plugin = eval(plugin);
  879: 	var obj = new plugin(this, args);
  880: 	if (obj) {
  881: 		var clone = {};
  882: 		var info = plugin._pluginInfo;
  883: 		for (var i in info)
  884: 			clone[i] = info[i];
  885: 		clone.instance = obj;
  886: 		clone.args = args;
  887: 		this.plugins[plugin._pluginInfo.name] = clone;
  888: 	} else
  889: 		alert("Can't register plugin " + plugin.toString() + ".");
  890: };
  891: 
  892: // Create the specified plugin and register it with this HTMLArea
  893: HTMLArea.prototype.registerPlugin = function() {
  894: 	var plugin = arguments[0];
  895: 	var args = [];
  896: 	for (var i = 1; i < arguments.length; ++i)
  897: 		args.push(arguments[i]);
  898: 	this.registerPlugin2(plugin, args);
  899: };
  900: 
  901: // static function that loads the required plugin and lang file, based on the
  902: // language loaded already for HTMLArea.  You better make sure that the plugin
  903: // _has_ that language, otherwise shit might happen ;-)
  904: HTMLArea.loadPlugin = function(pluginName) {
  905: 	var dir = _editor_url + "plugins/" + pluginName;
  906: 	var plugin = pluginName.replace(/([a-z])([A-Z])([a-z])/g,
  907: 					function (str, l1, l2, l3) {
  908: 						return l1 + "-" + l2.toLowerCase() + l3;
  909: 					}).toLowerCase() + ".js";
  910: 	var plugin_file = dir + "/" + plugin;
  911: 	var plugin_lang = dir + "/lang/" + HTMLArea.I18N.lang + ".js";
  912: 	HTMLArea._scripts.push(plugin_file, plugin_lang);
  913: 	document.write("<script type='text/javascript' src='" + plugin_file + "'></script>");
  914: 	document.write("<script type='text/javascript' src='" + plugin_lang + "'></script>");
  915: };
  916: 
  917: HTMLArea.loadStyle = function(style, plugin) {
  918: 	var url = _editor_url || '';
  919: 	if (typeof plugin != "undefined") {
  920: 		url += "plugins/" + plugin + "/";
  921: 	}
  922: 	url += style;
  923: 	document.write("<style type='text/css'>@import url(" + url + ");</style>");
  924: };
  925: HTMLArea.loadStyle("htmlarea.css");
  926: 
  927: /***************************************************
  928:  *  Category: EDITOR UTILITIES
  929:  ***************************************************/
  930: 
  931: // The following function is a slight variation of the word cleaner code posted
  932: // by Weeezl (user @ InteractiveTools forums).
  933: HTMLArea.prototype._wordClean = function() {
  934: 	var D = this.getInnerHTML();
  935: 	if (D.indexOf('class=Mso') >= 0) {
  936: 
  937: 		// make one line
  938: 		D = D.replace(/\r\n/g, ' ').
  939: 			replace(/\n/g, ' ').
  940: 			replace(/\r/g, ' ').
  941: 			replace(/\&nbsp\;/g,' ');
  942: 
  943: 		// keep tags, strip attributes
  944: 		D = D.replace(/ class=[^\s|>]*/gi,'').
  945: 			//replace(/<p [^>]*TEXT-ALIGN: justify[^>]*>/gi,'<p align="justify">').
  946: 			replace(/ style=\"[^>]*\"/gi,'').
  947: 			replace(/ align=[^\s|>]*/gi,'');
  948: 
  949: 		//clean up tags
  950: 		D = D.replace(/<b [^>]*>/gi,'<b>').
  951: 			replace(/<i [^>]*>/gi,'<i>').
  952: 			replace(/<li [^>]*>/gi,'<li>').
  953: 			replace(/<ul [^>]*>/gi,'<ul>');
  954: 
  955: 		// replace outdated tags
  956: 		D = D.replace(/<b>/gi,'<strong>').
  957: 			replace(/<\/b>/gi,'</strong>');
  958: 
  959: 		// mozilla doesn't like <em> tags
  960: 		D = D.replace(/<em>/gi,'<i>').
  961: 			replace(/<\/em>/gi,'</i>');
  962: 
  963: 		// kill unwanted tags
  964: 		D = D.replace(/<\?xml:[^>]*>/g, '').       // Word xml
  965: 			replace(/<\/?st1:[^>]*>/g,'').     // Word SmartTags
  966: 			replace(/<\/?[a-z]\:[^>]*>/g,'').  // All other funny Word non-HTML stuff
  967: 			replace(/<\/?font[^>]*>/gi,'').    // Disable if you want to keep font formatting
  968: 			replace(/<\/?span[^>]*>/gi,' ').
  969: 			replace(/<\/?div[^>]*>/gi,' ').
  970: 			replace(/<\/?pre[^>]*>/gi,' ').
  971: 			replace(/<\/?h[1-6][^>]*>/gi,' ');
  972: 
  973: 		//remove empty tags
  974: 		//D = D.replace(/<strong><\/strong>/gi,'').
  975: 		//replace(/<i><\/i>/gi,'').
  976: 		//replace(/<P[^>]*><\/P>/gi,'');
  977: 
  978: 		// nuke double tags
  979: 		oldlen = D.length + 1;
  980: 		while(oldlen > D.length) {
  981: 			oldlen = D.length;
  982: 			// join us now and free the tags, we'll be free hackers, we'll be free... ;-)
  983: 			D = D.replace(/<([a-z][a-z]*)> *<\/\1>/gi,' ').
  984: 				replace(/<([a-z][a-z]*)> *<([a-z][^>]*)> *<\/\1>/gi,'<$2>');
  985: 		}
  986: 		D = D.replace(/<([a-z][a-z]*)><\1>/gi,'<$1>').
  987: 			replace(/<\/([a-z][a-z]*)><\/\1>/gi,'<\/$1>');
  988: 
  989: 		// nuke double spaces
  990: 		D = D.replace(/  */gi,' ');
  991: 
  992: 		this.setHTML(D);
  993: 		this.updateToolbar();
  994: 	}
  995: };
  996: 
  997: HTMLArea.prototype.forceRedraw = function() {
  998: 	this._doc.body.style.visibility = "hidden";
  999: 	this._doc.body.style.visibility = "visible";
 1000: 	// this._doc.body.innerHTML = this.getInnerHTML();
 1001: };
 1002: 
 1003: // focuses the iframe window.  returns a reference to the editor document.
 1004: HTMLArea.prototype.focusEditor = function() {
 1005: 	switch (this._editMode) {
 1006: 	    case "wysiwyg" : this._iframe.contentWindow.focus(); break;
 1007: 	    case "textmode": this._textArea.focus(); break;
 1008: 	    default	   : alert("ERROR: mode " + this._editMode + " is not defined");
 1009: 	}
 1010: 	return this._doc;
 1011: };
 1012: 
 1013: // takes a snapshot of the current text (for undo)
 1014: HTMLArea.prototype._undoTakeSnapshot = function() {
 1015: 	++this._undoPos;
 1016: 	if (this._undoPos >= this.config.undoSteps) {
 1017: 		// remove the first element
 1018: 		this._undoQueue.shift();
 1019: 		--this._undoPos;
 1020: 	}
 1021: 	// use the fasted method (getInnerHTML);
 1022: 	var take = true;
 1023: 	var txt = this.getInnerHTML();
 1024: 	if (this._undoPos > 0)
 1025: 		take = (this._undoQueue[this._undoPos - 1] != txt);
 1026: 	if (take) {
 1027: 		this._undoQueue[this._undoPos] = txt;
 1028: 	} else {
 1029: 		this._undoPos--;
 1030: 	}
 1031: };
 1032: 
 1033: HTMLArea.prototype.undo = function() {
 1034: 	if (this._undoPos > 0) {
 1035: 		var txt = this._undoQueue[--this._undoPos];
 1036: 		if (txt) this.setHTML(txt);
 1037: 		else ++this._undoPos;
 1038: 	}
 1039: };
 1040: 
 1041: HTMLArea.prototype.redo = function() {
 1042: 	if (this._undoPos < this._undoQueue.length - 1) {
 1043: 		var txt = this._undoQueue[++this._undoPos];
 1044: 		if (txt) this.setHTML(txt);
 1045: 		else --this._undoPos;
 1046: 	}
 1047: };
 1048: 
 1049: // updates enabled/disable/active state of the toolbar elements
 1050: HTMLArea.prototype.updateToolbar = function(noStatus) {
 1051: 	var doc = this._doc;
 1052: 	var text = (this._editMode == "textmode");
 1053: 	var ancestors = null;
 1054: 	if (!text) {
 1055: 		ancestors = this.getAllAncestors();
 1056: 		if (this.config.statusBar && !noStatus) {
 1057: 			this._statusBarTree.innerHTML = HTMLArea.I18N.msg["Path"] + ": "; // clear
 1058: 			for (var i = ancestors.length; --i >= 0;) {
 1059: 				var el = ancestors[i];
 1060: 				if (!el) {
 1061: 					// hell knows why we get here; this
 1062: 					// could be a classic example of why
 1063: 					// it's good to check for conditions
 1064: 					// that are impossible to happen ;-)
 1065: 					continue;
 1066: 				}
 1067: 				var a = document.createElement("a");
 1068: 				a.href = "#";
 1069: 				a.el = el;
 1070: 				a.editor = this;
 1071: 				a.onclick = function() {
 1072: 					this.blur();
 1073: 					this.editor.selectNodeContents(this.el);
 1074: 					this.editor.updateToolbar(true);
 1075: 					return false;
 1076: 				};
 1077: 				a.oncontextmenu = function() {
 1078: 					// TODO: add context menu here
 1079: 					this.blur();
 1080: 					var info = "Inline style:\n\n";
 1081: 					info += this.el.style.cssText.split(/;\s*/).join(";\n");
 1082: 					alert(info);
 1083: 					return false;
 1084: 				};
 1085: 				var txt = el.tagName.toLowerCase();
 1086: 				a.title = el.style.cssText;
 1087: 				if (el.id) {
 1088: 					txt += "#" + el.id;
 1089: 				}
 1090: 				if (el.className) {
 1091: 					txt += "." + el.className;
 1092: 				}
 1093: 				a.appendChild(document.createTextNode(txt));
 1094: 				this._statusBarTree.appendChild(a);
 1095: 				if (i != 0) {
 1096: 					this._statusBarTree.appendChild(document.createTextNode(String.fromCharCode(0xbb)));
 1097: 				}
 1098: 			}
 1099: 		}
 1100: 	}
 1101: 	for (var i in this._toolbarObjects) {
 1102: 		var btn = this._toolbarObjects[i];
 1103: 		var cmd = i;
 1104: 		var inContext = true;
 1105: 		if (btn.context && !text) {
 1106: 			inContext = false;
 1107: 			var context = btn.context;
 1108: 			var attrs = [];
 1109: 			if (/(.*)\[(.*?)\]/.test(context)) {
 1110: 				context = RegExp.$1;
 1111: 				attrs = RegExp.$2.split(",");
 1112: 			}
 1113: 			context = context.toLowerCase();
 1114: 			var match = (context == "*");
 1115: 			for (var k in ancestors) {
 1116: 				if (!ancestors[k]) {
 1117: 					// the impossible really happens.
 1118: 					continue;
 1119: 				}
 1120: 				if (match || (ancestors[k].tagName.toLowerCase() == context)) {
 1121: 					inContext = true;
 1122: 					for (var ka in attrs) {
 1123: 						if (!eval("ancestors[k]." + attrs[ka])) {
 1124: 							inContext = false;
 1125: 							break;
 1126: 						}
 1127: 					}
 1128: 					if (inContext) {
 1129: 						break;
 1130: 					}
 1131: 				}
 1132: 			}
 1133: 		}
 1134: 		btn.state("enabled", (!text || btn.text) && inContext);
 1135: 		if (typeof cmd == "function") {
 1136: 			continue;
 1137: 		}
 1138: 		// look-it-up in the custom dropdown boxes
 1139: 		var dropdown = this.config.customSelects[cmd];
 1140: 		if ((!text || btn.text) && (typeof dropdown != "undefined")) {
 1141: 			dropdown.refresh(this);
 1142: 			continue;
 1143: 		}
 1144: 		switch (cmd) {
 1145: 		    case "fontname":
 1146: 		    case "fontsize":
 1147: 		    case "formatblock":
 1148: 			if (!text) try {
 1149: 				var value = ("" + doc.queryCommandValue(cmd)).toLowerCase();
 1150: 				if (!value) {
 1151: 					// FIXME: what do we do here?
 1152: 					break;
 1153: 				}
 1154: 				// HACK -- retrieve the config option for this
 1155: 				// combo box.  We rely on the fact that the
 1156: 				// variable in config has the same name as
 1157: 				// button name in the toolbar.
 1158: 				var options = this.config[cmd];
 1159: 				var k = 0;
 1160: 				// btn.element.selectedIndex = 0;
 1161: 				for (var j in options) {
 1162: 					// FIXME: the following line is scary.
 1163: 					if ((j.toLowerCase() == value) ||
 1164: 					    (options[j].substr(0, value.length).toLowerCase() == value)) {
 1165: 						btn.element.selectedIndex = k;
 1166: 						break;
 1167: 					}
 1168: 					++k;
 1169: 				}
 1170: 			} catch(e) {};
 1171: 			break;
 1172: 		    case "textindicator":
 1173: 			if (!text) {
 1174: 				try {with (btn.element.style) {
 1175: 					backgroundColor = HTMLArea._makeColor(
 1176: 						doc.queryCommandValue(HTMLArea.is_ie ? "backcolor" : "hilitecolor"));
 1177: 					if (/transparent/i.test(backgroundColor)) {
 1178: 						// Mozilla
 1179: 						backgroundColor = HTMLArea._makeColor(doc.queryCommandValue("backcolor"));
 1180: 					}
 1181: 					color = HTMLArea._makeColor(doc.queryCommandValue("forecolor"));
 1182: 					fontFamily = doc.queryCommandValue("fontname");
 1183: 					fontWeight = doc.queryCommandState("bold") ? "bold" : "normal";
 1184: 					fontStyle = doc.queryCommandState("italic") ? "italic" : "normal";
 1185: 				}} catch (e) {
 1186: 					// alert(e + "\n\n" + cmd);
 1187: 				}
 1188: 			}
 1189: 			break;
 1190: 		    case "htmlmode": btn.state("active", text); break;
 1191: 		    case "lefttoright":
 1192: 		    case "righttoleft":
 1193: 			var el = this.getParentElement();
 1194: 			while (el && !HTMLArea.isBlockElement(el))
 1195: 				el = el.parentNode;
 1196: 			if (el)
 1197: 				btn.state("active", (el.style.direction == ((cmd == "righttoleft") ? "rtl" : "ltr")));
 1198: 			break;
 1199: 		    default:
 1200: 			try {
 1201: 				btn.state("active", (!text && doc.queryCommandState(cmd)));
 1202: 			} catch (e) {}
 1203: 		}
 1204: 	}
 1205: 	// take undo snapshots
 1206: 	if (this._customUndo && !this._timerUndo) {
 1207: 		this._undoTakeSnapshot();
 1208: 		var editor = this;
 1209: 		this._timerUndo = setTimeout(function() {
 1210: 			editor._timerUndo = null;
 1211: 		}, this.config.undoTimeout);
 1212: 	}
 1213: 	// check if any plugins have registered refresh handlers
 1214: 	for (var i in this.plugins) {
 1215: 		var plugin = this.plugins[i].instance;
 1216: 		if (typeof plugin.onUpdateToolbar == "function")
 1217: 			plugin.onUpdateToolbar();
 1218: 	}
 1219: };
 1220: 
 1221: /** Returns a node after which we can insert other nodes, in the current
 1222:  * selection.  The selection is removed.  It splits a text node, if needed.
 1223:  */
 1224: HTMLArea.prototype.insertNodeAtSelection = function(toBeInserted) {
 1225: 	if (!HTMLArea.is_ie) {
 1226: 		var sel = this._getSelection();
 1227: 		var range = this._createRange(sel);
 1228: 		// remove the current selection
 1229: 		sel.removeAllRanges();
 1230: 		range.deleteContents();
 1231: 		var node = range.startContainer;
 1232: 		var pos = range.startOffset;
 1233: 		switch (node.nodeType) {
 1234: 		    case 3: // Node.TEXT_NODE
 1235: 			// we have to split it at the caret position.
 1236: 			if (toBeInserted.nodeType == 3) {
 1237: 				// do optimized insertion
 1238: 				node.insertData(pos, toBeInserted.data);
 1239: 				range = this._createRange();
 1240: 				range.setEnd(node, pos + toBeInserted.length);
 1241: 				range.setStart(node, pos + toBeInserted.length);
 1242: 				sel.addRange(range);
 1243: 			} else {
 1244: 				node = node.splitText(pos);
 1245: 				var selnode = toBeInserted;
 1246: 				if (toBeInserted.nodeType == 11 /* Node.DOCUMENT_FRAGMENT_NODE */) {
 1247: 					selnode = selnode.firstChild;
 1248: 				}
 1249: 				node.parentNode.insertBefore(toBeInserted, node);
 1250: 				this.selectNodeContents(selnode);
 1251: 				this.updateToolbar();
 1252: 			}
 1253: 			break;
 1254: 		    case 1: // Node.ELEMENT_NODE
 1255: 			var selnode = toBeInserted;
 1256: 			if (toBeInserted.nodeType == 11 /* Node.DOCUMENT_FRAGMENT_NODE */) {
 1257: 				selnode = selnode.firstChild;
 1258: 			}
 1259: 			node.insertBefore(toBeInserted, node.childNodes[pos]);
 1260: 			this.selectNodeContents(selnode);
 1261: 			this.updateToolbar();
 1262: 			break;
 1263: 		}
 1264: 	} else {
 1265: 		return null;	// this function not yet used for IE <FIXME>
 1266: 	}
 1267: };
 1268: 
 1269: // Returns the deepest node that contains both endpoints of the selection.
 1270: HTMLArea.prototype.getParentElement = function() {
 1271: 	var sel = this._getSelection();
 1272: 	var range = this._createRange(sel);
 1273: 	if (HTMLArea.is_ie) {
 1274: 		switch (sel.type) {
 1275: 		    case "Text":
 1276: 		    case "None":
 1277: 			// It seems that even for selection of type "None",
 1278: 			// there _is_ a parent element and it's value is not
 1279: 			// only correct, but very important to us.  MSIE is
 1280: 			// certainly the buggiest browser in the world and I
 1281: 			// wonder, God, how can Earth stand it?
 1282: 			return range.parentElement();
 1283: 		    case "Control":
 1284: 			return range.item(0);
 1285: 		    default:
 1286: 			return this._doc.body;
 1287: 		}
 1288: 	} else try {
 1289: 		var p = range.commonAncestorContainer;
 1290: 		if (!range.collapsed && range.startContainer == range.endContainer &&
 1291: 		    range.startOffset - range.endOffset <= 1 && range.startContainer.hasChildNodes())
 1292: 			p = range.startContainer.childNodes[range.startOffset];
 1293: 		/*
 1294: 		alert(range.startContainer + ":" + range.startOffset + "\n" +
 1295: 		      range.endContainer + ":" + range.endOffset);
 1296: 		*/
 1297: 		while (p.nodeType == 3) {
 1298: 			p = p.parentNode;
 1299: 		}
 1300: 		return p;
 1301: 	} catch (e) {
 1302: 		return null;
 1303: 	}
 1304: };
 1305: 
 1306: // Returns an array with all the ancestor nodes of the selection.
 1307: HTMLArea.prototype.getAllAncestors = function() {
 1308: 	var p = this.getParentElement();
 1309: 	var a = [];
 1310: 	while (p && (p.nodeType == 1) && (p.tagName.toLowerCase() != 'body')) {
 1311: 		a.push(p);
 1312: 		p = p.parentNode;
 1313: 	}
 1314: 	a.push(this._doc.body);
 1315: 	return a;
 1316: };
 1317: 
 1318: // Selects the contents inside the given node
 1319: HTMLArea.prototype.selectNodeContents = function(node, pos) {
 1320: 	this.focusEditor();
 1321: 	this.forceRedraw();
 1322: 	var range;
 1323: 	var collapsed = (typeof pos != "undefined");
 1324: 	if (HTMLArea.is_ie) {
 1325: 		range = this._doc.body.createTextRange();
 1326: 		range.moveToElementText(node);
 1327: 		(collapsed) && range.collapse(pos);
 1328: 		range.select();
 1329: 	} else {
 1330: 		var sel = this._getSelection();
 1331: 		range = this._doc.createRange();
 1332: 		range.selectNodeContents(node);
 1333: 		(collapsed) && range.collapse(pos);
 1334: 		sel.removeAllRanges();
 1335: 		sel.addRange(range);
 1336: 	}
 1337: };
 1338: 
 1339: /** Call this function to insert HTML code at the current position.  It deletes
 1340:  * the selection, if any.
 1341:  */
 1342: HTMLArea.prototype.insertHTML = function(html) {
 1343: 	var sel = this._getSelection();
 1344: 	var range = this._createRange(sel);
 1345: 	if (HTMLArea.is_ie) {
 1346: 		range.pasteHTML(html);
 1347: 	} else {
 1348: 		// construct a new document fragment with the given HTML
 1349: 		var fragment = this._doc.createDocumentFragment();
 1350: 		var div = this._doc.createElement("div");
 1351: 		div.innerHTML = html;
 1352: 		while (div.firstChild) {
 1353: 			// the following call also removes the node from div
 1354: 			fragment.appendChild(div.firstChild);
 1355: 		}
 1356: 		// this also removes the selection
 1357: 		var node = this.insertNodeAtSelection(fragment);
 1358: 	}
 1359: };
 1360: 
 1361: /**
 1362:  *  Call this function to surround the existing HTML code in the selection with
 1363:  *  your tags.  FIXME: buggy!  This function will be deprecated "soon".
 1364:  */
 1365: HTMLArea.prototype.surroundHTML = function(startTag, endTag) {
 1366: 	var html = this.getSelectedHTML();
 1367: 	// the following also deletes the selection
 1368: 	this.insertHTML(startTag + html + endTag);
 1369: };
 1370: 
 1371: /// Retrieve the selected block
 1372: HTMLArea.prototype.getSelectedHTML = function() {
 1373: 	var sel = this._getSelection();
 1374: 	var range = this._createRange(sel);
 1375: 	var existing = null;
 1376: 	if (HTMLArea.is_ie) {
 1377: 		existing = range.htmlText;
 1378: 	} else {
 1379: 		existing = HTMLArea.getHTML(range.cloneContents(), false, this);
 1380: 	}
 1381: 	return existing;
 1382: };
 1383: 
 1384: /// Return true if we have some selection
 1385: HTMLArea.prototype.hasSelectedText = function() {
 1386: 	// FIXME: come _on_ mishoo, you can do better than this ;-)
 1387: 	return this.getSelectedHTML() != '';
 1388: };
 1389: 
 1390: HTMLArea.prototype._createLink = function(link) {
 1391: 	var editor = this;
 1392: 	var outparam = null;
 1393: 	if (typeof link == "undefined") {
 1394: 		link = this.getParentElement();
 1395: 		if (link && !/^a$/i.test(link.tagName))
 1396: 			link = null;
 1397: 	}
 1398: 	if (link) outparam = {
 1399: 		f_href   : HTMLArea.is_ie ? editor.stripBaseURL(link.href) : link.getAttribute("href"),
 1400: 		f_title  : link.title,
 1401: 		f_target : link.target
 1402: 	};
 1403: 	this._popupDialog("link.html", function(param) {
 1404: 		if (!param)
 1405: 			return false;
 1406: 		var a = link;
 1407: 		if (!a) {
 1408: 			editor._doc.execCommand("createlink", false, param.f_href);
 1409: 			a = editor.getParentElement();
 1410: 			var sel = editor._getSelection();
 1411: 			var range = editor._createRange(sel);
 1412: 			if (!HTMLArea.is_ie) {
 1413: 				a = range.startContainer;
 1414: 				if (!/^a$/i.test(a.tagName))
 1415: 					a = a.nextSibling;
 1416: 			}
 1417: 		} else a.href = param.f_href.trim();
 1418: 		if (!/^a$/i.test(a.tagName))
 1419: 			return false;
 1420: 		a.target = param.f_target.trim();
 1421: 		a.title = param.f_title.trim();
 1422: 		editor.selectNodeContents(a);
 1423: 		editor.updateToolbar();
 1424: 	}, outparam);
 1425: };
 1426: 
 1427: // Called when the user clicks on "InsertImage" button.  If an image is already
 1428: // there, it will just modify it's properties.
 1429: HTMLArea.prototype._insertImage = function(image) {
 1430: 	var editor = this;	// for nested functions
 1431: 	var outparam = null;
 1432: 	if (typeof image == "undefined") {
 1433: 		image = this.getParentElement();
 1434: 		if (image && !/^img$/i.test(image.tagName))
 1435: 			image = null;
 1436: 	}
 1437: 	if (image) outparam = {
 1438: 		f_url    : HTMLArea.is_ie ? editor.stripBaseURL(image.src) : image.getAttribute("src"),
 1439: 		f_alt    : image.alt,
 1440: 		f_border : image.border,
 1441: 		f_align  : image.align,
 1442: 		f_vert   : image.vspace,
 1443: 		f_horiz  : image.hspace
 1444: 	};
 1445: 	this._popupDialog("insert_image.html", function(param) {
 1446: 		if (!param) {	// user must have pressed Cancel
 1447: 			return false;
 1448: 		}
 1449: 		var img = image;
 1450: 		if (!img) {
 1451: 			var sel = editor._getSelection();
 1452: 			var range = editor._createRange(sel);
 1453: 			editor._doc.execCommand("insertimage", false, param.f_url);
 1454: 			if (HTMLArea.is_ie) {
 1455: 				img = range.parentElement();
 1456: 				// wonder if this works...
 1457: 				if (img.tagName.toLowerCase() != "img") {
 1458: 					img = img.previousSibling;
 1459: 				}
 1460: 			} else {
 1461: 				img = range.startContainer.previousSibling;
 1462: 			}
 1463: 		} else {
 1464: 			img.src = param.f_url;
 1465: 		}
 1466: 		for (field in param) {
 1467: 			var value = param[field];
 1468: 			switch (field) {
 1469: 			    case "f_alt"    : img.alt	 = value; break;
 1470: 			    case "f_border" : img.border = parseInt(value || "0"); break;
 1471: 			    case "f_align"  : img.align	 = value; break;
 1472: 			    case "f_vert"   : img.vspace = parseInt(value || "0"); break;
 1473: 			    case "f_horiz"  : img.hspace = parseInt(value || "0"); break;
 1474: 			}
 1475: 		}
 1476: 	}, outparam);
 1477: };
 1478: 
 1479: // Called when the user clicks the Insert Table button
 1480: HTMLArea.prototype._insertTable = function() {
 1481: 	var sel = this._getSelection();
 1482: 	var range = this._createRange(sel);
 1483: 	var editor = this;	// for nested functions
 1484: 	this._popupDialog("insert_table.html", function(param) {
 1485: 		if (!param) {	// user must have pressed Cancel
 1486: 			return false;
 1487: 		}
 1488: 		var doc = editor._doc;
 1489: 		// create the table element
 1490: 		var table = doc.createElement("table");
 1491: 		// assign the given arguments
 1492: 		for (var field in param) {
 1493: 			var value = param[field];
 1494: 			if (!value) {
 1495: 				continue;
 1496: 			}
 1497: 			switch (field) {
 1498: 			    case "f_width"   : table.style.width = value + param["f_unit"]; break;
 1499: 			    case "f_align"   : table.align	 = value; break;
 1500: 			    case "f_border"  : table.border	 = parseInt(value); break;
 1501: 			    case "f_spacing" : table.cellspacing = parseInt(value); break;
 1502: 			    case "f_padding" : table.cellpadding = parseInt(value); break;
 1503: 			}
 1504: 		}
 1505: 		var tbody = doc.createElement("tbody");
 1506: 		table.appendChild(tbody);
 1507: 		for (var i = 0; i < param["f_rows"]; ++i) {
 1508: 			var tr = doc.createElement("tr");
 1509: 			tbody.appendChild(tr);
 1510: 			for (var j = 0; j < param["f_cols"]; ++j) {
 1511: 				var td = doc.createElement("td");
 1512: 				tr.appendChild(td);
 1513: 				// Mozilla likes to see something inside the cell.
 1514: 				(HTMLArea.is_gecko) && td.appendChild(doc.createElement("br"));
 1515: 			}
 1516: 		}
 1517: 		if (HTMLArea.is_ie) {
 1518: 			range.pasteHTML(table.outerHTML);
 1519: 		} else {
 1520: 			// insert the table
 1521: 			editor.insertNodeAtSelection(table);
 1522: 		}
 1523: 		return true;
 1524: 	}, null);
 1525: };
 1526: 
 1527: /***************************************************
 1528:  *  Category: EVENT HANDLERS
 1529:  ***************************************************/
 1530: 
 1531: // el is reference to the SELECT object
 1532: // txt is the name of the select field, as in config.toolbar
 1533: HTMLArea.prototype._comboSelected = function(el, txt) {
 1534: 	this.focusEditor();
 1535: 	var value = el.options[el.selectedIndex].value;
 1536: 	switch (txt) {
 1537: 	    case "fontname":
 1538: 	    case "fontsize": this.execCommand(txt, false, value); break;
 1539: 	    case "formatblock":
 1540: 		(HTMLArea.is_ie) && (value = "<" + value + ">");
 1541: 		this.execCommand(txt, false, value);
 1542: 		break;
 1543: 	    default:
 1544: 		// try to look it up in the registered dropdowns
 1545: 		var dropdown = this.config.customSelects[txt];
 1546: 		if (typeof dropdown != "undefined") {
 1547: 			dropdown.action(this);
 1548: 		} else {
 1549: 			alert("FIXME: combo box " + txt + " not implemented");
 1550: 		}
 1551: 	}
 1552: };
 1553: 
 1554: // the execCommand function (intercepts some commands and replaces them with
 1555: // our own implementation)
 1556: HTMLArea.prototype.execCommand = function(cmdID, UI, param) {
 1557: 	var editor = this;	// for nested functions
 1558: 	this.focusEditor();
 1559: 	cmdID = cmdID.toLowerCase();
 1560: 	switch (cmdID) {
 1561: 	    case "htmlmode" : this.setMode(); break;
 1562: 	    case "hilitecolor":
 1563: 		(HTMLArea.is_ie) && (cmdID = "backcolor");
 1564: 	    case "forecolor":
 1565: 		this._popupDialog("select_color.html", function(color) {
 1566: 			if (color) { // selection not canceled
 1567: 				editor._doc.execCommand(cmdID, false, "#" + color);
 1568: 			}
 1569: 		}, HTMLArea._colorToRgb(this._doc.queryCommandValue(cmdID)));
 1570: 		break;
 1571: 	    case "createlink":
 1572: 		this._createLink();
 1573: 		break;
 1574: 	    case "popupeditor":
 1575: 		// this object will be passed to the newly opened window
 1576: 		HTMLArea._object = this;
 1577: 		if (HTMLArea.is_ie) {
 1578: 			//if (confirm(HTMLArea.I18N.msg["IE-sucks-full-screen"]))
 1579: 			{
 1580: 				window.open(this.popupURL("fullscreen.html"), "ha_fullscreen",
 1581: 					    "toolbar=no,location=no,directories=no,status=no,menubar=no," +
 1582: 					    "scrollbars=no,resizable=yes,width=640,height=480");
 1583: 			}
 1584: 		} else {
 1585: 			window.open(this.popupURL("fullscreen.html"), "ha_fullscreen",
 1586: 				    "toolbar=no,menubar=no,personalbar=no,width=640,height=480," +
 1587: 				    "scrollbars=no,resizable=yes");
 1588: 		}
 1589: 		break;
 1590: 	    case "undo":
 1591: 	    case "redo":
 1592: 		if (this._customUndo)
 1593: 			this[cmdID]();
 1594: 		else
 1595: 			this._doc.execCommand(cmdID, UI, param);
 1596: 		break;
 1597: 	    case "inserttable": this._insertTable(); break;
 1598: 	    case "insertimage": this._insertImage(); break;
 1599: 	    case "about"    : this._popupDialog("about.html", null, this); break;
 1600: 	    case "showhelp" : window.open(_editor_url + "reference.html", "ha_help"); break;
 1601: 
 1602: 	    case "killword": this._wordClean(); break;
 1603: 
 1604: 	    case "cut":
 1605: 	    case "copy":
 1606: 	    case "paste":
 1607: 		try {
 1608: 			if (this.config.killWordOnPaste)
 1609: 				this._wordClean();
 1610: 			this._doc.execCommand(cmdID, UI, param);
 1611: 		} catch (e) {
 1612: 			if (HTMLArea.is_gecko) {
 1613: 				if (confirm("Unprivileged scripts cannot access Cut/Copy/Paste programatically " +
 1614: 					    "for security reasons.  Click OK to see a technical note at mozilla.org " +
 1615: 					    "which shows you how to allow a script to access the clipboard."))
 1616: 					window.open("http://mozilla.org/editor/midasdemo/securityprefs.html");
 1617: 			}
 1618: 		}
 1619: 		break;
 1620: 	    case "lefttoright":
 1621: 	    case "righttoleft":
 1622: 		var dir = (cmdID == "righttoleft") ? "rtl" : "ltr";
 1623: 		var el = this.getParentElement();
 1624: 		while (el && !HTMLArea.isBlockElement(el))
 1625: 			el = el.parentNode;
 1626: 		if (el) {
 1627: 			if (el.style.direction == dir)
 1628: 				el.style.direction = "";
 1629: 			else
 1630: 				el.style.direction = dir;
 1631: 		}
 1632: 		break;
 1633: 	    default: this._doc.execCommand(cmdID, UI, param);
 1634: 	}
 1635: 	this.updateToolbar();
 1636: 	return false;
 1637: };
 1638: 
 1639: /** A generic event handler for things that happen in the IFRAME's document.
 1640:  * This function also handles key bindings. */
 1641: HTMLArea.prototype._editorEvent = function(ev) {
 1642: 	var editor = this;
 1643: 	var keyEvent = (HTMLArea.is_ie && ev.type == "keydown") || (ev.type == "keypress");
 1644: 	if (keyEvent) {
 1645: 		for (var i in editor.plugins) {
 1646: 			var plugin = editor.plugins[i].instance;
 1647: 			if (typeof plugin.onKeyPress == "function") plugin.onKeyPress(ev);
 1648: 		}
 1649: 	}
 1650: 	if (keyEvent && ev.ctrlKey) {
 1651: 		var sel = null;
 1652: 		var range = null;
 1653: 		var key = String.fromCharCode(HTMLArea.is_ie ? ev.keyCode : ev.charCode).toLowerCase();
 1654: 		var cmd = null;
 1655: 		var value = null;
 1656: 		switch (key) {
 1657: 		    case 'a':
 1658: 			if (!HTMLArea.is_ie) {
 1659: 				// KEY select all
 1660: 				sel = this._getSelection();
 1661: 				sel.removeAllRanges();
 1662: 				range = this._createRange();
 1663: 				range.selectNodeContents(this._doc.body);
 1664: 				sel.addRange(range);
 1665: 				HTMLArea._stopEvent(ev);
 1666: 			}
 1667: 			break;
 1668: 
 1669: 			// simple key commands follow
 1670: 
 1671: 		    case 'b': cmd = "bold"; break;
 1672: 		    case 'i': cmd = "italic"; break;
 1673: 		    case 'u': cmd = "underline"; break;
 1674: 		    case 's': cmd = "strikethrough"; break;
 1675: 		    case 'l': cmd = "justifyleft"; break;
 1676: 		    case 'e': cmd = "justifycenter"; break;
 1677: 		    case 'r': cmd = "justifyright"; break;
 1678: 		    case 'j': cmd = "justifyfull"; break;
 1679: 		    case 'z': cmd = "undo"; break;
 1680: 		    case 'y': cmd = "redo"; break;
 1681: 		    case 'v': cmd = "paste"; break;
 1682: 
 1683: 		    case '0': cmd = "killword"; break;
 1684: 
 1685: 			// headings
 1686: 		    case '1':
 1687: 		    case '2':
 1688: 		    case '3':
 1689: 		    case '4':
 1690: 		    case '5':
 1691: 		    case '6':
 1692: 			cmd = "formatblock";
 1693: 			value = "h" + key;
 1694: 			if (HTMLArea.is_ie) {
 1695: 				value = "<" + value + ">";
 1696: 			}
 1697: 			break;
 1698: 		}
 1699: 		if (cmd) {
 1700: 			// execute simple command
 1701: 			this.execCommand(cmd, false, value);
 1702: 			HTMLArea._stopEvent(ev);
 1703: 		}
 1704: 	}
 1705: 	/*
 1706: 	else if (keyEvent) {
 1707: 		// other keys here
 1708: 		switch (ev.keyCode) {
 1709: 		    case 13: // KEY enter
 1710: 			// if (HTMLArea.is_ie) {
 1711: 			this.insertHTML("<br />");
 1712: 			HTMLArea._stopEvent(ev);
 1713: 			// }
 1714: 			break;
 1715: 		}
 1716: 	}
 1717: 	*/
 1718: 	// update the toolbar state after some time
 1719: 	if (editor._timerToolbar) {
 1720: 		clearTimeout(editor._timerToolbar);
 1721: 	}
 1722: 	editor._timerToolbar = setTimeout(function() {
 1723: 		editor.updateToolbar();
 1724: 		editor._timerToolbar = null;
 1725: 	}, 50);
 1726: };
 1727: 
 1728: // retrieve the HTML
 1729: HTMLArea.prototype.getHTML = function() {
 1730: 	switch (this._editMode) {
 1731: 	    case "wysiwyg"  :
 1732: 		if (!this.config.fullPage) {
 1733: 			return HTMLArea.getHTML(this._doc.body, false, this);
 1734: 		} else
 1735: 			return this.doctype + "\n" + HTMLArea.getHTML(this._doc.documentElement, true, this);
 1736: 	    case "textmode" : return this._textArea.value;
 1737: 	    default	    : alert("Mode <" + mode + "> not defined!");
 1738: 	}
 1739: 	return false;
 1740: };
 1741: 
 1742: // retrieve the HTML (fastest version, but uses innerHTML)
 1743: HTMLArea.prototype.getInnerHTML = function() {
 1744: 	switch (this._editMode) {
 1745: 	    case "wysiwyg"  :
 1746: 		if (!this.config.fullPage)
 1747: 			return this._doc.body.innerHTML;
 1748: 		else
 1749: 			return this.doctype + "\n" + this._doc.documentElement.innerHTML;
 1750: 	    case "textmode" : return this._textArea.value;
 1751: 	    default	    : alert("Mode <" + mode + "> not defined!");
 1752: 	}
 1753: 	return false;
 1754: };
 1755: 
 1756: // completely change the HTML inside
 1757: HTMLArea.prototype.setHTML = function(html) {
 1758: 	switch (this._editMode) {
 1759: 	    case "wysiwyg"  :
 1760: 		if (!this.config.fullPage)
 1761: 			this._doc.body.innerHTML = html;
 1762: 		else
 1763: 			// this._doc.documentElement.innerHTML = html;
 1764: 			this._doc.body.innerHTML = html;
 1765: 		break;
 1766: 	    case "textmode" : this._textArea.value = html; break;
 1767: 	    default	    : alert("Mode <" + mode + "> not defined!");
 1768: 	}
 1769: 	return false;
 1770: };
 1771: 
 1772: // sets the given doctype (useful when config.fullPage is true)
 1773: HTMLArea.prototype.setDoctype = function(doctype) {
 1774: 	this.doctype = doctype;
 1775: };
 1776: 
 1777: /***************************************************
 1778:  *  Category: UTILITY FUNCTIONS
 1779:  ***************************************************/
 1780: 
 1781: // browser identification
 1782: 
 1783: HTMLArea.agt = navigator.userAgent.toLowerCase();
 1784: HTMLArea.is_ie	   = ((HTMLArea.agt.indexOf("msie") != -1) && (HTMLArea.agt.indexOf("opera") == -1));
 1785: HTMLArea.is_opera  = (HTMLArea.agt.indexOf("opera") != -1);
 1786: HTMLArea.is_mac	   = (HTMLArea.agt.indexOf("mac") != -1);
 1787: HTMLArea.is_mac_ie = (HTMLArea.is_ie && HTMLArea.is_mac);
 1788: HTMLArea.is_win_ie = (HTMLArea.is_ie && !HTMLArea.is_mac);
 1789: HTMLArea.is_gecko  = (navigator.product == "Gecko");
 1790: 
 1791: // variable used to pass the object to the popup editor window.
 1792: HTMLArea._object = null;
 1793: 
 1794: // function that returns a clone of the given object
 1795: HTMLArea.cloneObject = function(obj) {
 1796: 	var newObj = new Object;
 1797: 
 1798: 	// check for array objects
 1799: 	if (obj.constructor.toString().indexOf("function Array(") == 1) {
 1800: 		newObj = obj.constructor();
 1801: 	}
 1802: 
 1803: 	// check for function objects (as usual, IE is fucked up)
 1804: 	if (obj.constructor.toString().indexOf("function Function(") == 1) {
 1805: 		newObj = obj; // just copy reference to it
 1806: 	} else for (var n in obj) {
 1807: 		var node = obj[n];
 1808: 		if (typeof node == 'object') { newObj[n] = HTMLArea.cloneObject(node); }
 1809: 		else                         { newObj[n] = node; }
 1810: 	}
 1811: 
 1812: 	return newObj;
 1813: };
 1814: 
 1815: // FIXME!!! this should return false for IE < 5.5
 1816: HTMLArea.checkSupportedBrowser = function() {
 1817: 	if (HTMLArea.is_gecko) {
 1818: 		if (navigator.productSub < 20021201) {
 1819: 			window.status="WYSIWYG Editor: You need at least Mozilla-1.3 Alpha. " +
 1820: 			      "Sorry, your Gecko is not supported.";
 1821: 			return false;
 1822: 		}
 1823: 		if (navigator.productSub < 20030210) {
 1824: 			window.status="WYSIWYG Editor: Mozilla < 1.3 Beta is not supported! " +
 1825: 			      "I'll try, though, but it might not work.";
 1826: 		}
 1827: 	}
 1828: 	return HTMLArea.is_gecko || HTMLArea.is_ie;
 1829: };
 1830: 
 1831: // selection & ranges
 1832: 
 1833: // returns the current selection object
 1834: HTMLArea.prototype._getSelection = function() {
 1835: 	if (HTMLArea.is_ie) {
 1836: 		return this._doc.selection;
 1837: 	} else {
 1838: 		return this._iframe.contentWindow.getSelection();
 1839: 	}
 1840: };
 1841: 
 1842: // returns a range for the current selection
 1843: HTMLArea.prototype._createRange = function(sel) {
 1844: 	if (HTMLArea.is_ie) {
 1845: 		return sel.createRange();
 1846: 	} else {
 1847: 		this.focusEditor();
 1848: 		if (typeof sel != "undefined") {
 1849: 			try {
 1850: 				return sel.getRangeAt(0);
 1851: 			} catch(e) {
 1852: 				return this._doc.createRange();
 1853: 			}
 1854: 		} else {
 1855: 			return this._doc.createRange();
 1856: 		}
 1857: 	}
 1858: };
 1859: 
 1860: // event handling
 1861: 
 1862: HTMLArea._addEvent = function(el, evname, func) {
 1863: 	if (HTMLArea.is_ie) {
 1864: 		el.attachEvent("on" + evname, func);
 1865: 	} else {
 1866: 		el.addEventListener(evname, func, true);
 1867: 	}
 1868: };
 1869: 
 1870: HTMLArea._addEvents = function(el, evs, func) {
 1871: 	for (var i in evs) {
 1872: 		HTMLArea._addEvent(el, evs[i], func);
 1873: 	}
 1874: };
 1875: 
 1876: HTMLArea._removeEvent = function(el, evname, func) {
 1877: 	if (HTMLArea.is_ie) {
 1878: 		el.detachEvent("on" + evname, func);
 1879: 	} else {
 1880: 		el.removeEventListener(evname, func, true);
 1881: 	}
 1882: };
 1883: 
 1884: HTMLArea._removeEvents = function(el, evs, func) {
 1885: 	for (var i in evs) {
 1886: 		HTMLArea._removeEvent(el, evs[i], func);
 1887: 	}
 1888: };
 1889: 
 1890: HTMLArea._stopEvent = function(ev) {
 1891: 	if (HTMLArea.is_ie) {
 1892: 		ev.cancelBubble = true;
 1893: 		ev.returnValue = false;
 1894: 	} else {
 1895: 		ev.preventDefault();
 1896: 		ev.stopPropagation();
 1897: 	}
 1898: };
 1899: 
 1900: HTMLArea._removeClass = function(el, className) {
 1901: 	if (!(el && el.className)) {
 1902: 		return;
 1903: 	}
 1904: 	var cls = el.className.split(" ");
 1905: 	var ar = new Array();
 1906: 	for (var i = cls.length; i > 0;) {
 1907: 		if (cls[--i] != className) {
 1908: 			ar[ar.length] = cls[i];
 1909: 		}
 1910: 	}
 1911: 	el.className = ar.join(" ");
 1912: };
 1913: 
 1914: HTMLArea._addClass = function(el, className) {
 1915: 	// remove the class first, if already there
 1916: 	HTMLArea._removeClass(el, className);
 1917: 	el.className += " " + className;
 1918: };
 1919: 
 1920: HTMLArea._hasClass = function(el, className) {
 1921: 	if (!(el && el.className)) {
 1922: 		return false;
 1923: 	}
 1924: 	var cls = el.className.split(" ");
 1925: 	for (var i = cls.length; i > 0;) {
 1926: 		if (cls[--i] == className) {
 1927: 			return true;
 1928: 		}
 1929: 	}
 1930: 	return false;
 1931: };
 1932: 
 1933: HTMLArea.isBlockElement = function(el) {
 1934: 	var blockTags = " body form textarea fieldset ul ol dl li div " +
 1935: 		"p h1 h2 h3 h4 h5 h6 quote pre table thead " +
 1936: 		"tbody tfoot tr td iframe address ";
 1937: 	return (blockTags.indexOf(" " + el.tagName.toLowerCase() + " ") != -1);
 1938: };
 1939: 
 1940: HTMLArea.needsClosingTag = function(el) {
 1941: 	var closingTags = " head script style div span tr td tbody table em strong font a title ";
 1942: 	return (closingTags.indexOf(" " + el.tagName.toLowerCase() + " ") != -1);
 1943: };
 1944: 
 1945: // performs HTML encoding of some given string
 1946: HTMLArea.htmlEncode = function(str) {
 1947: 	// we don't need regexp for that, but.. so be it for now.
 1948: 	str = str.replace(/&/ig, "&amp;");
 1949: 	str = str.replace(/</ig, "&lt;");
 1950: 	str = str.replace(/>/ig, "&gt;");
 1951: 	str = str.replace(/\x22/ig, "&quot;");
 1952: 	// \x22 means '"' -- we use hex reprezentation so that we don't disturb
 1953: 	// JS compressors (well, at least mine fails.. ;)
 1954: 	return str;
 1955: };
 1956: 
 1957: // Retrieves the HTML code from the given node.	 This is a replacement for
 1958: // getting innerHTML, using standard DOM calls.
 1959: HTMLArea.getHTML = function(root, outputRoot, editor) {
 1960: 	var html = "";
 1961: 	switch (root.nodeType) {
 1962: 	    case 1: // Node.ELEMENT_NODE
 1963: 	    case 11: // Node.DOCUMENT_FRAGMENT_NODE
 1964: 		var closed;
 1965: 		var i;
 1966: 		var root_tag = (root.nodeType == 1) ? root.tagName.toLowerCase() : '';
 1967: 		if (HTMLArea.is_ie && root_tag == "head") {
 1968: 			if (outputRoot)
 1969: 				html += "<head>";
 1970: 			// lowercasize
 1971: 			var save_multiline = RegExp.multiline;
 1972: 			RegExp.multiline = true;
 1973: 			var txt = root.innerHTML.replace(HTMLArea.RE_tagName, function(str, p1, p2) {
 1974: 				return p1 + p2.toLowerCase();
 1975: 			});
 1976: 			RegExp.multiline = save_multiline;
 1977: 			html += txt;
 1978: 			if (outputRoot)
 1979: 				html += "</head>";
 1980: 			break;
 1981: 		} else if (outputRoot) {
 1982: 			closed = (!(root.hasChildNodes() || HTMLArea.needsClosingTag(root)));
 1983: 			html = "<" + root.tagName.toLowerCase();
 1984: 			var attrs = root.attributes;
 1985: 			for (i = 0; i < attrs.length; ++i) {
 1986: 				var a = attrs.item(i);
 1987: 				if (!a.specified) {
 1988: 					continue;
 1989: 				}
 1990: 				var name = a.nodeName.toLowerCase();
 1991: 				if (/_moz|contenteditable|_msh/.test(name)) {
 1992: 					// avoid certain attributes
 1993: 					continue;
 1994: 				}
 1995: 				var value;
 1996: 				if (name != "style") {
 1997: 					// IE5.5 reports 25 when cellSpacing is
 1998: 					// 1; other values might be doomed too.
 1999: 					// For this reason we extract the
 2000: 					// values directly from the root node.
 2001: 					// I'm starting to HATE JavaScript
 2002: 					// development.  Browser differences
 2003: 					// suck.
 2004: 					//
 2005: 					// Using Gecko the values of href and src are converted to absolute links
 2006: 					// unless we get them using nodeValue()
 2007: 					if (typeof root[a.nodeName] != "undefined" && name != "href" && name != "src") {
 2008: 						value = root[a.nodeName];
 2009: 					} else {
 2010: 						value = a.nodeValue;
 2011: 						// IE seems not willing to return the original values - it converts to absolute
 2012: 						// links using a.nodeValue, a.value, a.stringValue, root.getAttribute("href")
 2013: 						// So we have to strip the baseurl manually -/
 2014: 						if (HTMLArea.is_ie && (name == "href" || name == "src")) {
 2015: 							value = editor.stripBaseURL(value);
 2016: 						}
 2017: 					}
 2018: 				} else { // IE fails to put style in attributes list
 2019: 					// FIXME: cssText reported by IE is UPPERCASE
 2020: 					value = root.style.cssText;
 2021: 				}
 2022: 				if (/(_moz|^$)/.test(value)) {
 2023: 					// Mozilla reports some special tags
 2024: 					// here; we don't need them.
 2025: 					continue;
 2026: 				}
 2027: 				html += " " + name + '="' + value + '"';
 2028: 			}
 2029: 			html += closed ? " />" : ">";
 2030: 		}
 2031: 		for (i = root.firstChild; i; i = i.nextSibling) {
 2032: 			html += HTMLArea.getHTML(i, true, editor);
 2033: 		}
 2034: 		if (outputRoot && !closed) {
 2035: 			html += "</" + root.tagName.toLowerCase() + ">";
 2036: 		}
 2037: 		break;
 2038: 	    case 3: // Node.TEXT_NODE
 2039: 		// If a text node is alone in an element and all spaces, replace it with an non breaking one
 2040: 		// This partially undoes the damage done by moz, which translates '&nbsp;'s into spaces in the data element
 2041: 		if ( !root.previousSibling && !root.nextSibling && root.data.match(/^\s*$/i) ) html = '&nbsp;';
 2042: 		else html = HTMLArea.htmlEncode(root.data);
 2043: 		break;
 2044: 	    case 8: // Node.COMMENT_NODE
 2045: 		html = "<!--" + root.data + "-->";
 2046: 		break;		// skip comments, for now.
 2047: 	}
 2048: 	return html;
 2049: };
 2050: 
 2051: HTMLArea.prototype.stripBaseURL = function(string) {
 2052: 	var baseurl = this.config.baseURL;
 2053: 
 2054: 	// strip to last directory in case baseurl points to a file
 2055: 	baseurl = baseurl.replace(/[^\/]+$/, '');
 2056: 	var basere = new RegExp(baseurl);
 2057: 	string = string.replace(basere, "");
 2058: 
 2059: 	// strip host-part of URL which is added by MSIE to links relative to server root
 2060: 	baseurl = baseurl.replace(/^(https?:\/\/[^\/]+)(.*)$/, '$1');
 2061: 	basere = new RegExp(baseurl);
 2062: 	return string.replace(basere, "");
 2063: };
 2064: 
 2065: String.prototype.trim = function() {
 2066: 	a = this.replace(/^\s+/, '');
 2067: 	return a.replace(/\s+$/, '');
 2068: };
 2069: 
 2070: // creates a rgb-style color from a number
 2071: HTMLArea._makeColor = function(v) {
 2072: 	if (typeof v != "number") {
 2073: 		// already in rgb (hopefully); IE doesn't get here.
 2074: 		return v;
 2075: 	}
 2076: 	// IE sends number; convert to rgb.
 2077: 	var r = v & 0xFF;
 2078: 	var g = (v >> 8) & 0xFF;
 2079: 	var b = (v >> 16) & 0xFF;
 2080: 	return "rgb(" + r + "," + g + "," + b + ")";
 2081: };
 2082: 
 2083: // returns hexadecimal color representation from a number or a rgb-style color.
 2084: HTMLArea._colorToRgb = function(v) {
 2085: 	if (!v)
 2086: 		return '';
 2087: 
 2088: 	// returns the hex representation of one byte (2 digits)
 2089: 	function hex(d) {
 2090: 		return (d < 16) ? ("0" + d.toString(16)) : d.toString(16);
 2091: 	};
 2092: 
 2093: 	if (typeof v == "number") {
 2094: 		// we're talking to IE here
 2095: 		var r = v & 0xFF;
 2096: 		var g = (v >> 8) & 0xFF;
 2097: 		var b = (v >> 16) & 0xFF;
 2098: 		return "#" + hex(r) + hex(g) + hex(b);
 2099: 	}
 2100: 
 2101: 	if (v.substr(0, 3) == "rgb") {
 2102: 		// in rgb(...) form -- Mozilla
 2103: 		var re = /rgb\s*\(\s*([0-9]+)\s*,\s*([0-9]+)\s*,\s*([0-9]+)\s*\)/;
 2104: 		if (v.match(re)) {
 2105: 			var r = parseInt(RegExp.$1);
 2106: 			var g = parseInt(RegExp.$2);
 2107: 			var b = parseInt(RegExp.$3);
 2108: 			return "#" + hex(r) + hex(g) + hex(b);
 2109: 		}
 2110: 		// doesn't match RE?!  maybe uses percentages or float numbers
 2111: 		// -- FIXME: not yet implemented.
 2112: 		return null;
 2113: 	}
 2114: 
 2115: 	if (v.substr(0, 1) == "#") {
 2116: 		// already hex rgb (hopefully :D )
 2117: 		return v;
 2118: 	}
 2119: 
 2120: 	// if everything else fails ;)
 2121: 	return null;
 2122: };
 2123: 
 2124: // modal dialogs for Mozilla (for IE we're using the showModalDialog() call).
 2125: 
 2126: // receives an URL to the popup dialog and a function that receives one value;
 2127: // this function will get called after the dialog is closed, with the return
 2128: // value of the dialog.
 2129: HTMLArea.prototype._popupDialog = function(url, action, init) {
 2130: 	Dialog(this.popupURL(url), action, init);
 2131: };
 2132: 
 2133: // paths
 2134: 
 2135: HTMLArea.prototype.imgURL = function(file, plugin) {
 2136: 	if (typeof plugin == "undefined")
 2137: 		return _editor_url + file;
 2138: 	else
 2139: 		return _editor_url + "plugins/" + plugin + "/img/" + file;
 2140: };
 2141: 
 2142: HTMLArea.prototype.popupURL = function(file) {
 2143: 	var url = "";
 2144: 	if (file.match(/^plugin:\/\/(.*?)\/(.*)/)) {
 2145: 		var plugin = RegExp.$1;
 2146: 		var popup = RegExp.$2;
 2147: 		if (!/\.html$/.test(popup))
 2148: 			popup += ".html";
 2149: 		url = _editor_url + "plugins/" + plugin + "/popups/" + popup;
 2150: 	} else
 2151: 		url = _editor_url + this.config.popupURL + file;
 2152: 	return url;
 2153: };
 2154: 
 2155: /**
 2156:  * FIX: Internet Explorer returns an item having the _name_ equal to the given
 2157:  * id, even if it's not having any id.  This way it can return a different form
 2158:  * field even if it's not a textarea.  This workarounds the problem by
 2159:  * specifically looking to search only elements having a certain tag name.
 2160:  */
 2161: HTMLArea.getElementById = function(tag, id) {
 2162: 	var el, i, objs = document.getElementsByTagName(tag);
 2163: 	for (i = objs.length; --i >= 0 && (el = objs[i]);)
 2164: 		if (el.id == id)
 2165: 			return el;
 2166: 	return null;
 2167: };
 2168: 
 2169: 
 2170: 
 2171: // EOF
 2172: // Local variables: //
 2173: // c-basic-offset:8 //
 2174: // indent-tabs-mode:t //
 2175: // End: //

FreeBSD-CVSweb <freebsd-cvsweb@FreeBSD.org>