function TextEditor(textarea_name, options) {
	this.Version = "5.00";
	this.textarea_name = textarea_name;
	this.isLoaded = false;
	this.loadingIndex = 0;
	this.plugins = new Array();
	this.themes = new Array();
	this.baseURL = "";
	this.baseDOC = "";
	this.baseIMG = "";
	this.baseJS = "";
	this.baseCSS = "";
	this.buttonMap = new Array();
	return this.init(options);
}

TextEditor.prototype = {
	blockElms: 'H[1-6]|P|DIV|ADDRESS|PRE|FORM|TABLE|LI|OL|UL|TD|CAPTION|BLOCKQUOTE|CENTER|DL|DT|DD|DIR|FIELDSET|FORM|NOSCRIPT|NOFRAMES|MENU|ISINDEX|SAMP',
	posKeyCodes: new Array(13,45,36,35,33,34,37,38,39,40),
	uniqueTag: '<div id="TMPElement" style="display: none">TMP</div>',
	uniqueURL: 'javascript:void(0);',
	// Нужно добавить использование "callbacks"
	callbacks: new Array('onInit', 'onChange', 'onPageLoad', 'setupContent', 'handleNodeChange', 'execCommand', 'getControlHTML', 'handleEvent', 'cleanup'),

	options: {
		base_href: "",
		base_img: "",
		default_document: "",
		css_path: "",
		use_main_css: true,
		valid_elements: "+a[name|href|style|target],-strong/-b[style],-em/-i[style],-strike[style],-u[style],#p[align|style],-ol[style],-ul[style],-li[style],br,img[src|border|alt|hspace|vspace|width|height|align|style],-sub[style],-sup[style],-blockquote[style],-table[border|cellspacing|cellpadding|width|height|align|bgcolor|background|bordercolor|style],-tr[rowspan|width|height|align|valign|bgcolor|background|bordercolor|style],tbody[style],thead[style],tfoot[class],#td[colspan|rowspan|width|height|align|valign|bgcolor|background|bordercolor|scope|style],-th[colspan|rowspan|width|height|align|valign|bgcolor|background|bordercolor|scope|style],caption[style],-div[align|style],-span[align|style],-pre[align|style],address[align|style],-h1[align|style],-h2[align|style],-h3[align|style],-h4[align|style],-h5[align|style],-h6[align|style],hr[width|size|align|style],-font[face|size|color|style],dd[style],dl[style],dt[style],del[style],cite[style],abbr[style],acronym[style],ins[style|datetime|cite]",
		invalid_elements: "",
		add_unload_trigger: true,
		spellcheck: false,
		theme: "simple",
		plugins: "",
		auto_resize: false,
		auto_focus: true,
		remove_linebreaks: false,
		visible_source: false,
		maxHeight: 0
	},

	init: function(options) {
		if (typeof(document.execCommand) == "undefined" || oldBrowser) {
//			setHide($(this.textarea_name).parentNode);
			return;
		}
		window.TextEditor = this;
		Object.extend(this.options, options || {});
		this.blockRegExp = new RegExp("^(" + this.blockElms + ")$", "i");

		if (this.options.base_href) this.baseURL = this.options.base_href;
		this.getBaseURL();

		this.loadJS('gui_' + this.options.theme);
		this.loadCSS('editor');
		var p = this.options.plugins.replace(/[\s\t\r\n]/g, '').split(',');
		for (var i=0; i<p.length; i++) if (p[i]) this.loadJS(p[i]);

		this.addEvent(window, "DOMContentLoaded", this.onLoad.bind(this));
		this.addEvent(document.body, "readystatechange", this.onLoad.bind(this));
		this.addEvent(document, "readystatechange", this.onLoad.bind(this));
		this.addEvent(window, "load", this.onLoad.bind(this));
		if (this.options.add_unload_trigger) {
			var _t = this, ts = function(e){ _t.triggerSave(true, true); };
			this.addEvent(window, "unload", ts.bind(this));
			this.addEvent(window.document, "beforeunload", ts.bind(this));
		}
		this.onLoad();
	},

	execInstanceCommand: function(command, user_interface, value, focus) {
		if (typeof(focus) == "undefined") focus = true;
		var r = this.Control.Selection.getRng();
		if (focus && (!r || !r.item)) this.Control.iframeWindow.focus();
		this.Control.autoResetDesignMode();
		this.selectedElement = this.Control.Selection.getFocusElement();
		this.execCommand(command, user_interface, value);
		if (window.event != null) this.cancelEvent(window.event);
	},

	execCommand: function(command, user_interface, value) {
		user_interface = user_interface ? user_interface : false;
		value = value ? value : null;

		switch (command) {
			case 'Focus':
				this.Control.iframeWindow.focus();
				return;
			case "ResetDesignMode":
				this.Control.setDesignMode();
				return;
		}
		this.Control.execCommand(command, user_interface, value);
	},

	onLoad: function(event) {
		this.removeEvent(window, "load", this.onLoad.bind(this));
		this.removeEvent(document, "readystatechange", this.onLoad.bind(this));
		this.removeEvent(document.body, "readystatechange", this.onLoad.bind(this));
		this.removeEvent(window, "DOMContentLoaded", this.onLoad.bind(this));
		if (this.loadingIndex > 0) {
			window.setTimeout(this.onLoad.bind(this), 5);
			return;
		}
		if (event && event.type == "readystatechange" && document.readyState != "complete") return true;
		if (this.isLoaded) return true;
		this.isLoaded = true;

		if (document.body && document.body.createTextRange && window.location.href != window.top.location.href) {
			var r = document.body.createTextRange();
			r.collapse(true);
			r.select();
		}
		this.dispatchCallback('onpageload', 'onPageLoad');
		if (document.forms && !this.submitTriggers) {
			var form = this.getParentElement($(this.textarea_name), "form");
			if (form) {
				var _t = this, f = function(e) {_t.handleEvent(e)};
				this.addEvent(form, "submit", f.bind(this));
				this.addEvent(form, "reset", f.bind(this));
				try {
					form.oldSubmit = form.submit;
					form.submit = this.submitPatch.bind(this);
				} catch (e) { }
				this.form = form;
			}
			this.submitTriggers = true;
		}

		this.Control = new TextEditor_Control(this);
		this.ForceParagraphs = new TextEditor_ForceParagraphs(this);
		this.WindowManager = new TextEditor_WindowManager(this);
		this.dispatchCallback('oninit', 'onInit');
	},

	repaint: function() {
	},

	hideMenus: function() {
		var e = this.lastSelectedMenuBtn;
		if (this.lastMenu) {
			this.lastMenu.hide();
			this.lastMenu = null;
		}
		if (e) {
			this.switchClass(e, this.lastMenuBtnClass);
			this.lastSelectedMenuBtn = null;
		}
	},

	loadJS: function(url) {
		this.loadingIndex++;
		var el = document.createElementNS ? document.createElementNS('http://www.w3.org/1999/xhtml', 'script') : document.createElement('script');
		el.language = 'javascript';
		el.type = 'text/javascript';
		el.src = this.baseJS + '/editor/' + url + '.js';
		document.getElementsByTagName("head")[0].appendChild(el);
	},

	loadCSS: function(url, doc) {
		if (!doc) doc = document;
		var el = doc.createElement('link');
		el.rel = 'stylesheet';
		el.type = 'text/css';
		el.href = this.baseCSS + '/' + url + '.css';
		doc.getElementsByTagName('head')[0].appendChild(el);
	},

	applyMainCSS: function() {
		var doc = this.Control.iframeDocument, testRe = new RegExp('([\.#])|(unknown)', 'i');
		var style = doc.createElement('style');
		doc.getElementsByTagName('head')[0].appendChild(style);
		style = doc.styleSheets[doc.styleSheets.length - 1];

		function _parseRule(rs) {
			if (!rs || rs.length == 0) return;
			for (var i=0, l=rs.length; i<l; i++) {
				if (rs[i].styleSheet) _parseRule(rs[i].styleSheet.cssRules || rs[i].styleSheet.rules);
				else if (rs[i].cssText) {
					var parts = rs[i].cssText.split(/\s*[{}]\s*/);
					for (var j=0, k=parts.length; j<k; j+=2) {
						if (parts[j] && parts[j+1] && !testRe.test(parts[j]))
							try {_addRule(parts[j], parts[j+1])}catch(e){};
					}
				}
			}
		}
		function _addRule(s,r) {
			if (style.insertRule) style.insertRule(s + " { " + r + " }", style.cssRules.length);
			else if (style.addRule) style.addRule(s, r);
		}
		for (var i=0, l=document.styleSheets.length; i<l; i++) {
			var s = document.styleSheets[i];
			var r = s.cssRules || ((s.imports && s.imports.length>0) ? s.imports : s.rules);
			if (r) _parseRule(r);
		}
	},

	setBaseHREF: function(doc) {
		if (!doc) doc = document;
		var el = doc.getElementsByTagName("base"), b = el.length > 0 ? el[0] : null;
		if (!b) {
			b = doc.createElement('base');
			b.href = (this.baseURL || this.baseDOC) + '/';
			doc.getElementsByTagName('head')[0].appendChild(b);
		} else {
			b.href = (this.baseURL || this.baseDOC) + '/';
		}
	},

	callFunc: function(p, n, m, a) {
		var l, i, on, o, s, v;
		s = m == 2;
		l = (typeof(this.options[p]) == "undefined") ? '' : this.options[p];
		if (l !== '' && (v = this.evalFunc(l, a)) == s && m > 0) return true;

		for (i=0, l=this.plugins.length; i<l; i++) {
			o = this.plugins[l[i]];
			if (o[n] && (v = this.evalFunc(n, a, o)) == s && m > 0) return true;
		}

		l = this.themes;
		for (on in l) {
			o = l[on];
			if (o[n] && (v = this.evalFunc(n, a, o)) == s && m > 0) return true;
		}
		return false;
	},

	evalFunc: function(f, a, o) {
		o = !o ? window : o;
		f = typeof(f) == 'function' ? f : o[f];
		return f.apply(o, Array.prototype.slice.call(a, 2));
	},

	setupContent: function() {
		var doc = this.Control.iframeWindow.document, head = doc.getElementsByTagName('head')[0];
		if (!head || !doc.body) {
			if (this.cnt_setupContent++ > 100) return;
			window.setTimeout(this.setupContent.bind(this), 10);
			return;
		}
		this.Control.iframeDocument = doc;

		this.setBaseHREF(doc);
		this.loadCSS('editor_content', doc);
		if (this.options.use_main_css) window.setTimeout(this.applyMainCSS.bind(this), 10);

//		var html = this.startContent.replace(/&lt;/g, '<').replace(/&gt;/g, '>').replace(/&quot;/g, '"').replace(/&amp;/g, '&');
		var html = this.startContent;

		html = this.Control.Cleanup._customCleanup("insert_to_editor", html);

		this.insertHTML(html);
		this.Control._addBogusBR();
		if (this.options.auto_focus) {
			var _t = this;
			window.setTimeout(function () {
				_t.Control.Selection.selectNode(_t.Control.iframeDocument.body, true, true);
				_t.Control.iframeWindow.focus();
			}, 100);
		}
		this.dispatchCallback('setupcontent_callback', 'setupContent');

		this.addEventHandlers();
		this.selectedElement = this.Control.iframeWindow.document.body;
		this.Control.Cleanup._customCleanup("insert_to_editor_dom");
		this.Control.Cleanup._customCleanup("setup_content_dom");

		this.convertFontsToSpans();
		this.startContent = doc.body.innerHTML.strip();
		this.Control.undoRedo.add({ content : this.startContent });
		this.Control.iframeDocument.body.spellcheck = this.options.spellcheck;
		this.triggerNodeChange(false, true);
	},

	triggerSave: function(skip_cleanup, skip_callback) {
		if (typeof(skip_cleanup) == "undefined") skip_cleanup = false;
		if (typeof(skip_callback) == "undefined") skip_callback = false;

		this.WindowManager.close();
		this.insertHTML(this.Control.iframeDocument.body.innerHTML);

		this.Control.Cleanup._customCleanup("submit_content_dom", this.Control.iframeDocument.body);
		var htm = skip_cleanup ? this.Control.iframeDocument.body.innerHTML : this.Control.Cleanup._cleanupHTML(this.Control.iframeDocument.body, true, true);
		if (htm.indexOf('<') == -1) htm = '<p>' + htm + '</p>';
		htm = this.Control.Cleanup._customCleanup("submit_content", htm);

		if (!skip_callback && typeof(this.options.save_callback) == 'function') htm = this.options.save_callback(htm);

		htm = htm.replace(new RegExp("&#40;", 'gi'), "(").replace(new RegExp("&#41;", 'gi'), ")").replace(new RegExp("&#59;", 'gi'), ";").replace(new RegExp("&#34;", 'gi'), "&quot;").replace(new RegExp("&#94;", 'gi'), "^");
		this.Control.textarea.value = htm;
	},

	triggerNodeChange: function(focus, setup_content) {
		var elm = (typeof(setup_content) != "undefined" && setup_content) ? this.selectedElement : this.Control.Selection.getFocusElement();

		if (this.options.auto_resize) this.Control.resizeToContent();
		var anySelection = !this.Control.Selection.isCollapsed();
		var undoIndex = this.Control.undoRedo.undoIndex;
		var undoLevels = this.Control.undoRedo.undoLevels.length;

		this.dispatchCallback('handle_node_change_callback', 'handleNodeChange', elm, undoIndex, undoLevels, anySelection, setup_content);
		if (typeof(focus) == undefined || focus) this.Control.iframeWindow.focus();
	},

	getBaseURL: function() {
		if (this.baseDOC) return;
		// baseDOC
		this.baseDOC = document.location.href.substring(0, document.location.href.lastIndexOf('/'));
		var base_tmp = (this.baseURL || this.baseDOC).replace(/\/$/, '');

		function getUrl(url) {
			var r = "";
			if (url.indexOf('://') != -1) r = url;
			else if (url.charAt(0) != '/') r = base_tmp + '/' + url;
				else r = document.location.protocol + '//' + document.location.hostname + url;
			return r;
		}

		var el;
		// baseURL
		if (!this.baseURL) {
			el = document.getElementsByTagName('base');
			for (var i=0,l=el.length;i<l;i++) if (el[i].href) this.baseURL = el[i].href;
			if (this.baseURL) base_tmp = this.baseURL.replace(/\/$/, '');
		}

		// baseIMG
		if (this.options.base_img) this.baseIMG = getUrl(this.options.base_img.replace(/\/$/, ''));
		else {
			el = document.getElementsByTagName('img');
			for (var i=0,l=el.length;i<l;i++) {
				if (el[i].src) {
					this.baseIMG = getUrl(el[i].src.substring(0, el[i].src.lastIndexOf('/')));
					break;
				}
			}
		}

		// baseJS
		el = document.getElementsByTagName('script');
		for (var i=0,l=el.length;i<l;i++) {
			if (el[i].src && (el[i].src.indexOf("editor.") != -1)) {
				this.baseJS = getUrl(el[i].src.substring(0, el[i].src.lastIndexOf('/')));
				break;
			}
		}

		// baseCSS
		if (this.options.css_path) {
			this.baseCSS = this.options.css_path;
			return;
		}
		el = document.getElementsByTagName('link');
		for (var i=0,l=el.length;i<l;i++) {
			if ((el[i].rel == 'stylesheet' || el[i].type == 'text/css') && (el[i].href)) {
				this.baseCSS = getUrl(el[i].href.substring(0, el[i].href.lastIndexOf('/')));
				if (el[i].href && (el[i].href.indexOf("editor.") != -1)) break;
			}
		}
		if (this.baseCSS) return;

		for (var i=0,l=document.styleSheets.length;i<l;i++) {
			var node = document.styleSheets[i].ownerNode || document.styleSheets[i].owningElement;
			var href = "";
			if (node.tagName.toLowerCase() == 'link') href = document.styleSheets[i].href;
			else if (node.tagName.toLowerCase() == 'style') {
				var rules = document.styleSheets[i].cssRules || ((document.styleSheets[i].imports && document.styleSheets[i].imports.length>0) ? document.styleSheets[i].imports : document.styleSheets[i].rules);
				if (rules && rules.length>0) href = rules[0].href;
			}
			if (href) {
				this.baseCSS = getUrl(href.substring(0, href.lastIndexOf('/')));
				if (href.indexOf("editor.") != -1) break;
			}
		}
	},

	addPlugin: function(n, p) { this.plugins[n] = p; this.loadingIndex--; },
	addTheme: function(n, t) { this.themes[n] = t; this.loadingIndex--; },

// *******************************************************************************
// TextEditor DOM
// *******************************************************************************
	switchClass: function(ei, c) {
		if (!this.switchClassCache) this.switchClassCache = new Array();
		var e;
		if (this.switchClassCache[ei]) e = this.switchClassCache[ei];
		else e = this.switchClassCache[ei] = $(ei);
		if (e) e.className = c;
	},

	selectElements: function(el, f) {
		var a = new Array();
		for (var i=0,el=el.split(','),l=el.length; i<l; i++)
			for (var j=0,n=this.Control.iframeDocument.getElementsByTagName(el[i]),k=n.length; j<k; j++)
				(!f || f(n[j])) && a.push(n[j]);
		return a;
	},

	selectNodes: function(n, f, a) {
		if (!a) a = new Array();
		if (f(n)) a.push(n);
		if (n.hasChildNodes())
			for (var i=0,l=n.childNodes.length; i<l; i++) this.selectNodes(n.childNodes[i], f, a);
		return a;
	},

	getNodeTree: function(n, na, t, nn) {
		return this.selectNodes(n, function(n) {
			return (!t || n.nodeType == t) && (!nn || n.nodeName == nn);
		}, na ? na : []);
	},

	getParentElement: function(n, el, f, r) {
		var re = el ? new RegExp('^(' + el.toUpperCase().replace(/,/g, '|') + ')$') : 0;

		return this.getParentNode(n, function(n) {
			return ((n.nodeType == 1 && !re) || (re && re.test(n.nodeName))) && (!f || f(n));
		}, r);
	},

	getParentNode: function(n, f, r) {
		while (n) {
			if (n == r) return null;
			if (f(n)) return n;
			n = n.parentNode;
		}
		return null;
	},

	isBlockElement: function(n) {
		return n != null && n.nodeType == 1 && this.blockRegExp.test(n.nodeName);
	},

	getParentBlockElement: function(n, r) {
		if (typeof(r) == "undefined") r = this.Control.iframeDocument.body;
		var _t = this;
		return this.getParentNode(n, function(n) {
			return _t.isBlockElement(n);
		}, r);
		return null;
	},

// *******************************************************************************
// TextEditor HTML
// *******************************************************************************
	insertHTML: function(html) { // _setHTML()
		html = this.cleanupHTMLCode(html);

		if (this.Control.iframeDocument.outerHTML) {
			var p = this.Control.iframeDocument.getElementsByTagName("P");
			for (var i=0,l=p.length; i<l; i++) {
				var n = p[i];
				while ((n = n.parentNode) != null) if (n.nodeName == "P") n.outerHTML = n.innerHTML;
				html = this.Control.iframeDocument.body.innerHTML;
			}
		}
		try {
			this.setInnerHTML(html);
		} catch (e) {
			if (this.Control.iframeDocument.body.createTextRange) this.Control.iframeDocument.body.createTextRange().pasteHTML(html);
		}
		this.cleanupAnchors();
	},

	setInnerHTML: function(h) {
		h = h.replace(/<embed([^>]*)>/gi, '<tmpembed$1>');
		h = h.replace(/<em([^>]*)>/gi, '<i$1>');
		h = h.replace(/<tmpembed([^>]*)>/gi, '<embed$1>');
		h = h.replace(/<strong([^>]*)>/gi, '<b$1>');
		h = h.replace(/<\/strong>/gi, '</b>');
		h = h.replace(/<\/em>/gi, '</i>');

		h = h.replace(/\s\/>/g, '>');
		h = h.replace(/<p([^>]*)>\u00A0?<\/p>/gi, '<p$1 keep="true">&nbsp;</p>');
		h = h.replace(/<p([^>]*)>\s*&nbsp;\s*<\/p>/gi, '<p$1 keep="true">&nbsp;</p>');
		h = h.replace(/<p([^>]*)>\s+<\/p>/gi, '<p$1 keep="true">&nbsp;</p>');

		this.Control.iframeDocument.body.innerHTML = this.uniqueTag + h;
		this.Control.iframeDocument.body.removeChild(this.Control.iframeDocument.body.firstChild);

		var p = this.Control.iframeDocument.body.getElementsByTagName("p");
		for (var i=p.length-1; i>=0; i--) {
			var n = p[i];
			if (n.nodeName == 'P' && !n.hasChildNodes() && !n.keep) n.parentNode.removeChild(n);
		}
	},

	cleanupHTMLCode: function(s) {
		s = s.replace(new RegExp('<p \\/>', 'gi'), '<p>&nbsp;</p>');
		s = s.replace(new RegExp('<p>\\s*<\\/p>', 'gi'), '<p>&nbsp;</p>');
		s = s.replace(new RegExp('<br>\\s*<\\/br>', 'gi'), '<br />');
		s = s.replace(new RegExp('<(h[1-6]|p|div|address|pre|form|table|li|ol|ul|td|b|font|em|strong|i|strike|u|span|a|ul|ol|li|blockquote)([a-z]*)([^\\\\|>]*)\\/>', 'gi'), '<$1$2$3></$1$2>');
		s = s.replace(new RegExp('\\s+></', 'gi'), '></');
		s = s.replace(new RegExp('<(img|br|hr)([^>]*)><\\/(img|br|hr)>', 'gi'), '<$1$2 />');
		s = s.replace(new RegExp('<p><hr \\/><\\/p>', 'gi'), "<hr>");
		if (document.body.outerHTML) s = s.replace(/<!(\s*)\/>/g, '');
		return s;
	},

	cleanupAnchors: function() {
		var el = this.Control.iframeDocument.getElementsByTagName("a");
		for (var i=el.length-1; i>=0; i--) {
			if (el[i].name !== '' && el[i].href == '') {
				var n = el[i].childNodes;
				for (var j=n.length-1; j>=0; j--) {
					if (el[i].nextSibling) el[i].parentNode.insertBefore(n[j], el[i].nextSibling);
					else el[i].parentNode.appendChild(n[j]);
				}
			}
		}
	},

	convertFontsToSpans: function() {
		var s = this.selectElements('span,font');
		for (var i=0,l=s.length; i<l; i++) {
			if (s[i].face !== '') {
				s[i].style.fontFamily = s[i].face;
				s[i].removeAttribute('face');
			}
			if (s[i].color !== '') {
				s[i].style.color = s[i].color;
				s[i].removeAttribute('color');
			}
		}
	},

	addButtonMap: function(m) {
		for (var i=0, l=m.length; i<l; i++) {
			var a = m[i].replace(/\s+/, '').split(',');
			this.buttonMap[i] = new Array();
			for (var j=0, k=a.length; j<k; j++) this.buttonMap[i][a[j]] = j;
		}
	},

	getButtonHTML: function(id, cmd, ui, val) {
		cmd = 'window.TextEditor.execInstanceCommand(\'' + cmd + '\'';
		if (typeof(ui) != "undefined" && ui != null) cmd += ',' + ui;
		if (typeof(val) != "undefined" && val != null) cmd += ",'" + val + "'";
		cmd += ');';

		var m, h = '', x = 0, y = 0;
		for (var i=0, l=this.buttonMap.length; i<l; i++) {
			if ((m = this.buttonMap[i][id]) != null) {
				x = 0 - (m * 20) == 0 ? '0' : 0 - (m * 20);
				y = 0 - (i * 20) == 0 ? '0' : 0 - (i * 20);
				var img = '<div style="background-position: ' + x + 'px ' + y + 'px"></div>';
				if (id == 'separator') return '<a class="Separator">' + img + '</a>';
				h += '<a id="editor_btn_' + id + '" href="javascript:void(0);" onclick="' + cmd + 'return false;" onmousedown="return false;" class="ButtonNormal" target="_self">';
				h += img + '</a>';
				break;
			}
		}
		return h;
	},

// *******************************************************************************
// TextEditor Event
// *******************************************************************************
	addEventHandlers: function() {
		var e_doc = ['keypress', 'keyup', 'keydown', 'click', 'dblclick', 'mouseup', 'mousedown', 'controlselect', 'dragdrop', 'focus', 'blur'];
		var e_body = ['mousemove', 'beforedeactivate', 'beforepaste', 'drop', 'focus', 'blur'];
		var _t = this, f = function(e) {_t.handleEvent(e)};
		for (var i=0,l=e_doc.length; i<l; i++) this.addEvent(this.Control.iframeDocument, e_doc[i], f.bind(this));
		for (var i=0,l=e_body.length; i<l; i++) this.addEvent(this.Control.iframeDocument.body, e_body[i], f.bind(this));
		this.Control.setDesignMode();
	},

	resetForm: function() {
		this.Control.iframeDocument.body.innerHTML = this.startContent;
	},
	submitPatch: function() { this.formSubmit(true); },
	formSubmit: function(p) {
		if (this.form) {
			this.triggerSave();
			var res = true;
			if (typeof(this.form.onsubmit) == 'function') res = this.form.onsubmit();
			if (res && this.form.oldSubmit && p) this.form.oldSubmit();
		}
	},

	handleEvent: function(e) {
		if (!e.target) e.target = e.srcElement || window.event.srcElement;
		if (this.executeCallback('handle_event_callback', 'handleEvent', e)) return false;

		switch (e.type) {
			case "beforedeactivate":
			case "blur":
				this.execCommand('EndTyping');
				this.hideMenus();
				return;
			case "drop":
			case "beforepaste":
				return;
			case "submit":
				this.formSubmit();
				return;
			case "reset":
				this.resetForm();
				return;
			case "keypress":
				if (e.keyCode == 13 && e.ctrlKey) {
					this.formSubmit();
					return;
				}
				if (e.keyCode == 13 && !e.shiftKey) {
					if (this.ForceParagraphs.add()) {
						this.execCommand("AddUndoLevel");
						return this.cancelEvent(e);
					}
				}
				if (e.keyCode == 8 || e.keyCode == 46) {
					if (!e.shiftKey) this.ForceParagraphs.del();
					this.selectedElement = e.target;
					this.linkElement = this.getParentElement(e.target, "a");
					this.imgElement = this.getParentElement(e.target, "img");
					this.triggerNodeChange(false);
				}
				return false;
			case "keyup":
			case "keydown":
				this.hideMenus();
				if ((e.keyCode == 8 || e.keyCode == 46) && !e.shiftKey) this.ForceParagraphs.del();

				this.selectedElement = null;
				this.selectedNode = null;
				var elm = this.Control.Selection.getFocusElement();
				this.linkElement = this.getParentElement(elm, "a");
				this.imgElement = this.getParentElement(elm, "img");
				this.selectedElement = elm;

				if (e.type == "keydown" && e.keyCode == 13) this.enterKeyElement = this.Control.Selection.getFocusElement();
				if (e.type == "keyup" && e.keyCode == 13 && !e.ctrlKey) {
					var elm = this.enterKeyElement;
					if (elm) {
						var re = new RegExp('^HR|IMG|BR$','g');
						var dre = new RegExp('^H[1-6]$','g');
						if (!elm.hasChildNodes() && !re.test(elm.nodeName)) {
							if (dre.test(elm.nodeName)) elm.innerHTML = "&nbsp;&nbsp;";
							else elm.innerHTML = "&nbsp;";
						}
					}
				}

				var keys = this.posKeyCodes;
				var posKey = false;
				for (i=0; i<keys.length; i++) {
					if (keys[i] == e.keyCode) {
						posKey = true;
						break;
					}
				}

				keys = [8, 46]; // Backspace,Delete
				for (i=0; i<keys.length; i++) {
					if ((keys[i] == e.keyCode) && (e.type == "keyup")) this.triggerNodeChange(false);
				}
				if (e.keyCode == 17) return true;

				if (!posKey && e.type == "keyup" && !e.ctrlKey || (e.ctrlKey && e.keyCode == 86)) this.execCommand("StartTyping");
				if (e.type == "keydown" && (posKey || e.ctrlKey)) this.undoBookmark = this.Control.Selection.getBookmark();
				if (e.type == "keyup" && (posKey || e.ctrlKey)) this.execCommand("EndTyping");
				if ((e.type == "keyup" && posKey) || e.ctrlKey) {
					var _t = this, tnc = function(e){ _t.triggerNodeChange(false); };
					window.setTimeout(tnc.bind(this), 1);
				}
			break;
			case "mousedown":
			case "mouseup":
			case "click":
			case "dblclick":
			case "focus":
				this.hideMenus();
				var targetBody = this.getParentElement(e.target, "html");
				this.Control.autoResetDesignMode();
				if (this.Control.iframeDocument.body.parentNode == targetBody) {
					this.selectedElement = e.target;
					this.linkElement = this.getParentElement(this.selectedElement, "a");
					this.imgElement = this.getParentElement(this.selectedElement, "img");
				}
				if (!this.Control.undoRedo.undoLevels[0].bookmark && (e.type == "mouseup" || e.type == "dblclick")) this.Control.undoRedo.undoLevels[0].bookmark = this.Control.Selection.getBookmark();
				if (e.type != "focus") this.selectedNode = null;
				if (e.type == "mousedown" || e.type == "focus") this.undoBookmark = this.Control.Selection.getBookmark();
				this.triggerNodeChange(false);
				this.execCommand("EndTyping");
				if (e.type == "mouseup") this.execCommand("AddUndoLevel");
				return false;
		}
	},

	addEvent: function(o, n, h) {
		if (n != 'unload') {
			var _t = this;
			function clean() {
				var ex;
				try {
					_t.removeEvent(o, n, h);
					_t.removeEvent(window, 'unload', clean);
					o = n = h = null;
				} catch (ex) {}
			}
			this.addEvent(window, 'unload', clean);
		}
		if (o.addEventListener) o.addEventListener(n, h, false);
		else o.attachEvent("on" + n, h);
	},

	removeEvent: function(o, n, h) {
		if (o.removeEventListener) o.removeEventListener(n, h, false);
		else o.detachEvent("on" + n, h);
	},

	cancelEvent: function(e) {
		if (!e) return false;

		e.returnValue = false;
		e.cancelBubble = true;
		e.preventDefault && e.preventDefault();
		e.stopPropagation && e.stopPropagation();
		return false;
	},

	dispatchCallback: function(p, n) {
		return this.callFunc(p, n, 0, this.dispatchCallback.arguments);
	},
	executeCallback: function(p, n) {
		return this.callFunc(p, n, 1, this.executeCallback.arguments);
	},
	execCommandCallback: function(p, n) {
		return this.callFunc(p, n, 2, this.execCommandCallback.arguments);
	}
};

// *******************************************************************************
// TextEditor_WindowManager
// *******************************************************************************
function TextEditor_WindowManager(obj) {
	this.Editor = obj;
	this.Control = this.Editor.Control;
	this.bookmark = null;
	this.mask = null;
	this.dimArea = {};
	this.win = {};
	this.moving = null;
	this.opened = null;
}

TextEditor_WindowManager.prototype = {
	open: function(w, p) {
		this.bookmark = this.Control.Selection.getBookmark();
		this.opened = w.name;
		this.setMask();
		var _t = this;

		if (!this.win[w.name]) {
			var l = parseInt((this.dimArea.width / 2) - (w.width / 2)) + this.dimArea.left;
			var t = parseInt((this.dimArea.height / 2) - (w.height / 2)) + this.dimArea.top;

			var div = document.createElement('div');
			div.className = "EditorFloatWin";
			div.style.position = 'absolute';
			div.style.display = 'none';
			div.style.left = l + 'px';
			div.style.top = t + 'px';
			div.style.width = w.width + 'px';
			div.style.height = w.height + 'px';
			document.body.appendChild(div);
			insertHTML(div, '<div class="EditorFloatWin_title">' + w.title + '</div>');

			var tpl = (typeof w.tpl != 'function') ? w.tpl : w.tpl.apply(this);
			this.win[w.name] = { div: div, tpl: tpl, left: l, top: t };

			window.setTimeout(function(){_t.addTitleEvent(div);}, 10);
		}

		var html = this.win[w.name].tpl.replace(new RegExp('\\{\\$([a-z0-9_]+)\\}', 'gi'), function(m, s) {
			if (p[s] != 'undefined') return p[s];
			if (w[s] != 'undefined') return w[s];
			return m;
		});
		Array.from(this.win[w.name].div.childNodes).slice(1).each(function(c){_t.win[w.name].div.removeChild(c)});
		insertHTML(this.win[w.name].div, html);

		this.win[w.name].div.style.display = '';
		this.Editor.addEvent(document, "keydown", this.winClose.bind(this));
		window.setTimeout(this.winFocus.bind(this),10);
		return this.win[w.name];
	},

	close: function() {
		var param = {};
		if (this.opened && this.opened != null) {
			param = this.getParam();
			this.win[this.opened].div.style.display = 'none';
			this.opened = null;
			this.mask.style.display = 'none';
			this.Control.iframeWindow.focus();
			if (this.bookmark) {
				this.Control.Selection.moveToBookmark(this.bookmark);
				this.bookmark = null;
			}
		}
		this.Control.autoResetDesignMode();
		return param;
	},

	getParam: function() {
		var ret = {};
		function _getV(a) {
			if (!a.name) return;
			switch (a.tagName.toLowerCase()) {
				case 'select':
					if (!a.multiple) {
						var i = a.selectedIndex;
						if (i >= 0) {
							var opt = a.options[i];
							ret[a.name] = opt.value || opt.text;
						}
					} else {
						if (!a.length) break;
						var v = new Array();
						for (var i=0, l=a.length; i<l; i++) {
							var opt = a.options[i];
							v.push(opt.value || opt.text);
						}
						ret[a.name] = v;
					}
					break;
				default:
					if (a.type && /checkbox|radio/.test(a.type.toLowerCase())) { if (a.checked) ret[a.name] = a.value; }
					else ret[a.name] = a.value;
					break;
			}
		}

		Array.from(this.win[this.opened].div.getElementsByTagName('input')).each(_getV);
		Array.from(this.win[this.opened].div.getElementsByTagName('select')).each(_getV);
		Array.from(this.win[this.opened].div.getElementsByTagName('textarea')).each(_getV);
		return ret;
	},

	addTitleEvent: function(div) {
		this.Editor.addEvent(div.firstChild, "mousedown", this.startMove.bind(this));
		this.Editor.addEvent(div.firstChild, "mouseup", this.endMove.bind(this));
		this.Editor.addEvent(div.firstChild, "mousemove", this.winMove.bind(this));
	},

	winFocus: function() {
		var firstEl = null, btnOK = null;
		function _getEl(el) {
			if (firstEl == null && 'hidden' != el.type && !el.disabled) firstEl = el;
			if (btnOK == null && 'button' == el.type && !/close/.test(el.onclick)) btnOK = el;
		}

		if (this.opened && this.opened != null) {
			this.win[this.opened].btnOK = null;
			this.win[this.opened].div.focus();
			if (firstEl == null) Array.from(this.win[this.opened].div.getElementsByTagName('input')).each(_getEl);
			if (firstEl == null) Array.from(this.win[this.opened].div.getElementsByTagName('select')).each(_getEl);
			if (firstEl == null) Array.from(this.win[this.opened].div.getElementsByTagName('textarea')).each(_getEl);
			if (firstEl != null) firstEl.focus();
			this.win[this.opened].btnOK = btnOK;
		}
	},

	winClose: function(e) {
		if (e.keyCode == 27 && this.close()) return this.Editor.cancelEvent(e);
		if (e.keyCode == 13 && this.win[this.opened] && this.win[this.opened].btnOK != null) {
			this.win[this.opened].btnOK.click();
			return this.Editor.cancelEvent(e);
		}
		return true;
	},

	startMove: function(e) {
		this.moving = { x: e.screenX, y: e.screenY };
		return this.Editor.cancelEvent(e);
	},
	endMove: function(e) {
		this.moving = null;
		return this.Editor.cancelEvent(e);
	},
	winMove: function(e) {
		if (!this.moving) return;
		var dx = e.screenX - this.moving.x, dy = e.screenY - this.moving.y;
		this.moving.x = e.screenX; this.moving.y = e.screenY;
		var el = $(e.target || e.srcElement || window.event.srcElement);
		while (el.parentNode && (el.className != "EditorFloatWin")) el = el.parentNode;
		if (el && (el.className == "EditorFloatWin")) {
			var l = parseInt(el.style.left) + dx, t = parseInt(el.style.top) + dy;
			var r = l + parseInt(el.style.width), b = t + parseInt(el.style.height);
			if ((l > this.dimArea.left) && (r < this.dimArea.left+this.dimArea.width-2)) el.style.left = l + 'px';
			if ((t > this.dimArea.top) && (b < this.dimArea.top+this.dimArea.height-2)) el.style.top = t + 'px';
		}
		return this.Editor.cancelEvent(e);
	},

	setMask: function() {
		if (!this.mask) {
			var div = document.createElement('div');
			div.className = "EditorMask";
			div.style.position = 'absolute';
			document.body.appendChild(div);
			this.mask = div;
		}
		this.dimArea = this._getDimArea('editor_parent');
		this.mask.style.left = (this.dimArea.left + 1) + 'px';
		this.mask.style.top = (this.dimArea.top + 1) + 'px';
		this.mask.style.width = (this.dimArea.width - 2) + 'px';
		this.mask.style.height = (this.dimArea.height - 2) + 'px';
		this.mask.style.display = '';
	},

	_getDimArea: function(el) {
		el = $(el);
		var vT = 0, vL = 0, vW = el.offsetWidth, vH = el.offsetHeight;
		do {
			vT += el.offsetTop  || 0;
			vL += el.offsetLeft || 0;
			el = el.offsetParent;
			if (el) {
				if (el.tagName == 'BODY') break;
				var s = getStyle(el, 'position');
				if (s == 'absolute') break;
			}
		} while (el);
		return { left: vL, top: vT, width: vW, height: vH };
	}
};

// *******************************************************************************
// TextEditor_Control
// *******************************************************************************
function TextEditor_Control(editor) {
	this.Editor = editor;
	this.textarea = $(this.Editor.textarea_name);

	this.undoRedoLevel = true;
	this.Selection = new TextEditor_Selection(this);
	this.undoRedo = new TextEditor_UndoRedo(this);
	this.Cleanup = new TextEditor_Cleanup(this, {
		remove_linebreaks: this.Editor.options.remove_linebreaks,
		valid_elements: this.Editor.options.valid_elements,
		invalid_elements: this.Editor.options.invalid_elements
	});

	return this.init();
}

TextEditor_Control.prototype = {
	options: {
		doctype: '<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.0 Transitional//EN">',
		width: 0,
		height: 0
	},

	init: function() {
		Object.extend(this.options, this.Editor.options || {});
		var tpl = this.Editor.themes[this.options.theme];

		var html = '<div id="editor_parent" class="EditorParent">' + tpl.getEditorTemplate(this.options) + '</div>';
		if (!this.options.default_document) this.options.default_document = this.Editor.baseDOC + "/blank.htm";

		if (this.options.width == 0) this.options.width = (this.textarea.offsetWidth ? this.textarea.offsetWidth + 'px' : 0);
		if (this.options.height == 0) this.options.height = (this.textarea.offsetHeight ? this.textarea.offsetHeight + 'px' : 0);
		var aw = getStyle(this.textarea, "width"), ah = getStyle(this.textarea, "height");
		if (this.options.width == 0 || aw.indexOf('%') != -1) this.options.width = aw;
		if (this.options.height == 0 || ah.indexOf('%') != -1) this.options.height = ah;

		html = this.applyTemplate(html);
		this.Editor.startContent = this.textarea.value;

		insertHTML(this.textarea, html, "before");
		return this.initIframe();
	},

	initIframe: function() {
		// Setup iframe
		var iframe = this._createIFrame($('editor_holster'), document.parentWindow || window);
		setHide(this.textarea);
		$('editor_parent').style.width = this.options.width;
//		$('editor_parent').style.height = this.options.height;
		this.iframeElement = iframe;
		this.iframeWindow = this.iframeElement.contentWindow || this.iframeElement.window;
		this.iframeDocument = this.iframeElement.contentDocument || this.iframeWindow.document;

		var content = this.options.doctype + '<html><head xmlns="http://www.w3.org/1999/xhtml"><title>blank_page</title><meta http-equiv="Content-Type" content="text/html; charset=windows-1251"></head><body class="ContentBody"></body></html>';
		try {
			this.iframeDocument.open("text/html","replace");
			this.iframeDocument.write(content);
			this.iframeDocument.close();
		} catch (e) {
			this.iframeDocument.location.href = this.options.default_document;
		}
		window.setTimeout(this.setDesignMode.bind(this), 1);
		window.setTimeout(this.Editor.setupContent.bind(this.Editor), 10);
		return this;
	},

	execCommand: function(command, user_interface, value) {
		var focusElm = this.Selection.getFocusElement();
//		debug("[execCommand]: " + focusElm);
		if (!new RegExp('StartTyping|EndTyping|BeginUndoLevel|EndUndoLevel|AddUndoLevel', 'gi').test(command)) this.Editor.undoBookmark = null;
		if (!/StartTyping|EndTyping/.test(command)) {
			if (this.Editor.execCommandCallback('execcommand_callback', 'execCommand', command, user_interface, value)) return;
		}
		if (focusElm && focusElm.nodeName == "IMG") {
			var align = focusElm.getAttribute('align');
			var img = command == "JustifyCenter" ? focusElm.cloneNode(false) : focusElm;
			switch (command) {
				case "JustifyLeft":
				case "JustifyRight":
					var oa = command == "JustifyLeft" ? 'left' : 'right';
					if (align == oa) {
						img.setAttribute('align', '');
						img.removeAttribute('align');
					} else img.setAttribute('align', oa);

					var div = focusElm.parentNode;
					if (div && div.nodeName == "DIV" && div.childNodes.length == 1 && div.parentNode) div.parentNode.replaceChild(img, div);
					this.Selection.selectNode(img);
					this.Editor.repaint();
					this.Editor.triggerNodeChange();
					return;
				case "JustifyCenter":
					img.setAttribute('align', '');
					img.removeAttribute('align');

					var div = this.Editor.getParentElement(focusElm, "div");
					if (div && div.style.textAlign == "center") {
						if (div.nodeName == "DIV" && div.childNodes.length == 1 && div.parentNode) div.parentNode.replaceChild(img, div);
					} else {
						div = this.iframeDocument.createElement("div");
						div.style.textAlign = 'center';
						div.appendChild(img);
						focusElm.parentNode.replaceChild(div, focusElm);
					}
					this.Selection.selectNode(img);
					this.Editor.repaint();
					this.Editor.triggerNodeChange();
					return;
			}
		}

		var _t = this.Editor, tnc = function(e){ _t.triggerNodeChange(false); };
		switch (command) {
			case "closeWin":
				this.Editor.WindowManager.close();
				break;
			case "insertLink":
				var param = this.Editor.WindowManager.close();
				this.Editor.execCommand('BeginUndoLevel');
				var elm = this.Selection.getFocusElement();
				this.Editor.linkElement = this.Editor.getParentElement(elm, "a");
				this.Editor.imgElement = this.Editor.getParentElement(elm, "img");
				if (!this.Editor.linkElement) {
					if (param.href) {
						this.Editor.execCommand('CreateLink', false, this.Editor.uniqueURL);
						Array.from(this.iframeDocument.getElementsByTagName('a')).each(function(a) { if (a.href == _t.uniqueURL) {a.href = param.href; a.target = param.target;} });
					}
				} else {
					if (param.href) {
						this.Editor.linkElement.href = param.href;
						this.Editor.linkElement.target = param.target;
					} else return this.execCommand('unlink', false);
				}
				this.Editor.triggerNodeChange();
				return true;
			case "Repaint":
				this.Editor.repaint();
				return true;
			case "JustifyLeft":
			case "JustifyCenter":
			case "JustifyFull":
			case "JustifyRight":
				var el = this.Editor.getParentNode(focusElm, function(n) {return n.getAttribute('align') || '';});
				if (el) {
					el.setAttribute('align', '');
					el.removeAttribute('align');
				} else this.iframeDocument.execCommand(command, user_interface, value);
				this.Editor.triggerNodeChange();
				return true;
			case "unlink":
				var sel = this.Selection.getSel();
				if (sel.collapseToEnd && sel.isCollapsed) {
					focusElm = this.Editor.getParentElement(focusElm, 'A');
					if (focusElm) this.Selection.selectNode(focusElm, false);
				}
				this.iframeDocument.execCommand(command, user_interface, value);
				if (sel.collapseToEnd) sel.collapseToEnd();
				this.Editor.triggerNodeChange();
				return true;
			case "InsertUnorderedList":
			case "InsertOrderedList":
				this.iframeDocument.execCommand(command, user_interface, value);
				this.Editor.triggerNodeChange();
				break;
			case "Strikethrough":
				this.iframeDocument.execCommand(command, user_interface, value);
				this.Editor.triggerNodeChange();
				break;
			case "SelectNode":
				this.Selection.selectNode(value);
				this.Editor.triggerNodeChange();
				this.Editor.selectedNode = value;
				break;
			case "FormatBlock":
//...
				this.Editor.triggerNodeChange();
				break;
			case "RemoveNode":
				if (!value) value = this.Editor.getParentElement(this.Selection.getFocusElement());
				if (value && value.outerHTML) {
					value.outerHTML = value.innerHTML;
				} else {
					var rng = value.ownerDocument.createRange();
					rng.setStartBefore(value);
					rng.setEndAfter(value);
					rng.deleteContents();
					rng.insertNode(rng.createContextualFragment(value.innerHTML));
				}
				this.Editor.triggerNodeChange();
				break;
			case "SelectNodeDepth":
				var parentNode = this.Selection.getFocusElement();
				for (var i=0; parentNode; i++) {
					if (parentNode.nodeName.toLowerCase() == "body") break;
					if (parentNode.nodeName.toLowerCase() == "#text") {
						i--;
						parentNode = parentNode.parentNode;
						continue;
					}
					if (i == value) {
						this.Selection.selectNode(parentNode, false);
						this.Editor.triggerNodeChange();
						this.Editor.selectedNode = parentNode;
						return;
					}
					parentNode = parentNode.parentNode;
				}
				break;
			case "SetStyleInfo":
//...
				this.Editor.triggerNodeChange();
				break;
			case "FontName":
				if (value == null) {
					var sel = this.getSel();
					if (window.getSelection && sel.isCollapsed) {
						var f = this.Editor.getParentElement(this.Selection.getFocusElement(), "font");
						if (f != null) this.Selection.selectNode(f, false);
					}
					this.iframeDocument.execCommand("RemoveFormat", false, null);
					if (f != null && window.getSelection) {
						var r = this.getRng().cloneRange();
						r.collapse(true);
						s.removeAllRanges();
						s.addRange(r);
					}
				} else this.iframeDocument.execCommand('FontName', false, value);
				window.setTimeout(tnc.bind(_t), 1);
				return;
			case "FontSize":
				this.iframeDocument.execCommand('FontSize', false, value);
				window.setTimeout(tnc.bind(_t), 1);
				return;
			case "forecolor":
//...
				break;
			case "HiliteColor":
//...
				break;
			case "Cut":
			case "Copy":
			case "Paste":
				var cmdFailed = false;
				eval('try {this.getDoc().execCommand(command, user_interface, value);} catch (e) {cmdFailed = true;}');
				if (cmdFailed) return;
				else this.Editor.triggerNodeChange();
				break;
			case "SetContent":
				if (!value) value = "";
				value = this.Cleanup._customCleanup("insert_to_editor", value);
				if (this.iframeDocument.body.nodeName == 'BODY') this.Editor.insertHTML(value);
				else this.iframeDocument.body.innerHTML = value;
				this.Editor.setInnerHTML(this.Cleanup._cleanupHTML(this.iframeDocument.body, false, true));
				this._addBogusBR();
				return true;
			case "Cleanup":
				var b = this.Selection.getBookmark();
				this.Editor.insertHTML(this.iframeDocument.body.innerHTML);
				this.Editor.setInnerHTML(this.Cleanup._cleanupHTML(this.iframeDocument.body, false, true));
				this._addBogusBR();
				this.Editor.repaint();
				this.Selection.moveToBookmark(b);
				this.Editor.triggerNodeChange();
				break;
			case "ReplaceContent":
				if (!value) value = '';
				this.iframeWindow.focus();
				var selectedText = "";
				if (window.getSelection) selectedText = this.Selection.getSel().toString();
				else {
					var rng = doc.selection.createRange();
					selectedText = rng.text;
				}
				if (selectedText.length > 0) {
					value = value.replace(new RegExp('{\\\$selection}', 'g'), selectedText);
					this.Editor.execCommand('InsertContent', false, value);
				}
				this._addBogusBR();
				this.Editor.triggerNodeChange();
				break;
			case "InsertContent":
				if (!value) value = '';
				var r = this.Selection.getRng();
				if (r.insertNode) {
					r.deleteContents();
					r.insertNode(this.Selection.getRng().createContextualFragment(value));
				} else {
					if (r.item) r.item(0).outerHTML = value;
					else r.pasteHTML(value);
				}
				this.execCommand("AddUndoLevel");
				this.Editor.triggerNodeChange();
				break;
			case "StartTyping":
				if (this.undoRedo.typingUndoIndex == -1) {
					this.undoRedo.typingUndoIndex = this.undoRedo.undoIndex;
					this.execCommand('AddUndoLevel');
				}
				break;
			case "EndTyping":
				if (this.undoRedo.typingUndoIndex != -1) {
					this.execCommand('AddUndoLevel');
					this.undoRedo.typingUndoIndex = -1;
				}
				break;
			case "BeginUndoLevel":
				this.undoRedoLevel = false;
				break;
			case "EndUndoLevel":
				this.undoRedoLevel = true;
				this.execCommand('AddUndoLevel');
				break;
			case "AddUndoLevel":
				if (this.undoRedoLevel && this.undoRedo.add()) this.Editor.triggerNodeChange(false);
				break;
			case "Undo":
				this.execCommand("EndTyping");
				this.undoRedo.undo();
				this.Editor.triggerNodeChange();
				break;
			case "Redo":
				this.execCommand("EndTyping");
				this.undoRedo.redo();
				this.Editor.triggerNodeChange();
				break;
			case "Indent":
				this.iframeDocument.execCommand(command, user_interface, value);
				this.Editor.triggerNodeChange();
				var n = this.Editor.getParentElement(this.Selection.getFocusElement(), "blockquote");
				do {
					if (n && n.nodeName == "BLOCKQUOTE") {
						n.removeAttribute("dir");
						n.removeAttribute("style");
					}
				} while (n != null && (n = n.parentNode) != null);
				break;
			case "RemoveFormat":
			case "removeformat":
				var text = this.Selection.getSelectedText();
//...
				this.Editor.triggerNodeChange();
				break;

			default:
				this.iframeDocument.execCommand(command, user_interface, value);
				window.setTimeout(tnc.bind(_t), 1);
		}
		if (command != "AddUndoLevel" && command != "Undo" && command != "Redo" && command != "StartTyping" && command != "EndTyping") this.execCommand("AddUndoLevel");
	},

	resizeToContent: function() {
//...
	},

	setDesignMode: function() {
		if (!$('editor_frame').offsetWidth) return;
		try {
			this.iframeDocument.designMode = "On";
			this.iframeDocument.execCommand("useCSS", false, true);
		} catch(e) { }
	},

	autoResetDesignMode: function() {
		this.setDesignMode();
//			eval('try { this.getDoc().designMode = "On"; this.useCSS = false; } catch(e) {}');
	},

	_addBogusBR: function() {
		var b = this.iframeDocument.body;
		if (window.getSelection && !b.hasChildNodes()) b.innerHTML = '<br _moz_editor_bogus_node="TRUE" />';
	},

	_createIFrame: function(el, win) {
		var name = 'editor_frame', iframe = document.createElement("iframe");
		iframe.setAttribute("id", name);
		iframe.setAttribute("name", name);
		iframe.className = "EditorIframe";
		iframe.border = "0";
		iframe.frameBorder = "0";
		iframe.marginWidth = "0";
		iframe.marginHeight = "0";
		iframe.leftMargin = "0";
		iframe.topMargin = "0";
		iframe.style.width = '100%';
		iframe.style.height = '100%';
		iframe.setAttribute("allowtransparency", "true");
		if (this.options.auto_resize) iframe.setAttribute("scrolling", "no");
		iframe.setAttribute("src", this.options.default_document);
		el.parentNode.replaceChild(iframe, el);
		if (document.frames) iframe = document.frames[name];
		return iframe;
	},

	applyTemplate: function(h) {
		var _t = this;
		return h.replace(new RegExp('\\{\\$([a-z0-9_]+)\\}', 'gi'), function(m, s) {
			if (_t.options[s]) return _t.options[s];
			return m;
		});
	}
};

// *******************************************************************************
// TextEditor_Cleanup
// *******************************************************************************
function TextEditor_Cleanup(obj, options) {
	this.Control = obj;
	this.Editor = this.Control.Editor;
	this.isIE = (navigator.appName == "Microsoft Internet Explorer");
	this.rules = new Array();
	this.options = {
		newline_before_elements: 'h1,h2,h3,h4,h5,h6,pre,address,div,ul,ol,li,meta,option,area,title,link,base,script,td',
		newline_after_elements: 'br,hr,p,pre,address,div,ul,ol,meta,option,area,link,base,script',
		newline_before_after_elements: 'html,head,body,table,thead,tbody,tfoot,tr,form,ul,ol,blockquote,p,object,param,hr,div',
		valid_elements: '*[*]',
		invalid_elements: ''
	};
	this.vElements = new Array();
	this.vElementsRe = '';
	this.closeElementsRe = /^(IMG|BR|HR|LINK|META|BASE|INPUT|AREA)$/;
	this.codeElementsRe = /^(SCRIPT|STYLE)$/;
	this.serializationId = 0;
	return this.init(options);
}

TextEditor_Cleanup.prototype = {
	init: function(options) {
		Object.extend(this.options, options || {});
		this.nlBeforeRe = this._arrayToRe(this.options.newline_before_elements.split(','), 'gi', '<(',  ')([^>]*)>');
		this.nlAfterRe = this._arrayToRe(this.options.newline_after_elements.split(','), 'gi', '<(',  ')([^>]*)>');
		this.nlBeforeAfterRe = this._arrayToRe(this.options.newline_before_after_elements.split(','), 'gi', '<(\\/?)(', ')([^>]*)>');
		this.serializedNodes = new Array();
		this.serializationId = 0;
		this.idCount = 0;
		this.xmlEncodeRe = new RegExp('[<>&"]', 'g');
		this.addRuleStr(this.options.valid_elements);
	},

	addRuleStr: function(s) {
		var r = this.parseRuleStr(s);
		for (var n in r) if (r[n]) this.rules[n] = r[n];
		for (var n in this.rules) if (this.rules[n] && this.rules[n].tag) this.vElements.push(this.rules[n].tag);
		this.vElementsRe = this._arrayToRe(this.vElements, '');
	},

	parseRuleStr: function(s) {
		var or = new Array();
		if (s == null || s.length == 0) return or;

		var ta = s.split(',');
		for (var i=0, l=ta.length; i<l; i++) {
			s = ta[i];
			if (!s || s.length == 0) continue;

			var p = s.split(/\[|\]/);
			var t = (p == null || p.length < 1) ? s.toUpperCase() : p[0].toUpperCase();
			var tn = t.split('/');
			for (var j=0, k=tn.length; j<k; j++) {
				if (!tn[j] || tn[j] == '') continue;
				var r = {};
				r.tag = tn[j];
				r.forceAttribs = null;
				r.defaultAttribs = null;
				r.validAttribValues = null;
				var px = r.tag.charAt(0);
				r.forceOpen = px == '+';
				r.removeEmpty = px == '-';
				r.fill = px == '#';
				r.tag = r.tag.replace(/\+|-|#/g, '');
				r.oTagName = tn[0].replace(/\+|-|#/g, '').toLowerCase();
				r.isWild = new RegExp('\\*|\\?|\\+', 'g').test(r.tag);
				r.validRe = new RegExp(this._wildcardToRe('^' + r.tag + '$'));
				if (p.length > 1) {
					r.vAttribsRe = '^(';
					var a = p[1].split("|");
					for (var x=0; x<a.length; x++) {
						t = a[x];
						if (t.charAt(0) == '!') {
							a[x] = t = t.substring(1);
							if (!r.reqAttribsRe) r.reqAttribsRe = '\\s+(' + t;
							else r.reqAttribsRe += '|' + t;
						}
						var av = new RegExp('(=|:|<)(.*?)$').exec(t);
						t = t.replace(new RegExp('(=|:|<).*?$'), '');
						if (av && av.length > 0) {
							if (av[0].charAt(0) == ':') {
								if (!r.forceAttribs) r.forceAttribs = new Array();
								r.forceAttribs[t.toLowerCase()] = av[0].substring(1);
							} else if (av[0].charAt(0) == '=') {
								if (!r.defaultAttribs) r.defaultAttribs = new Array();
								var dv = av[0].substring(1);
								r.defaultAttribs[t.toLowerCase()] = (dv == '') ? "_empty" : dv;
							} else if (av[0].charAt(0) == '<') {
								if (!r.validAttribValues) r.validAttribValues = new Array();
								r.validAttribValues[t.toLowerCase()] = this._arrayToRe(av[0].substring(1).split('?'), 'i');
							}
						}
						r.vAttribsRe += '' + t.toLowerCase() + (x != a.length - 1 ? '|' : '');
						a[x] = t.toLowerCase();
					}
					if (r.reqAttribsRe) r.reqAttribsRe = new RegExp(r.reqAttribsRe + ')=\"', 'g');
					r.vAttribsRe += ')$';
					r.vAttribsRe = this._wildcardToRe(r.vAttribsRe);
					r.vAttribsReIsWild = new RegExp('\\*|\\?|\\+', 'g').test(r.vAttribsRe);
					r.vAttribsRe = new RegExp(r.vAttribsRe);
					r.vAttribs = a.reverse();
				} else {
					r.vAttribsRe = '';
					r.vAttribs = new Array();
					r.vAttribsReIsWild = false;
				}
				or[r.tag] = r;
			}
		}
		return or;
	},

	_cleanupHTML: function(elm, on_save, inn) {
		on_save = typeof(on_save) == 'undefined' ? false : on_save;
		inn = typeof(inn) == 'undefined' ? false : inn;

		this.Editor.convertFontsToSpans();
		this._fixListElements();
		if (inn) this._fixTags();
		this._customCleanup(on_save ? "get_from_editor_dom" : "insert_to_editor_dom", this.Control.iframeDocument.body);

		this.on_save = on_save;
		this.idCount = 0;
		this.serializationId++;
		this.serializedNodes = new Array();
		this.sourceIndex = -1;
		var h = this.serializeNode(elm, inn);

		h = h.replace(/<\/?(body|head|html)[^>]*>/gi, '');
		h = h.replace(new RegExp(' (rowspan="1"|colspan="1")', 'g'), '');
		h = h.replace(/<p><hr\s*\/*><\/p>/g, '<hr />');
		h = h.replace(/<p>(&nbsp;|&#160;)<\/p><hr\s*\/*><p>(&nbsp;|&#160;)<\/p>/g, '<hr />');
		h = h.replace(/<td>\s*<br\s*\/*>\s*<\/td>/g, '<td>&nbsp;</td>');
		h = h.replace(/<p>\s*<br\s*\/*>\s*<\/p>/g, '<p>&nbsp;</p>');
		h = h.replace(/<br\s*\/*>$/, ''); // Remove last BR for Gecko
		h = h.replace(/<br\s*\/*><\/p>/g, '</p>'); // Remove last BR in P tags for Gecko
		h = h.replace(/<p>\s*(&nbsp;|&#160;)\s*<br\s*\/*>\s*(&nbsp;|&#160;)\s*<\/p>/g, '<p>&nbsp;</p>');
		h = h.replace(/<p>\s*(&nbsp;|&#160;)\s*<br\s*\/*>\s*<\/p>/g, '<p>&nbsp;</p>');
		h = h.replace(/<p>\s*<br\s*\/*>\s*&nbsp;\s*<\/p>/g, '<p>&nbsp;</p>');
		h = h.replace(new RegExp('<a>(.*?)<\\/a>', 'g'), '$1');
		h = h.replace(/<p([^>]*)>\s*<\/p>/g, '<p$1>&nbsp;</p>');
		if (/^\s*(<br\s*\/*>|<p>&nbsp;<\/p>|<p>&#160;<\/p>|<p><\/p>)\s*$/.test(h)) h = '';
		h = h.replace(/<br\s*\/*>\s*<\/li>/g, '</li>');
		h = h.replace(/&nbsp;\s*<\/(dd|dt)>/g, '</$1>');
		h = h.replace(/<o:p _moz-userdefined=""\s*\/*>/g, '');
		h = h.replace(/<td([^>]*)>\s*<br\s*\/*>\s*<\/td>/g, '<td$1>&nbsp;</td>');
		h = this._customCleanup(on_save ? "get_from_editor" : "insert_to_editor", h);
		if (this.options.remove_linebreaks) h = h.replace(/\n|\r/g, ' ');
		return h;
		/* */
	},

	_customCleanup: function(type, html) {
		if (this.Editor.options.cleanup_callback) html = this.Editor.options.cleanup_callback(type, html);

		var t = this.Editor.themes[this.Editor.options.theme];
		if (t && t.cleanup) html = t.cleanup(type, html);

		var p = this.Editor.plugins;
		for (var i=0,l=p.length; i<l; i++) {
			p = this.Editor.plugins[p[i]];
			if (p && p.cleanup) html = p.cleanup(type, html);
		}
		return html;
	},

	serializeNode: function(n, inn) {
		if (this._isDuplicate(n)) return '';

		var en, st, t, hc, h = '', f = false, va = false;
		if (n.parentNode && this.childRules != null) {
			var cr = this.childRules[n.parentNode.nodeName];
			if (typeof(cr) != "undefined" && !cr.test(n.nodeName)) {
				st = true;
				t = null;
			}
		}

		switch (n.nodeType) {
			case 1:
				hc = n.hasChildNodes();
				if (st) break;
				nn = n.nodeName;

				if (n.nodeName.indexOf('/') != -1) break;
				if (n.scopeName && n.scopeName != 'HTML') nn = n.scopeName.toUpperCase() + ':' + nn.toUpperCase();
				if (nn.indexOf(':') > 0) nn = nn.toUpperCase();
				if (this.on_save && nn == 'FONT') nn = 'SPAN';

				if (this.vElementsRe.test(nn) && !inn) {
					va = true;
					var r = this.rules[nn];
					if (!r) {
						for (var i=0, l=this.rules.length; i<l; i++)
							if (this.rules[i].validRe.test(nn)) {
								r = this.rules[i];
								break;
							}
					}

					en = r.isWild ? nn.toLowerCase() : r.oTagName;
					f = r.fill;
					if (r.removeEmpty && !hc) return "";
					t = '<' + en;
					if (r.vAttribsReIsWild) {
						var at = n.attributes;
						for (var i=at.length-1; i>-1; i--) {
							var no = at[i];
							if (no.specified && r.vAttribsRe.test(no.nodeName)) t += this._serializeAttribute(n, r, no.nodeName);
						}
					} else for (var i=r.vAttribs.length-1; i>-1; i--) t += this._serializeAttribute(n, r, r.vAttribs[i]);

					if (r.reqAttribsRe && !t.match(r.reqAttribsRe)) t = null;
					if (t != null && this.closeElementsRe.test(nn)) return t + ' />';
					if (t != null) h += t + '>';
					if (this.codeElementsRe.test(nn)) h += n.innerHTML;
				}
			break;

			case 3:
				if (st) break;
				if (n.parentNode && this.codeElementsRe.test(n.parentNode.nodeName)) return this.isIE ? '' : n.nodeValue;
				return this.xmlEncode(n.nodeValue);

			case 8:
				if (st) break;
				return "<!--" + n.nodeValue + "-->";
		}

		if (hc) {
			var cn = n.childNodes;
			for (var i=0,l=cn.length; i<l; i++) h += this.serializeNode(cn[i]);
		}
		if (f && !hc) h += "&nbsp;";
		if (t != null && va) h += '</' + en + '>';
		return h;
	},

	_serializeAttribute: function(n, r, an) {
		if (this.on_save && an.indexOf('_moz') == 0) return '';

		var av = '', t;
		if (av.length == 0) av = this._getAttrib(n, an);
		if (av.length == 0 && r.defaultAttribs && (t = r.defaultAttribs[an])) {
			av = t;
			if (av == "_empty") return " " + an + '=""';
		}
		if (r.forceAttribs && (t = r.forceAttribs[an])) av = t;
		if (av.length != 0 && r.validAttribValues && r.validAttribValues[an] && !r.validAttribValues[an].test(av)) return "";
		if (av.length != 0 && av == "{$uid}") av = "uid_" + (this.idCount++);
		if (av.length != 0) {
			if (an.indexOf('on') != 0) av = this.xmlEncode(av, 1);
			return " " + an + "=" + '"' + av + '"';
		}
		return "";
	},

	_getAttrib: function(e, n, d) {
		if (typeof(d) == "undefined") d = "";
		if (!e || e.nodeType != 1) return d;

		var v;
		try {
			v = e.getAttribute(n, 0);
		} catch (ex) {
			v = e.getAttribute(n, 2);
		}

		if (n == "class" && !v) v = e.className;

		if (this.isIE) {
			if (n == "http-equiv") v = e.httpEquiv;
			var nn = e.nodeName;
			if (nn == "FORM" && n == "enctype" && v == "application/x-www-form-urlencoded") v = "";
			if (nn == "INPUT" && n == "size" && v == "20") v = "";
			if (nn == "INPUT" && n == "maxlength" && v == "2147483647") v = "";
			if (n == "width" || n == "height") v = e.getAttribute(n, 2);
		}

		if (n == 'style' && v) {
			if (!window.opera) v = e.style.cssText;
			v = this.serializeStyle(this.parseStyle(v));
		}
		return (v && v !== '') ? '' + v : d;
	},

	parseStyle: function(str) {
		var ar = [];
		if (str == null) return ar;
		var st = str.split(';');
		for (var i=0, l=st.length; i<l; i++) {
			if (st[i] == '') continue;
			var re = new RegExp('^\\s*([^:]*):\\s*(.*)\\s*$');
			var pa = st[i].replace(re, '$1||$2').split('||');
			if (pa.length == 2) ar[pa[0].toLowerCase()] = pa[1];
		}
		return ar;
	},

	compressStyle: function(ar, pr, sf, res) {
		var box = [];
		box[0] = ar[pr + '-top' + sf];
		box[1] = ar[pr + '-left' + sf];
		box[2] = ar[pr + '-right' + sf];
		box[3] = ar[pr + '-bottom' + sf];
		for (var i=0, l=box.length; i<l; i++) {
			if (box[i] == null) return;
			if (i && box[i] != box[i-1]) return;
		}
		ar[res] = box[0];
		ar[pr + '-top' + sf] = null;
		ar[pr + '-left' + sf] = null;
		ar[pr + '-right' + sf] = null;
		ar[pr + '-bottom' + sf] = null;
	},

	serializeStyle: function(ar) {
		var str = "";
		this.compressStyle(ar, "border", "", "border");
		this.compressStyle(ar, "border", "-width", "border-width");
		this.compressStyle(ar, "border", "-color", "border-color");
		this.compressStyle(ar, "border", "-style", "border-style");
		this.compressStyle(ar, "padding", "", "padding");
		this.compressStyle(ar, "margin", "", "margin");

		for (var key in ar) {
			var val = ar[key];
			if (typeof(val) == 'function') continue;
			if (key.indexOf('mso-') == 0) continue;
			if (val != null && val !== '') {
				val = '' + val;
				val = val.replace(new RegExp("url\\(\\'?([^\\']*)\\'?\\)", 'gi'), "url('$1')");
				val = val.replace(/\"/g, '\'');
				if (val != "url('')") str += key.toLowerCase() + ": " + val + "; ";
			}
		}
		if (new RegExp('; $').test(str)) str = str.substring(0, str.length - 2);
		return str;
	},

	xmlEncode: function(s) {
		return ('' + s).replace(this.xmlEncodeRe, function (c) {
			switch (c) {
				case '&': return '&amp;';
				case '"': return '&quot;';
				case '<': return '&lt;';
				case '>': return '&gt;';
			}
			return c;
		});
	},

	_fixTags: function() {
		var h = this.Control.iframeDocument.body.innerHTML;
		if (h.indexOf('<') == -1) h = '<p>' + h + '</p>';
		this.Control.iframeDocument.body.innerHTML = h;
	},

	_fixListElements: function() {
		var a = ['ol', 'ul'], r = new RegExp('^(OL|UL)$');
		for (var i=0, l=a.length; i<l; i++) {
			var nl = this.Control.iframeDocument.getElementsByTagName(a[i]);
			for (var j=0, k=nl.length; i<k; j++) {
				var p = nl[j].parentNode;
				if (r.test(p.nodeName)) {
					var np = null, e = nl[j];
					while ((e = e.previousSibling) != null) {
						if (e.nodeName == 'LI') {
							np = e;
							break;
						}
					}
					if (!np) {
						np = this.Control.iframeDocument.createElement('li');
						np.innerHTML = '&nbsp;';
						np.appendChild(nl[j]);
						p.insertBefore(np, p.firstChild);
					} else
						np.appendChild(nl[j]);
				}
			}
		}
	},

	_arrayToRe: function(a, op, be, af) {
		op = typeof(op) == "undefined" ? "gi" : op;
		be = typeof(be) == "undefined" ? "^(" : be;
		af = typeof(af) == "undefined" ? ")$" : af;

		var r = new Array();
		for (var i=0,l=a.length; i<l; i++) r.push(this._wildcardToRe(a[i]));
		return new RegExp(be + r.join("|") + af, op);
	},

	_wildcardToRe: function(s) {
		s = s.replace(/\?/g, '(\\S?)');
		s = s.replace(/\+/g, '(\\S+)');
		s = s.replace(/\*/g, '(\\S*)');
		return s;
	},

	_isDuplicate: function(n) {
		if (n.nodeType == 1) {
			if (n._serialized == this.serializationId) return true;
			n._serialized = this.serializationId;
		} else {
			for (var i=0, l = this.serializedNodes.length; i<l; i++) if (this.serializedNodes[i] == n) return true;
			this.serializedNodes.push(n);
		}

		return false;
	}
};

// *******************************************************************************
// TextEditor_Selection
// *******************************************************************************
function TextEditor_Selection(obj) {
	this.Control = obj;
	this.Editor = this.Control.Editor;
}

TextEditor_Selection.prototype = {
	getSelectedHTML: function() {
		var r = this.getRng();
		if (!r) return null;

		var e = document.createElement("body");

		if (r.cloneContents) e.appendChild(document.importNode(r.cloneContents(), true));
		else if (typeof(r.item) != 'undefined' || typeof(r.htmlText) != 'undefined') e.innerHTML = r.item ? r.item(0).outerHTML : r.htmlText;
		else e.innerHTML = r.toString();

		return this.Control.Cleanup._cleanupHTML(e, false, false);
	},

	getSelectedText: function() {
		var t;
		if (this.Control.iframeWindow.getSelection) {
			var s = this.getSel();
			if (s && s.toString) t = s.toString();
			else t = '';
		} else {
			if (this.Control.iframeDocument.selection && this.Control.iframeDocument.selection.type == "Text") {
				var r = this.Control.iframeDocument.selection.createRange();
				t = r.text;
			} else t = '';
		}
		return t;
	},

	getBookmark: function(simple) {
		var rng = this.getRng(), vp = this.getViewPort();
		var sx = vp.left, sy = vp.top;
		if (simple) return {rng: rng, scrollX: sx, scrollY: sy};

		if (this.Control.iframeWindow.getSelection) {
			var s = this.getSel();
			if (!s) return null;
			var e = this.getFocusElement();
			if (e && e.nodeName == 'IMG') {
				return {
					start : -1,
					end : -1,
					index : -1,
					scrollX : sx,
					scrollY : sy
				};
			}
			if (s.anchorNode == s.focusNode && s.anchorOffset == s.focusOffset) {
				e = this._getPosText(s.anchorNode, s.focusNode);
				if (!e) return {scrollX : sx, scrollY : sy};
				return {
					start : e.start + s.anchorOffset,
					end : e.end + s.focusOffset,
					scrollX : sx,
					scrollY : sy
				};
			} else {
				e = this._getPosText(rng.startContainer, rng.endContainer);
				if (!e) return {scrollX : sx, scrollY : sy};

				return {
					start : e.start + rng.startOffset,
					end : e.end + rng.endOffset,
					scrollX : sx,
					scrollY : sy
				};
			}
		} else {
			if (rng.item) {
				var sp, e = rng.item(0);
				var nl = this.Control.iframeDocument.body.getElementsByTagName(e.nodeName);
				for (var i=0, l=nl.length; i<l; i++) {
					if (e == nl[i]) {
						sp = i;
						break;
					}
				}
				return {
					tag : e.nodeName,
					index : sp,
					scrollX : sx,
					scrollY : sy
				};
			} else {
				var trng = this.Control.iframeDocument.body.createTextRange();
				trng.moveToElementText(this.Control.iframeDocument.body);
				trng.collapse(true);
				var bp = Math.abs(trng.move('character', -999999999));

				trng = rng.duplicate();
				trng.collapse(true);
				var sp = Math.abs(trng.move('character', -999999999));

				trng = rng.duplicate();
				trng.collapse(false);
				var le = Math.abs(trng.move('character', -999999999)) - sp;

				return {
					start : sp - bp,
					length : le,
					scrollX : sx,
					scrollY : sy
				};
			}
		}
		return null;
	},

	moveToBookmark: function(bookmark) {
		var sel = this.getSel();
		if (!bookmark || !sel) return false;

		if (sel.setBaseAndExtent && bookmark.rng) {
			sel.setBaseAndExtent(bookmark.rng.startContainer, bookmark.rng.startOffset, bookmark.rng.endContainer, bookmark.rng.endOffset);
			return true;
		}
		if (this.Control.iframeDocument.selection && sel.createRange) { // IE
			if (bookmark.rng) {
				try { bookmark.rng.select(); } catch (ex) { }
				return true;
			}
			this.Control.iframeWindow.focus();
			var rng;
			if (bookmark.tag) {
				rng = this.Control.iframeDocument.body.createControlRange();
				var nl = this.Control.iframeDocument.body.getElementsByTagName(bookmark.tag);
				if (nl.length > bookmark.index) {
					try {
						rng.addElement(nl[bookmark.index]);
					} catch (ex) { }
				}
			} else {
				try {
					if (bookmark.start < 0) return true;
					rng = sel.createRange();
					rng.moveToElementText(this.Control.iframeDocument.body);
					rng.collapse(true);
					rng.moveStart('character', bookmark.start);
					rng.moveEnd('character', bookmark.length);
				} catch (ex) {
					return true;
				}
			}
			rng.select();
			this.Control.iframeWindow.scrollTo(bookmark.scrollX, bookmark.scrollY);
			return true;
		}

		if (this.Control.iframeDocument.createRange) {
			if (bookmark.rng) {
				sel.removeAllRanges();
				sel.addRange(bookmark.rng);
			}
			if (bookmark.start != -1 && bookmark.end != -1) {
				try {
					var sd = this._getTextPos(bookmark.start, bookmark.end);
					var rng = this.Control.iframeDocument.createRange();
					rng.setStart(sd.startNode, sd.startOffset);
					rng.setEnd(sd.endNode, sd.endOffset);
					sel.removeAllRanges();
					sel.addRange(rng);
					this.Control.iframeWindow.focus();
				} catch (ex) {
					return false;
				}
			}
			this.Control.iframeWindow.scrollTo(bookmark.scrollX, bookmark.scrollY);
			return true;
		}
		return false;
	},

	_getPosText: function(sn, en) {
		var w = document.createTreeWalker(this.Control.iframeDocument.body, NodeFilter.SHOW_TEXT, null, false), n, p = 0, d = {};
		while ((n = w.nextNode()) != null) {
			if (n == sn) d.start = p;
			if (n == en) {
				d.end = p;
				return d;
			}
			p += n.nodeValue ? n.nodeValue.length : 0;
		}
		return null;
	},

	_getTextPos: function(sp, ep) {
		var w = document.createTreeWalker(this.Control.iframeDocument.body, NodeFilter.SHOW_TEXT, null, false), n, p = 0, d = {};
		while ((n = w.nextNode()) != null) {
			p += n.nodeValue ? n.nodeValue.length : 0;
			if (p >= sp && !d.startNode) {
				d.startNode = n;
				d.startOffset = sp - (p - n.nodeValue.length);
			}
			if (p >= ep) {
				d.endNode = n;
				d.endOffset = ep - (p - n.nodeValue.length);
				return d;
			}
		}
		return null;
	},

	selectNode: function(node, collapse, select_text_node, to_start) {
		if (!node) return;
		if (typeof(collapse) == "undefined") collapse = true;
		if (typeof(select_text_node) == "undefined") select_text_node = false;
		if (typeof(to_start) == "undefined") to_start = true;
		if (this.Editor.options.auto_resize) this.Control.resizeToContent();

		if (this.Control.iframeWindow.getSelection) {
			var s = this.getSel();
			if (!s) return;
			if (s.setBaseAndExtent) {
				s.setBaseAndExtent(node, 0, node, node.innerText.length);
				if (collapse) {
					if (to_start) s.collapseToStart();
					else s.collapseToEnd();
				}
				this.scrollToNode(node);
				return;
			}

			var r = this.Control.iframeDocument.createRange();
			if (select_text_node) {
				var nodes = this.Editor.getNodeTree(node, [], 3);
				if (nodes.length > 0) r.selectNodeContents(nodes[0]);
				else r.selectNodeContents(node);
			} else r.selectNode(node);

			if (collapse) {
				if (!to_start && node.nodeType == 3) {
					r.setStart(node, node.nodeValue.length);
					r.setEnd(node, node.nodeValue.length);
				} else r.collapse(to_start);
			}
			s.removeAllRanges();
			s.addRange(r);
		} else {
			var r = this.Control.iframeDocument.body.createTextRange();
			try {
				r.moveToElementText(node);
				if (collapse) r.collapse(to_start);
				r.select();
			} catch (e) { }
		}
		this.scrollToNode(node);
		this.Editor.selectedElement = null;
		if (node.nodeType == 1) this.Editor.selectedElement = node;
	},

	scrollToNode: function(node) {
		var vp = this.getViewPort(), pos = this.getAbsPositionGlobal(node);
		if (pos.absLeft < vp.left || pos.absLeft > vp.left + vp.width || pos.absTop < vp.top || pos.absTop > vp.top + (vp.height-25))
			this.Control.iframeWindow.scrollTo(pos.absLeft, pos.absTop - vp.height + 25);

		if (this.Editor.options.auto_resize) {
			var cvp = this.getViewPort(window), cpos = this.getAbsPosition(node);
			if (cpos.absLeft < cvp.left || cpos.absLeft > cvp.left + cvp.width || cpos.absTop < cvp.top || cpos.absTop > cvp.top + cvp.height)
				window.scrollTo(cpos.absLeft, cpos.absTop - cvp.height + 25);
		}
	},

	getViewPort: function(w) {
		if (typeof(w) == "undefined") w = this.Control.iframeWindow;
		var d = w.document, m = d.compatMode == 'CSS1Compat', b = d.body, de = d.documentElement;
		return {
			left : w.pageXOffset || (m ? de.scrollLeft : b.scrollLeft),
			top : w.pageYOffset || (m ? de.scrollTop : b.scrollTop),
			width : w.innerWidth || (m ? de.clientWidth : b.clientWidth),
			height : w.innerHeight || (m ? de.clientHeight : b.clientHeight)
		};
	},

	getAbsPositionGlobal: function(n) {
		var l = 0, t = 0;
		while (n) {
			l += n.offsetLeft;
			t += n.offsetTop;
			n = n.offsetParent;
		}
		return {absLeft : l, absTop : t};
	},

	getAbsPosition: function(n) {
		var pos = this.getAbsPositionGlobal(n), ipos = this.getAbsPositionGlobal(this.Control.iframeElement);
		return {
			absLeft : ipos.absLeft + pos.absLeft,
			absTop : ipos.absTop + pos.absTop
		};
	},

	getSel: function() {
		return this.Control.iframeWindow.getSelection ? this.Control.iframeWindow.getSelection() : this.Control.iframeDocument.selection;
	},

	getRng: function() {
		var s = this.getSel();
		if (s == null) return null;
		if (s.createRange) return s.createRange();
		if (!s.getRangeAt) return '' + window.getSelection();
		if (s.rangeCount > 0) return s.getRangeAt(0);
		return null;
	},

	isCollapsed: function() {
		var r = this.getRng();
		if (r.item) return false;
		return r.boundingWidth == 0 || this.getSel().isCollapsed;
	},

	collapse: function(b) {
		var r = this.getRng(), s = this.getSel();
		if (r.select) {
			r.collapse(b);
			r.select();
		} else {
			if (b) s.collapseToStart();
			else s.collapseToEnd();
		}
	},

	getFocusElement: function() {
		if (this.Control.iframeWindow.getSelection) {
			var sel = this.getSel();
			var rng = this.getRng();
			if (!sel || !rng) return null;

			var elm = rng.commonAncestorContainer;
			if ((!rng.collapsed) && (rng.startContainer == rng.endContainer) && (rng.startOffset - rng.endOffset < 2) && (rng.startContainer.hasChildNodes()))
				elm = rng.startContainer.childNodes[rng.startOffset];
			return this.Editor.getParentElement(elm);
		} else {
			var rng = this.Control.iframeDocument.selection.createRange();
			return rng.item ? rng.item(0) : rng.parentElement();
		}
	}
};

// *******************************************************************************
// TextEditor_ForceParagraphs
// *******************************************************************************
function TextEditor_ForceParagraphs(obj) {
	this.Editor = obj;
	this.Control = this.Editor.Control;
	this.Selection = this.Control.Selection;
	this.isOpera = window['opera'] && opera.buildNumber ? true : false;
}

TextEditor_ForceParagraphs.prototype = {
	_isEmpty: function(para) {
		function isEmptyHTML(html) {
			return html.replace(new RegExp('[ \t\r\n]+', 'g'), '').toLowerCase() == '';
		}

		if (para.getElementsByTagName("img").length > 0) return false;
		if (para.getElementsByTagName("table").length > 0) return false;
		if (para.getElementsByTagName("hr").length > 0) return false;
		var nodes = this.Editor.getNodeTree(para, [], 3);
		for (var i=0, l=nodes.length; i<l; i++) if (!isEmptyHTML(nodes[i].nodeValue)) return false;
		return true;
	},

	add: function() {
		var s = this.Selection.getSel(), r = s.getRangeAt ? s.getRangeAt(0) : null, b = this.Selection.getBookmark();
		if (!r) return false;
		var rngBefore = this.Control.iframeDocument.createRange();
		rngBefore.setStart(s.anchorNode, s.anchorOffset);
		rngBefore.collapse(true);

		var rngAfter = this.Control.iframeDocument.createRange();
		rngAfter.setStart(s.focusNode, s.focusOffset);
		rngAfter.collapse(true);

		var direct = rngBefore.compareBoundaryPoints(rngBefore.START_TO_END, rngAfter) < 0;
		var startNode = direct ? s.anchorNode : s.focusNode;
		var endNode = direct ? s.focusNode : s.anchorNode;
		var startOffset = direct ? s.anchorOffset : s.focusOffset;
		var endOffset = direct ? s.focusOffset : s.anchorOffset;
		startNode = startNode.nodeName == "HTML" ? this.Control.iframeDocument.body : startNode;
		startNode = startNode.nodeName == "BODY" ? startNode.firstChild : startNode;
		endNode = endNode.nodeName == "BODY" ? endNode.firstChild : endNode;

		var startBlock = this.Editor.getParentBlockElement(startNode);
		var endBlock = this.Editor.getParentBlockElement(endNode);
		if (startBlock && (startBlock.nodeName == 'CAPTION' || /absolute|relative|static/gi.test(startBlock.style.position))) startBlock = null;
		if (endBlock && (endBlock.nodeName == 'CAPTION' || /absolute|relative|static/gi.test(endBlock.style.position))) endBlock = null;

		var blockName = "P";
		if (startBlock != null) {
			blockName = startBlock.nodeName;
			if (/(TD|TABLE|TH|CAPTION)/.test(blockName) || (blockName == "DIV" && /left|right/gi.test(startBlock.style.cssFloat))) blockName = "P";
		}
		if (this.Editor.getParentElement(startBlock, "OL,UL", null, this.Control.iframeDocument.body) != null) return false;
		if ((startBlock != null && startBlock.nodeName == "TABLE") || (endBlock != null && endBlock.nodeName == "TABLE")) startBlock = endBlock = null;

		var paraBefore = (startBlock != null && startBlock.nodeName == blockName) ? startBlock.cloneNode(false) : this.Control.iframeDocument.createElement(blockName);
		var paraAfter = (endBlock != null && endBlock.nodeName == blockName) ? endBlock.cloneNode(false) : this.Control.iframeDocument.createElement(blockName);
		if (/^(H[1-6])$/.test(blockName)) paraAfter = this.Control.iframeDocument.createElement("p");

		var startChop = startNode, endChop = endNode, node;
		node = startChop;
		do {
			if (node == this.Control.iframeDocument.body || node.nodeType == 9 || this.Editor.isBlockElement(node)) break;
			startChop = node;
		} while ((node = node.previousSibling ? node.previousSibling : node.parentNode));
		node = endChop;
		do {
			if (node == this.Control.iframeDocument.body || node.nodeType == 9 || this.Editor.isBlockElement(node)) break;
			endChop = node;
		} while ((node = node.nextSibling ? node.nextSibling : node.parentNode));
		if (startChop.nodeName == "TD") startChop = startChop.firstChild;
		if (endChop.nodeName == "TD") endChop = endChop.lastChild;

		if (startBlock == null) {
			r.deleteContents();
			s.removeAllRanges();
			if (startChop != this.Control.iframeDocument.documentElement && endChop != this.Control.iframeDocument.documentElement) {
				rngBefore = r.cloneRange();
				if (startChop == this.Control.iframeDocument.body) rngBefore.setStart(startChop, 0);
				else rngBefore.setStartBefore(startChop);
				paraBefore.appendChild(rngBefore.cloneContents());

				if (endChop.parentNode.nodeName == blockName) endChop = endChop.parentNode;
				r.setEndAfter(endChop);
				if (endChop.nodeName != "#text" && endChop.nodeName != "BODY") rngBefore.setEndAfter(endChop);

				var contents = r.cloneContents();
				if (contents.firstChild && (contents.firstChild.nodeName == blockName || contents.firstChild.nodeName == "BODY")) paraAfter.innerHTML = contents.firstChild.innerHTML;
				else paraAfter.appendChild(contents);

				if (this._isEmpty(paraBefore)) paraBefore.innerHTML = "&nbsp;";
				if (this._isEmpty(paraAfter)) paraAfter.innerHTML = "&nbsp;";

				r.deleteContents();
				rngAfter.deleteContents();
				rngBefore.deleteContents();

				if (this.isOpera) {
					paraBefore.normalize();
					rngBefore.insertNode(paraBefore);
					paraAfter.normalize();
					rngBefore.insertNode(paraAfter);
				} else {
					paraAfter.normalize();
					rngBefore.insertNode(paraAfter);
					paraBefore.normalize();
					rngBefore.insertNode(paraBefore);
				}
			} else {
				this.Control.iframeDocument.body.innerHTML = "<" + blockName + ">&nbsp;</" + blockName + "><" + blockName + ">&nbsp;</" + blockName + ">";
				paraAfter = this.Control.iframeDocument.body.childNodes[1];
			}

			this.Selection.moveToBookmark(b);
			this.Selection.selectNode(paraAfter, true, true);
			return true;
		}
		if (startChop.nodeName == blockName) rngBefore.setStart(startChop, 0);
		else rngBefore.setStartBefore(startChop);

		rngBefore.setEnd(startNode, startOffset);
		paraBefore.appendChild(rngBefore.cloneContents());

		rngAfter.setEndAfter(endChop);
		rngAfter.setStart(endNode, endOffset);
		var contents = rngAfter.cloneContents();
		if (contents.firstChild && contents.firstChild.nodeName == blockName) paraAfter.innerHTML = contents.firstChild.innerHTML;
		else paraAfter.appendChild(contents);

		if (this._isEmpty(paraBefore)) paraBefore.innerHTML = "&nbsp;";
		if (this._isEmpty(paraAfter)) paraAfter.innerHTML = "&nbsp;";
		r = this.Control.iframeDocument.createRange();
		if (!startChop.previousSibling && startChop.parentNode.nodeName.toUpperCase() == blockName) r.setStartBefore(startChop.parentNode);
		else {
			if (rngBefore.startContainer.nodeName.toUpperCase() == blockName && rngBefore.startOffset == 0) r.setStartBefore(rngBefore.startContainer);
			else r.setStart(rngBefore.startContainer, rngBefore.startOffset);
		}

		if (!endChop.nextSibling && endChop.parentNode.nodeName.toUpperCase() == blockName) r.setEndAfter(endChop.parentNode);
		else r.setEnd(rngAfter.endContainer, rngAfter.endOffset);

		r.deleteContents();
		if (this.isOpera) {
			r.insertNode(paraBefore);
			r.insertNode(paraAfter);
		} else {
			r.insertNode(paraAfter);
			r.insertNode(paraBefore);
		}
		paraAfter.normalize();
		paraBefore.normalize();
		this.Selection.moveToBookmark(b);
		this.Selection.selectNode(paraAfter, true, true);
		return true;
	},

	del: function() {
		var r = this.Selection.getRng(), sn = r.startContainer;
		if (sn && sn.nextSibling && sn.nextSibling.nodeName == "BR" && sn.parentNode.nodeName != "BODY") {
			var nv = sn.nodeValue;
			if (nv != null && r.startOffset == nv.length) sn.nextSibling.parentNode.removeChild(sn.nextSibling);
		}
		if (this.Editor.options.auto_resize) this.Control.resizeToContent();
	}
};

// *******************************************************************************
// TextEditor_UndoRedo
// *******************************************************************************
function TextEditor_UndoRedo(obj) {
	this.Control = obj;
	this.Editor = this.Control.Editor;
	this.Selection = this.Control.Selection;
	this.undoLevels = new Array();
	this.undoIndex = 0;
	this.typingUndoIndex = -1;
	this.undoRedo = true;
}

TextEditor_UndoRedo.prototype = {
	add: function(l) {
		if (l) {
			this.undoLevels[this.undoLevels.length] = l;
			return true;
		}

		if (this.typingUndoIndex != -1) this.undoIndex = this.typingUndoIndex;

		var newHTML = this.Control.iframeDocument.body.innerHTML.strip();
		if (this.undoLevels[this.undoIndex] && newHTML != this.undoLevels[this.undoIndex].content) {
			this.Editor.isNotDirty = false;
			this.Editor.dispatchCallback('onchange_callback', 'onChange');
			var b = this.Editor.undoBookmark;
			if (!b) b = this.Selection.getBookmark();
			this.undoIndex++;
			this.undoLevels[this.undoIndex] = {
				content: newHTML,
				bookmark: b
			};
			this.undoLevels.length = this.undoIndex + 1;
			return true;
		}
		return false;
	},

	_prc: function() {
		this.Editor.setInnerHTML(this.undoLevels[this.undoIndex].content);
		this.Editor.repaint();
		this.Selection.moveToBookmark(this.undoLevels[this.undoIndex].bookmark);
	},

	undo: function() {
		if (this.undoIndex > 0) {
			this.undoIndex--;
			this._prc();
		}
	},

	redo: function() {
		this.Editor.execCommand("EndTyping");
		if (this.undoIndex < (this.undoLevels.length-1)) {
			this.undoIndex++;
			this._prc();
		}
		this.Editor.triggerNodeChange();
	}
};
