Project

General

Profile

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.1.1.1 2005/01/30 10:30:55 rdjurovich 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.php", 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.php", 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
			alert("You need at least Mozilla-1.3 Alpha.\n" +
1820
			      "Sorry, your Gecko is not supported.");
1821
			return false;
1822
		}
1823
		if (navigator.productSub < 20030210) {
1824
			alert("Mozilla < 1.3 Beta is not supported!\n" +
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: //
(3-3/9)