(function (ice, $) {
	/**
	 * TODO
	 * 1. Each time an ice node is removed, refresh change set
	 */

	"use strict";

	var exports = ice,
		rangy = ice.rangy,
		defaults, InlineChangeEditor;
	
	/* constants */
	var BREAK_ELEMENT = "br",
		PARAGRAPH_ELEMENT = "p",
		INSERT_TYPE = "insertType",
		DELETE_TYPE = "deleteType",
		ignoreKeyCodes = [
			{start: 0, end: 31}, // everything below space, special cases handled separately
			{start: 33, end: 40}, // nav keys
			{start: 45, end: 45}, // insert
			{start: 91, end: 93}, // windows keys
			{start: 112, end: 123}, // function keys
			{start: 144, end: 145}
		];

	defaults = {
	// ice node attribute names:
		attributes: {
			changeId: "data-cid",
			userId: "data-userid",
			userName: "data-username",
			sessionId: "data-session-id",
			time: "data-time",
			lastTime: "data-last-change-time",
			changeData: "data-changedata" // arbitrary data to associate with the node, e.g. version
		},
		// Prepended to `changeType.alias` for classname uniqueness, if needed
		attrValuePrefix: '',
		
		// Block element tagname, which wrap text and other inline nodes in `this.element`
		blockEl: 'p',
		
		// All permitted block element tagnames
		blockEls: ['div','p', 'ol', 'ul', 'li', 'h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'blockquote'],
		
		// Unique style prefix, prepended to a digit, incremented for each encountered user, and stored
		// in ice node class attributes - cts1, cts2, cts3, ...
		stylePrefix: 'cts',
		currentUser: {
			id: null,
			name: null
		},
	
		// Default change types are insert and delete. Plugins or outside apps should extend this
		// if they want to manage new change types. The changeType name is used as a primary
		// reference for ice nodes; the `alias`, is dropped in the class attribute and is the
		// primary method of identifying ice nodes; and `tag` is used for construction only.
		// Invoking `this.getCleanContent()` will remove all delete type nodes and remove the tags
		// for the other types, leaving the html content in place.
		changeTypes: {
			insertType: {
				tag: 'ins',
				alias: 'ins',
				action: 'Inserted'
			},
			deleteType: {
				tag: 'del',
				alias: 'del',
				action: 'Deleted'
			}
		},

		// Sets this.element with the contentEditable element
		contentEditable: undefined,//dfl, start with a neutral value
	
		// Switch for toggling track changes on/off - when `false` events will be ignored.
		_isTracking: true,
	
		tooltips: false,
		
		tooltipsDelay: 1,
	
		_isVisible : true, // state of change tracking visibility
		
		_changeData : null, // a string you can associate with the current change set, e.g. version
		
		_handleSelectAll: false, // if true, handle ctrl/cmd-A in the change tracker
		
		_sessionId: null
	};
	
	function isIgnoredKeyCode(key) {
		if (! key) {
			return true;
		}
		var i, len = ignoreKeyCodes.length, rec;
		
		for (i = 0; i < len; ++i) {
			rec = ignoreKeyCodes[i];
			if (key >= rec.start && key <= rec.end) {
				return true;
			}
		}
		return false;
	}

	/**
	 * @class ice.InlineChangeEditor
	 * The change tracking engine
	 * interacts with a <code>contenteditable</code> DOM element
	 */
	InlineChangeEditor = function (options) {

		// Data structure for modelling changes in the element according to the following model:
		//	[changeid] => {`type`, `time`, `userid`, `username`}
		options || (options = {});
		if (!options.element) {
			throw new Error("options.element must be defined for ice construction.");
		}
	
		this._changes = {};
		// Tracks all of the styles for users according to the following model:
		//	[userId] => styleId; where style is "this.stylePrefix" + "this.uniqueStyleIndex"
		this._userStyles = {};
		this.currentUser = {name: '', id: ''};
		this._styles = {}; // dfl, moved from prototype
		this._savedNodesMap = {};
		this.$this = $(this);
		this._browser = ice.dom.browser();
		this._tooltipMouseOver = this._tooltipMouseOver.bind(this);
		this._tooltipMouseOut = this._tooltipMouseOut.bind(this);
		
		$.extend(true, this, defaults, options);
		if (options.tooltips && (! $.isFunction(options.hostMethods.showTooltip) || ! $.isFunction(options.hostMethods.hideTooltip))) {
			throw new Error("hostMethods.showTooltip and hostMethods.hideTooltip must be defined if tooltips is true");
		}
		var us = options.userStyles || {}; // dfl, moved from prototype, allow preconfig
		for (var id in us) {
			if (us.hasOwnProperty(id)) {
				var st = us[id];
				if (! isNaN(st)) {
					this._userStyles[id] = this.stylePrefix + '-' + st;
					this._uniqueStyleIndex = Math.max(st, this._uniqueStyleIndex);
					this._styles[st] = true;
				}
			}
		}
		logError = options.hostMethods.logError || function(){ return undefined; };
		// cache css selectors
		this._insertSelector = '.' + this._getIceNodeClass(INSERT_TYPE);
		this._deleteSelector = '.' + this._getIceNodeClass(DELETE_TYPE);
		this._iceSelector = this._insertSelector + ',' + this._deleteSelector;
		
/*		this._domObserver = new window.MutationObserver(this._onDomMutation.bind(this));
		this._domObserverConfig = {
//			attributes: true,
			childList: true,
			characterData: false,
			subtree: true
		};
		this._domObserverTimeout = null; */
	};

	InlineChangeEditor.prototype = {
	
		// Incremented for each new user and appended to they style prefix, and dropped in the
		// ice node class attribute.
		_uniqueStyleIndex: 0,
	
		_browserType: null,
	
		// One change may create multiple ice nodes, so this keeps track of the current batch id.
		_batchChangeId: null,
	
		// Incremented for each new change, dropped in the changeIdAttribute.
		_uniqueIDIndex: 1,
	
		// Temporary bookmark tags for deletes, when delete placeholding is active.
		_delBookmark: 'tempdel',
		isPlaceHoldingDeletes: false,
	
		/**
		 * Turns on change tracking - sets up events, if needed, and initializes the environment,
		 * range, and editor.
		 */
		startTracking: function (options) {
			// dfl:set contenteditable only if it has been explicitly set
			if (typeof(this.contentEditable) == "boolean") {
				this.element.setAttribute('contentEditable', this.contentEditable);
			}
		
			
			this.initializeEnvironment();
			this.initializeEditor();
			this.initializeRange();
			this._updateTooltipsState(); //dfl
			
			return this;
		},
	
		/**
		 * Removes contenteditability and stops event handling.
		 * @param {Boolean} onlyICE if true, stop tracking but don't remove the contenteditable property of the tracked element
		 */
		stopTracking: function (onlyICE) {
	
			this._isTracking = false;
			try { // dfl added try/catch for ie
				// If we are handling events setup the delegate to handle various events on `this.element`.
				var e = this.element;
				if (e) {
					this.unlistenToEvents();
				}
		
				// dfl:reset contenteditable unless requested not to do so
				if (! onlyICE && (typeof(this.contentEditable) !== "undefined")) {
					this.element.setAttribute('contentEditable', !this.contentEditable);
				}
			}
			catch (e){
				logError(e, "While trying to stop tracking");
			}

			this._updateTooltipsState();
			return this;
		},
		
		listenToEvents: function() {
			if (this.element && ! this._boundEventHandler) {
				this.unlistenToEvents();
				this._boundEventHandler = this.handleEvent.bind(this);
				this.element.addEventListener("keydown", this._boundEventHandler, true);
			}
		},
		
		unlistenToEvents: function() {
			if (this.element && this._boundEventHandler) {
				this.element.removeEventListener("keydown", this._boundEventHandler, true);
			}
			this._boundEventHandler = null;
		},
	
		/**
		 * Initializes the `env` object with pointers to key objects of the page.
		 */
		initializeEnvironment: function () {
			this.env || (this.env = {});
			this.env.element = this.element;
			this.env.document = this.element.ownerDocument;
			this.env.window = this.env.document.defaultView || this.env.document.parentWindow || window;
			this.env.frame = this.env.window.frameElement;
			this.env.selection = this.selection = new ice.Selection(this.env);
		},
	
		/**
		 * Initializes the internal range object and sets focus to the editing element.
		 */
		initializeRange: function () {
		},
	
		/**
		 * Initializes the content in the editor - cleans non-block nodes found between blocks and
		 * initializes the editor with any tracking tags found in the editing element.
		 */
		initializeEditor: function () {
			this._loadFromDom(); // refactored by dfl
			this._updateTooltipsState(); // dfl
		},
		
		/**
		 * Check whether or not this tracker is tracking changes.
		 * @return {Boolean} Is this tracker tracking?
		 */
		isTracking: function() {
			return this._isTracking;
		},
	
		/**
		 * Turn on change tracking and event handling.
		 */
		enableChangeTracking: function () {
			this._isTracking = true;
		},
	
		/**
		 * Turn off change tracking and event handling.
		 */
		disableChangeTracking: function () {
			this._isTracking = false;
		},
	
		/**
		 * Sets or toggles the tracking and event handling state.
		 * @param {Boolean} bTrack if undefined, the tracking state is toggled, otherwise set to the parameter
		 */
		toggleChangeTracking: function (bTrack) {
			bTrack = (undefined === bTrack) ? ! this._isTracking : Boolean(bTrack);
			this._isTracking = bTrack;
		},
		
		/**
		 * Gets the current user
		 * @return {Object} an object with the properties id, name
		 */
		getCurrentUser: function() {
			var u = this.currentUser || {},
				id = (u.id === null || u.id === undefined) ? "" : String(u.id);
			return {name: u.name || "", id: id};
		},
		
		/**
		 * Set the user to be tracked. 
		 * @param {Object} inUser and object has the following properties:
		 * {`id`, `name`}
		 */
		setCurrentUser: function (inUser) {
			var user = {};
			inUser = inUser || {};
			user.name = inUser.name? String(inUser.name) : "";
			if (inUser.id !== undefined && inUser.id !== null) {
				user.id = String(inUser.id);
			}
			else {
				user.id = "";
			}
			
			this.currentUser = user;
			for (var key in this._changes) {
				var change = this._changes[key];
				if (change.userid == user.id) {
					change.username = user.name;
				}
			}
			var nodes = this.getIceNodes(),
				userId,
				userIdAttr = this.attributes.userId;
			nodes.each((function(i,node) {
				userId = node.getAttribute(userIdAttr);
				if (userId === null || userId === user.id) {
					node.setAttribute(this.attributes.userName, user.name);
				}
			}).bind(this));
		},

		/**
		 * Set the session id. If the session id is not null, the tracker aggregates change span
		 * from the same user only if they have the same session id
		 */
		setSessionId: function (sid) {
			this._sessionId = sid;
		},
		
		/**
		 * Sets or toggles the tooltips state.
		 * @param {Boolean} bTooltips if undefined, the tracking state is toggled, otherwise set to the parameter
		 */
		toggleTooltips: function(bTooltips) {
			bTooltips = (undefined === bTooltips) ? ! this.tooltips : Boolean(bTooltips);
			this.tooltips = bTooltips;
			this._updateTooltipsState();
		},
	
		visible: function(el) {
			if(el.nodeType === ice.dom.TEXT_NODE) el = el.parentNode;
			var rect = el.getBoundingClientRect();
			return ( rect.top > 0 && rect.left > 0);
		},

	 	
		/**
		 * Returns a tracking tag for the given `changeType`, with the optional `childNode` appended.
		 * @private
		 */
		_createIceNode: function (changeType, childNode, changeId) {
			var node = this.env.document.createElement(this.changeTypes[changeType].tag);
			node.setAttribute("class", this._getIceNodeClass(changeType));
	
			if (childNode) {
				node.appendChild(childNode);
			}
			this._addChange(changeType, [node], changeId);
	
			return node;
		},
	
		/**
		 * Inserts the given string/node into the given range with tracking tags, collapsing (deleting)
		 * the range first if needed. If range is undefined, then the range from the Selection object
		 * is used. If the range is in a parent delete node, then the range is positioned after the delete.
		 * @param options may contain <strong>nodes</strong> (DOM element or array of dom elements) or <strong>text</strong> (string). 
		 * @return {Boolean} true if the action should continue, false if the action was finished in the insert sequence
		 */
		insert: function (options) {
			this.hostMethods.beforeInsert && this.hostMethods.beforeInsert();

			var _rng = this.getCurrentRange(),
				range = this._isRangeInElement(_rng, this.element),
				hostRange = range ? null : this.hostMethods.getHostRange(),
				changeid = this._startBatchChange(),
				hadSelection = Boolean(range && !range.collapsed),
				ret = false;
			
			options = options || {};
			
			
		// If we have any nodes selected, then we want to delete them before inserting the new text.
			try {
				if (hadSelection) {
					this._deleteContents(false, range); 
				// Update the range
					range = this.getCurrentRange();
				}
				if (range || hostRange) {
					var nodes = options.nodes;
					if (nodes && ! $.isArray(nodes)) {
						nodes = [nodes];
					}
			
					// If we are in a non-tracking/void element, move the range to the end/outside.
					this._moveRangeToValidTrackingPos(range, hostRange);
			
					// insertnodes returns true if the text was inserted
					ret = this._insertNodes(range, hostRange, {nodes: nodes, text: options.text, insertStubText: options.insertStubText !== false});
				}
			}
			catch(e) {
				logError(e, "while trying to insert nodes");
			}
			finally {
				this._endBatchChange(changeid, nodes || options.text || ret);
			}
			return ret;//isPropagating;
		},
	
		/**
		 * Deletes the contents in the given range or the range from the Selection object. If the range
		 * is not collapsed, then a selection delete is handled; otherwise, it deletes one character
		 * to the left or right if the right parameter is false or true, respectively.
		 * @return true if deletion was handled.
		 * @private
		 */
		_deleteContents: function (right, range) {
			var prevent = true, changeid,
				browser = this._browser;
			
			this.hostMethods.beforeDelete && this.hostMethods.beforeDelete();
			if (range) {
				this.selection.addRange(range);
			} 
			else {
				range = this.getCurrentRange();
			}
			changeid = this._startBatchChange();
			try {
				if (range.collapsed === false) {
					range = this._deleteSelection(range);
	/*				if(this._browser.mozilla){
						if(range.startContainer.parentNode.previousSibling){
							range.setEnd(range.startContainer.parentNode.previousSibling, 0);
							range.moveEnd(ice.dom.CHARACTER_UNIT, ice.dom.getNodeCharacterLength(range.endContainer));
						}
						else { 
							range.setEndAfter(range.startContainer.parentNode);
						}
						range.collapse(false);
					}
					else { */
						if(range && ! this.visible(range.endContainer)) {
							range.setEnd(range.endContainer, Math.max(0, range.endOffset - 1));
							range.collapse(false);
						}
//					}
				}
				else {
					this._cleanupSelection(range, false, true);
					// if we're inside a current insert range, let the editor take care of the deletion
					if (this._isCurrentUserIceNode(this._getIceNode(range.startContainer, INSERT_TYPE))) {
						return false;
					}

			        if (right) {
						// RIGHT DELETE
						if(browser["type"] === "mozilla"){
							prevent = this._deleteRight(range);
							// Handling track change show/hide
							if(!this.visible(range.endContainer)){
								if(range.endContainer.parentNode.nextSibling){
			//						range.setEnd(range.endContainer.parentNode.nextSibling, 0);
									range.setEndBefore(range.endContainer.parentNode.nextSibling);
								} else {
									range.setEndAfter(range.endContainer);
								}
								range.collapse(false);
							}
						}
						else {
							// Calibrate Cursor before deleting
							if(range.endOffset === ice.dom.getNodeCharacterLength(range.endContainer)){
								var next = range.startContainer.nextSibling;
								if ($(next).is(this._deleteSelector)) {
									while(next){
										if ($(next).is(this._deleteSelector)) {
											next = next.nextSibling;
											continue;
										}
										range.setStart(next, 0);
										range.collapse(true);
										break;
									}
								}
							}
			
							// Delete
							prevent = this._deleteRight(range);
			
							// Calibrate Cursor after deleting
							if(!this.visible(range.endContainer)){
								if ($(range.endContainer.parentNode).is(this._iceSelector)) {
			//						range.setStart(range.endContainer.parentNode.nextSibling, 0);
									range.setStartAfter(range.endContainer.parentNode);
									range.collapse(true);
								}
							}
						}
					}
					else {
						// LEFT DELETE
						if(browser.mozilla){
							prevent = this._deleteLeft(range);
							// Handling track change show/hide
							if(!this.visible(range.startContainer)){
								if(range.startContainer.parentNode.previousSibling){
									range.setEnd(range.startContainer.parentNode.previousSibling, 0);
								} else {
									range.setEnd(range.startContainer.parentNode, 0);
								}
								range.moveEnd(ice.dom.CHARACTER_UNIT, ice.dom.getNodeCharacterLength(range.endContainer));
								range.collapse(false);
							}
						}
						else {
							if(!this.visible(range.startContainer)){
								if(range.endOffset === ice.dom.getNodeCharacterLength(range.endContainer)){
									var prev = range.startContainer.previousSibling;
									if ($(prev).is(this._deleteSelector)) {
										while(prev){
											if ($(prev).is(this._deleteSelector)) {
												prev = prev.prevSibling;
												continue;
											}
											range.setEndBefore(prev.nextSibling, 0);
											range.collapse(false);
											break;
										}
									}
								}
							}
							prevent = this._deleteLeft(range);
						}
					}
				}
		
				range && this.selection.addRange(range);
			}
			finally {
				this._endBatchChange(changeid, prevent);
			}
			return prevent;
		},
	
		/**
		 * Returns the changes - a hash of objects with the following properties:
		 * [changeid] => {`type`, `time`, `userid`, `username`, `lastTime`, `data`}
		 * @param {LITE.AcceptRejectOptions} [options=null] filtering options for the changes to be accepted
		 */
		getChanges: function (options) {
			var changes = options ? this._filterChanges(options) : this._changes;
			return $.extend({}, changes);
		},
	
		/**
		 * Returns an array with the user ids who made the changes
		 */
		getChangeUserids: function () {
			var self = this,
				keys = Object.keys(this._changes),
				result = keys.map(function(key) {
					return self._changes[keys[key]].userid
				});

			// probably makes the list unique
			return result.sort().filter(function (el, i, a) {
				if (i === a.indexOf(el)) return 1;
				return 0;
			});
		},
	
		/**
		 * Returns the html contents for the tracked element.
		 */
		getElementContent: function () {
			return this.element.innerHTML;
		},
	
		/**
		 * Returns the html contents, without tracking tags, for `this.element` or
		 * the optional `body` param which can be of either type string or node.
		 * Delete tags, and their html content, are completely removed; all other
		 * change type tags are removed, leaving the html content in place. After
		 * cleaning, the optional `callback` is executed, which should further
		 * modify and return the element body.
		 *
		 * prepare gets run before the body is cleaned by ice.
		 */
		getCleanContent: function (body, callback, prepare) {
			var newBody = this.getCleanDOM(body, {callback:callback, prepare: prepare, clone: true});
			return (newBody && newBody.innerHTML) || "";
		},
		
		/**
		 * Returns a clone of the DOM, without tracking tags, for `this.element` or
		 * the optional `body` param which can be of either type string or node.
		 * Delete tags, and their html content, are completely removed; all other
		 * change type tags are removed, leaving the html content in place. 
		 * @param body If not null, the node or html to process
		 * @param options may contain:
		 * <ul><li>callback - executed after cleaning, should return the processed body</li>
		 * <li>clone If true, process a clone of the target element</li>
		 * <li>prepare function to run on body before the cleaning</li>
		 */
		getCleanDOM : function(body, options) {
			var classList = '',
				self = this;
			options = options || {};
			$.each(this.changeTypes, function (type, i) {
				if (type !== DELETE_TYPE) {
					if (i > 0) {
						classList += ',';
					}
					classList += '.' + self._getIceNodeClass(type);
				}
			});
			if (body) {
				if (typeof body === 'string') {
					body = $('<div>' + body + '</div>');
				}
				else if (options.clone){
					body = $(body).clone()[0];
				}
			} 
			else {
				body = options.clone? $(this.element).clone()[0] : this.element;
			}
			return this._cleanBody(body, classList, options);
		},
		
		_cleanBody: function(body, classList, options) {
			body = options.prepare ? options.prepare.call(this, body) : body;
			var $body = $(body), deletes,
				changes = $body.find(classList);
			$.each(changes, function (i,el) {
				while (el.firstChild) {
					el.parentNode.insertBefore(el.firstChild, el);
				}
				el.parentNode.removeChild(el);
			});
			
			$body.find(this._deleteSelector).remove();
	
			body = options.callback ? options.callback.call(this, body) : body;
	
			return body;
		},
	
		/**
		 * Accepts all changes in the element body - removes delete nodes, and removes outer
		 * insert tags keeping the inner content in place.
		 * @param {LITE.AcceptRejectOptions} options=null filtering options for the changes to be accepted
		 */
		acceptAll: function (options) {
			if (options) {
				return this._acceptRejectSome(options, true);
			}
			else {
				this.getCleanDOM(this.element, {
					clone: false
				});
				this._changes = {}; // dfl, reset the changes table
				this._triggerChange({ isText: true }); // notify the world that our change count has changed
			}
		},
	
		/**
		 * Rejects all changes in the element body - removes insert nodes, and removes outer
		 * delete tags keeping the inner content in place.*
		 * @param {LITE.AcceptRejectOptions} options=null filtering options for the changes to be accepted
		 */
		rejectAll: function (options) {
			if (options) {
				return this._acceptRejectSome(options, false);
			}
			else {
				var insSel = this._insertSelector,
					delSel = this._deleteSelector,
					content, self = this,
					$element = $(this.element);
		
				$element.find(insSel).each(function(i,e) {
					self._removeNode(e);
				});
				$element.find(delSel).each(
					function (i, el) {
					content = ice.dom.contents(el);
					ice.dom.replaceWith(el, content);
					$.each(content, function(i,e) {
						var parent = e && e.parentNode;
						self._normalizeNode(parent);
					});
				});
				this._changes = {}; // dfl, reset the changes table
				this._triggerChange({ isText: true }); // notify the world that our change count has changed
			}
		},
	
		/**
		 * Accepts the change at the given, or first tracking parent node of, `node`.	If
		 * `node` is undefined then the startContainer of the current collapsed range will be used.
		 * In the case of insert, inner content will be used to replace the containing tag; and in
		 * the case of delete, the node will be removed.
		 */
		acceptChange: function (node) {
			this.acceptRejectChange(node, { isAccept: true });
		},
	
		/**
		 * Rejects the change at the given, or first tracking parent node of, `node`.	If
		 * `node` is undefined then the startContainer of the current collapsed range will be used.
		 * In the case of delete, inner content will be used to replace the containing tag; and in
		 * the case of insert, the node will be removed.
		 */
		rejectChange: function (node) {
			this.acceptRejectChange(node, { isAccept: false });
		},
	
		/**
		 * Handles accepting or rejecting tracking changes
		 */
		acceptRejectChange: function (node, options) {
			var delSel, insSel, selector, removeSel, replaceSel, 
				trackNode, changes, dom = ice.dom, nChanges,
				self = this, changeId, content, userStyle,
				$element = $(this.element),
				userStyles = this._userStyles,
				userId, userAttr = this.attributes.userId,
				delClass = this._getIceNodeClass(DELETE_TYPE), 
				insClass = this._getIceNodeClass(INSERT_TYPE),
				isAccept = options && options.isAccept,
				dontNotify = options && (options.notify === false);
		
			if (!node) {
				var range = this.getCurrentRange();
				if (! range || !range.collapsed) {
					return;
				}
				node = range.startContainer;
			}
		
			delSel = removeSel = '.' + delClass;
			insSel = replaceSel = '.' + insClass;
			if (!isAccept) {
				removeSel = insSel;
				replaceSel = delSel;
			}
	
			selector = delSel + ',' + insSel;
			trackNode = dom.getNode(node, selector);
			changeId = trackNode.getAttribute(this.attributes.changeId);
				// Some changes are done in batches so there may be other tracking
				// nodes with the same `changeIdAttribute` batch number.
			changes = $element.find(removeSel + '[' + this.attributes.changeId + '=' + changeId + ']');
			nChanges = changes.length;
			changes.each(function(i, changeNode) {
				self._removeNode(changeNode);
			});

			// we handle the replaced nodes after the deleted nodes because, well, the engine may b buggy, resulting in some nesting
			changes = $element.find(replaceSel + '[' + this.attributes.changeId + '=' + changeId + ']');
			nChanges += changes.length;
		
			$.each(changes, function (i, node) {
				if (isNewlineNode(node)) {
					return stripNode(node);
				}
				userId = node.getAttribute(userAttr);
				userStyle = userId !== null ? userStyles[userId] || "" :"";
				
				content = ice.dom.contents(node); 
				// work around a situation where the browser extracts the node style and applies it to the content
				$(node).removeClass(insClass + ' ' + delClass + ' ' + userStyle);
				dom.replaceWith(node, content);
				$.each(content, function(i,e) {
					var txt = ice.dom.TEXT_NODE == e.nodeType && e.nodeValue;
					if (txt) {
						var found = false;
						while (txt.indexOf("  ") >= 0) {
							found = true;
							txt = txt.replace("  ", " \u00a0"); // replace two spaces with space+nbsp
						}
						if (found) {
							e.nodeValue = txt;
						}
					}
					var parent = e && e.parentNode;
					self._normalizeNode(parent);
				});
			});

			/* begin dfl: if changes were accepted/rejected, remove change trigger change event */
			delete this._changes[changeId];
			if (nChanges > 0 && ! dontNotify) {
				this._triggerChange({ isText: true });
			}
			/* end dfl */
		},
	
		/**
		 * Returns true if the given `node`, or the current collapsed range is in a tracking
		 * node; otherwise, false.
		 * @param node The node to test or null to test the selection
		 * @param onlyNode if true, test only the node
		 * @param cleanupDOM - if false, don't mess with the selection, just test
		 */
		isInsideChange: function (node, onlyNode, cleanupDOM) {
			try {
				return Boolean(this.currentChangeNode(node, onlyNode, cleanupDOM));
			}
			catch (e) {
				logError(e, "While testing if isInsideChange");
				return false;
			}
		},
	
		/**
		 * Returns a jquery list of all the tracking nodes in the current editable element
		 */
		getIceNodes : function() {
			var classList = [];
			var self = this;
			$.each(this.changeTypes, // iterate over type map
				function (type) {
					classList.push('.' + self._getIceNodeClass(type));
				});
			classList = classList.join(',');
			return $(this.element).find(classList);
		},
		
		/**
		 * Returns this `node` or the first parent tracking node with the given `changeType`.
		 * @private
		 */
		_getIceNode: function (node, changeType) {
			var selector = this.changeTypes[changeType].tag + '.' + this._getIceNodeClass(changeType);
			return ice.dom.getNode((node && node.$) || node, selector);
		},
		
		_isNodeOfChangeType: function(node, changeType) {
			if (! node) {
				return false;
			}
			var selector = '.' + this._getIceNodeClass(changeType);
			return $(node.$ || node).is(selector);
		},
		
		_isInsertNode: function(node) {
			return this._isNodeOfChangeType(node, INSERT_TYPE);
		},
		
		_isDeleteNode: function(node) {
			return this._isNodeOfChangeType(node, DELETE_TYPE);
		},
		
		_normalizeNode: function(node) {
			return 	ice.dom.normalizeNode(node, this._browser.msie);
		},
	
		/**
		 * Sets the given `range` to the first position, to the right, where it is outside of
		 * void elements.
		 * @private
		 */
		_moveRangeToValidTrackingPos: function (range, hostRange) {
			// set range to hostRange if available
			if (! (range = (hostRange || range))) {
				return;
			}
			
			var voidEl,
				el, searchBack = -1, elNode,
				visited = [], newEdge, edgeNode,
				fnode = hostRange ? this.hostMethods.getHostNode : nativeElement,
				found = false;
			while (! found) {
				el = range.startContainer;
				if (! el || visited.indexOf(el) >= 0) {
					return; // loop
				}
				elNode = fnode(el);
				visited.push(el);
				voidEl = this._getVoidElement(elNode);
				if (voidEl) {
					if ((voidEl !== el) && (visited.indexOf(voidEl) >= 0)) {
						return; // loop
					}
					visited.push(voidEl);
				}
				else {
					found = ice.dom.isTextContainer(elNode);
				}
				if (! found) { // in void element or non text container
					if (-1 == searchBack) {
						searchBack = ! isOnRightEdge(fnode(range.startContainer), range.startOffset);
					}
					newEdge = searchBack ? ice.dom.findPrevTextContainer(voidEl || elNode, this.element) :
							ice.dom.findNextTextContainer(voidEl || elNode, this.element);
					edgeNode = newEdge.node;
					// we have a new edge node

					if (hostRange) {
						edgeNode = this.hostMethods.makeHostElement(edgeNode);
					}
					try { 
						if (searchBack) {
							range.setStart(edgeNode, newEdge.offset);
						}
						else {
							range.setEnd(edgeNode, newEdge.offset);
						}
						range.collapse(searchBack);
					}
					catch (e) { // if we can't set the selection for whatever reason, end of document etc., break
						logError(e, "While trying to move range to valid tracking position");
						break;
					}
				}
			}
		},
		
		/**
		 * Utility function
		 * Returns the range if its startcontainer is a descendant of (or equal to) the given top element
		 * @private
		 */
		_isRangeInElement: function(range, top) {
			var start = range && range.startContainer;
			while (start) {
				if (start == top) {
					return range;
				}
				start = start.parentNode;
			}
			return null;
		},
	
	
		/**
		 * Returns the given `node` or the first parent node that matches against the list of void elements.
		 * dfl: added try/catch
		 * @private
		 */
		_getVoidElement: function (node) {
			
			try {
				var voidParent = this._getIceNode(node, DELETE_TYPE);
				if (! voidParent) {
					if (3 == node.nodeType && node.nodeValue == '\u200B') {
						return node;
					}
				}
				return voidParent;
			}
			catch(e) {
				logError(e, "While trying to get void element of", node);
				return null;
			}
		},
		
		/**
		 * @private
		 * If the range is collapsed, removes empty nodes around the caret
		 * @param range the range to clean up
		 * @param isHostRange if true, the range is a ckeditor range
		 * @param changeSelection if true, the selected node can also be cleaned up
		 */
		_cleanupSelection: function(range, isHostRange, changeSelection) {
			var start;
			if (! range || ! range.collapsed || ! (start = range.startContainer)) {
				return;
			}
			if (isHostRange) {
				start = this.hostMethods.getHostNode(start);
			}
			var nt = start.nodeType;
			if (ice.dom.TEXT_NODE == nt) {
				return this._cleanupTextSelection(range, start, isHostRange, changeSelection);
			}
			else {
				return this._cleanupElementSelection(range, isHostRange);
			}
		},
		
		/**
		 * @private
		 * assumes range is valid for this operation
		 */
		_cleanupTextSelection: function(range, start, isHostRange, changeSelection) {
			this._cleanupAroundNode(start);
			if (changeSelection && ice.dom.isEmptyTextNode(start)) {
				var parent = start.parentNode, 
					ind = ice.dom.getNodeIndex(start),
					f = isHostRange ? this.hostMethods.makeHostElement : nativeElement;
				parent.removeChild(start);
				ind = Math.max(0, ind);
				range.setStart(f(parent), ind);
				range.setEnd(f(parent), ind);
			}
		},

			
			/**
		 * @private
		 * assumes range is valid for this operation
		 */
		_cleanupElementSelection: function(range, isHostRange) {
			var start, includeStart = false,
				parent = isHostRange ? this.hostMethods.getHostNode(range.startContainer) : range.startContainer,
				childCount = parent.childNodes.length;
			if (childCount < 1) {
				return;
			}
			try {
				if (range.startOffset > 0) {
					start = parent.childNodes[range.startOffset - 1];
				}
				else {
					start = parent.firstChild;
					includeStart = true;
				}
				if (! start) {
					return;
				}
			}
			catch(e) {
				return;
			}
			this._cleanupAroundNode(start, includeStart);
			if (range.startOffset === 0) {
				return;
			}
			var ind = ice.dom.getNodeIndex(start) + 1;
			if (ice.dom.isEmptyTextNode(start)) {
				ind = Math.max(0, ind - 1);
				parent.removeChild(start);
			}
			if (parent.childNodes.length !== childCount) {
				var f = isHostRange ? this.hostMethods.makeHostElement : nativeElement;
				range.setStart(f(parent), ind);
				range.setEnd(f(parent), ind);
			}
		},
		
		_cleanupAroundNode: function(node, includeNode) {
			var parent = node.parentNode,
				anchor = node.nextSibling,
				isEmpty,
				tmp;
			while (anchor) {
				isEmpty = ($(anchor).is(this._iceSelector) && ice.dom.hasNoTextOrStubContent(anchor)) 
					|| ice.dom.isEmptyTextNode(anchor);
				if (isEmpty) {
					tmp = anchor;
					anchor = anchor.nextSibling;
					parent.removeChild(tmp);
				}
				else {
					anchor = anchor.nextSibling;
				}
			}
			anchor = node.previousSibling;
			while (anchor) {
				isEmpty = ($(anchor).is(this._iceSelector) && ice.dom.hasNoTextOrStubContent(anchor)) 
				|| ice.dom.isEmptyTextNode(anchor);
				if (isEmpty) {
					tmp = anchor;
					anchor = anchor.previousSibling;
					parent.removeChild(tmp);
				}
				else {
					anchor = anchor.previousSibling;
				}
			}
			if (includeNode && ice.dom.isEmptyTextNode(node)) {
				parent.removeChild(node);
			}
		},
	
		/**
		 * Returns true if node has a user id attribute that matches the current user id.
		 * @private
		 */
		_isCurrentUserIceNode: function (node) {
			var ret = Boolean(node && $(node).attr(this.attributes.userId) === this.currentUser.id);
			if (ret && this._sessionId) {
				ret = node.getAttribute(this.attributes.sessionId) === this._sessionId;
			}
			return ret;
		},
	
		/**
		 * With the given alias, searches the changeTypes objects and returns the
		 * associated key for the alias.
		 * @private
		 */
		_getChangeTypeFromAlias: function (alias) {
			var type, ctnType = null;
			for (type in this.changeTypes) {
				if (this.changeTypes.hasOwnProperty(type)) {
					if (this.changeTypes[type].alias == alias) {
						ctnType = type;
					}
				}
			}
	
			return ctnType;
		},
	
/**
 * @private
 */				
		_getIceNodeClass: function (changeType) {
			return this.attrValuePrefix + this.changeTypes[changeType].alias;
		},
	
		/**
		 * @private
		 */				
		_getUserStyle: function (userid) {
			if (userid === null || userid === "" || "undefined" == typeof userid) {
				return this.stylePrefix;
			}
			var styleIndex = null;
			if (this._userStyles[userid]) {
				styleIndex = this._userStyles[userid];
			}
			else {
				styleIndex = this._setUserStyle(userid, this._getNewStyleId());
			}
			return styleIndex;
		},
	
		/**
		 * @private
		 */
		_setUserStyle: function (userid, styleIndex) {
			var style = this.stylePrefix + '-' + styleIndex;
			if (!this._styles[styleIndex]) {
				this._styles[styleIndex] = true;
			}
			return this._userStyles[userid] = style;
		},
	
		_getNewStyleId: function () {
			var id = ++this._uniqueStyleIndex;
			if (this._styles[id]) {
			// Dupe.. create another..
				return this._getNewStyleId();
			} 
			else {
				this._styles[id] = true;
				return id;
			}
		},
	
		_addChange: function (ctnType, ctNodes, changeIdToUse) {
			var changeid = changeIdToUse || this._batchChangeId || this.getNewChangeId(),
				self = this;

			if (!this._changes[changeid]) {
				var now =  (new Date()).getTime();
				// Create the change object.
				this._changes[changeid] = {
					type: ctnType,
					time: now,
					lastTime: now,
					sessionId: this._sessionId,
					userid: String(this.currentUser.id),// dfl: must stringify for consistency - when we read the props from dom attrs they are strings
					username: this.currentUser.name,
					data : this._changeData || ""
				};
				this._triggerChange({ text: false }); //dfl
			}
			$.each(ctNodes, function (i) {
				self._addNodeToChange(changeid, ctNodes[i]);
			});
	
			return changeid;
		},
	
		/**
		 * Adds tracking attributes from the change with changeid to the ctNode.
		 * @param changeid Id of an existing change.
		 * @param ctNode The element to add for the change.
		 * @private
		 */
		_addNodeToChange: function (changeid, ctNode) {
			var change = this.getChange(changeid),
				attributes = {};
			
			if (!ctNode.getAttribute(this.attributes.changeId)) {
				attributes[this.attributes.changeId] = changeid;
			}
// handle missing userid, try to set username according to userid
			var userId = ctNode.getAttribute(this.attributes.userId); 
			if (! userId) {
				userId = change.userid;
				attributes[this.attributes.userId] = userId;
			}
			if (userId == change.userid) {
				attributes[this.attributes.userName] = change.username;
			}
			
// add change data
			var changeData = ctNode.getAttribute(this.attributes.changeData);
			if (null === changeData) {
				attributes[this.attributes.changeData] = this._changeData || "";
			}
			
			if (!ctNode.getAttribute(this.attributes.time)) {
				attributes[this.attributes.time] = change.time;
			}
			
			if (!ctNode.getAttribute(this.attributes.lastTime)) {
				attributes[this.attributes.lastTime] = change.lastTime;
			}
			
			if (change.sessionId && ! ctNode.getAttribute(this.attributes.sessionId)) {
				attributes[this.attributes.sessionId] = change.sessionId;
			}
			
			if (! change.style) {
				change.style = this._getUserStyle(change.userid);
			}
			$(ctNode).attr(attributes).addClass(change.style);
			/* Added by dfl */
			this._updateNodeTooltip(ctNode);
		},
	
		getChange: function (changeid) {
			return this._changes[changeid] || null;
		},
	
		getNewChangeId: function () {
			var id = ++this._uniqueIDIndex;
			if (this._changes[id]) {
				// Dupe.. create another..
				id = this.getNewChangeId();
			}
			return id;
		},
	
		/**
		 * @private
		 * Start a batch change if none is already underway
		 * @return a change id if a new batch has been started, otherwise null
		 */
		_startBatchChange: function () {
			return this._batchChangeId ? null : 
				(this._batchChangeId = this.getNewChangeId());
		},
		
		/**
		 * Returns the top level DOM element handled by this change tracker
		 */
		getContentElement: function() {
			return this.element;
		},
		
		/**
		 * @private
		 * End the batch change
		 * @param changeid If not identical to the current change id, no action is taken
		 * this allows callers to start a batch change but end it only if the change was really started by the caller
		 * @param wasTextChanged if true, notify that text was changed in this batch
		 */
		_endBatchChange: function (changeid, wasTextChanged) {
			if (changeid && (changeid === this._batchChangeId)) {
				this._batchChangeId = null;
				
				if (wasTextChanged) {
					this._triggerChange({ isText: true });
				}
			}
		},
	
		getCurrentRange: function () {
			try {
				return this.selection.getRangeAt(0);
			}
			catch (e) {
				logError(e, "While trying to get current range");
				return null;
			}
		},
	
		_insertNodes: function (_range, hostRange, _data) {
			var range = hostRange || _range,
				data = _data || {},
				_start = range.startContainer,
				start = (_start && _start.$) || _start,
				f = hostRange ? this.hostMethods.makeHostElement : nativeElement,
				nodes = data.nodes,
				insertStubText = data.insertStubText !== false,
				text = data.text, i, len,
				doc= this.env.document,
				inserted = false;
				
			var ctNode = this._getIceNode(start, INSERT_TYPE),
				inCurrentUserInsert = this._isCurrentUserIceNode(ctNode);
	
			this._cleanupSelection(range, Boolean(hostRange), true);
			if (inCurrentUserInsert) {
				var head = nodes && nodes[0],
					changeId = ctNode.getAttribute(this.attributes.changeId);
				if (head) {
					inserted = true;
					range.insertNode(f(head));
					var parent = head.parentNode,
						sibling = head.nextSibling;

					len = nodes.length;
					for (i = 1; i < len; ++i) {
						if (sibling) {
							parent.insertBefore(nodes[i], sibling);
						}
						else {
							parent.appendChild(nodes[i]);
						}
					}
					/* Now move the caret to the end of the last node inserted */
					var tail = nodes[len - 1];
					if (ice.dom.TEXT_NODE == tail.nodeType) {
						range.setEnd(tail, (tail.nodeValue && tail.nodeValue.length) || 0);
					}
					else {
						range.setEndAfter(tail);
					}
					range.collapse();
					if (hostRange) {
						this.hostMethods.setHostRange(hostRange);
					}
					else {
						this.selection.addRange(range);
					}
				}
				else {
					prepareSelectionForInsert(null, range, doc, true);
				}
				// even if there was no data to insert, we are probably setting up for a char insertion
				this._updateChangeTime(changeId);
			}
			else {
				// If we aren't in an insert node which belongs to the current user, then create a new ins node
				var node = this._createIceNode(INSERT_TYPE);
				if (ctNode) {
					var nChildren = ctNode.childNodes.length;
					this._normalizeNode(ctNode);
					if (nChildren !== ctNode.childNodes.length) { // normalization removed nodes, refresh range
						if (hostRange) {
							hostRange = range = this.hostMethods.getHostRange();
						}
						else {
							range.refresh();
						}
					}
					if (ctNode) {
						var end = (hostRange && this.hostMethods.getHostNode(hostRange.endContainer)) || range.endContainer;
						// if inserting before the end of a tracked node by another user
						if ((end.nodeType == 3 && range.endOffset < range.endContainer.length) || (end !== ctNode.lastChild)) {
							ctNode = this._splitNode(ctNode, range.endContainer, range.endOffset);
						}
					}
				}
				if (ctNode) {
					range.setStartAfter(f(ctNode));
					range.collapse(true);
				}

				
				range.insertNode(f(node));
				len = (nodes && nodes.length) || 0;
				if (len) {
					inserted = true;
					for (i = 0; i < len; ++i) {
						node.appendChild(nodes[i]);
					}
					range.setEndAfter(f(node.lastChild));
					range.collapse();
				}
				else if (text) {
					inserted = true;
					var tn = doc.createTextNode(text);
					node.appendChild(tn);
					range.setEnd(tn, 1);
					range.collapse();
				}
				else {
					prepareSelectionForInsert(node, range, doc, insertStubText);
				}
				if (hostRange) {
					this.hostMethods.setHostRange(hostRange);
				}
				else {
					this.selection.addRange(range);
				}
			}
			return inserted;
		},
		
		/**
		 * @private
		 * updates the change with the current time stamp and copies to change nodes
		 */
		_updateChangeTime: function(changeId) {
			var change = this._changes[changeId];
			if (change) {
				var now = (new Date()).getTime(),
					nodes = $(this.element).find('[' + this.attributes.changeId + '=' + changeId + ']'),
					attr = this.attributes.lastTime;
				change.lastTime = now; 
				nodes.each(function(index, node) {
					node.setAttribute(attr, now);
				});
			}
		},
	
		_handleVoidEl: function(el, range) {
			// If `el` is or is in a void element, but not a delete
			// then collapse the `range` and return `true`.
			var voidEl = el && this._getVoidElement(el);
			if (voidEl && !this._getIceNode(voidEl, DELETE_TYPE)) {
				range.collapse(true);
				return true;
			}
			return false;
		},
	
		_deleteSelection: function (range) {
	
			// Bookmark the range and get elements between.
			var bookmark = new ice.Bookmark(this.env, range),
				elements = ice.dom.getElementsBetween(bookmark.start, bookmark.end),
				betweenBlocks = [],
				deleteNodes = [], // used to collect the new deletion nodes
				addDeleteOptions = { deleteNodesCollection: deleteNodes, moveLeft: true, range: null };

			// elements length may change during the loop so don't optimize
			for (var i = 0; i < elements.length; i++) {
				var elem = elements[i];
				if (! elem || ! elem.parentNode) { // maybe removed as a side effect of removing other stuff
					continue;
				}
				if (ice.dom.isBlockElement(elem)) {
					betweenBlocks.push(elem);
					if (!ice.dom.canContainTextElement(elem)) {
						// Ignore containers that are not supposed to contain text. Check children instead.
						for (var k = 0; k < elem.childNodes.length; k++) {
							elements.push(elem.childNodes[k]);
						}
						continue;
					}
				}
				// Ignore empty space nodes
				if (ice.dom.isEmptyTextNode(elem)) {
					this._removeNode(elem);
					continue;
				}
		
				if (!this._getVoidElement(elem)) {
					// If the element is not a text or stub node, go deeper and check the children.
					if (elem.nodeType !== ice.dom.TEXT_NODE) {
						// Browsers like to insert breaks into empty paragraphs - remove them
						if (isBRNode(elem)) {
							this._addDeleteTrackingToBreak(elem, addDeleteOptions);
							continue;
						}
			
						if (ice.dom.isStubElement(elem)) {
							this._addDeleteTracking(elem, addDeleteOptions);
							continue;
						}
						if (ice.dom.hasNoTextOrStubContent(elem)) {
							this._removeNode(elem);
							continue;
						}
						
//						if (isParagraphNode(elem)) {
//							this._addDeleteTrackingToBreak(elem, addDeleteOptions);
//						}
			
						for (var j = 0; j < elem.childNodes.length; j++) {
							var child = elem.childNodes[j];
							elements.push(child);
						}
						continue;
					}
					var parentBlock = ice.dom.getBlockParent(elem);
					this._addDeleteTracking(elem, addDeleteOptions);
					if (ice.dom.hasNoTextOrStubContent(parentBlock)) {
						ice.dom.remove(parentBlock);
					}
				}
			}
			
			if (deleteNodes.length) {
				bookmark.remove();
				this._cleanupAroundNode(deleteNodes[0]);
				range.setStartBefore(deleteNodes[0]);
				range.collapse(true);
				this.selection.addRange(range);
			}
			else {	
				bookmark.selectStartAndCollapse();
				if (range = this.getCurrentRange()) {
					this._cleanupSelection(range, false, false);
					range = this.getCurrentRange();			
				}
			}
			return range;
		},
	
		/**
		 * Deletes to the right (delete key)
		 * @private
		 */
		_deleteRight: function (range) {
	
			var parentBlock = ice.dom.isBlockElement(range.startContainer) && range.startContainer || ice.dom.getBlockParent(range.startContainer, this.element) || null,
				isEmptyBlock = parentBlock ? (ice.dom.hasNoTextOrStubContent(parentBlock)) : false,
				nextBlock = parentBlock && ice.dom.getNextContentNode(parentBlock, this.element),
				nextBlockIsEmpty = nextBlock ? (ice.dom.hasNoTextOrStubContent(nextBlock)) : false,
				initialContainer = range.endContainer,
				initialOffset = range.endOffset, i,
				commonAncestor = range.commonAncestorContainer,
				nextContainer, returnValue = false;
	
	
			// If the current block is empty then let the browser handle the delete/event.
			if (isEmptyBlock) {
				return false;
			}
	
			// Some bugs in Firefox and Webkit make the caret disappear out of text nodes, so we try to put them back in.
			if (isBRNode(commonAncestor)) {
				this._addDeleteTrackingToBreak(commonAncestor, {range: range});
				return true;
			}
			
			if (commonAncestor.nodeType !== ice.dom.TEXT_NODE) {
				// If placed at the beginning of a container that cannot contain text, such as an ul element, place the caret at the beginning of the first item.
				if (initialOffset === 0 && ice.dom.isBlockElement(commonAncestor) && (!ice.dom.canContainTextElement(commonAncestor))) {
					var firstItem = commonAncestor.firstElementChild;
					if (firstItem) {
						range.setStart(firstItem, 0);
						range.collapse();
						return this._deleteRight(range);
					}
				}
		
				if (commonAncestor.childNodes.length > initialOffset) {
					var next = commonAncestor.childNodes[initialOffset];
					if (isBRNode(next)) {
						this._addDeleteTrackingToBreak(next, {range: range});
						return true;
					}
					range.setStart(commonAncestor.childNodes[initialOffset], 0);
					range.collapse(true);
					returnValue = this._deleteRight(range);
					range.refresh();
					return returnValue;
				}
				else {
					nextContainer = ice.dom.getNextContentNode(commonAncestor, this.element);
			
					if (nextContainer) {
						if (isBRNode(nextContainer)) {
							this._addDeleteTrackingToBreak(nextContainer, { range: range }); 
							return true;
						}
						range.setEnd(nextContainer, 0);
					}
					range.collapse();
					return this._deleteRight(range);
				}
			}
	
			// Move range to position the cursor on the inside of any adjacent container that it is going
			// to potentially delete into or after a stub element.	E.G.:	test|<em>text</em>	->	test<em>|text</em> or
			// text1 |<img> text2 -> text1 <img>| text2

			try {
				range.moveEnd(ice.dom.CHARACTER_UNIT, 1);
				range.moveEnd(ice.dom.CHARACTER_UNIT, -1);
			}
			catch (ignore){}
	
			// Handle cases of the caret is at the end of a container or placed directly in a block element
			if (initialOffset === initialContainer.data.length && (!ice.dom.hasNoTextOrStubContent(initialContainer))) {
				nextContainer = ice.dom.getNextNode(initialContainer, this.element);
		
				// If the next container is outside of ICE then do nothing.
				if (!nextContainer) {
					range.selectNodeContents(initialContainer);
					range.collapse();
					return false;
				}
		
				// If the next container is <br> element find the next node
				if (isBRNode(nextContainer)) {
					this._addDeleteTrackingToBreak(nextContainer, { range: range }); 
					return true;
//					nextContainer = ice.dom.getNextNode(nextContainer, this.element);
				}
		
				// If the next container is a text node, look at the parent node instead.
				if (nextContainer.nodeType === ice.dom.TEXT_NODE) {
					nextContainer = nextContainer.parentNode;
				}
		
				// If the next container is non-editable, enclose it with a delete ice node and add an empty text node after it to position the caret.
				if (!nextContainer.isContentEditable) {
					returnValue = this._addDeleteTracking(nextContainer, {range:null, moveLeft:false, merge: true});
					var emptySpaceNode = this.env.document.createTextNode('');
					nextContainer.parentNode.insertBefore(emptySpaceNode, nextContainer.nextSibling);
					range.selectNode(emptySpaceNode);
					range.collapse(true);
					return returnValue;
				}
		
				if (this._handleVoidEl(nextContainer, range)) {
					return true;
				}
		
				// If the caret was placed directly before a stub element, enclose the element with a delete ice node.
				if (ice.dom.isChildOf(nextContainer, parentBlock) && ice.dom.isStubElement(nextContainer)) {
					return this._addDeleteTracking(nextContainer, {range:range, moveLeft:false, merge:true});
				}
	
			}
	
			if (this._handleVoidEl(nextContainer, range)) {
				return true;
			}
	
			if (ice.dom.isOnBlockBoundary(range.startContainer, range.endContainer, this.element)) {
				if (this.mergeBlocks && $(ice.dom.getBlockParent(nextContainer, this.element)).is(this.blockEl)) {
					// Since the range is moved by character, it may have passed through empty blocks.
					// <p>text {RANGE.START}</p><p></p><p>{RANGE.END} text</p>
					if (nextBlock !== ice.dom.getBlockParent(range.endContainer, this.element)) {
						range.setEnd(nextBlock, 0);
					}
					// The browsers like to auto-insert breaks into empty paragraphs - remove them.
					var elements = ice.dom.getElementsBetween(range.startContainer, range.endContainer);
					for (i = 0; i < elements.length; i++) {
						ice.dom.remove(elements[i]);
					}
					return ice.dom.mergeBlockWithSibling(range, ice.dom.getBlockParent(range.endContainer, this.element) || parentBlock);
				}
				else {
					// If the next block is empty, remove the next block.
					if (nextBlockIsEmpty) {
						ice.dom.remove(nextBlock);
						range.collapse(true);
						return true;
					}
		
					// Place the caret at the start of the next block.
					range.setStart(nextBlock, 0);
					range.collapse(true);
					return true;
				}
			}
	
			var entireTextNode = range.endContainer,
				deletedCharacter = splitTextAt(entireTextNode, range.endOffset, 1);
	
			return this._addDeleteTracking(deletedCharacter, {range:range, moveLeft:false, merge:true});
	
		},
	
		/**
		 * Deletes to the left (backspace)
		 * @private
		 */
		_deleteLeft: function (range) {
			var parentBlock = ice.dom.isBlockElement(range.startContainer) && range.startContainer || ice.dom.getBlockParent(range.startContainer, this.element) || null,
			isEmptyBlock = parentBlock ? ice.dom.hasNoTextOrStubContent(parentBlock) : false,
			prevBlock = parentBlock && ice.dom.getPrevContentNode(parentBlock, this.element), // || ice.dom.getBlockParent(parentBlock, this.element) || null,
			prevBlockIsEmpty = prevBlock ? ice.dom.hasNoTextOrStubContent(prevBlock) : false,
			initialContainer = range.startContainer,
			initialOffset = range.startOffset,
			commonAncestor = range.commonAncestorContainer,
			lastSelectable, prevContainer;
	
			// If the current block is empty, then let the browser handle the key/event.
			if (isEmptyBlock) {
				return false;
			}

			if (isBRNode(commonAncestor)) {
				this._addDeleteTrackingToBreak(commonAncestor, {range: range, moveLeft: true});
				return true;
			}
			
			// Handle cases of the caret is at the start of a container or outside a text node
			if (initialOffset === 0 || commonAncestor.nodeType !== ice.dom.TEXT_NODE) {
			// If placed at the end of a container that cannot contain text, such as an ul element, place the caret at the end of the last item.
				if (ice.dom.isBlockElement(commonAncestor) && (!ice.dom.canContainTextElement(commonAncestor))) {
					if (initialOffset === 0) {
						var firstItem = commonAncestor.firstElementChild;
						if (firstItem) {
							range.setStart(firstItem, 0);
							range.collapse();
							return this._deleteLeft(range);
						}
					} 
					else {
						var lastItem = commonAncestor.lastElementChild;
						if (lastItem) {
							lastSelectable = range.getLastSelectableChild(lastItem);
							if (lastSelectable) {
								range.setStart(lastSelectable, lastSelectable.data.length);
								range.collapse();
								return this._deleteLeft(range);
							}
						}
					}
				}
		
				if (initialOffset === 0) {
					prevContainer = ice.dom.getPrevContentNode(initialContainer, this.element);
				} 
				else {
					prevContainer = commonAncestor.childNodes[initialOffset - 1];
				}
		
				// If the previous container is outside of ICE then do nothing.
				if (!prevContainer) {
					return false;
				}
		
				// Firefox finds an ice node wrapped around an image instead of the image itself sometimes, so we make sure to look at the image instead.
				if ($(prevContainer).is(this._iceSelector) && prevContainer.childNodes.length > 0 && prevContainer.lastChild) {
					prevContainer = prevContainer.lastChild;
				}
				
				if (isBRNode(prevContainer)) {
					this._addDeleteTrackingToBreak(prevContainer, { range: range, moveLeft: true });
					return true;
				}
		
				// If the previous container is a text node, look at the parent node instead.
				if (prevContainer.nodeType === ice.dom.TEXT_NODE) {
					prevContainer = prevContainer.parentNode;
				}
		
				// If the previous container is non-editable, enclose it with a delete ice node and add an empty text node before it to position the caret.
				if (!prevContainer.isContentEditable) {
					var returnValue = this._addDeleteTracking(prevContainer, {range:null, moveLeft:true, merge:true});
					var emptySpaceNode = document.createTextNode('');
					prevContainer.parentNode.insertBefore(emptySpaceNode, prevContainer);
					range.selectNode(emptySpaceNode);
					range.collapse(true);
					return returnValue;
				}
		
				if (this._handleVoidEl(prevContainer, range)) {
					return true;
				}
		
				// If the caret was placed directly after a stub element, enclose the element with a delete ice node.
				if (ice.dom.isStubElement(prevContainer) && ice.dom.isChildOf(prevContainer, parentBlock) || !prevContainer.isContentEditable) {
					 this._addDeleteTracking(prevContainer, {range:range, moveLeft:true, merge:true});
					 return true;
				}
		
				// If the previous container is a stub element between blocks
				// then just delete and leave the range/cursor in place.
				if (ice.dom.isStubElement(prevContainer)) {
					ice.dom.remove(prevContainer);
					range.collapse(true);
					return false;
				}
		
				if (prevContainer !== parentBlock && !ice.dom.isChildOf(prevContainer, parentBlock)) {
		
					if (!ice.dom.canContainTextElement(prevContainer)) {
						prevContainer = prevContainer.lastElementChild;
					}
					// Before putting the caret into the last selectable child, lets see if the last element is a stub element. If it is, we need to put the caret there manually.
					if (prevContainer.lastChild && prevContainer.lastChild.nodeType !== ice.dom.TEXT_NODE && ice.dom.isStubElement(prevContainer.lastChild) && prevContainer.lastChild.tagName !== 'BR') {
						range.setStartAfter(prevContainer.lastChild);
						range.collapse(true);
						return true;
					}
					// Find the last selectable part of the prevContainer. If it exists, put the caret there.
					lastSelectable = range.getLastSelectableChild(prevContainer);
		
					if (lastSelectable && !ice.dom.isOnBlockBoundary(range.startContainer, lastSelectable, this.element)) {
						range.selectNodeContents(lastSelectable);
						range.collapse();
						return true;
					}
				}
			}
	
			// Firefox: If an image is at the start of the paragraph and the user has just deleted the image using backspace, an empty text node is created in the delete node before
			// the image, but the caret is placed with the image. We move the caret to the empty text node and execute deleteFromLeft again.
			if (initialOffset === 1 && !ice.dom.isBlockElement(commonAncestor) && range.startContainer.childNodes.length > 1 && range.startContainer.childNodes[0].nodeType === ice.dom.TEXT_NODE && range.startContainer.childNodes[0].data.length === 0) {
				range.setStart(range.startContainer, 0);
				return this._deleteLeft(range);
			}
	
			// Move range to position the cursor on the inside of any adjacent container that it is going
			// to potentially delete into or before a stub element.	E.G.: <em>text</em>| test	->	<em>text|</em> test or
			// text1 <img>| text2 -> text1 |<img> text2
			try {
				range.moveStart(ice.dom.CHARACTER_UNIT, -1);
				range.moveStart(ice.dom.CHARACTER_UNIT, 1);
			}
			catch(ignore){}
	
			// Handles cases in which the caret is at the start of the block.
			if (ice.dom.isOnBlockBoundary(range.startContainer, range.endContainer, this.element)) {
		
				// If the previous block is empty, remove the previous block.
				if (prevBlockIsEmpty) {
					ice.dom.remove(prevBlock);
					range.collapse();
					return true;
				}
		
				// If the previous Block ends with a stub element, set the caret behind it.
				if (prevBlock && prevBlock.lastChild && ice.dom.isStubElement(prevBlock.lastChild)) {
					range.setStartAfter(prevBlock.lastChild);
					range.collapse(true);
					return true;
				}
		
				// Place the caret at the end of the previous block.
				lastSelectable = range.getLastSelectableChild(prevBlock);
				if (lastSelectable) {
					range.setStart(lastSelectable, lastSelectable.data.length);
					range.collapse(true);
				} 
				else if (prevBlock) {
					range.setStart(prevBlock, prevBlock.childNodes.length);
					range.collapse(true);
				}
		
				return true;
			}
	
			var entireTextNode = range.startContainer;
			if (entireTextNode && (entireTextNode.nodeType === ice.dom.TEXT_NODE)) {
				var deletedCharacter = splitTextAt(entireTextNode, range.startOffset - 1, 1);
				this._addDeleteTracking(deletedCharacter, {range:range, moveLeft:true, merge:true});
				return true;
			}
			
			return false;
	
		},
		
		_removeNode: function(node) {
			var parent = node && node.parentNode;
			if (parent) {
				parent.removeChild(node);
				if (parent !== this.element && ice.dom.hasNoTextOrStubContent(parent)) {
					this._removeNode(parent);
				}
			}
		},
	
		/**
		 * @private
		 * Adds delete tracking to the provided node. The node is checked for containment in various tracking contexts
		 * (e.g. inside an insert block, delete block)
		 */
		_addDeleteTracking: function (contentNode, options) {
	
			var moveLeft = options && options.moveLeft,
				contentAddNode = this._getIceNode(contentNode, INSERT_TYPE),
				ctNode, range;
			options = options || {};
	
			if (contentAddNode) {
				return this._addDeletionInInsertNode(contentNode, contentAddNode, options);
			}
			
			range = options.range;
			if (range && this._getIceNode(contentNode, DELETE_TYPE)) {
				return this._deleteInDeleted(contentNode, options);
	
			}
			// Webkit likes to insert empty text nodes next to elements. We remove them here.
			if (contentNode.previousSibling && ice.dom.isEmptyTextNode(contentNode.previousSibling)) {
				contentNode.parentNode.removeChild(contentNode.previousSibling);
			}
			if (contentNode.nextSibling && ice.dom.isEmptyTextNode(contentNode.nextSibling)) {
				contentNode.parentNode.removeChild(contentNode.nextSibling);
			}
			var prevDelNode = this._getIceNode(contentNode.previousSibling, DELETE_TYPE),
				nextDelNode = this._getIceNode(contentNode.nextSibling, DELETE_TYPE);
	
			if (prevDelNode && this._isCurrentUserIceNode(prevDelNode)) {
				ctNode = prevDelNode;
				ctNode.appendChild(contentNode);
				if (nextDelNode && this._isCurrentUserIceNode(nextDelNode)) {
					var nextDelContents = ice.dom.extractContent(nextDelNode);
					ctNode.appendChild(nextDelContents);
					nextDelNode.parentNode.removeChild(nextDelNode);
				}
			} 
			else if (nextDelNode && this._isCurrentUserIceNode(nextDelNode)) {
				ctNode = nextDelNode;
				ctNode.insertBefore(contentNode, ctNode.firstChild);
			} 
			else { // not in the neighborhood of a delete node
				var changeId = this.getAdjacentChangeId(contentNode, moveLeft);
				ctNode = this._createIceNode(DELETE_TYPE, null, changeId);
				if (options.deleteNodesCollection) {
					options.deleteNodesCollection.push(ctNode);
				}
				contentNode.parentNode.insertBefore(ctNode, contentNode);
				ctNode.appendChild(contentNode);
			}
			if (range) {
				if (ice.dom.isStubElement(contentNode)) {
					range.selectNode(contentNode);
				} 
				else {
					range.selectNodeContents(contentNode);
				}
				if (moveLeft) {
					range.collapse(true);
				} 
				else {
					range.collapse();
				}
			}
			if (ctNode) {
				this._normalizeNode(ctNode);
				range && range.refresh();
			}
	
			return true;
	
		},
		
		/**
		 * @private
		 * Adds delete tracking to a BR node
		 */
		_addDeleteTrackingToBreak: function (brNode, options) {
			var moveLeft = Boolean(options && options.moveLeft);
			function move() {
				var range = options && options.range;
				if (range) {
					if (isBRNode(brNode) || ice.dom.hasNoTextOrStubContent(brNode) || moveLeft) {
						if (moveLeft) {
							range.setStartBefore(brNode);
							range.setEndBefore(brNode);
						}
						else {
							range.setStartAfter(brNode);
							range.setEndAfter(brNode);
						}
					}
					else if (brNode.firstChild) {
						range.setStartBefore(brNode.firstChild);
						range.setEndBefore(brNode.firstChild);
					}
					range.collapse();
				}	
			}
			
			if (! isBRNode(brNode)) {
				logError("addDeleteTracking to BR: not a break element");
				return;
			}
			
			
			// if this is a delete node, just move the caret
			if (this._isDeleteNode(brNode)) {
				return move();
			}
			// remove all attrs and classes from the node'
			stripNode(brNode);
			var type = DELETE_TYPE;
			
			ice.dom.addClass(brNode, this._getIceNodeClass(type));
			var changeId = this.getAdjacentChangeId(brNode, moveLeft);
			
			this._addChange(type, [brNode], changeId);
			
			move();
		},
		
		/**
		 * Handle the case of deletion inside a delete element
		 * @private
		 */
		_deleteInDeleted: function(contentNode, options) {
			var range = options.range, 
				moveLeft = options.moveLeft,
				ctNode;

			// It if the contentNode a text node, merge it with text nodes before and after it.
			this._normalizeNode(contentNode);// dfl - support ie8
	
			var found = false;
			if (moveLeft) {
				// Move to the left until there is valid sibling.
				var previousSibling = ice.dom.getPrevContentNode(contentNode, this.element);
				while (!found) {
					ctNode = this._getIceNode(previousSibling, DELETE_TYPE);
					if (!ctNode) {
						found = true;
					} 
					else {
						previousSibling = ice.dom.getPrevContentNode(previousSibling, this.element);
					}
				}
				if (previousSibling) {
					var lastSelectable = range.getLastSelectableChild(previousSibling);
					if (lastSelectable) {
						previousSibling = lastSelectable;
					}
					range.setStart(previousSibling, ice.dom.getNodeCharacterLength(previousSibling));
					range.collapse(true);
				}
			} 
			else {
				// Move the range to the right until there is valid sibling.
	
				var nextSibling = ice.dom.getNextContentNode(contentNode, this.element);
				while (!found) {
					ctNode = this._getIceNode(nextSibling, DELETE_TYPE);
					if (!ctNode) {
						found = true;
					} 
					else {
						nextSibling = ice.dom.getNextContentNode(nextSibling, this.element);
					}
				}
	
				if (nextSibling) {
					range.selectNodeContents(nextSibling);
					range.collapse(true);
				}
			}
			return true;
		},
		

/**
 * @private
 * Adds delete tracking markup around a content node
 * @param contentNode the content to be marked as deleted
 * @param contentAddNode the insert node surrounding the content
 * @param options may contain range, moveLeft, deleteNodesCollection, merge
 */
		_addDeletionInInsertNode: function(contentNode, contentAddNode, options) {
			var range = options && options.range,
				moveLeft = options && options.moveLeft;
			options = options || {};
			if (this._isCurrentUserIceNode(contentAddNode)) {
				if (range) {
					if (moveLeft) {
						range.setStartBefore(contentNode);
					}
					else {
						range.setStartAfter(contentNode);
					}
					range.collapse(moveLeft);
				}
				contentNode.parentNode.removeChild(contentNode);
				if (! this._browser.msie) {
					this._normalizeNode(contentAddNode);	
				}
				var $can = $(contentAddNode),
					bmCount = $can.find(".iceBookmark").length,
					cleanNode;
				if (bmCount > 0) {
					cleanNode = $can.clone();
					cleanNode.find('.iceBookmark').remove();
					cleanNode = cleanNode[0];
				}
				else {
					cleanNode = contentAddNode;
				}
					
				// Remove a potential empty tracking container
				if (ice.dom.hasNoTextOrStubContent(cleanNode)) {
					if (range) {
						range.setStartBefore(contentAddNode);
						range.collapse(true);
					}
					ice.dom.replaceWith(contentAddNode, ice.dom.contents(contentAddNode));
				}
			}
			else { // other user insert
				var cInd = rangy.dom.getNodeIndex(contentNode),
					parent = contentNode.parentNode,
					nChildren = parent.childNodes.length,
					ctNode;
				parent.removeChild(contentNode);
				ctNode = this._createIceNode(DELETE_TYPE);
				if (options.deleteNodesCollection) {
					options.deleteNodesCollection.push(ctNode);
				}
				ctNode.appendChild(contentNode);
				if (cInd > 0 && cInd >= (nChildren - 1)) {
					ice.dom.insertAfter(contentAddNode, ctNode);
				}
				else {
					if (cInd > 0) {
						var splitNode = this._splitNode(contentAddNode, parent, cInd);
						this._deleteEmptyNode(splitNode);
					}
					contentAddNode.parentNode.insertBefore(ctNode, contentAddNode);
				}
				this._deleteEmptyNode(contentAddNode);


				if (range && moveLeft) {
					range.setStartBefore(ctNode);
					range.collapse(true);
					this.selection.addRange(range);
				}
				if (options && options.merge) {
					this._mergeDeleteNode(ctNode);
				}
				if (range) {
					range.refresh();
				}

			}
			return true;
		},
		

		/**
		 * @private
		 * Deletes a node if it does not contain anything 
		 */
		_deleteEmptyNode: function(node) {
			var parent = node && node.parentNode;
			if (parent && ice.dom.hasNoTextOrStubContent(node)) {
				parent.removeChild(node);
			}
		},
	
		/**
		 * Merges a delete node with its siblings if they belong to the same user
		 * @private
		 */
		_mergeDeleteNode: function(delNode) {
			var siblingDel,
				content;
	
			if (this._isCurrentUserIceNode(siblingDel = this._getIceNode(delNode.previousSibling, DELETE_TYPE))) {
				content = ice.dom.extractContent(delNode);
				delNode.parentNode.removeChild(delNode);
				siblingDel.appendChild(content);
				this._mergeDeleteNode(siblingDel);
			}
			else if (this._isCurrentUserIceNode(siblingDel = this._getIceNode(delNode.nextSibling, DELETE_TYPE))) {
					content = ice.dom.extractContent(siblingDel);
					delNode.parentNode.removeChild(siblingDel);
					delNode.appendChild(content);
					this._mergeDeleteNode(delNode);
			} 
		},
	
	
		/**
		 * If tracking is on, handles event e when it is one of the following types:
		 * keypress, keydown. Prevents default handling if the event
		 * was fully handled.
		 */
		handleEvent: function (e) {
			if (!this._isTracking) {
				return true;
			}
			var preventEvent = false;
			if ('keypress' == e.type) {
				preventEvent = this.keyPress(e);
			} 
			else if ('keydown' == e.type) {
				preventEvent = ! this.handleKeyDown(e);
			}
			if (preventEvent) {
				e.stopImmediatePropagation();
				e.preventDefault();
			}
			return ! preventEvent;
		},

		/**
		 * @private
		 * Handles arrow, delete key events, and others.
		 * @param {Event} e Event object.
		 * @return {void|boolean} Returns true if default event needs to be blocked.
		 */
		_handleAncillaryKey: function (key) {
			var browser = this._browser,
				preventDefault = false,
				self = this,
				range = self.getCurrentRange();
			switch (key) {
				case ice.dom.DOM_VK_DELETE:
					preventDefault = this._deleteContents();
					break;
		
				case 46:
					// Key 46 is the DELETE key.
					preventDefault = this._deleteContents(true);
					break;
		
		/* ***********************************************************************************/
		/* BEGIN: Handling of caret movements inside hidden .ins/.del elements on Firefox **/
		/*  *Fix for carets getting stuck in .del elements when track changes are hidden  **/
				case ice.dom.DOM_VK_DOWN:
				case ice.dom.DOM_VK_UP:
				case ice.dom.DOM_VK_LEFT:
					if(browser["type"] === "mozilla"){
						if(!this.visible(range.startContainer)){
							// if Previous sibling exists in the paragraph, jump to the previous sibling
							if(range.startContainer.parentNode.previousSibling){
								// When moving left and moving into a hidden element, skip it and go to the previousSibling
								range.setEnd(range.startContainer.parentNode.previousSibling, 0);
								range.moveEnd(ice.dom.CHARACTER_UNIT, ice.dom.getNodeCharacterLength(range.endContainer));
								range.collapse(false);
							}
							// if Previous sibling doesn't exist, get out of the hidden zone by moving to the right
							else {
								range.setEnd(range.startContainer.parentNode.nextSibling, 0);
								range.collapse(false);
							}
						}
					  }	
			          preventDefault = false;
			          break;
				case ice.dom.DOM_VK_RIGHT:
					if(browser["type"] === "mozilla"){
						if(!this.visible(range.startContainer)){
							if(range.startContainer.parentNode.nextSibling){
								// When moving right and moving into a hidden element, skip it and go to the nextSibling
								range.setStart(range.startContainer.parentNode.nextSibling,0);
								range.collapse(true);
							}
						}
					}
					break;
		/* END: Handling of caret movements inside hidden .ins/.del elements ***************/

				default:
					// Ignore key.
					break;
			} //end switch
	
			return preventDefault;
	
		},
		
		/**
		 * Returns false if the event should be cancelled
		 */
		handleKeyDown: function (e) {
			if (this._handleSpecialKey(e)) {
				return true;
			} 
	
			return ! this.keyPress(e);
		},


		/**
		 * @private
		 * @param e event
		 * returns true if the event needs to be prevented
		 */
		onKeyDown: function (e) {
			if (this._handleSpecialKey(e)) {
				return false;
			} 
	
			return this._handleAncillaryKey(e);
		},

		/**
		 * Returns true if the event should be cancelled
		 */
		keyPress: function (e) {
			var c = null;
			if (e.ctrlKey || e.metaKey) {
				return false;
			}
	
			// Inside a br - most likely in a placeholder of a new block - delete before handling.
			var range = this.getCurrentRange(), text,
				br = range && ice.dom.parents(range.startContainer, 'br')[0] || null;
			if (br) {
				range.moveToNextEl(br);
			}
	
//			if (c !== null) {
				var key = e.keyCode ? e.keyCode : e.which;
    		    switch (key) {
    		    	case 32: //ckeditor does funny stuff with spaces, so insert it ourselves
    		    		return this.insert({ text: ' ' });
					case ice.dom.DOM_VK_DELETE:
					case 46:
					case ice.dom.DOM_VK_DOWN:
					case ice.dom.DOM_VK_UP:
					case ice.dom.DOM_VK_LEFT:
					case ice.dom.DOM_VK_RIGHT:
						return this._handleAncillaryKey(key);
					case ice.dom.DOM_VK_ENTER:
						this._handleEnter();
						return false;
					default:
						if (isIgnoredKeyCode(key))  {
							return false;
						}
						c = e["char"] || String.fromCharCode(key);

						if (c) { // covers null and empty string
							var text = this._browser.msie ? {text: c} : null;
							return this.insert(text);
						}
						return false;
				}
	//		}
	
//			return false; //this._handleAncillaryKey(e);
		},
	

		_handleEnter: function () {
			var range = this.getCurrentRange();
			if (range && !range.collapsed) {
				this._deleteContents();
			}
/*
 			this._domObserver.observe(this.element, this._domObserverConfig);
			this._setDomObserverTimeout();
*/
		},

		/**
		 * @private
		 * returns true if the keytcombination was handled. This does not mean that the event should
		 * be preventDefault()ed, just that we don't need further processing
		 */
		_handleSpecialKey: function (e) {
			var keyCode = e.which;
			if (keyCode === null) {
			// IE.
				keyCode = e.keyCode;
			}
	
			switch (keyCode) {
				case 120:
				case 88:
					if (true === e.ctrlKey || true === e.metaKey) {
						this.prepareToCut();
						return true;
					}
					break;
				case 67:
				case 99:
					if (true === e.ctrlKey || true === e.metaKey) {
						this.prepareToCopy();
						return true;
					}
					break;		
				default:
					// Not a special key.
					break;
			} //end switch
			return false;
		},
	
		/**
		 * Returns the first ice node in the hierarchy of the given node, or the current collapsed range.
		 * @param node if null, check the current selection
		 * @param onlyNode if true, check only the node, not its parents
		 * @param cleanup if false, don't clean up empty nodes around selection
		 * null if not in a track changes hierarchy
		 */
		currentChangeNode: function (node, onlyNode, cleanup) {
			var selector = this._iceSelector,
				range = null;
			if (!node) {
				range = this.getCurrentRange();
				if (! range) {
					return false;
				}
				if (cleanup !== false && range.collapsed) {
					this._cleanupSelection(range, false, false);
					node = range.startContainer;
				}
				else {
					node = range.commonAncestorContainer;
				}
			}
			
			var ret = onlyNode ? $(node).is(selector) && node : ice.dom.getNode(node, selector);
			if ((! ret) && range && range.collapsed) {
				var end = range.endContainer,
					endOffset = range.endOffset,
					nextNode = null;
				if (end.nodeType === ice.dom.TEXT_NODE) {
					if (endOffset === end.length) {
						nextNode = ice.dom.getNextNode(end);
					}
					else if (endOffset === 0) {
						nextNode = ice.dom.getPrevNode(end, this.element);
					}
				}
				else if (end.nodeType === ice.dom.ELEMENT_NODE) {
					if (endOffset === 0) {
						nextNode = ice.dom.getPrevNode(end, this.element);
					}
					else if (end.childNodes.length > endOffset) {
						end = end.childNodes[endOffset - 1];
						if ($(end).is(selector)) {
							return end;
						}
						nextNode = ice.dom.getNextNode(end);
					}
				}
				if (nextNode) {
					ret = $(nextNode).is(selector);
				}
			}
			return ret;
		},
		
		setShowChanges: function(bShow) {
			var $body = $(this.element);
			bShow = Boolean(bShow);
			this._isVisible = bShow;
			$body.toggleClass("ICE-Tracking", bShow);
			this._showTitles(bShow);
			this._updateTooltipsState();
		},
	
		reload: function() {
			this._loadFromDom();
		},
		
		hasChanges: function() {
			for (var key in this._changes) {
				var change = this._changes[key];
				if (change && change.type) {
					return true;
				}
			}
			return false;
		},
		
		countChanges: function(options) {
			var changes = this._filterChanges(options);
			return changes.count;
		},
		
		setChangeData: function(data) {
			if (null == data || (typeof data == "undefined")) {
				data = "";
			}
			this._changeData = String(data);
		},
		
		getDeleteClass: function() {
			return this._getIceNodeClass(DELETE_TYPE);
		},
		
		/**
		 * called before a copy operation. 
		 * This function processes the current selection to remove the tracking style.
		 * The tracking is restored immediately after the copy operation 
		 */
		prepareToCopy: function() {
			var range = this.getCurrentRange();
			if (range && ! range.collapsed) {
				this._removeTrackingInRange(range);
			}
		},
		
		/**
		 * Preprocesses the document selection so that a deleted span is left after the browser cut
		 * @return true if there's a selection 
		 */
		prepareToCut: function() {
			var range = this.getCurrentRange(),
				hostRange = this.hostMethods.getHostRange();
			
			if (range && hostRange && range.collapsed && ! hostRange.collapsed) {
				// special case of IE showing collapsed selection when ckeditor thinks otherwise
				try {
					var data = this.hostMethods.getHostRangeData(hostRange);
					range.setStart(data.startContainer, data.startOffset);
					range.setEnd(data.endContainer, data.endOffset);
				}
				catch (e) {
					return;
				}
			}
			if (! range || range.collapsed) {
				return false;
			}
			fixSelection(range, this.element);
			var frag = range.cloneContents(),
				origRange = range.cloneRange(),
				head = frag.firstChild,tail = frag.lastChild;
//			printRange(range, "before cut");
			this.hostMethods.beforeEdit();
			
			range.collapse(false);
			range.insertNode(frag);
			range.setStartBefore(head);
//			printRange(range, "after set start before the head");
			range.setEndAfter(tail);
//			printRange(range, "after set end after the tail");
			var cid = this._startBatchChange();
			try {
				this._deleteSelection(range);
			}
			catch (e) {
				logError(e, "While trying to delete selection");
			}
			finally {
				this._endBatchChange(cid);
				this.selection.addRange(origRange);
				this._removeTrackingInRange(origRange, false);
//				printRange(this.selection.getRangeAt(0), "range after deletion");
			}
			return true;
		},

		toString: function() {
			return "ICE " + ((this.element && this.element.id) || "(no element id)");
		},
		
		_splitNode: function(node, atNode, atOffset) {
			var parent = node.parentNode,
			  	parentOffset = rangy.dom.getNodeIndex(node),
			  	doc = atNode.ownerDocument, 
			  	leftRange = doc.createRange(),
			  	left;
			  leftRange.setStart(parent, parentOffset);
			  leftRange.setEnd(atNode, atOffset);
			  left = leftRange.extractContents();
			  parent.insertBefore(left, node);
			  if (this.isInsideChange(node, true)) {
				  this._updateNodeTooltip(node.previousSibling);
			  }
			  return node.previousSibling;
		},
		
		/**
		 * Notify that the DOM has changed
		 * if options.isText === true, also notify that text has changed
		 */
		_triggerChange: function(options) {
			if (this._isTracking) {
				this.$this.trigger("change");
				if (options && options.isText) {
					this.$this.trigger("textChange");
				}
			}
		},
	
		_updateNodeTooltip: function(node) {
			if (this.tooltips && this._isVisible) {
				this._addTooltip(node);
			}
		},
	
		_acceptRejectSome: function(options, isAccept) {
			var f = (function(index, node) {
				this.acceptRejectChange(node, { isAccept: isAccept, notify: false });
			}).bind(this);
			var changes = this._filterChanges(options);
			for (var id in changes.changes) {
				var nodes = $(this.element).find('[' + this.attributes.changeId + '=' + id + ']');
				nodes.each(f);
			}
			if (changes.count) {
				this._triggerChange({ isText: true });
			}
		},
		
		/**
		 * Filters the current change set based on options
		 * @param _options may contain one of:<ul>
		 * <li>exclude: an array of user ids to exclude
		 * <li>include: an array of user ids to include
		 * <li>filter: a filter function of the form function({userid, time, data}):boolean
		 * <li>verify: a boolean indicating whether or not to verify that there are matching dom nodes for each matching change
		 * </ul>
		 *	@return {Object} an object with two members: count, changes (map of id:changeObject)
		 * @private
		 */
		_filterChanges: function(_options) {
			var count = 0, changes = {},
				change,
				options = _options || {},
				filter = options.filter,
				exclude = options.exclude ? $.map(options.exclude, function(e) { return String(e); }) : null,
				include = options.include ? $.map(options.include, function(e) { return String(e); }) : null,
				verify = options.verify,
				elements = null;
			for (var key in this._changes) {
				change = this._changes[key];
				if (change && change.type) {	
					var skip = (filter && ! filter({userid: change.userid, time: change.time, data:change.data})) || 
						(exclude && exclude.indexOf(change.userid) >= 0) ||
						(include && include.indexOf(change.userid) < 0);
					if (! skip) {
						if (verify) {
							elements = $(this.element).find("[" + this.attributes.changeId + "]");
							skip = ! elements.length;
						}
						if (! skip) {
							++count;
							changes[key] = change;
						}
					}
				}
			}
			
			return { count : count, changes : changes };
		},
		
		_loadFromDom : function() {
			this._changes = {};
			this._uniqueStyleIndex = 0;
			var myUserId = this.currentUser && this.currentUser.id,
				myUserName = (this.currentUser && this.currentUser.name) || "",
				now = (new Date()).getTime(),
				styleMatch,
				styleRegex = new RegExp(this.stylePrefix + '-(\\d+)'),
			// Grab class for each changeType
				changeTypeClasses = [];
			for (var changeType in this.changeTypes) {
				changeTypeClasses.push(this._getIceNodeClass(changeType));
			}
	
			var nodes = this.getIceNodes();
			var f = function(i, el) {
				var styleIndex = 0,
					styleName,
					ctnType = '', i,
					classList = el.className.split(' ');
				//TODO optimize this - create a map of regexp
				for (i = 0; i < classList.length; i++) {
					styleMatch = styleRegex.exec(classList[i]);
					if (styleMatch) {
						styleName = styleMatch[0];
						styleIndex = styleMatch[1];
					}
					var ctnReg = new RegExp('(' + changeTypeClasses.join('|') + ')').exec(classList[i]);
					if (ctnReg) {
						ctnType = this._getChangeTypeFromAlias(ctnReg[1]);
					}
				}
				var userid = el.getAttribute(this.attributes.userId);
				var userName;
				if (myUserId && (userid == myUserId)) {
					userName = myUserName;
					el.setAttribute(this.attributes.userName, myUserName);
				}
				else {
					userName = el.getAttribute(this.attributes.userName);
				}
				this._setUserStyle(userid, Number(styleIndex));
				var changeid = parseInt(el.getAttribute(this.attributes.changeId) || "");
				if (isNaN(changeid)) {
					changeid = this.getNewChangeId();
					el.setAttribute(this.attributes.changeId, changeid);
				}
				var timeStamp = parseInt(el.getAttribute(this.attributes.time) || "");
				if (isNaN(timeStamp)) {
					timeStamp = now;
				}
				var lastTimeStamp = parseInt(el.getAttribute(this.attributes.lastTime) || "");
				if (isNaN(lastTimeStamp)) {
					lastTimeStamp = timeStamp;
				}
				var sessionId = el.getAttribute(this.attributes.sessionId);
			
				var changeData = el.getAttribute(this.attributes.changeData) || "";
				this._changes[changeid] = {
					type: ctnType,
					style: styleName,
					userid: String(userid),// dfl: must stringify for consistency - when we read the props from dom attrs they are strings
					username: userName,
					time: timeStamp,
					lastTime: lastTimeStamp,
					sessionId: sessionId,
					data : changeData
				};
				this._updateNodeTooltip(el);
			}.bind(this);
			nodes.each(f);
			this._triggerChange();
		},
		
		_showTitles : function(bShow) {
			var nodes = this.getIceNodes();
			if (bShow) {
				$(nodes).each((function(i, node) {
					this._updateNodeTooltip(node);
				}).bind(this));
			}
			else {
				$(nodes).removeAttr("title");
			}
		},
		
		_updateTooltipsState: function() {
			var $nodes,
				self = this;
			// show tooltips if they are enabled and change tracking is on
			if (this.tooltips && this._isVisible) {
				if (! this._showingTips) {
					this._showingTips = true;
					$nodes = this.getIceNodes();
					$nodes.each(function(i, node) {
						self._addTooltip(node);
					});					
				}
			}
			else if (this._showingTips) {
				this._showingTips = false;
				$nodes = this.getIceNodes();
				$nodes.each(function(i, node) {
					$(node).unbind("mouseover").unbind("mouseout");
				});					
			}
		},
		
		_addTooltip: function(node) {
			$(node).unbind("mouseover").unbind("mouseout").mouseover(this._tooltipMouseOver).mouseout(this._tooltipMouseOut);
		},
		
		_tooltipMouseOver: function(event) {
			var node = event.currentTarget,
				$node = $(node), to,
				self = this;
			if (event.buttons || $node.data("_tooltip_t")) {
				return;
			}
			to = setTimeout(function() {
				var iceNode = self.currentChangeNode(node),
					cid = iceNode && iceNode.getAttribute(self.attributes.changeId),
					change = cid && self.getChange(cid);
				if (change) {
					var type = ice.dom.hasClass(iceNode, self._getIceNodeClass(INSERT_TYPE)) ? "insert" : "delete";
					$node.removeData("_tooltip_t");
					self.hostMethods.showTooltip(node, {
						userName: change.username,
						changeId: cid,
						userId: change.userid,
						time: change.time,
						lastTime: change.lastTime,
						type: type
					});
				}
			}, this.tooltipsDelay);
			$node.data("_tooltip_t", to);
		},
		
		_tooltipMouseOut: function(event) {
			var node = event.currentTarget,
				$node = $(node),
				to = $node.data("_tooltip_t");
			$node.removeData("_tooltip_t");
			if (to) {
				clearTimeout(to);
			}
			else {
				this.hostMethods.hideTooltip(node);
			}
		},
		
		/**
		 * Finds all the tracking nodes involved in the range and removes their tracking classes.
		 * A timeout is set to restore the tracking classes immediately.
		 * This allows the editor to copy tracked text without its style
		 * @private
		 */
		_removeTrackingInRangeOld: function (range) {
			var insClass = this._getIceNodeClass(INSERT_TYPE), 
				delClass = this._getIceNodeClass(DELETE_TYPE),
				clsSelector = '.' + insClass+",."+delClass,
				clsAttr = "data-ice-class",
				filter = function(node) {
					var iceNode,
						$iceNode = null;
					if (node.nodeType == ice.dom.TEXT_NODE) {
						$iceNode = $(node).parents(clsSelector);
					}
					else {
						var $node = $(node);
						if ($node.is(clsSelector)) {
							$iceNode = $node; 
						}
						else {
							$iceNode = $node.parents(clsSelector);
						}
					}
					iceNode = ($iceNode && $iceNode[0]);
					if (iceNode) {
						var cls = iceNode.className;
						iceNode.setAttribute(clsAttr, cls);
						iceNode.setAttribute("class", "ice-no-decoration");
						return true;
					}
					return false;
				};
			range.getNodes(null, filter);
			var el = this.element;
			setTimeout(function() {
				var nodes = $(el).find('['+ clsAttr + ']');
				nodes.each(function(i, node) {
					var cls = node.getAttribute(clsAttr);
					if (cls) {
						node.setAttribute("class", cls);
						node.removeAttribute(clsAttr);
					}
				});
				
			}, 10);
		},
		/**
		 * Finds all the tracking nodes involved in the range and removes their tracking classes.
		 * A timeout is set to restore the tracking classes immediately.
		 * This allows the editor to copy tracked text without its style
		 * @private
		 */
		_removeTrackingInRange: function (range) {
			var insClass = this._getIceNodeClass(INSERT_TYPE), 
				delClass = this._getIceNodeClass(DELETE_TYPE),
				clsSelector = '.' + insClass+",."+delClass,
				saveMap = this._savedNodesMap,
				clsAttr = "data-ice-class",
				base = Date.now() % 1000000,
				filter = function(node) {
					var $node,iceNode,
						$iceNode = null;
					if (node.nodeType == ice.dom.TEXT_NODE) {
						$iceNode = $(node).parents(clsSelector);
					}
					else {
						$node = $(node);
						if ($node.is(clsSelector)) {
							$iceNode = $node; 
						}
						else {
							$iceNode = $node.parents(clsSelector);
						}
					}
					if (iceNode = ($iceNode && $iceNode[0])) {
						var attrs = getNodeAttributes(iceNode),
							cls = iceNode.className,
							dataId = String(base++);
						
						saveMap[dataId] = {
							attributes: attrs,
							className: cls
						};
						removeAllAttributes(iceNode);
						iceNode.setAttribute(clsAttr, dataId);
						iceNode.setAttribute("class", "ice-no-decoration");
						return true;
					}
					return false;
				};
			range.getNodes(null, filter);
			var el = this.element;
			setTimeout(function() {
				var nodes = $(el).find('['+ clsAttr + ']');
				nodes.each(function(i, node) {
					var dataId = node.getAttribute(clsAttr),
						nodeData = saveMap[dataId];
					if (dataId) {
						delete saveMap[dataId];
						Object.keys(nodeData.attributes).forEach(function(key) {
							node.setAttribute(key, nodeData.attributes[key]);
						});
						node.setAttribute("class", nodeData.className);
						node.removeAttribute(clsAttr);
					}
					else {
						logError("missing save data for node");
					}
				});
				
			}, 10);
		},
		
		_onDomMutation: function(mutations) {
			var i, len = mutations.length, m,
				nodeIndex, lst,
				node;
			for (i = 0; i < len; ++i) {
				m = mutations[i];
				switch (m.type) {
					case "childList":
						lst  = m.addedNodes;
						for (nodeIndex = lst.length - 1; nodeIndex >= 0; --nodeIndex) {
							node = lst[nodeIndex];
							console.log("mutation: added node", node.tagName);
						}
						break;
				}
			}
		},
		
		_setDomObserverTimeout: function() {
			var self = this;
			if (this._domObserverTimeout) {
				window.clearTimeout(this._domObserverTimeout);
			}
			this._domObserverTimeout = window.setTimeout(function() {
				self._domObserverTimeout = null;
				self._domObserver.disconnect();
			}, 1);
		},
		
		getAdjacentChangeId: function(node, left) {
			var next = left ? ice.dom.getNextNode(node) : ice.dom.getPrevNode(node),
				nextChange,
				changeId = null;
			
			nextChange = this._getIceNode(next, INSERT_TYPE) || this._getIceNode(next, DELETE_TYPE);
			if (! nextChange) {
				if (this._isInsertNode(next) || this._isDeleteNode(next)) {
					nextChange = next;
				}
			}
			if (nextChange && this._isCurrentUserIceNode(nextChange)) {
				changeId = nextChange.getAttribute(this.attributes.changeId);
			}
			return changeId;
		}
		

	};
	
	var console = (window && window.console) || {
		log: function(){},
		error: function(){},
		info: function(){},
		assert:function(){},
		count: function(){}
	} ;
	
	/** Utility functions **/
	
	function getNodeAttributes(node) {
		var attrs = node.attributes,
			attr,
			len = attrs && attrs.length,
			ret = {};
		for (var i = 0; i < len; ++i) {
			attr = attrs[i];
			ret[attr.name] = attr.value;
		}
		return ret;
	}
	
	function removeAllAttributes(node) {
		var last = null,
			next;
		try {
			while (node.attributes.length > 0) {
				next = node.attributes[0];
				if (next === last) {
					return;
				}
				last = next;
				node.removeAttribute(next.name);
			}
		}
		catch(ignore){}
	}

	function nativeElement(e) {
		return e;
	}
	
	/**
	 * Strip all attributes and classes from a node
	 * @param node
	 */
	function stripNode(node) {
		// remove all attrs and classes from the node
		var	attributes = $.map(node.attributes, function(attr) {
			return attr.name;
		});
		$(node).removeClass(); // remove all classes
		$.each(attributes, function(i, item) {
			node.removeAttribute(item);
		});
	}
	
	function isBRNode(node) {
		return BREAK_ELEMENT == ice.dom.getTagName(node);
	}

	function isNewlineNode(node) {
		var tag = ice.dom.getTagName(node);
		return BREAK_ELEMENT === tag || PARAGRAPH_ELEMENT === tag;
	}

	function isOnRightEdge(el, offset) {
		if (! el) {
			return false;
		}
		var type = el.nodeType;
		if (ice.dom.TEXT_NODE == type) {
			return offset && el.nodeValue && (offset >= el.nodeValue.length - 1);
		}
		if (ice.dom.ELEMENT_NODE == type) {
			return el.childNodes && el.childNodes.length && (offset >= el.childNodes.length);
		}
		return false;
	}
	
	var logError = null;
	
	function fixSelection(range, top) {
		if (! range || ! top || range.collapsed) {
			return range;
		}
		var current;
		// fix end
		try {
			while ((current = range.endContainer) && (current !== top) && (range.endOffset == 0) && ! range.collapsed) {
				if (current.previousSibling) {
					range.setEndBefore(current);
				}
				else if (current.parentNode && current.parentNode !== top) {
					range.setEndBefore(current.parentNode);
				}
				if (range.endContainer == current) {
					break;
				}
			}
		}
		catch (e) {
			logError(e, "fixSelection, while trying to set end");
		}

		
		try {
			while ((current = range.startContainer) && (current !== top) && ! range.collapsed) {
				current = range.startContainer;

				if (current.nodeType == ice.dom.TEXT_NODE) {
					if (range.startOffset >= current.nodeValue.length) {
						range.setStartAfter(current);
					}
				}
				else { // element
					if (range.startOffset >= current.childNodes.length) {
						range.setStartAfter(current);
					}
				}
				if (range.startContainer == current) {
					break;
				}
			}
		}
		catch (e) {
			logError(e, "fixSelection, while trying to set start");
		}
	}
	
	function splitTextAt(textNode, at, count) {
		var textLength = textNode.length,
			splitText;
		if (at < 0 || at >= textLength) {
			return textNode;
		}
		if (at + count >= textLength) {
			count = textLength - at;
		}
		if (count === textLength) {
			return textNode;
		}
		splitText = at > 0 ? textNode.splitText(at) : textNode;
		if (splitText.length > count) {
			splitText.splitText(count);
		}
		return splitText;		
	}
	
	function prepareSelectionForInsert(node, range, doc, insertStub) {
		if (insertStub) {
			if (range.collapsed && range.startContainer && range.startContainer.nodeType === ice.dom.TEXT_NODE && range.startContainer.length) {
				return;
			}
		// create empty node and select it, to be replaced with the typed char
			var tn = doc.createTextNode('\uFEFF');
			if (node) {
				node.appendChild(tn);
			}
			else {
				range.insertNode(tn);
			}
			range.selectNode(tn);
		}
		else if (node) {
			range.selectNodeContents(node);
		}
	}
	
	function printRange(range, message) {
		if (! range || ! range.startContainer || ! range.endContainer) {
			return;
		}
		var parts = [];
		function printText(txt) {
			if (! txt) {
				return "";
			}
			txt = txt.replace('/\n/g', "\\n").replace('/\r/g', "").replace('\u200B', "{filler}").replace('\uFEFF', "{filler}");
			if (txt.length <= 15) {
				return txt;
			}
			return txt.substring(0, 5)+ "..." + txt.substring(txt.length - 5);
		}
		function addNode(node) {
			var str;
			if (node.nodeType === 3) {
				str = "Text:" + printText(node.nodeValue); 
			}
			else {
				var txt = node.innerText;
				str = node.nodeName + (txt ? "(" + printText(txt) + ")" :'');
			}
			parts.push("<" + str + " />");
		}
		function printNode(node, offset1, offset2) {
			if ("number" !== typeof offset2) {
				offset2 = -1;
			}
			if (3 == node.nodeType) { // text
				var txt = node.nodeValue;
				parts.push(printText(txt.substring(0, offset1)));
				parts.push("|");
				if (offset2 > offset1) {
					parts.push(printText(txt.substring(offset1, offset2)));
					parts.push("|");
					parts.push(printText(txt.substring(offset2)));
				}
				else {
					parts.push(printText(txt.substring(offset1)));
				}
			}
			else if (1 == node.nodeType) {
				var i = 0,
					children = node.childNodes,
					start = 0;
				addNode(node);
				for (i = start; i < offset1; ++i) {
					addNode(children[i]);
				}
				parts.push("|");
				if (offset2 > offset1) {
					for (i = offset1; i < offset2; ++i) {
						addNode(children[i]);
					}
					parts.push('|');
				}
				if (offset2 > 0 && offset2 < children.length){
					var child = children[offset2];
					while (child) {
						addNode(child);
						child = child.nextSibling;
					}
				}

			}
		}
		if (range.startContainer === range.endContainer) {
			printNode(range.startContainer, range.startOffset, range.endOffset);
		}
		else {
			printNode(range.startContainer, range.startOffset);
			printNode(range.endContainer, range.endOffset);
		}
		var ret = parts.join(' ');
		if (message) {
			console.log(message + ":" + ret);
		}
		return ret;
	}

	ice.printRange = printRange;
	ice.InlineChangeEditor = InlineChangeEditor;

}(this.ice || window.ice, window.jQuery));