/* Copyright (c) 2011, Geert Bergman (geert@scrivo.nl)
 * All rights reserved.
 *
 * Redistribution and use in source and binary forms, with or without
 * modification, are permitted provided that the following conditions are met:
 *
 * 1. Redistributions of source code must retain the above copyright notice,
 *    this list of conditions and the following disclaimer.
 * 2. Redistributions in binary form must reproduce the above copyright notice,
 *    this list of conditions and the following disclaimer in the documentation
 *    and/or other materials provided with the distribution.
 * 3. Neither the name of "Scrivo" nor the names of its contributors may be
 *    used to endorse or promote products derived from this software without
 *    specific prior written permission.
 *
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
 * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
 * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
 * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
 * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
 * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
 * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
 * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
 * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
 * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
 * POSSIBILITY OF SUCH DAMAGE.
 *
 * $Id: HTMLEditControl.js 743 2013-07-18 10:12:39Z geert $
 */

"use strict";

SUI.control.HTMLEditControl = SUI.defineClass(
	/** @lends SUI.control.HTMLEditControl.prototype */{

	/** @ignore */ baseClass: SUI.control.BaseHTMLEditControl,

	/**
	 * @class
	 * SUI.control.HTMLEditControl implements a WYSIWIG/HTML edit control. It
	 * should provide a standardized interface to the different implementations
	 * of contenteditable in various browsers. Because of code size of the
	 * class it is splitted in two parts: This class is extended from
	 * BaseHTMLEditControl which implements all of the technical structure.
	 * This part implements the actions that can be performed on the content.
	 *
	 * @augments SUI.control.BaseHTMLEditControl
	 *
	 * @description
	 * SUI.control.HTMLEditControl constructor
	 *
	 * @constructs
	 * 
	 * @param {inherit} arg 
	 * @param {Function} arg.onContextMenu event handler, see onContextMenu()
	 * @param {Function} arg.onKeyDown event handler, see onKeyDown()
	 * @param {Function} arg.onLoad event handler, see onLoad()
	 * @param {Function} arg.onSelectionChange event handler, see
	 *     onSelectionChange()
	 */
	initializer: function(arg) {
		SUI.control.HTMLEditControl.initializeBase(this, arg);
	},

	/**
	 * Return an object containing the command that are enabled given the
	 * current cursor position/selection in the editor. the following command
	 * are supported: cut, copy, paste, undo, redo, link, image, anchor,
	 * pageBreak, removeFormatting, orderedList, unorderedList, indent,
	 * deIndent.
	 * @return {Object} an object with the enabled state of all supported
	 *     commands
	 */
	commandsEnabled: function() {

		var cut = false, copy = false, paste = false;

		if (SUI.browser.isWebKit) {
			// WebKit does not support these so this is an alternative
			var sel = this._getSelection();
			cut = copy = !sel.isCollapsed;
			paste = true;
		} else {
			// normal method for other browsers
			cut = this._editDoc.queryCommandEnabled("Cut");
			copy = this._editDoc.queryCommandEnabled("Copy");
			// This also triggers onbeforepaste event
			this._ieDoBeforePaste = false;
			paste = this._editDoc.queryCommandEnabled("Paste");
			this._ieDoBeforePaste = true;
		}

		return {
			cut: cut,
			copy: copy,
			paste: paste,
			undo: this._editDoc.queryCommandEnabled("Undo"),
			redo: this._editDoc.queryCommandEnabled("Redo"),
			link: this._editDoc.queryCommandEnabled("createlink"),
			image: this._editDoc.queryCommandEnabled("insertimage"),
			anchor: this._editDoc.queryCommandEnabled("createlink"),
			pageBreak:
				this._editDoc.queryCommandEnabled("inserthorizontalrule"),
			removeFormatting:
				this._editDoc.queryCommandEnabled("removeformat"),
			orderedList:
				this._editDoc.queryCommandEnabled("insertorderedlist"),
			unorderedList:
				this._editDoc.queryCommandEnabled("insertunorderedlist"),
			indent: this._editDoc.queryCommandEnabled("indent"),
			deIndent: this._editDoc.queryCommandEnabled("outdent")
		};
	},

	/**
	 * Return an object containing the command state of the commands
	 * applicable at the current cursor position/selection in the editor.
	 * the following commands are supported: bold, underline, italic,
	 * justifyleft, justifyCenter, justifyRight.
	 * @return {Object} an object with the state of all supported commands
	 */
	commandStates: function() {
		return {
			bold: this._editDoc.queryCommandState("Bold"),
			underline: this._editDoc.queryCommandState("Underline"),
			italic: this._editDoc.queryCommandState("Italic"),
			alignLeft: this._editDoc.queryCommandState("justifyleft"),
			alignCenter: this._editDoc.queryCommandState("justifyCenter"),
			alignRight: this._editDoc.queryCommandState("justifyRight"),
			insertUnorderedList:
				this._editDoc.queryCommandState("insertUnorderedList"),
			insertOrderedList:
				this._editDoc.queryCommandState("insertOrderedList")
		};
	},

	/**
	 * Set the background color of the current editor selection.
	 * This command transfers the focus to the editor.
	 * @param {String} col CSS color to set the background to
	 */
	doBackColor: function(col) {
		if (SUI.browser.isIE) {
			// In IE we do a setTimeout to settle the focus (r.select creates
			// an application error)
			var that = this;
			setTimeout(function() {
				that._execCommand("BackColor", col);
				that._onCommandExecuted();
			}, 10);
		} else {
			this._selectWord();
			this._execCommand("styleWithCSS", true);
			this._execCommand("HiliteColor", col);
			this._execCommand("styleWithCSS", false);
			this._onCommandExecuted();
		}
	},

	/**
	 * Toggle the bold state of the current editor selection.
	 * This command transfers the focus to the editor.
	 */
	doBold: function() {
		this._selectWord();
		this._execCommand("Bold");
		this._onCommandExecuted();
	},

	/**
	 * Find text in the editor. Gecko ignores the wholeWord parameter.
	 * @param {String} what text to search
	 * @param {boolean} caseSensitive case sensitive search
	 * @param {boolean} backward search directions
	 * @param {boolean} wholeWord search on whole words only (not Gecko)
	 * @return {boolean} true if the text was found
	 */
	doFind: function(what, caseSensitive, backward, wholeWord) {

		this.focus();

		// normalize parameter values (false i.o. null)
		caseSensitive =    caseSensitive ? true : false;
		backward = backward ? true : false;
		wholeWord = wholeWord ? true : false;

		// find the text in the editor
		var strFound = SUI.browser.isIE
			? this._ieFind(what, caseSensitive, backward, wholeWord)
			: this._editWin.find(what, caseSensitive, backward, false,
				wholeWord, false, false);

		this._onCommandExecuted();
		// return if the search was successful
		return strFound ? true : false;
	},

	/**
	 * Set the font of the current editor selection.
	 * This command transfers the focus to the editor.
	 * @param {String} val The font name (prefixed by "f_")
	 */
	doFontName: function(val) {
		this._selectWord();
		if (SUI.browser.isIE) {
			// Note: prefix the font name with "f_" (so "f_arial" instead of
			// "arial"). This will cause the font tags to be replaced by span
			// tags (IE). Browsers that support a span instead of a font tag
			// will use that.
			this._execCommand("FontName", "f_" + val);
			this._fontFix();
		} else {
			this._execCommand("styleWithCSS", true);
		this._execCommand("FontName", val);
			this._execCommand("styleWithCSS", false);
		}
		this._onCommandExecuted();
	},

	/**
	 * Set the font size of the current editor selection.
	 * This command transfers the focus to the editor.
	 * @param {String} val The font size (prefixed by "s_")
	 */
	doFontSize: function(val) {
		this._selectWord();
		// Prefix the font name with "s_" (so "s_200P" instead of "200%").
		// This will cause the font tags to be replaced by span tags (IE).
		// Browsers that support a span instead of a font tag will use that.
		// Unfortunately execCommand fontsize can't be used: no browser
		// supports it well.
		val = "s_" + val.replace("%", "P");
		if (SUI.browser.isIE) {
			this._execCommand("FontName", val);
		this._fontFix();
		} else {
			this._execCommand("styleWithCSS", true);
			this._execCommand("FontName", val);
			this._execCommand("styleWithCSS", false);
			this._spanFix();
		}
		this._onCommandExecuted();
	},

	/**
	 * Set the foreground color of the current editor selection.
	 * This command transfers the focus to the editor.
	 * @param {String} col CSS color to set the background to
	 */
	doForeColor: function(col) {
		this._selectWord();
		if (SUI.browser.isIE) {
			// In IE we do a setTimeout to settle the focus (r.select creates
			// an application error)
			var that = this;
			setTimeout(function() {
				that._execCommand("ForeColor", col);
				that._fontFix();
				that._onCommandExecuted();
			}, 10);
		} else {
			this._execCommand("styleWithCSS", true);
			this._execCommand("ForeColor", col);
			this._execCommand("styleWithCSS", false);
			this._onCommandExecuted();
		}
	},

	/**
	 * Format the block that is currently selected (ie. P, H1, H2 etc)
	 * This command transfers the focus to the editor.
	 * @param {String} val the block format tag name
	 */
	doFormatBlock: function(val) {
		this._execCommand("FormatBlock", "<"+val+">");
		this._onCommandExecuted();
	},

	/**
	 * Get the acronym node that is parent to the current editor selection.
	 * @return {HTMLElementNode} the parent acronym node or null if there is
	 *     none
	 */
	doGetAbbr: function() {
		var e = this.getSelectedElement();
		// traverse up the dom tree until the root only checking element nodes
		while (e != null && e.nodeType != 9) {
			if (e.nodeType == 1 && e.tagName=="ACRONYM") {
				return e;
			} else {
				e = e.parentNode;
			}
		}
		return null;
	},

	/**
	 * Get the span:lang node that is parent to the current editor selection.
	 * @return {HTMLElementNode} the parent span:lang node or null if there is
	 *     none
	 */
	doGetLang: function() {
		var e = this.getSelectedElement();
		// traverse up the dom tree until the root only checking element nodes
		while (e != null && e.nodeType != 9) {
			if (e.nodeType == 1 && e.tagName=="SPAN") {
				if (e.lang != "" && e.lang != null) {
					return e;
				}
			}
			e = e.parentNode;
		}
		return null;
	},

	/**
	 * Get the a node that is parent to the current editor selection.
	 * @return {HTMLElementNode} the parent a node or null if there is none
	 */
	doGetLink: function() {
		var e = this.getSelectedElement();
		// traverse up the dom tree until the root only checking element nodes
		while (e!=null && e.nodeType != 9) {
			if (e.nodeType == 1 && e.tagName=="A") {
				return e;
			} else {
				e=e.parentNode;
			}
		}
		return null;
	},

	/**
	 * Get the selected image is selected in the editor. Note: for IE we
	 * return a reference to the img node, for other browsers we create
	 * a copy.
	 * @return {HTMLElementNode} an img node or null if there is none
	 */
	doGetImg: function() {

		var img = null;

		if (SUI.browser.isIE) {

			var s = this._getSelection();
			// it is proven code but I'm not sure about this (why not
			// s.type == "Control" ?)
			if (s.type != "None" && s.type != "Text") {
				img = s.createRange().item(0);
				// and then this loop up the dom tree ???
				while (img != null && img.tagName != "IMG"){
					img = img.parentNode;
				}
			}

		} else {

			var r = this._getCurrentRange();
			// create a copy: overwrite later
			var fr = r.cloneContents();
			// find the first image in the selected range
			for (var i=0; i<fr.childNodes.length; i++) {
				var n = fr.childNodes[i];
				if (n.nodeType==1 && n.tagName=="IMG") {
					img = n;
					break;
				}
			}

		}

		return img;
	},

	/**
	 * Indent the current paragraph or create a deeper level in a list.
	 * This command transfers the focus to the editor.
	 */
	doIndent: function() {
		this._execCommand("Indent");
		this._onCommandExecuted();
	},

	/**
	 * Insert a acronym node into the editor content. Unfortunately no abbr
	 * because IE does not support this. The acronym gets the 'sys_abbr'
	 *  system style.
	 * This command transfers the focus to the editor.
	 * @param {String} fullText the full text for the abbreviation
	 */
	doInsertAbbr: function(fullText) {
		this.doInsertInlineElement("ACRONYM",
			{"class": "sys_abbr", title: fullText});
	},

	/**
	 * Insert an anchor node into the editor content. The anchor gets the
	 * 'sys_anchor' system style.
	 * This command transfers the focus to the editor.
	 * @param {String} name the name for the anchor
	 */
	doInsertAnchor: function(name) {
		name=name.replace(/[\s]+/g,"_");
		this.doInsertInlineElement("A", {name: name, "class": "sys_anchor"});
	},

	/**
	 * Insert a horizontal ruler into the editor content.
	 * This command transfers the focus to the editor.
	 */
	doInsertHorizontalRule: function() {
		this._execCommand("InsertHorizontalRule");
		this._onCommandExecuted();
	},

	/**
	 * Insert any html you like into the editor content.
	 * This command transfers the focus to the editor.
	 * @param {String} html the HTML code to insert
	 */
	doInsertHTML: function(html) {
		if (SUI.browser.isIE) {
			var that = this;
			setTimeout(function() {
				that.focus();
				var r = that._getCurrentRange();
				r.collapse(true);
				r.select();
				r.pasteHTML(html);
				that._onCommandExecuted();
			}, 10);
		} else {
			this._execCommand("inserthtml", html);
			this._onCommandExecuted();
		}
	},

	/**
	 * Insert an image into the editor content.
	 * This command transfers the focus to the editor.
	 * @param {HTMLElementNode} img an HTML image node to insert
	 */
	doInsertImg: function(img) {

		if (SUI.browser.isIE) {
			// In IE we do a setTimeout to settle the focus (r.select creates
			// an application error)
			var that = this;
			setTimeout(function() {
				that.focus();
				// why not doInsertHTML ??
				var r = that._getCurrentRange();
				r.pasteHTML(img.outerHTML);
				r.select();
				that._onCommandExecuted();
			}, 10);
		} else {
			this.focus();
			// doUpdateImg does an insert
			this.doUpdateImg(img);
			// make it possible to select the image in WebKit
			this._webKitImgFix();
			this._onCommandExecuted();
		}
	},

	/**
	 * Insert an inline element into the editor content.
	 * This command transfers the focus to the editor.
	 * @param {String} tag The html tag name of the element
	 * @param {Object} attr an object with name-values pairs
	 */
	doInsertInlineElement: function(tag, attr) {

		if (SUI.browser.isIE) {

			// In IE we do a setTimeout to settle the focus (r.select creates
			// an application error)
			var that = this;
			setTimeout(function() {

			that.focus();

			var elem = that._editDoc.createElement(tag);
			SUI.browser.setAttributes(elem, attr);

			var s = that._getSelection();
			var r = that._getCurrentRange(s);

			// if necessary expand a collapsed range to single word selection
			if (s.type == "None") {
				// if necessary expand collapsed range to single word selection
					that._rangeSelectWord(r);
				r.select();
			}

			// if the previous step failed and range is still collapsed ...
			if (r.text == "" || s.type == "None") {
				// ... then there is nothing to do
				return;
			}

			if (s.type == "Text") {

				// if there is a text selection overwrite it with the html
				elem.innerHTML = r.htmlText;
				r.pasteHTML(elem.outerHTML);

			} else {

					// if the selection was element then replace that element
				// with ours and then append it to ours
				//el = r.commonParentElement();
				//el.parentElement.replaceChild(elem, el);
				//elem.appendChild(el);
					// TODO: No create a childnode instead, but that is also not
				// fully correct (common parent can be more that the selection)
				el = SUI.browser.version < 9
					? r.commonParentElement() : r.item(0);
				elem.innerHTML = el.innerHTML;
				r.pasteHTML(elem.outerHTML);

			}

				that._onCommandExecuted();

			}, 10);

		} else {

			this.focus();

			var elem = this._editDoc.createElement(tag);
			SUI.browser.setAttributes(elem, attr);

			var s = this._getSelection();
			var r = this._getCurrentRange(s);

			if (r.collapsed) {
				// if necessary expand collapsed range to single word selection
				this._rangeSelectWord(r);
			}

			// if the previous step failed and range is still collapsed ...
			if (r.collapsed) {
				// ... then there is nothing to do
				return;
			}

			// set the innerHTML of our element to the selected html fragment
			elem.innerHTML = this._outerHTML(r.cloneContents());
			var html = this._outerHTML(elem);

			// add the range to the selection
			s.removeAllRanges();
			s.addRange(r);

			// and paste the new html over it
			this._execCommand("inserthtml", html);

			this._onCommandExecuted();
		}

	},

	/**
	 * Insert a language mark into the editor content. The language mark gets
	 * the 'sys_anchor' system style.
	 * This command transfers the focus to the editor.
	 * @param {String} language a language code
	 */
	doInsertLang: function(language) {
		this.doInsertInlineElement("SPAN",
			{"class": "sys_language", lang: language});
	},

	/**
	 * Insert a hyperlink into the editor content.
	 * This command transfers the focus to the editor.
	 * @param {Object} attr the attributes of the link
	 */
	doInsertLink: function(attr) {

		if (SUI.browser.isIE) {

			// In IE we do a setTimeout to settle the focus (r.select creates
			// an application error)
			var that = this;
			setTimeout(function() {

			that.focus();

			// don't use doInsertInlineElement because we can use execCommand
				var s = that._editDoc.selection;
				var r = that._getCurrentRange(s);

			// if necessary expand a collapsed range to single word selection
			if (s.type == "None") {
				// if necessary expand collapsed range to single word selection
					that._rangeSelectWord(r);
				r.select();
			}

			// if the previous step failed and range is still collapsed ...
			if (r.text == "" || s.type == "None") {
				// ... then there is nothing to do
				return;
			}

				if (s.type == "Text" || s.type == "None") {

				// craete the link with execCommand
					that._execCommand("CreateLink", attr.href);

				// an try to find it in the editor content
				var anA = r.parentElement();
				if (anA.tagName != "A") {
					anA = anA.firstChild;
				}

				// if we found it then set the other attributes
				if (anA.tagName == "A") {
					SUI.browser.setAttributes(anA, attr);
					r.collapse(false);
				}
			} else {

				var anAnchor = that._editDoc.createElement("A");
				SUI.browser.setAttributes(anAnchor, attr);
				// if the selection was element then replace this element
				// with ours and then append it to ours
				el = SUI.browser.version < 9
					? r.commonParentElement() : r.item(0);
				el.parentElement.replaceChild(anAnchor, el);
				anAnchor.appendChild(el);

			}
				that._onCommandExecuted();

			}, 10);

		} else {

			this.focus();
			this.doInsertInlineElement("A", attr);

		}
	},

	/**
	 * Insert an node into the editor content. Unlike doInsertInlineElement
	 * this command overwrites the current selection.
	 * This command transfers the focus to the editor.
	 * @param {HTMLElementNode} node The node to insert
	 */
	doInsertNode: function(node) {
		this.doInsertHTML(this._outerHTML(node));
		this._onCommandExecuted();
	},

	/**
	 * Change a paragraph into the first element of (or a couple of
	 * paragraphs into) an ordered list.
	 * This command transfers the focus to the editor.
	 */
	doInsertOrderedList: function() {
		this._execCommand("InsertOrderedList");
		this._onCommandExecuted();
	},

	/**
	 * Change a paragraph into the first element of (or a couple of
	 * paragraphs into) an unordered list.
	 * This command transfers the focus to the editor.
	 */
	doInsertUnorderedList: function() {
		this._execCommand("InsertUnorderedList");
		this._onCommandExecuted();
	},

	/**
	 * Toggle the italic state of the current editor selection.
	 * This command transfers the focus to the editor.
	 */
	doItalic: function() {
		this._selectWord();
		this._execCommand("Italic");
		this._onCommandExecuted();
	},

	/**
	 * Justify current editor paragraph or paragraphs in a selection
	 * to the Center.
	 * This command transfers the focus to the editor.
	 */
	doJustifyCenter: function() {
		this._execCommand("JustifyCenter");
		this._onCommandExecuted();
	},

	/**
	 * Justify current editor paragraph or paragraphs in a selection
	 * to the Left.
	 * This command transfers the focus to the editor.
	 */
	doJustifyLeft: function() {
		this._execCommand("JustifyLeft");
		this._onCommandExecuted();
	},

	/**
	 * Justify current editor paragraph or paragraphs in a selection
	 * to the Right.
	 * This command transfers the focus to the editor.
	 */
	doJustifyRight: function() {
		this._execCommand("JustifyRight");
		this._onCommandExecuted();
	},

	/**
	 * De-indent the current paragraph or move a list item to a higher level
	 * in a list.
	 * This command transfers the focus to the editor.
	 */
	doOutdent: function() {
		this._execCommand("Outdent");
		this._onCommandExecuted();
	},

	/**
	 * Move up on the editor's undo stack.
	 * This command transfers the focus to the editor.
	 */
	doRedo: function() {
		this._execCommand("Redo");
		this._onCommandExecuted();
	},

	/**
	 * Remove an anchor from the editor content.
	 * This command transfers the focus to the editor.
	 * @param {HTMLElementNode} link reference to the anchor element to remove
	 */
	doRemoveAnchor: function(link) {
		if (link.href) {
			// if the anchor a href too only remove the name and class
			this.doUpdateInlineElement(link, {name: null, "class": ""});
		} else {
			this.doRemoveNode(link);
		}
	},

	/**
	 * Remove all formatting from the current editor selection.
	 * This command transfers the focus to the editor.
	 */
	doRemoveFormat: function() {

		if (SUI.browser.isIE) {

			this._execCommand("RemoveFormat");

			// And do some more work to remove the style attributes in
			// the current selection too
			var sel = this._editDoc.selection.createRange();
			var oRng1 = this._editDoc.body.createTextRange();

			var l = this._editDoc.body.getElementsByTagName("*");
			for (var i=l.length-1; i>=0; i--) {
				oRng1.moveToElementText(l[i]);
				if (1 == oRng1.compareEndPoints("StartToStart", sel)
						&& -1 == oRng1.compareEndPoints("StartToEnd", sel)) {
					l[i].removeAttribute("style");
				}
			}

		} else {

			this._execCommand("RemoveFormat");

		}
		this._onCommandExecuted();
	},

	/**
	 * Remove all formatting from the current editor selection.
	 * @param {HTMLElementNode} link reference to the anchor element to remove
	 */
	doRemoveLink: function(link) {
		this.doRemoveNode(link);
		// if the link also was an anchor reappend the anchor
		if (link.name) {
			this.doInsertAnchor(link.name);
		}
	},

	/**
	 * Remove node from the editor but keep its contents.
	 * @param {HTMLElementNode} el reference to the node to remove
	 */
	doRemoveNode: function(el) {

		if (SUI.browser.isIE) {

			// can't use execCommand, this breaks the undo stack
			el.removeNode(false);

		} else {

			// select node
			var r = this._getCurrentRange();
			r.selectNode(el);
			var s = this._getSelection();
			s.removeAllRanges();
			s.addRange(r);
			// remove it
			this._execCommand("delete", false);
			// and paste the contents
			this._execCommand("inserthtml", el.innerHTML);
		}

		this._onCommandExecuted();
	},

	/**
	 * Replace selected text in the editor (think search and replace). There
	 * needs to be selected text, this is not a text insert function.
	 * @param {String} text text to replace the selected text
	 */
	doReplace: function(text) {

		if (SUI.browser.isIE) {

			this.focus();
			// get the current range
			var rng = this._getCurrentRange();
			// check if there is data to replace, then replace
			if (rng.text != "") {
				rng.text = text;
			}

		} else {

			// get the current range
			var r = this._getCurrentRange();
			// check if there is data to replace, then replace
			if (r.toString() != "") {
				this._execCommand("inserthtml", text);
			}

		}

		this._onCommandExecuted();
	},

	/**
	 * Replace all occurances of a text in the editor. The search direction
	 * is ignored: replaceAll from current cursor position might be
	 * implemented in the future. Gecko ignores the wholeWord parameter.
	 * @param {String} what text to search and replace
	 * @param {String} text new text
	 * @param {boolean} caseSensitive case sensitive search
	 * @param {boolean} backward search direction, ignored: always foreward
	 * @param {boolean} wholeWord search on whole words only
	 * @return {boolean} the number of replacements made
	 */
	doReplaceAll: function(what, text, caseSensitive, backward, wholeWord) {

		var cnt = 0;
		this.focus();

		// normalize parameter values (false i.o. null)
		caseSensitive =    caseSensitive ? true : false;
		backward = false;
		wholeWord = wholeWord ? true : false;

		if (SUI.browser.isIE) {

			// set the current range to the beginning of the document
			var rng = this._getCurrentRange();
			rng.expand("textedit");
			rng.collapse(true);
			rng.select();

			// while found replace text
			while (this._ieFind(what, caseSensitive, backward, wholeWord)) {
				cnt++;
				this.doReplace(text);
			}

		} else {

			// set the current selection to the beginning of the document
			var s = this._getSelection();
			var rng = this._editDoc.createRange();
			rng.selectNode(this._editDoc.body);
			rng.collapse(true);
			s.removeAllRanges();
			s.addRange(rng);

			// while found replace text
			while (this._editWin.find(what, caseSensitive, backward,
					false, wholeWord, false, false)) {
				cnt++;
				this.doReplace(text);
			}

		}

		this._onCommandExecuted();

		return cnt;
	},

	/**
	 * Select the whole content of the editor.
	 * This command transfers the focus to the editor.
	 */
	doSelectAll: function() {
		this._execCommand("SelectAll");
		this._onCommandExecuted();
	},

	/**
	 * Toggle the underline state of the current editor selection.
	 * This command transfers the focus to the editor.
	 */
	doUnderline: function() {
		this._selectWord();
		this._execCommand("Underline");
		this._onCommandExecuted();
	},

	/**
	 * Move down on the editor's undo stack.
	 * This command transfers the focus to the editor.
	 */
	doUndo: function() {
		this._execCommand("Undo");
		this._onCommandExecuted();
	},

	/**
	 * Update an abbreviation in the editor content.
	 * This command transfers the focus to the editor.
	 * @param {HTMLElementNode} el a reference to the abbreviation element to
	 *     update
	 * @param {String} fullText the new full text for the abbreviation
	 */
	doUpdateAbbr: function(el, fullText) {
		this.doUpdateInlineElement(el, {title: fullText});
	},

	/**
	 * Update an anchor element in the editor content.
	 * This command transfers the focus to the editor.
	 * @param {HTMLElementNode} link a reference to the anchor element to
	 *     update
	 * @param {String} name the new anchor name
	 */
	doUpdateAnchor: function(link, name) {
		name=name.replace(/[\s]+/g,"_");
		this.doUpdateInlineElement(link, {name: name, "class": "sys_anchor"});
	},

	/**
	 * Update an image element in the editor content.
	 * This command transfers the focus to the editor.
	 * @param {HTMLElementNode} img te new image node to replace the old one
	 */
	doUpdateImg: function(img) {

		if (SUI.browser.isIE) {
			this.focus();
			// done by setting properties
		} else {
			// overwrite the current selection with new img-html
			this._execCommand("inserthtml", this._outerHTML(img));
			this._webKitImgFix();
		}
		this._onCommandExecuted();
	},

	/**
	 * Update an inline element in the editor content.
	 * This command transfers the focus to the editor.
	 * @param {HTMLElementNode} el a reference to element to update
	 * @param {Object} attr the new attributes for the element
	 */
	doUpdateInlineElement: function(el, attr) {

		if (SUI.browser.isIE) {

			this.focus();
			SUI.browser.setAttributes(el, attr);

		} else {

			// place te node into a range
			var r = this._editDoc.createRange();
			r.selectNode(el);

			// clone that range an add attributes
			var c = r.cloneContents();
			SUI.browser.setAttributes(c.childNodes[0], attr);

			// select te range
			var s = this._getSelection();
			s.removeAllRanges();
			r.selectNode(el);
			s.addRange(r);

			// and overwrite with new html
			this._execCommand("inserthtml", this._outerHTML(c));
		}
		this._onCommandExecuted();
	},

	/**
	 * Update a language mark in the editor content.
	 * This command transfers the focus to the editor.
	 * @param {HTMLElementNode} el a reference to the language mark to update
	 * @param {String} language the new language code for the marker
	 */
	doUpdateLang: function(el, language) {
		this.doUpdateInlineElement(el, {lang: language});
	},

	/**
	 * Update a link in the editor content.
	 * This command transfers the focus to the editor.
	 * @param {HTMLElementNode} link a reference to the link to update
	 * @param {String} attr the new attributes for the link
	 */
	doUpdateLink: function(link, attr) {
		this.doUpdateInlineElement(link, attr);
	},

	_onCommandExecuted: function() {
		// update the undo state
		this._undoState = (this._editDoc.queryCommandEnabled("Undo") ? 1 : 0)
			+ (this._editDoc.queryCommandEnabled("Redo") ? 2 : 0);
		this.callListener("onCommandExecuted");
	}

});