text: /*
  @license textAngular
  Author : Austin Anderson
  License : 2013 MIT
  Version 1.4.5
  
  See README.md or https://github.com/fraywing/textAngular/wiki for requirements and use.
  */
  
  /*
  Commonjs package manager support (eg componentjs).
  */
  
  /* istanbul ignore next:  */
  'undefined'!=typeof module&&'undefined'!=typeof exports&&module.exports===exports&&(module.exports='textAngular');
  
  (function(){ // encapsulate all variables so they don't become global vars
  "use strict";
  // IE version detection - http://stackoverflow.com/questions/4169160/javascript-ie-detection-why-not-use-simple-conditional-comments
  // We need this as IE sometimes plays funny tricks with the contenteditable.
  // ----------------------------------------------------------
  // If you're not in IE (or IE version is less than 5) then:
  // ie === undefined
  // If you're in IE (>=5) then you can determine which version:
  // ie === 7; // IE7
  // Thus, to detect IE:
  // if (ie) {}
  // And to detect the version:
  // ie === 6 // IE6
  // ie > 7 // IE8, IE9, IE10 ...
  // ie < 9 // Anything less than IE9
  // ----------------------------------------------------------
  /* istanbul ignore next: untestable browser check */
  var _browserDetect = {
  	ie: (function(){
  		var undef,
  			v = 3,
  			div = document.createElement('div'),
  			all = div.getElementsByTagName('i');
  
  		while (
  			div.innerHTML = '<!--[if gt IE ' + (++v) + ']><i></i><![endif]-->',
  			all[0]
  		);
  
  		return v > 4 ? v : undef;
  	}()),
  	webkit: /AppleWebKit\/([\d.]+)/i.test(navigator.userAgent)
  };
  
  // fix a webkit bug, see: https://gist.github.com/shimondoodkin/1081133
  // this is set true when a blur occurs as the blur of the ta-bind triggers before the click
  var globalContentEditableBlur = false;
  /* istanbul ignore next: Browser Un-Focus fix for webkit */
  if(_browserDetect.webkit) {
  	document.addEventListener("mousedown", function(_event){
  		var e = _event || window.event;
  		var curelement = e.target;
  		if(globalContentEditableBlur && curelement !== null){
  			var isEditable = false;
  			var tempEl = curelement;
  			while(tempEl !== null && tempEl.tagName.toLowerCase() !== 'html' && !isEditable){
  				isEditable = tempEl.contentEditable === 'true';
  				tempEl = tempEl.parentNode;
  			}
  			if(!isEditable){
  				document.getElementById('textAngular-editableFix-010203040506070809').setSelectionRange(0, 0); // set caret focus to an element that handles caret focus correctly.
  				curelement.focus(); // focus the wanted element.
  				if (curelement.select) {
  					curelement.select(); // use select to place cursor for input elements.
  				}
  			}
  		}
  		globalContentEditableBlur = false;
  	}, false); // add global click handler
  	angular.element(document).ready(function () {
  		angular.element(document.body).append(angular.element('<input id="textAngular-editableFix-010203040506070809" class="ta-hidden-input" aria-hidden="true" unselectable="on" tabIndex="-1">'));
  	});
  }
  
  // Gloabl to textAngular REGEXP vars for block and list elements.
  
  var BLOCKELEMENTS = /^(address|article|aside|audio|blockquote|canvas|dd|div|dl|fieldset|figcaption|figure|footer|form|h1|h2|h3|h4|h5|h6|header|hgroup|hr|noscript|ol|output|p|pre|section|table|tfoot|ul|video)$/i;
  var LISTELEMENTS = /^(ul|li|ol)$/i;
  var VALIDELEMENTS = /^(address|article|aside|audio|blockquote|canvas|dd|div|dl|fieldset|figcaption|figure|footer|form|h1|h2|h3|h4|h5|h6|header|hgroup|hr|noscript|ol|output|p|pre|section|table|tfoot|ul|video|li)$/i;
  
  // https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String/Trim#Compatibility
  /* istanbul ignore next: trim shim for older browsers */
  if (!String.prototype.trim) {
  	String.prototype.trim = function () {
  		return this.replace(/^\s+|\s+$/g, '');
  	};
  }
  
  // tests against the current jqLite/jquery implementation if this can be an element
  function validElementString(string){
  	try{
  		return angular.element(string).length !== 0;
  	}catch(any){
  		return false;
  	}
  }
  
  /*
  	Custom stylesheet for the placeholders rules.
  	Credit to: http://davidwalsh.name/add-rules-stylesheets
  */
  var sheet, addCSSRule, removeCSSRule, _addCSSRule, _removeCSSRule, _getRuleIndex;
  /* istanbul ignore else: IE <8 test*/
  if(_browserDetect.ie > 8 || _browserDetect.ie === undefined){
  	var _sheets = document.styleSheets;
  	/* istanbul ignore next: preference for stylesheet loaded externally */
  	for(var i = 0; i < _sheets.length; i++){
  		if(_sheets[i].media.length === 0 || _sheets[i].media.mediaText.match(/(all|screen)/ig)){
  			if(_sheets[i].href){
  				if(_sheets[i].href.match(/textangular\.(min\.|)css/ig)){
  					sheet = _sheets[i];
  					break;
  				}
  			}
  		}
  	}
  	/* istanbul ignore next: preference for stylesheet loaded externally */
  	if(!sheet){
  		// this sheet is used for the placeholders later on.
  		sheet = (function() {
  			// Create the <style> tag
  			var style = document.createElement("style");
  			/* istanbul ignore else : WebKit hack :( */
  			if(_browserDetect.webkit) style.appendChild(document.createTextNode(""));
  
  			// Add the <style> element to the page, add as first so the styles can be overridden by custom stylesheets
  			document.getElementsByTagName('head')[0].appendChild(style);
  
  			return style.sheet;
  		})();
  	}
  
  	// use as: addCSSRule("header", "float: left");
  	addCSSRule = function(selector, rules) {
  		return _addCSSRule(sheet, selector, rules);
  	};
  	_addCSSRule = function(_sheet, selector, rules){
  		var insertIndex;
  		var insertedRule;
  		// This order is important as IE 11 has both cssRules and rules but they have different lengths - cssRules is correct, rules gives an error in IE 11
  		/* istanbul ignore next: browser catches */
  		if(_sheet.cssRules) insertIndex = Math.max(_sheet.cssRules.length - 1, 0);
  		else if(_sheet.rules) insertIndex = Math.max(_sheet.rules.length - 1, 0);
  
  		/* istanbul ignore else: untestable IE option */
  		if(_sheet.insertRule) {
  			_sheet.insertRule(selector + "{" + rules + "}", insertIndex);
  		}
  		else {
  			_sheet.addRule(selector, rules, insertIndex);
  		}
  		/* istanbul ignore next: browser catches */
  		if(sheet.rules) insertedRule = sheet.rules[insertIndex];
  		else if(sheet.cssRules) insertedRule = sheet.cssRules[insertIndex];
  		// return the inserted stylesheet rule
  		return insertedRule;
  	};
  
  	_getRuleIndex = function(rule, rules) {
  		var i, ruleIndex;
  		for (i=0; i < rules.length; i++) {
  			/* istanbul ignore else: check for correct rule */
  			if (rules[i].cssText === rule.cssText) {
  				ruleIndex = i;
  				break;
  			}
  		}
  		return ruleIndex;
  	};
  
  	removeCSSRule = function(rule){
  		_removeCSSRule(sheet, rule);
  	};
  	/* istanbul ignore next: tests are browser specific */
  	_removeCSSRule = function(sheet, rule){
  		var rules = sheet.cssRules || sheet.rules;
  		if(!rules || rules.length === 0) return;
  		var ruleIndex = _getRuleIndex(rule, rules);
  		if(sheet.removeRule){
  			sheet.removeRule(ruleIndex);
  		}else{
  			sheet.deleteRule(ruleIndex);
  		}
  	};
  }
  
  angular.module('textAngular.factories', [])
  .factory('taBrowserTag', [function(){
  	return function(tag){
  		/* istanbul ignore next: ie specific test */
  		if(!tag) return (_browserDetect.ie <= 8)? 'P' : 'p';
  		else if(tag === '') return (_browserDetect.ie === undefined)? 'div' : (_browserDetect.ie <= 8)? 'P' : 'p';
  		else return (_browserDetect.ie <= 8)? tag.toUpperCase() : tag;
  	};
  }]).factory('taApplyCustomRenderers', ['taCustomRenderers', 'taDOM', function(taCustomRenderers, taDOM){
  	return function(val){
  		var element = angular.element('<div></div>');
  		element[0].innerHTML = val;
  
  		angular.forEach(taCustomRenderers, function(renderer){
  			var elements = [];
  			// get elements based on what is defined. If both defined do secondary filter in the forEach after using selector string
  			if(renderer.selector && renderer.selector !== '')
  				elements = element.find(renderer.selector);
  			/* istanbul ignore else: shouldn't fire, if it does we're ignoring everything */
  			else if(renderer.customAttribute && renderer.customAttribute !== '')
  				elements = taDOM.getByAttribute(element, renderer.customAttribute);
  			// process elements if any found
  			angular.forEach(elements, function(_element){
  				_element = angular.element(_element);
  				if(renderer.selector && renderer.selector !== '' && renderer.customAttribute && renderer.customAttribute !== ''){
  					if(_element.attr(renderer.customAttribute) !== undefined) renderer.renderLogic(_element);
  				} else renderer.renderLogic(_element);
  			});
  		});
  
  		return element[0].innerHTML;
  	};
  }]).factory('taFixChrome', function(){
  	// get whaterever rubbish is inserted in chrome
  	// should be passed an html string, returns an html string
  	var taFixChrome = function(html){
  		if(!html || !angular.isString(html) || html.length <= 0) return html;
  		// grab all elements with a style attibute
  		var spanMatch = /<([^>\/]+?)style=("([^"]+)"|'([^']+)')([^>]*)>/ig;
  		var match, styleVal, newTag, finalHtml = '', lastIndex = 0;
  		while(match = spanMatch.exec(html)){
  			// one of the quoted values ' or "
  			/* istanbul ignore next: quotations match */
  			styleVal = match[3] || match[4];
  			// test for chrome inserted junk
  			if(styleVal && styleVal.match(/line-height: 1.[0-9]{3,12};|color: inherit; line-height: 1.1;/i)){
  				// replace original tag with new tag
  				styleVal = styleVal.replace(/( |)font-family: inherit;|( |)line-height: 1.[0-9]{3,12};|( |)color: inherit;/ig, '');
  				newTag = '<' + match[1].trim();
  				if(styleVal.trim().length > 0) newTag += ' style=' + match[2].substring(0,1) + styleVal + match[2].substring(0,1);
  				newTag += match[5].trim() + ">";
  				finalHtml += html.substring(lastIndex, match.index) + newTag;
  				lastIndex = match.index + match[0].length;
  			}
  		}
  		finalHtml += html.substring(lastIndex);
  		// only replace when something has changed, else we get focus problems on inserting lists
  		if(lastIndex > 0){
  			// replace all empty strings
  			return finalHtml.replace(/<span\s?>(.*?)<\/span>(<br(\/|)>|)/ig, '$1');
  		} else return html;
  	};
  	return taFixChrome;
  }).factory('taSanitize', ['$sanitize', function taSanitizeFactory($sanitize){
  
  	var convert_infos = [
  		{
  			property: 'font-weight',
  			values: [ 'bold' ],
  			tag: 'b'
  		},
  		{
  			property: 'font-style',
  			values: [ 'italic' ],
  			tag: 'i'
  		}
  	];
  	
  	var styleMatch = [];
  	for(var i = 0; i < convert_infos.length; i++){
  		var _partialStyle = '(' + convert_infos[i].property + ':\\s*(';
  		for(var j = 0; j < convert_infos[i].values.length; j++){
  			/* istanbul ignore next: not needed to be tested yet */
  			if(j > 0) _partialStyle += '|';
  			_partialStyle += convert_infos[i].values[j];
  		}
  		_partialStyle += ');)';
  		styleMatch.push(_partialStyle);
  	}
  	var styleRegexString = '(' + styleMatch.join('|') + ')';
  	
  	function wrapNested(html, wrapTag) {
  		var depth = 0;
  		var lastIndex = 0;
  		var match;
  		var tagRegex = /<[^>]*>/ig;
  		while(match = tagRegex.exec(html)){
  			lastIndex = match.index;
  			if(match[0].substr(1, 1) === '/'){
  				if(depth === 0) break;
  				else depth--;
  			}else depth++;
  		}
  		return wrapTag +
  			html.substring(0, lastIndex) +
  			// get the start tags reversed - this is safe as we construct the strings with no content except the tags
  			angular.element(wrapTag)[0].outerHTML.substring(wrapTag.length) +
  			html.substring(lastIndex);
  	}
  	
  	function transformLegacyStyles(html){
  		if(!html || !angular.isString(html) || html.length <= 0) return html;
  		var i;
  		var styleElementMatch = /<([^>\/]+?)style=("([^"]+)"|'([^']+)')([^>]*)>/ig;
  		var match, subMatch, styleVal, newTag, lastNewTag = '', newHtml, finalHtml = '', lastIndex = 0;
  		while(match = styleElementMatch.exec(html)){
  			// one of the quoted values ' or "
  			/* istanbul ignore next: quotations match */
  			styleVal = match[3] || match[4];
  			var styleRegex = new RegExp(styleRegexString, 'i');
  			// test for style values to change
  			if(angular.isString(styleVal) && styleRegex.test(styleVal)){
  				// remove build tag list
  				newTag = '';
  				// init regex here for exec
  				var styleRegexExec = new RegExp(styleRegexString, 'ig');
  				// find relevand tags and build a string of them
  				while(subMatch = styleRegexExec.exec(styleVal)){
  					for(i = 0; i < convert_infos.length; i++){
  						if(!!subMatch[(i*2) + 2]){
  							newTag += '<' + convert_infos[i].tag + '>';
  						}
  					}
  				}
  				// recursively find more legacy styles in html before this tag and after the previous match (if any)
  				newHtml = transformLegacyStyles(html.substring(lastIndex, match.index));
  				// build up html
  				if(lastNewTag.length > 0){
  					finalHtml += wrapNested(newHtml, lastNewTag);
  				}else finalHtml += newHtml;
  				// grab the style val without the transformed values
  				styleVal = styleVal.replace(new RegExp(styleRegexString, 'ig'), '');
  				// build the html tag
  				finalHtml += '<' + match[1].trim();
  				if(styleVal.length > 0) finalHtml += ' style="' + styleVal + '"';
  				finalHtml += match[5] + '>';
  				// update the start index to after this tag
  				lastIndex = match.index + match[0].length;
  				lastNewTag = newTag;
  			}
  		}
  		if(lastNewTag.length > 0){
  			finalHtml += wrapNested(html.substring(lastIndex), lastNewTag);
  		}
  		else finalHtml += html.substring(lastIndex);
  		return finalHtml;
  	}
  	
  	function transformLegacyAttributes(html){
  		if(!html || !angular.isString(html) || html.length <= 0) return html;
  		// replace all align='...' tags with text-align attributes
  		var attrElementMatch = /<([^>\/]+?)align=("([^"]+)"|'([^']+)')([^>]*)>/ig;
  		var match, finalHtml = '', lastIndex = 0;
  		// match all attr tags
  		while(match = attrElementMatch.exec(html)){
  			// add all html before this tag
  			finalHtml += html.substring(lastIndex, match.index);
  			// record last index after this tag
  			lastIndex = match.index + match[0].length;
  			// construct tag without the align attribute
  			var newTag = '<' + match[1] + match[5];
  			// add the style attribute
  			if(/style=("([^"]+)"|'([^']+)')/ig.test(newTag)){
  				/* istanbul ignore next: quotations match */
  				newTag = newTag.replace(/style=("([^"]+)"|'([^']+)')/i, 'style="$2$3 text-align:' + (match[3] || match[4]) + ';"');
  			}else{
  				/* istanbul ignore next: quotations match */
  				newTag += ' style="text-align:' + (match[3] || match[4]) + ';"';
  			}
  			newTag += '>';
  			// add to html
  			finalHtml += newTag;
  		}
  		// return with remaining html
  		return finalHtml + html.substring(lastIndex);
  	}
  	
  	return function taSanitize(unsafe, oldsafe, ignore){
  		// unsafe html should NEVER built into a DOM object via angular.element. This allows XSS to be inserted and run.
  		if ( !ignore ) {
  			try {
  				unsafe = transformLegacyStyles(unsafe);
  			} catch (e) {
  			}
  		}
  
  		// unsafe and oldsafe should be valid HTML strings
  		// any exceptions (lets say, color for example) should be made here but with great care
  		// setup unsafe element for modification
  		unsafe = transformLegacyAttributes(unsafe);
  		
  		var safe;
  		try {
  			safe = $sanitize(unsafe);
  			// do this afterwards, then the $sanitizer should still throw for bad markup
  			if(ignore) safe = unsafe;
  		} catch (e){
  			safe = oldsafe || '';
  		}
  		
  		// Do processing for <pre> tags, removing tabs and return carriages outside of them
  		
  		var _preTags = safe.match(/(<pre[^>]*>.*?<\/pre[^>]*>)/ig);
  		var processedSafe = safe.replace(/(&#(9|10);)*/ig, '');
  		var re = /<pre[^>]*>.*?<\/pre[^>]*>/ig;
  		var index = 0;
  		var lastIndex = 0;
  		var origTag;
  		safe = '';
  		while((origTag = re.exec(processedSafe)) !== null && index < _preTags.length){
  			safe += processedSafe.substring(lastIndex, origTag.index) + _preTags[index];
  			lastIndex = origTag.index + origTag[0].length;
  			index++;
  		}
  		return safe + processedSafe.substring(lastIndex);
  	};
  }]).factory('taToolExecuteAction', ['$q', '$log', function($q, $log){
  	// this must be called on a toolScope or instance
  	return function(editor){
  		if(editor !== undefined) this.$editor = function(){ return editor; };
  		var deferred = $q.defer(),
  			promise = deferred.promise,
  			_editor = this.$editor();
  		// pass into the action the deferred function and also the function to reload the current selection if rangy available
  		var result;
  		try{
  			result = this.action(deferred, _editor.startAction());
  			// We set the .finally callback here to make sure it doesn't get executed before any other .then callback.
  			promise['finally'](function(){
  				_editor.endAction.call(_editor);
  			});
  		}catch(exc){
  			$log.error(exc);
  		}
  		if(result || result === undefined){
  			// if true or undefined is returned then the action has finished. Otherwise the deferred action will be resolved manually.
  			deferred.resolve();
  		}
  	};
  }]);
  angular.module('textAngular.DOM', ['textAngular.factories'])
  .factory('taExecCommand', ['taSelection', 'taBrowserTag', '$document', function(taSelection, taBrowserTag, $document){
  	var listToDefault = function(listElement, defaultWrap){
  		var $target, i;
  		// if all selected then we should remove the list
  		// grab all li elements and convert to taDefaultWrap tags
  		var children = listElement.find('li');
  		for(i = children.length - 1; i >= 0; i--){
  			$target = angular.element('<' + defaultWrap + '>' + children[i].innerHTML + '</' + defaultWrap + '>');
  			listElement.after($target);
  		}
  		listElement.remove();
  		taSelection.setSelectionToElementEnd($target[0]);
  	};
  	var selectLi = function(liElement){
  		if(/(<br(|\/)>)$/i.test(liElement.innerHTML.trim())) taSelection.setSelectionBeforeElement(angular.element(liElement).find("br")[0]);
  		else taSelection.setSelectionToElementEnd(liElement);
  	};
  	var listToList = function(listElement, newListTag){
  		var $target = angular.element('<' + newListTag + '>' + listElement[0].innerHTML + '</' + newListTag + '>');
  		listElement.after($target);
  		listElement.remove();
  		selectLi($target.find('li')[0]);
  	};
  	var childElementsToList = function(elements, listElement, newListTag){
  		var html = '';
  		for(var i = 0; i < elements.length; i++){
  			html += '<' + taBrowserTag('li') + '>' + elements[i].innerHTML + '</' + taBrowserTag('li') + '>';
  		}
  		var $target = angular.element('<' + newListTag + '>' + html + '</' + newListTag + '>');
  		listElement.after($target);
  		listElement.remove();
  		selectLi($target.find('li')[0]);
  	};
  	return function(taDefaultWrap, topNode){
  		taDefaultWrap = taBrowserTag(taDefaultWrap);
  		return function(command, showUI, options, defaultTagAttributes){
  			var i, $target, html, _nodes, next, optionsTagName, selectedElement;
  			var defaultWrapper = angular.element('<' + taDefaultWrap + '>');
  			try{
  				selectedElement = taSelection.getSelectionElement();
  			}catch(e){}
  			var $selected = angular.element(selectedElement);
  			if(selectedElement !== undefined){
  				var tagName = selectedElement.tagName.toLowerCase();
  				if(command.toLowerCase() === 'insertorderedlist' || command.toLowerCase() === 'insertunorderedlist'){
  					var selfTag = taBrowserTag((command.toLowerCase() === 'insertorderedlist')? 'ol' : 'ul');
  					if(tagName === selfTag){
  						// if all selected then we should remove the list
  						// grab all li elements and convert to taDefaultWrap tags
  						return listToDefault($selected, taDefaultWrap);
  					}else if(tagName === 'li' && $selected.parent()[0].tagName.toLowerCase() === selfTag && $selected.parent().children().length === 1){
  						// catch for the previous statement if only one li exists
  						return listToDefault($selected.parent(), taDefaultWrap);
  					}else if(tagName === 'li' && $selected.parent()[0].tagName.toLowerCase() !== selfTag && $selected.parent().children().length === 1){
  						// catch for the previous statement if only one li exists
  						return listToList($selected.parent(), selfTag);
  					}else if(tagName.match(BLOCKELEMENTS) && !$selected.hasClass('ta-bind')){
  						// if it's one of those block elements we have to change the contents
  						// if it's a ol/ul we are changing from one to the other
  						if(tagName === 'ol' || tagName === 'ul'){
  							return listToList($selected, selfTag);
  						}else{
  							var childBlockElements = false;
  							angular.forEach($selected.children(), function(elem){
  								if(elem.tagName.match(BLOCKELEMENTS)) {
  									childBlockElements = true;
  								}
  							});
  							if(childBlockElements){
  								return childElementsToList($selected.children(), $selected, selfTag);
  							}else{
  								return childElementsToList([angular.element('<div>' + selectedElement.innerHTML + '</div>')[0]], $selected, selfTag);
  							}
  						}
  					}else if(tagName.match(BLOCKELEMENTS)){
  						// if we get here then all the contents of the ta-bind are selected
  						_nodes = taSelection.getOnlySelectedElements();
  						if(_nodes.length === 0){
  							// here is if there is only text in ta-bind ie <div ta-bind>test content</div>
  							$target = angular.element('<' + selfTag + '><li>' + selectedElement.innerHTML + '</li></' + selfTag + '>');
  							$selected.html('');
  							$selected.append($target);
  						}else if(_nodes.length === 1 && (_nodes[0].tagName.toLowerCase() === 'ol' || _nodes[0].tagName.toLowerCase() === 'ul')){
  							if(_nodes[0].tagName.toLowerCase() === selfTag){
  								// remove
  								return listToDefault(angular.element(_nodes[0]), taDefaultWrap);
  							}else{
  								return listToList(angular.element(_nodes[0]), selfTag);
  							}
  						}else{
  							html = '';
  							var $nodes = [];
  							for(i = 0; i < _nodes.length; i++){
  								/* istanbul ignore else: catch for real-world can't make it occur in testing */
  								if(_nodes[i].nodeType !== 3){
  									var $n = angular.element(_nodes[i]);
  									/* istanbul ignore if: browser check only, phantomjs doesn't return children nodes but chrome at least does */
  									if(_nodes[i].tagName.toLowerCase() === 'li') continue;
  									else if(_nodes[i].tagName.toLowerCase() === 'ol' || _nodes[i].tagName.toLowerCase() === 'ul'){
  										html += $n[0].innerHTML; // if it's a list, add all it's children
  									}else if(_nodes[i].tagName.toLowerCase() === 'span' && (_nodes[i].childNodes[0].tagName.toLowerCase() === 'ol' || _nodes[i].childNodes[0].tagName.toLowerCase() === 'ul')){
  										html += $n[0].childNodes[0].innerHTML; // if it's a list, add all it's children
  									}else{
  										html += '<' + taBrowserTag('li') + '>' + $n[0].innerHTML + '</' + taBrowserTag('li') + '>';
  									}
  									$nodes.unshift($n);
  								}
  							}
  							$target = angular.element('<' + selfTag + '>' + html + '</' + selfTag + '>');
  							$nodes.pop().replaceWith($target);
  							angular.forEach($nodes, function($node){ $node.remove(); });
  						}
  						taSelection.setSelectionToElementEnd($target[0]);
  						return;
  					}
  				}else if(command.toLowerCase() === 'formatblock'){
  					optionsTagName = options.toLowerCase().replace(/[<>]/ig, '');
  					if(optionsTagName.trim() === 'default') {
  						optionsTagName = taDefaultWrap;
  						options = '<' + taDefaultWrap + '>';
  					}
  					if(tagName === 'li') $target = $selected.parent();
  					else $target = $selected;
  					// find the first blockElement
  					while(!$target[0].tagName || !$target[0].tagName.match(BLOCKELEMENTS) && !$target.parent().attr('contenteditable')){
  						$target = $target.parent();
  						/* istanbul ignore next */
  						tagName = ($target[0].tagName || '').toLowerCase();
  					}
  					if(tagName === optionsTagName){
  						// $target is wrap element
  						_nodes = $target.children();
  						var hasBlock = false;
  						for(i = 0; i < _nodes.length; i++){
  							hasBlock = hasBlock || _nodes[i].tagName.match(BLOCKELEMENTS);
  						}
  						if(hasBlock){
  							$target.after(_nodes);
  							next = $target.next();
  							$target.remove();
  							$target = next;
  						}else{
  							defaultWrapper.append($target[0].childNodes);
  							$target.after(defaultWrapper);
  							$target.remove();
  							$target = defaultWrapper;
  						}
  					}else if($target.parent()[0].tagName.toLowerCase() === optionsTagName && !$target.parent().hasClass('ta-bind')){
  						//unwrap logic for parent
  						var blockElement = $target.parent();
  						var contents = blockElement.contents();
  						for(i = 0; i < contents.length; i ++){
  							/* istanbul ignore next: can't test - some wierd thing with how phantomjs works */
  							if(blockElement.parent().hasClass('ta-bind') && contents[i].nodeType === 3){
  								defaultWrapper = angular.element('<' + taDefaultWrap + '>');
  								defaultWrapper[0].innerHTML = contents[i].outerHTML;
  								contents[i] = defaultWrapper[0];
  							}
  							blockElement.parent()[0].insertBefore(contents[i], blockElement[0]);
  						}
  						blockElement.remove();
  					}else if(tagName.match(LISTELEMENTS)){
  						// wrapping a list element
  						$target.wrap(options);
  					}else{
  						// default wrap behaviour
  						_nodes = taSelection.getOnlySelectedElements();
  						if(_nodes.length === 0) _nodes = [$target[0]];
  						// find the parent block element if any of the nodes are inline or text
  						for(i = 0; i < _nodes.length; i++){
  							if(_nodes[i].nodeType === 3 || !_nodes[i].tagName.match(BLOCKELEMENTS)){
  								while(_nodes[i].nodeType === 3 || !_nodes[i].tagName || !_nodes[i].tagName.match(BLOCKELEMENTS)){
  									_nodes[i] = _nodes[i].parentNode;
  								}
  							}
  						}
  						if(angular.element(_nodes[0]).hasClass('ta-bind')){
  							$target = angular.element(options);
  							$target[0].innerHTML = _nodes[0].innerHTML;
  							_nodes[0].innerHTML = $target[0].outerHTML;
  						}else if(optionsTagName === 'blockquote'){
  							// blockquotes wrap other block elements
  							html = '';
  							for(i = 0; i < _nodes.length; i++){
  								html += _nodes[i].outerHTML;
  							}
  							$target = angular.element(options);
  							$target[0].innerHTML = html;
  							_nodes[0].parentNode.insertBefore($target[0],_nodes[0]);
  							for(i = _nodes.length - 1; i >= 0; i--){
  								/* istanbul ignore else:  */
  								if(_nodes[i].parentNode) _nodes[i].parentNode.removeChild(_nodes[i]);
  							}
  						}
  						else {
  							// regular block elements replace other block elements
  							for(i = 0; i < _nodes.length; i++){
  								$target = angular.element(options);
  								$target[0].innerHTML = _nodes[i].innerHTML;
  								_nodes[i].parentNode.insertBefore($target[0],_nodes[i]);
  								_nodes[i].parentNode.removeChild(_nodes[i]);
  							}
  						}
  					}
  					taSelection.setSelectionToElementEnd($target[0]);
  					return;
  				}else if(command.toLowerCase() === 'createlink'){
  					var tagBegin = '<a href="' + options + '" target="' +
  							(defaultTagAttributes.a.target ? defaultTagAttributes.a.target : '') +
  							'">',
  						tagEnd = '</a>',
  						_selection = taSelection.getSelection();
  					if(_selection.collapsed){
  						// insert text at selection, then select then just let normal exec-command run
  						taSelection.insertHtml(tagBegin + options + tagEnd, topNode);
  					}else if(rangy.getSelection().getRangeAt(0).canSurroundContents()){
  						var node = angular.element(tagBegin + tagEnd)[0];
  						rangy.getSelection().getRangeAt(0).surroundContents(node);
  					}
  					return;
  				}else if(command.toLowerCase() === 'inserthtml'){
  					taSelection.insertHtml(options, topNode);
  					return;
  				}
  			}
  			try{
  				$document[0].execCommand(command, showUI, options);
  			}catch(e){}
  		};
  	};
  }]).service('taSelection', ['$window', '$document', 'taDOM',
  /* istanbul ignore next: all browser specifics and PhantomJS dosen't seem to support half of it */
  function($window, $document, taDOM){
  	// need to dereference the document else the calls don't work correctly
  	var _document = $document[0];
  	var rangy = $window.rangy;
  	var brException = function (element, offset) {
  		/* check if selection is a BR element at the beginning of a container. If so, get
  		* the parentNode instead.
  		* offset should be zero in this case. Otherwise, return the original
  		* element.
  		*/
  		if (element.tagName && element.tagName.match(/^br$/i) && offset === 0 && !element.previousSibling) {
              return {
                  element: element.parentNode,
                  offset: 0
              };
  		} else {
  			return {
  				element: element,
  				offset: offset
  			};
  		}
  	};
  	var api = {
  		getSelection: function(){
  			var range = rangy.getSelection().getRangeAt(0);
  			var container = range.commonAncestorContainer;
  			var selection = {
  				start: brException(range.startContainer, range.startOffset),
  				end: brException(range.endContainer, range.endOffset),
  				collapsed: range.collapsed
  			};
  			// Check if the container is a text node and return its parent if so
  			container = container.nodeType === 3 ? container.parentNode : container;
  			if (container.parentNode === selection.start.element ||
  				container.parentNode === selection.end.element) {
  				selection.container = container.parentNode;
  			} else {
  				selection.container = container;
  			}
  			return selection;
  		},
  		getOnlySelectedElements: function(){
  			var range = rangy.getSelection().getRangeAt(0);
  			var container = range.commonAncestorContainer;
  			// Check if the container is a text node and return its parent if so
  			container = container.nodeType === 3 ? container.parentNode : container;
  			return range.getNodes([1], function(node){
  				return node.parentNode === container;
  			});
  		},
  		// Some basic selection functions
  		getSelectionElement: function () {
  			return api.getSelection().container;
  		},
  		setSelection: function(el, start, end){
  			var range = rangy.createRange();
  			
  			range.setStart(el, start);
  			range.setEnd(el, end);
  			
  			rangy.getSelection().setSingleRange(range);
  		},
  		setSelectionBeforeElement: function (el){
  			var range = rangy.createRange();
  			
  			range.selectNode(el);
  			range.collapse(true);
  			
  			rangy.getSelection().setSingleRange(range);
  		},
  		setSelectionAfterElement: function (el){
  			var range = rangy.createRange();
  			
  			range.selectNode(el);
  			range.collapse(false);
  			
  			rangy.getSelection().setSingleRange(range);
  		},
  		setSelectionToElementStart: function (el){
  			var range = rangy.createRange();
  			
  			range.selectNodeContents(el);
  			range.collapse(true);
  			
  			rangy.getSelection().setSingleRange(range);
  		},
  		setSelectionToElementEnd: function (el){
  			var range = rangy.createRange();
  			
  			range.selectNodeContents(el);
  			range.collapse(false);
  			if(el.childNodes && el.childNodes[el.childNodes.length - 1] && el.childNodes[el.childNodes.length - 1].nodeName === 'br'){
  				range.startOffset = range.endOffset = range.startOffset - 1;
  			}
  			rangy.getSelection().setSingleRange(range);
  		},
  		// from http://stackoverflow.com/questions/6690752/insert-html-at-caret-in-a-contenteditable-div
  		// topNode is the contenteditable normally, all manipulation MUST be inside this.
  		insertHtml: function(html, topNode){
  			var parent, secondParent, _childI, nodes, i, lastNode, _tempFrag;
  			var element = angular.element("<div>" + html + "</div>");
  			var range = rangy.getSelection().getRangeAt(0);
  			var frag = _document.createDocumentFragment();
  			var children = element[0].childNodes;
  			var isInline = true;
  			
  			if(children.length > 0){
  				// NOTE!! We need to do the following:
  				// check for blockelements - if they exist then we have to split the current element in half (and all others up to the closest block element) and insert all children in-between.
  				// If there are no block elements, or there is a mixture we need to create textNodes for the non wrapped text (we don't want them spans messing up the picture).
  				nodes = [];
  				for(_childI = 0; _childI < children.length; _childI++){
  					if(!(
  						(children[_childI].nodeName.toLowerCase() === 'p' && children[_childI].innerHTML.trim() === '') || // empty p element
  						(children[_childI].nodeType === 3 && children[_childI].nodeValue.trim() === '') // empty text node
  					)){
  						isInline = isInline && !BLOCKELEMENTS.test(children[_childI].nodeName);
  						nodes.push(children[_childI]);
  					}
  				}
  				for(var _n = 0; _n < nodes.length; _n++) lastNode = frag.appendChild(nodes[_n]);
  				if(!isInline && range.collapsed && /^(|<br(|\/)>)$/i.test(range.startContainer.innerHTML)) range.selectNode(range.startContainer);
  			}else{
  				isInline = true;
  				// paste text of some sort
  				lastNode = frag = _document.createTextNode(html);
  			}
  			
  			// Other Edge case - selected data spans multiple blocks.
  			if(isInline){
  				range.deleteContents();
  			}else{ // not inline insert
  				if(range.collapsed && range.startContainer !== topNode){
  					if(range.startContainer.innerHTML && range.startContainer.innerHTML.match(/^<[^>]*>$/i)){
  						// this log is to catch when innerHTML is something like `<img ...>`
  						parent = range.startContainer;
  						if(range.startOffset === 1){
  							// before single tag
  							range.setStartAfter(parent);
  							range.setEndAfter(parent);
  						}else{
  							// after single tag
  							range.setStartBefore(parent);
  							range.setEndBefore(parent);
  						}
  					}else{
  						// split element into 2 and insert block element in middle
  						if(range.startContainer.nodeType === 3 && range.startContainer.parentNode !== topNode){ // if text node
  							parent = range.startContainer.parentNode;
  							secondParent = parent.cloneNode();
  							// split the nodes into two lists - before and after, splitting the node with the selection into 2 text nodes.
  							taDOM.splitNodes(parent.childNodes, parent, secondParent, range.startContainer, range.startOffset);
  							
  							// Escape out of the inline tags like b
  							while(!VALIDELEMENTS.test(parent.nodeName)){
  								angular.element(parent).after(secondParent);
  								parent = parent.parentNode;
  								var _lastSecondParent = secondParent;
  								secondParent = parent.cloneNode();
  								// split the nodes into two lists - before and after, splitting the node with the selection into 2 text nodes.
  								taDOM.splitNodes(parent.childNodes, parent, secondParent, _lastSecondParent);
  							}
  						}else{
  							parent = range.startContainer;
  							secondParent = parent.cloneNode();
  							taDOM.splitNodes(parent.childNodes, parent, secondParent, undefined, undefined, range.startOffset);
  						}
  						
  						angular.element(parent).after(secondParent);
  						// put cursor to end of inserted content
  						range.setStartAfter(parent);
  						range.setEndAfter(parent);
  						
  						if(/^(|<br(|\/)>)$/i.test(parent.innerHTML.trim())){
  							range.setStartBefore(parent);
  							range.setEndBefore(parent);
  							angular.element(parent).remove();
  						}
  						if(/^(|<br(|\/)>)$/i.test(secondParent.innerHTML.trim())) angular.element(secondParent).remove();
  						if(parent.nodeName.toLowerCase() === 'li'){
  							_tempFrag = _document.createDocumentFragment();
  							for(i = 0; i < frag.childNodes.length; i++){
  								element = angular.element('<li>');
  								taDOM.transferChildNodes(frag.childNodes[i], element[0]);
  								taDOM.transferNodeAttributes(frag.childNodes[i], element[0]);
  								_tempFrag.appendChild(element[0]);
  							}
  							frag = _tempFrag;
  							if(lastNode){
  								lastNode = frag.childNodes[frag.childNodes.length - 1];
  								lastNode = lastNode.childNodes[lastNode.childNodes.length - 1];
  							}
  						}
  					}
  				}else{
  					range.deleteContents();
  				}
  			}
  			
  			range.insertNode(frag);
  			if(lastNode){
  				api.setSelectionToElementEnd(lastNode);
  			}
  		}
  	};
  	return api;
  }]).service('taDOM', function(){
  	var taDOM = {
  		// recursive function that returns an array of angular.elements that have the passed attribute set on them
  		getByAttribute: function(element, attribute){
  			var resultingElements = [];
  			var childNodes = element.children();
  			if(childNodes.length){
  				angular.forEach(childNodes, function(child){
  					resultingElements = resultingElements.concat(taDOM.getByAttribute(angular.element(child), attribute));
  				});
  			}
  			if(element.attr(attribute) !== undefined) resultingElements.push(element);
  			return resultingElements;
  		},
  		
  		transferChildNodes: function(source, target){
  			// clear out target
  			target.innerHTML = '';
  			while(source.childNodes.length > 0) target.appendChild(source.childNodes[0]);
  			return target;
  		},
  		
  		splitNodes: function(nodes, target1, target2, splitNode, subSplitIndex, splitIndex){
  			if(!splitNode && isNaN(splitIndex)) throw new Error('taDOM.splitNodes requires a splitNode or splitIndex');
  			var startNodes = document.createDocumentFragment();
  			var endNodes = document.createDocumentFragment();
  			var index = 0;
  			
  			while(nodes.length > 0 && (isNaN(splitIndex) || splitIndex !== index) && nodes[0] !== splitNode){
  				startNodes.appendChild(nodes[0]); // this removes from the nodes array (if proper childNodes object.
  				index++;
  			}
  			
  			if(!isNaN(subSplitIndex) && subSplitIndex >= 0 && nodes[0]){
  				startNodes.appendChild(document.createTextNode(nodes[0].nodeValue.substring(0, subSplitIndex)));
  				nodes[0].nodeValue = nodes[0].nodeValue.substring(subSplitIndex);
  			}
  			while(nodes.length > 0) endNodes.appendChild(nodes[0]);
  			
  			taDOM.transferChildNodes(startNodes, target1);
  			taDOM.transferChildNodes(endNodes, target2);
  		},
  		
  		transferNodeAttributes: function(source, target){
  			for(var i = 0; i < source.attributes.length; i++) target.setAttribute(source.attributes[i].name, source.attributes[i].value);
  			return target;
  		}
  	};
  	return taDOM;
  });
  angular.module('textAngular.validators', [])
  .directive('taMaxText', function(){
  	return {
  		restrict: 'A',
  		require: 'ngModel',
  		link: function(scope, elem, attrs, ctrl){
  			var max = parseInt(scope.$eval(attrs.taMaxText));
  			if (isNaN(max)){
  				throw('Max text must be an integer');
  			}
  			attrs.$observe('taMaxText', function(value){
  				max = parseInt(value);
  				if (isNaN(max)){
  					throw('Max text must be an integer');
  				}
  				if (ctrl.$dirty){
  					ctrl.$validate();
  				}
  			});
  			ctrl.$validators.taMaxText = function(viewValue){
  				var source = angular.element('<div/>');
  				source.html(viewValue);
  				return source.text().length <= max;
  			};
  		}
  	};
  }).directive('taMinText', function(){
  	return {
  		restrict: 'A',
  		require: 'ngModel',
  		link: function(scope, elem, attrs, ctrl){
  			var min = parseInt(scope.$eval(attrs.taMinText));
  			if (isNaN(min)){
  				throw('Min text must be an integer');
  			}
  			attrs.$observe('taMinText', function(value){
  				min = parseInt(value);
  				if (isNaN(min)){
  					throw('Min text must be an integer');
  				}
  				if (ctrl.$dirty){
  					ctrl.$validate();
  				}
  			});
  			ctrl.$validators.taMinText = function(viewValue){
  				var source = angular.element('<div/>');
  				source.html(viewValue);
  				return !source.text().length || source.text().length >= min;
  			};
  		}
  	};
  });
  angular.module('textAngular.taBind', ['textAngular.factories', 'textAngular.DOM'])
  .service('_taBlankTest', [function(){
  	var INLINETAGS_NONBLANK = /<(a|abbr|acronym|bdi|bdo|big|cite|code|del|dfn|img|ins|kbd|label|map|mark|q|ruby|rp|rt|s|samp|time|tt|var)[^>]*(>|$)/i;
  	return function(_defaultTest){
  		return function(_blankVal){
  			if(!_blankVal) return true;
  			// find first non-tag match - ie start of string or after tag that is not whitespace
  			var _firstMatch = /(^[^<]|>)[^<]/i.exec(_blankVal);
  			var _firstTagIndex;
  			if(!_firstMatch){
  				// find the end of the first tag removing all the
  				// Don't do a global replace as that would be waaayy too long, just replace the first 4 occurences should be enough
  				_blankVal = _blankVal.toString().replace(/="[^"]*"/i, '').replace(/="[^"]*"/i, '').replace(/="[^"]*"/i, '').replace(/="[^"]*"/i, '');
  				_firstTagIndex = _blankVal.indexOf('>');
  			}else{
  				_firstTagIndex = _firstMatch.index;
  			}
  			_blankVal = _blankVal.trim().substring(_firstTagIndex, _firstTagIndex + 100);
  			// check for no tags entry
  			if(/^[^<>]+$/i.test(_blankVal)) return false;
  			// this regex is to match any number of whitespace only between two tags
  			if (_blankVal.length === 0 || _blankVal === _defaultTest || /^>(\s|&nbsp;)*<\/[^>]+>$/ig.test(_blankVal)) return true;
  			// this regex tests if there is a tag followed by some optional whitespace and some text after that
  			else if (/>\s*[^\s<]/i.test(_blankVal) || INLINETAGS_NONBLANK.test(_blankVal)) return false;
  			else return true;
  		};
  	};
  }])
  .directive('taButton', [function(){
  	return {
  		link: function(scope, element, attrs){
  			element.attr('unselectable', 'on');
  			element.on('mousedown', function(e, eventData){
  				/* istanbul ignore else: this is for catching the jqLite testing*/
  				if(eventData) angular.extend(e, eventData);
  				// this prevents focusout from firing on the editor when clicking toolbar buttons
  				e.preventDefault();
  				return false;
  			});
  		}
  	};
  }])
  .directive('taBind', [
  		'taSanitize', '$timeout', '$window', '$document', 'taFixChrome', 'taBrowserTag',
  		'taSelection', 'taSelectableElements', 'taApplyCustomRenderers', 'taOptions',
  		'_taBlankTest', '$parse', 'taDOM', 'textAngularManager',
  		function(
  			taSanitize, $timeout, $window, $document, taFixChrome, taBrowserTag,
  			taSelection, taSelectableElements, taApplyCustomRenderers, taOptions,
  			_taBlankTest, $parse, taDOM, textAngularManager){
  	// Uses for this are textarea or input with ng-model and ta-bind='text'
  	// OR any non-form element with contenteditable="contenteditable" ta-bind="html|text" ng-model
  	return {
  		priority: 2, // So we override validators correctly
  		require: ['ngModel','?ngModelOptions'],
  		link: function(scope, element, attrs, controller){
  			var ngModel = controller[0];
  			var ngModelOptions = controller[1] || {};
  			// the option to use taBind on an input or textarea is required as it will sanitize all input into it correctly.
  			var _isContentEditable = element.attr('contenteditable') !== undefined && element.attr('contenteditable');
  			var _isInputFriendly = _isContentEditable || element[0].tagName.toLowerCase() === 'textarea' || element[0].tagName.toLowerCase() === 'input';
  			var _isReadonly = false;
  			var _focussed = false;
  			var _skipRender = false;
  			var _disableSanitizer = attrs.taUnsafeSanitizer || taOptions.disableSanitizer;
  			var _lastKey;
  			// see http://www.javascripter.net/faq/keycodes.htm for good information
  			// NOTE Mute On|Off 173 (Opera MSIE Safari Chrome) 181 (Firefox)
  			// BLOCKED_KEYS are special keys...
  			// Tab, pause/break, CapsLock, Esc, Page Up, End, Home,
  			// Left arrow, Up arrow, Right arrow, Down arrow, Insert, Delete,
  			// f1, f2, f3, f4, f5, f6, f7, f8, f9, f10, f11, f12
  			// NumLock, ScrollLock
  			var BLOCKED_KEYS = /^(9|19|20|27|33|34|35|36|37|38|39|40|45|112|113|114|115|116|117|118|119|120|121|122|123|144|145)$/i;
  			// UNDO_TRIGGER_KEYS - spaces, enter, delete, backspace, all punctuation
  			// Backspace, Enter, Space, Delete, (; :) (Firefox), (= +) (Firefox),
  			// Numpad +, Numpad -, (; :), (= +),
  			// (, <), (- _), (. >), (/ ?), (` ~), ([ {), (\ |), (] }), (' ")
  			// NOTE - Firefox: 173 = (- _) -- adding this to UNDO_TRIGGER_KEYS
  			var UNDO_TRIGGER_KEYS = /^(8|13|32|46|59|61|107|109|173|186|187|188|189|190|191|192|219|220|221|222)$/i;
  			var _pasteHandler;
  
  			// defaults to the paragraph element, but we need the line-break or it doesn't allow you to type into the empty element
  			// non IE is '<p><br/></p>', ie is '<p></p>' as for once IE gets it correct...
  			var _defaultVal, _defaultTest;
  
  			var _CTRL_KEY = 0x0001;
  			var _META_KEY = 0x0002;
  			var _ALT_KEY = 0x0004;
  			var _SHIFT_KEY = 0x0008;
  			// map events to special keys...
  			// mappings is an array of maps from events to specialKeys as declared in textAngularSetup
  			var _keyMappings = [
  				//		ctrl/command + z
  				{
  					specialKey: 'UndoKey',
  					forbiddenModifiers: _ALT_KEY + _SHIFT_KEY,
  					mustHaveModifiers: [_META_KEY + _CTRL_KEY],
  					keyCode: 90
  				},
  				//		ctrl/command + shift + z
  				{
  					specialKey: 'RedoKey',
  					forbiddenModifiers: _ALT_KEY,
  					mustHaveModifiers: [_META_KEY + _CTRL_KEY, _SHIFT_KEY],
  					keyCode: 90
  				},
  				//		ctrl/command + y
  				{
  					specialKey: 'RedoKey',
  					forbiddenModifiers: _ALT_KEY + _SHIFT_KEY,
  					mustHaveModifiers: [_META_KEY + _CTRL_KEY],
  					keyCode: 89
  				},
  				//		TabKey
  				{
  					specialKey: 'TabKey',
  					forbiddenModifiers: _META_KEY + _SHIFT_KEY + _ALT_KEY + _CTRL_KEY,
  					mustHaveModifiers: [],
  					keyCode: 9
  				},
  				//		shift + TabKey
  				{
  					specialKey: 'ShiftTabKey',
  					forbiddenModifiers: _META_KEY + _ALT_KEY + _CTRL_KEY,
  					mustHaveModifiers: [_SHIFT_KEY],
  					keyCode: 9
  				}
  			];
  			function _mapKeys(event) {
  				var specialKey;
  				_keyMappings.forEach(function (map){
  					if (map.keyCode === event.keyCode) {
  						var netModifiers = (event.metaKey ? _META_KEY: 0) +
  							(event.ctrlKey ? _CTRL_KEY: 0) +
  							(event.shiftKey ? _SHIFT_KEY: 0) +
  							(event.altKey ? _ALT_KEY: 0);
  						if (map.forbiddenModifiers & netModifiers) return;
  						if (map.mustHaveModifiers.every(function (modifier) { return netModifiers & modifier; })){
  							specialKey = map.specialKey;
  						}
  					}
  				});
  				return specialKey;
  			}
  
  			// set the default to be a paragraph value
  			if(attrs.taDefaultWrap === undefined) attrs.taDefaultWrap = 'p';
  			/* istanbul ignore next: ie specific test */
  			if(attrs.taDefaultWrap === ''){
  				_defaultVal = '';
  				_defaultTest = (_browserDetect.ie === undefined)? '<div><br></div>' : (_browserDetect.ie >= 11)? '<p><br></p>' : (_browserDetect.ie <= 8)? '<P>&nbsp;</P>' : '<p>&nbsp;</p>';
  			}else{
  				_defaultVal = (_browserDetect.ie === undefined || _browserDetect.ie >= 11)?
  					'<' + attrs.taDefaultWrap + '><br></' + attrs.taDefaultWrap + '>' :
  					(_browserDetect.ie <= 8)?
  						'<' + attrs.taDefaultWrap.toUpperCase() + '></' + attrs.taDefaultWrap.toUpperCase() + '>' :
  						'<' + attrs.taDefaultWrap + '></' + attrs.taDefaultWrap + '>';
  				_defaultTest = (_browserDetect.ie === undefined || _browserDetect.ie >= 11)?
  					'<' + attrs.taDefaultWrap + '><br></' + attrs.taDefaultWrap + '>' :
  					(_browserDetect.ie <= 8)?
  						'<' + attrs.taDefaultWrap.toUpperCase() + '>&nbsp;</' + attrs.taDefaultWrap.toUpperCase() + '>' :
  						'<' + attrs.taDefaultWrap + '>&nbsp;</' + attrs.taDefaultWrap + '>';
  			}
  
  			/* istanbul ignore else */
  			if(!ngModelOptions.$options) ngModelOptions.$options = {}; // ng-model-options support
  
  			var _blankTest = _taBlankTest(_defaultTest);
  
  			var _ensureContentWrapped = function(value){
  				if(_blankTest(value)) return value;
  				var domTest = angular.element("<div>" + value + "</div>");
  				if(domTest.children().length === 0){
  					value = "<" + attrs.taDefaultWrap + ">" + value + "</" + attrs.taDefaultWrap + ">";
  				}else{
  					var _children = domTest[0].childNodes;
  					var i;
  					var _foundBlockElement = false;
  					for(i = 0; i < _children.length; i++){
  						if(_foundBlockElement = _children[i].nodeName.toLowerCase().match(BLOCKELEMENTS)) break;
  					}
  					if(!_foundBlockElement){
  						value = "<" + attrs.taDefaultWrap + ">" + value + "</" + attrs.taDefaultWrap + ">";
  					}else{
  						value = "";
  						for(i = 0; i < _children.length; i++){
  							if(!_children[i].nodeName.toLowerCase().match(BLOCKELEMENTS)){
  								var _subVal = (_children[i].outerHTML || _children[i].nodeValue);
  								/* istanbul ignore else: Doesn't seem to trigger on tests, is tested though */
  								if(_subVal.trim() !== '')
  									value += "<" + attrs.taDefaultWrap + ">" + _subVal + "</" + attrs.taDefaultWrap + ">";
  								else value += _subVal;
  							}else{
  								value += _children[i].outerHTML;
  							}
  						}
  					}
  				}
  				return value;
  			};
  
  			if(attrs.taPaste) _pasteHandler = $parse(attrs.taPaste);
  
  			element.addClass('ta-bind');
  
  			var _undoKeyupTimeout;
  
  			scope['$undoManager' + (attrs.id || '')] = ngModel.$undoManager = {
  				_stack: [],
  				_index: 0,
  				_max: 1000,
  				push: function(value){
  					if((typeof value === "undefined" || value === null) ||
  						((typeof this.current() !== "undefined" && this.current() !== null) && value === this.current())) return value;
  					if(this._index < this._stack.length - 1){
  						this._stack = this._stack.slice(0,this._index+1);
  					}
  					this._stack.push(value);
  					if(_undoKeyupTimeout) $timeout.cancel(_undoKeyupTimeout);
  					if(this._stack.length > this._max) this._stack.shift();
  					this._index = this._stack.length - 1;
  					return value;
  				},
  				undo: function(){
  					return this.setToIndex(this._index-1);
  				},
  				redo: function(){
  					return this.setToIndex(this._index+1);
  				},
  				setToIndex: function(index){
  					if(index < 0 || index > this._stack.length - 1){
  						return undefined;
  					}
  					this._index = index;
  					return this.current();
  				},
  				current: function(){
  					return this._stack[this._index];
  				}
  			};
  
  			var _redoUndoTimeout;
  			var _undo = scope['$undoTaBind' + (attrs.id || '')] = function(){
  				/* istanbul ignore else: can't really test it due to all changes being ignored as well in readonly */
  				if(!_isReadonly && _isContentEditable){
  					var content = ngModel.$undoManager.undo();
  					if(typeof content !== "undefined" && content !== null){
  						_setInnerHTML(content);
  						_setViewValue(content, false);
  						if(_redoUndoTimeout) $timeout.cancel(_redoUndoTimeout);
  						_redoUndoTimeout = $timeout(function(){
  							element[0].focus();
  							taSelection.setSelectionToElementEnd(element[0]);
  						}, 1);
  					}
  				}
  			};
  
  			var _redo = scope['$redoTaBind' + (attrs.id || '')] = function(){
  				/* istanbul ignore else: can't really test it due to all changes being ignored as well in readonly */
  				if(!_isReadonly && _isContentEditable){
  					var content = ngModel.$undoManager.redo();
  					if(typeof content !== "undefined" && content !== null){
  						_setInnerHTML(content);
  						_setViewValue(content, false);
  						/* istanbul ignore next */
  						if(_redoUndoTimeout) $timeout.cancel(_redoUndoTimeout);
  						_redoUndoTimeout = $timeout(function(){
  							element[0].focus();
  							taSelection.setSelectionToElementEnd(element[0]);
  						}, 1);
  					}
  				}
  			};
  
  			// in here we are undoing the converts used elsewhere to prevent the < > and & being displayed when they shouldn't in the code.
  			var _compileHtml = function(){
  				if(_isContentEditable) return element[0].innerHTML;
  				if(_isInputFriendly) return element.val();
  				throw ('textAngular Error: attempting to update non-editable taBind');
  			};
  
  			var _setViewValue = function(_val, triggerUndo, skipRender){
  				_skipRender = skipRender || false;
  				if(typeof triggerUndo === "undefined" || triggerUndo === null) triggerUndo = true && _isContentEditable; // if not contentEditable then the native undo/redo is fine
  				if(typeof _val === "undefined" || _val === null) _val = _compileHtml();
  				if(_blankTest(_val)){
  					// this avoids us from tripping the ng-pristine flag if we click in and out with out typing
  					if(ngModel.$viewValue !== '') ngModel.$setViewValue('');
  					if(triggerUndo && ngModel.$undoManager.current() !== '') ngModel.$undoManager.push('');
  				}else{
  					_reApplyOnSelectorHandlers();
  					if(ngModel.$viewValue !== _val){
  						ngModel.$setViewValue(_val);
  						if(triggerUndo) ngModel.$undoManager.push(_val);
  					}
  				}
  				ngModel.$render();
  			};
  
  			//used for updating when inserting wrapped elements
  			scope['updateTaBind' + (attrs.id || '')] = function(){
  				if(!_isReadonly) _setViewValue(undefined, undefined, true);
  			};
  
  			// catch DOM XSS via taSanitize
  			// Sanitizing both ways is identical
  			var _sanitize = function(unsafe){
  				return (ngModel.$oldViewValue = taSanitize(taFixChrome(unsafe), ngModel.$oldViewValue, _disableSanitizer));
  			};
  
  			// trigger the validation calls
  			if(element.attr('required')) ngModel.$validators.required = function(modelValue, viewValue) {
  				return !_blankTest(modelValue || viewValue);
  			};
  			// parsers trigger from the above keyup function or any other time that the viewValue is updated and parses it for storage in the ngModel
  			ngModel.$parsers.push(_sanitize);
  			ngModel.$parsers.unshift(_ensureContentWrapped);
  			// because textAngular is bi-directional (which is awesome) we need to also sanitize values going in from the server
  			ngModel.$formatters.push(_sanitize);
  			ngModel.$formatters.unshift(_ensureContentWrapped);
  			ngModel.$formatters.unshift(function(value){
  				return ngModel.$undoManager.push(value || '');
  			});
  
  			//this code is used to update the models when data is entered/deleted
  			if(_isInputFriendly){
  				scope.events = {};
  				if(!_isContentEditable){
  					// if a textarea or input just add in change and blur handlers, everything else is done by angulars input directive
  					element.on('change blur', scope.events.change = scope.events.blur = function(){
  						if(!_isReadonly) ngModel.$setViewValue(_compileHtml());
  					});
  
  					element.on('keydown', scope.events.keydown = function(event, eventData){
  						/* istanbul ignore else: this is for catching the jqLite testing*/
  						if(eventData) angular.extend(event, eventData);
  						// Reference to http://stackoverflow.com/questions/6140632/how-to-handle-tab-in-textarea
  						/* istanbul ignore else: otherwise normal functionality */
  						if(event.keyCode === 9){ // tab was pressed
  							// get caret position/selection
  							var start = this.selectionStart;
  							var end = this.selectionEnd;
  
  							var value = element.val();
  							if(event.shiftKey){
  								// find \t
  								var _linebreak = value.lastIndexOf('\n', start), _tab = value.lastIndexOf('\t', start);
  								if(_tab !== -1 && _tab >= _linebreak){
  									// set textarea value to: text before caret + tab + text after caret
  									element.val(value.substring(0, _tab) + value.substring(_tab + 1));
  
  									// put caret at right position again (add one for the tab)
  									this.selectionStart = this.selectionEnd = start - 1;
  								}
  							}else{
  								// set textarea value to: text before caret + tab + text after caret
  								element.val(value.substring(0, start) + "\t" + value.substring(end));
  
  								// put caret at right position again (add one for the tab)
  								this.selectionStart = this.selectionEnd = start + 1;
  							}
  							// prevent the focus lose
  							event.preventDefault();
  						}
  					});
  
  					var _repeat = function(string, n){
  						var result = '';
  						for(var _n = 0; _n < n; _n++) result += string;
  						return result;
  					};
  
  					var recursiveListFormat = function(listNode, tablevel){
  						var _html = '', _children = listNode.childNodes;
  						tablevel++;
  						_html += _repeat('\t', tablevel-1) + listNode.outerHTML.substring(0, listNode.outerHTML.indexOf('<li'));
  						for(var _i = 0; _i < _children.length; _i++){
  							/* istanbul ignore next: browser catch */
  							if(!_children[_i].outerHTML) continue;
  							if(_children[_i].nodeName.toLowerCase() === 'ul' || _children[_i].nodeName.toLowerCase() === 'ol')
  								_html += '\n' + recursiveListFormat(_children[_i], tablevel);
  							else
  								_html += '\n' + _repeat('\t', tablevel) + _children[_i].outerHTML;
  						}
  						_html += '\n' + _repeat('\t', tablevel-1) + listNode.outerHTML.substring(listNode.outerHTML.lastIndexOf('<'));
  						return _html;
  					};
  					ngModel.$formatters.unshift(function(htmlValue){
  						// tabulate the HTML so it looks nicer
  						var _children = angular.element('<div>' + htmlValue + '</div>')[0].childNodes;
  						if(_children.length > 0){
  							htmlValue = '';
  							for(var i = 0; i < _children.length; i++){
  								/* istanbul ignore next: browser catch */
  								if(!_children[i].outerHTML) continue;
  								if(htmlValue.length > 0) htmlValue += '\n';
  								if(_children[i].nodeName.toLowerCase() === 'ul' || _children[i].nodeName.toLowerCase() === 'ol')
  									htmlValue += '' + recursiveListFormat(_children[i], 0);
  								else htmlValue += '' + _children[i].outerHTML;
  							}
  						}
  
  						return htmlValue;
  					});
  				}else{
  					// all the code specific to contenteditable divs
  					var _processingPaste = false;
  					/* istanbul ignore next: phantom js cannot test this for some reason */
  					var processpaste = function(text) {
  						/* istanbul ignore else: don't care if nothing pasted */
  						if(text && text.trim().length){
  							// test paste from word/microsoft product
  							if(text.match(/class=["']*Mso(Normal|List)/i)){
  								var textFragment = text.match(/<!--StartFragment-->([\s\S]*?)<!--EndFragment-->/i);
  								if(!textFragment) textFragment = text;
  								else textFragment = textFragment[1];
  								textFragment = textFragment.replace(/<o:p>[\s\S]*?<\/o:p>/ig, '').replace(/class=(["']|)MsoNormal(["']|)/ig, '');
  								var dom = angular.element("<div>" + textFragment + "</div>");
  								var targetDom = angular.element("<div></div>");
  								var _list = {
  									element: null,
  									lastIndent: [],
  									lastLi: null,
  									isUl: false
  								};
  								_list.lastIndent.peek = function(){
  									var n = this.length;
  									if (n>0) return this[n-1];
  								};
  								var _resetList = function(isUl){
  									_list.isUl = isUl;
  									_list.element = angular.element(isUl ? "<ul>" : "<ol>");
  									_list.lastIndent = [];
  									_list.lastIndent.peek = function(){
  										var n = this.length;
  										if (n>0) return this[n-1];
  									};
  									_list.lastLevelMatch = null;
  								};
  								for(var i = 0; i <= dom[0].childNodes.length; i++){
  									if(!dom[0].childNodes[i] || dom[0].childNodes[i].nodeName === "#text" || dom[0].childNodes[i].tagName.toLowerCase() !== "p") continue;
  									var el = angular.element(dom[0].childNodes[i]);
  									var _listMatch = (el.attr('class') || '').match(/MsoList(Bullet|Number|Paragraph)(CxSp(First|Middle|Last)|)/i);
  
  									if(_listMatch){
  										if(el[0].childNodes.length < 2 || el[0].childNodes[1].childNodes.length < 1){
  											continue;
  										}
  										var isUl = _listMatch[1].toLowerCase() === "bullet" || (_listMatch[1].toLowerCase() !== "number" && !(/^[^0-9a-z<]*[0-9a-z]+[^0-9a-z<>]</i.test(el[0].childNodes[1].innerHTML) || /^[^0-9a-z<]*[0-9a-z]+[^0-9a-z<>]</i.test(el[0].childNodes[1].childNodes[0].innerHTML)));
  										var _indentMatch = (el.attr('style') || '').match(/margin-left:([\-\.0-9]*)/i);
  										var indent = parseFloat((_indentMatch)?_indentMatch[1]:0);
  										var _levelMatch = (el.attr('style') || '').match(/mso-list:l([0-9]+) level([0-9]+) lfo[0-9+]($|;)/i);
  										// prefers the mso-list syntax
  
  										if(_levelMatch && _levelMatch[2]) indent = parseInt(_levelMatch[2]);
  
  										if ((_levelMatch && (!_list.lastLevelMatch || _levelMatch[1] !== _list.lastLevelMatch[1])) || !_listMatch[3] || _listMatch[3].toLowerCase() === "first" || (_list.lastIndent.peek() === null) || (_list.isUl !== isUl && _list.lastIndent.peek() === indent)) {
  											_resetList(isUl);
  											targetDom.append(_list.element);
  										} else if (_list.lastIndent.peek() != null && _list.lastIndent.peek() < indent){
  											_list.element = angular.element(isUl ? "<ul>" : "<ol>");
  											_list.lastLi.append(_list.element);
  										} else if (_list.lastIndent.peek() != null && _list.lastIndent.peek() > indent){
  											while(_list.lastIndent.peek() != null && _list.lastIndent.peek() > indent){
  												if(_list.element.parent()[0].tagName.toLowerCase() === 'li'){
  													_list.element = _list.element.parent();
  													continue;
  												}else if(/[uo]l/i.test(_list.element.parent()[0].tagName.toLowerCase())){
  													_list.element = _list.element.parent();
  												}else{ // else it's it should be a sibling
  													break;
  												}
  												_list.lastIndent.pop();
  											}
  											_list.isUl = _list.element[0].tagName.toLowerCase() === "ul";
  											if (isUl !== _list.isUl) {
  												_resetList(isUl);
  												targetDom.append(_list.element);
  											}
  										}
  
  										_list.lastLevelMatch = _levelMatch;
  										if(indent !== _list.lastIndent.peek()) _list.lastIndent.push(indent);
  										_list.lastLi = angular.element("<li>");
  										_list.element.append(_list.lastLi);
  										_list.lastLi.html(el.html().replace(/<!(--|)\[if !supportLists\](--|)>[\s\S]*?<!(--|)\[endif\](--|)>/ig, ''));
  										el.remove();
  									}else{
  										_resetList(false);
  										targetDom.append(el);
  									}
  								}
  								var _unwrapElement = function(node){
  									node = angular.element(node);
  									for(var _n = node[0].childNodes.length - 1; _n >= 0; _n--) node.after(node[0].childNodes[_n]);
  									node.remove();
  								};
  
  								angular.forEach(targetDom.find('span'), function(node){
  									node.removeAttribute('lang');
  									if(node.attributes.length <= 0) _unwrapElement(node);
  								});
  								angular.forEach(targetDom.find('font'), _unwrapElement);
  								text = targetDom.html();
  							}else{
  								// remove unnecessary chrome insert
  								text = text.replace(/<(|\/)meta[^>]*?>/ig, '');
  								if(text.match(/<[^>]*?(ta-bind)[^>]*?>/)){
  									// entire text-angular or ta-bind has been pasted, REMOVE AT ONCE!!
  									if(text.match(/<[^>]*?(text-angular)[^>]*?>/)){
  										var _el = angular.element("<div>" + text + "</div>");
  										_el.find('textarea').remove();
  										var binds = taDOM.getByAttribute(_el, 'ta-bind');
  										for(var _b = 0; _b < binds.length; _b++){
  											var _target = binds[_b][0].parentNode.parentNode;
  											for(var _c = 0; _c < binds[_b][0].childNodes.length; _c++){
  												_target.parentNode.insertBefore(binds[_b][0].childNodes[_c], _target);
  											}
  											_target.parentNode.removeChild(_target);
  										}
  										text = _el.html().replace('<br class="Apple-interchange-newline">', '');
  									}
  								}else if(text.match(/^<span/)){
  									// in case of pasting only a span - chrome paste, remove them. THis is just some wierd formatting
  									text = text.replace(/<(|\/)span[^>]*?>/ig, '');
  								}
  								// Webkit on Apple tags
  								text = text.replace(/<br class="Apple-interchange-newline"[^>]*?>/ig, '').replace(/<span class="Apple-converted-space">( |&nbsp;)<\/span>/ig, '&nbsp;');
  							}
  
  							if (/<li(\s.*)?>/i.test(text) && /(<ul(\s.*)?>|<ol(\s.*)?>).*<li(\s.*)?>/i.test(text) === false) {
  								// insert missing parent of li element
  								text = text.replace(/<li(\s.*)?>.*<\/li(\s.*)?>/i, '<ul>$&</ul>');
  							}
  														
  							// parse whitespace from plaintext input, starting with preceding spaces that get stripped on paste
  							text = text.replace(/^[ |\u00A0]+/gm, function (match) {
  								var result = '';
  								for (var i = 0; i < match.length; i++) {
  									result += '&nbsp;';
  								}
  								return result;
  							}).replace(/\n|\r\n|\r/g, '<br />').replace(/\t/g, '&nbsp;&nbsp;&nbsp;&nbsp;');
  
  							if(_pasteHandler) text = _pasteHandler(scope, {$html: text}) || text;
  
  							text = taSanitize(text, '', _disableSanitizer);
  
  							taSelection.insertHtml(text, element[0]);
  							$timeout(function(){
  								ngModel.$setViewValue(_compileHtml());
  								_processingPaste = false;
  								element.removeClass('processing-paste');
  							}, 0);
  						}else{
  							_processingPaste = false;
  							element.removeClass('processing-paste');
  						}
  					};
  
  					element.on('paste', scope.events.paste = function(e, eventData){
  						/* istanbul ignore else: this is for catching the jqLite testing*/
  						if(eventData) angular.extend(e, eventData);
  						if(_isReadonly || _processingPaste){
  							e.stopPropagation();
  							e.preventDefault();
  							return false;
  						}
  
  						// Code adapted from http://stackoverflow.com/questions/2176861/javascript-get-clipboard-data-on-paste-event-cross-browser/6804718#6804718
  						_processingPaste = true;
  						element.addClass('processing-paste');
  						var pastedContent;
  						var clipboardData = (e.originalEvent || e).clipboardData;
  						if (clipboardData && clipboardData.getData && clipboardData.types.length > 0) {// Webkit - get data from clipboard, put into editdiv, cleanup, then cancel event
  							var _types = "";
  							for(var _t = 0; _t < clipboardData.types.length; _t++){
  								_types += " " + clipboardData.types[_t];
  							}
  							/* istanbul ignore next: browser tests */
  							if (/text\/html/i.test(_types)) {
  								pastedContent = clipboardData.getData('text/html');
  							} else if (/text\/plain/i.test(_types)) {
  								pastedContent = clipboardData.getData('text/plain');
  							}
  
  							processpaste(pastedContent);
  							e.stopPropagation();
  							e.preventDefault();
  							return false;
  						} else {// Everything else - empty editdiv and allow browser to paste content into it, then cleanup
  							var _savedSelection = $window.rangy.saveSelection(),
  								_tempDiv = angular.element('<div class="ta-hidden-input" contenteditable="true"></div>');
  							$document.find('body').append(_tempDiv);
  							_tempDiv[0].focus();
  							$timeout(function(){
  								// restore selection
  								$window.rangy.restoreSelection(_savedSelection);
  								processpaste(_tempDiv[0].innerHTML);
  								element[0].focus();
  								_tempDiv.remove();
  							}, 0);
  						}
  					});
  					element.on('cut', scope.events.cut = function(e){
  						// timeout to next is needed as otherwise the paste/cut event has not finished actually changing the display
  						if(!_isReadonly) $timeout(function(){
  							ngModel.$setViewValue(_compileHtml());
  						}, 0);
  						else e.preventDefault();
  					});
  
  					element.on('keydown', scope.events.keydown = function(event, eventData){
  						/* istanbul ignore else: this is for catching the jqLite testing*/
  						if(eventData) angular.extend(event, eventData);
  						event.specialKey = _mapKeys(event);
  						var userSpecialKey;
  						/* istanbul ignore next: difficult to test */
  						taOptions.keyMappings.forEach(function (mapping) {
  							if (event.specialKey === mapping.commandKeyCode) {
  								// taOptions has remapped this binding... so
  								// we disable our own
  								event.specialKey = undefined;
  							}
  							if (mapping.testForKey(event)) {
  								userSpecialKey = mapping.commandKeyCode;
  							}
  							if ((mapping.commandKeyCode === 'UndoKey') || (mapping.commandKeyCode === 'RedoKey')) {
  								// this is necessary to fully stop the propagation.
  								if (!mapping.enablePropagation) {
  									event.preventDefault();
  								}
  							}
  						});
  						/* istanbul ignore next: difficult to test */
  						if (typeof userSpecialKey !== 'undefined') {
  							event.specialKey = userSpecialKey;
  						}
  						/* istanbul ignore next: difficult to test as can't seem to select */
  						if ((typeof event.specialKey !== 'undefined') && (
  								event.specialKey !== 'UndoKey' || event.specialKey !== 'RedoKey'
  							)) {
  							event.preventDefault();
  							textAngularManager.sendKeyCommand(scope, event);
  						}
  						/* istanbul ignore else: readonly check */
  						if(!_isReadonly){
  							if (event.specialKey==='UndoKey') {
  								_undo();
  								event.preventDefault();
  							}
  							if (event.specialKey==='RedoKey') {
  								_redo();
  								event.preventDefault();
  							}
  							/* istanbul ignore next: difficult to test as can't seem to select */
  							if(event.keyCode === 13 && !event.shiftKey){
  								var $selection;
  								var selection = taSelection.getSelectionElement();
  								if(!selection.tagName.match(VALIDELEMENTS)) return;
  								var _new = angular.element(_defaultVal);
  								if (/^<br(|\/)>$/i.test(selection.innerHTML.trim()) && selection.parentNode.tagName.toLowerCase() === 'blockquote' && !selection.nextSibling) {
  									// if last element in blockquote and element is blank, pull element outside of blockquote.
  									$selection = angular.element(selection);
  									var _parent = $selection.parent();
  									_parent.after(_new);
  									$selection.remove();
  									if(_parent.children().length === 0) _parent.remove();
  									taSelection.setSelectionToElementStart(_new[0]);
  									event.preventDefault();
  								}else if (/^<[^>]+><br(|\/)><\/[^>]+>$/i.test(selection.innerHTML.trim()) && selection.tagName.toLowerCase() === 'blockquote'){
  									$selection = angular.element(selection);
  									$selection.after(_new);
  									$selection.remove();
  									taSelection.setSelectionToElementStart(_new[0]);
  									event.preventDefault();
  								}
  							}
  						}
  					});
  					var _keyupTimeout;
  					element.on('keyup', scope.events.keyup = function(event, eventData){
  						/* istanbul ignore else: this is for catching the jqLite testing*/
  						if(eventData) angular.extend(event, eventData);
  						/* istanbul ignore next: FF specific bug fix */
  						if (event.keyCode === 9) {
  							var _selection = taSelection.getSelection();
  							if(_selection.start.element === element[0] && element.children().length) taSelection.setSelectionToElementStart(element.children()[0]);
  							return;
  						}
  						if(_undoKeyupTimeout) $timeout.cancel(_undoKeyupTimeout);
  						if(!_isReadonly && !BLOCKED_KEYS.test(event.keyCode)){
  							// if enter - insert new taDefaultWrap, if shift+enter insert <br/>
  							if(_defaultVal !== '' && event.keyCode === 13){
  								if(!event.shiftKey){
  									// new paragraph, br should be caught correctly
  									var selection = taSelection.getSelectionElement();
  									while(!selection.tagName.match(VALIDELEMENTS) && selection !== element[0]){
  										selection = selection.parentNode;
  									}
  
  									if(selection.tagName.toLowerCase() !== attrs.taDefaultWrap && selection.tagName.toLowerCase() !== 'li' && (selection.innerHTML.trim() === '' || selection.innerHTML.trim() === '<br>')){
  										var _new = angular.element(_defaultVal);
  										angular.element(selection).replaceWith(_new);
  										taSelection.setSelectionToElementStart(_new[0]);
  									}
  								}
  							}
  							var val = _compileHtml();
  							if(_defaultVal !== '' && val.trim() === ''){
  								_setInnerHTML(_defaultVal);
  								taSelection.setSelectionToElementStart(element.children()[0]);
  							}else if(val.substring(0, 1) !== '<' && attrs.taDefaultWrap !== ''){
  								var _savedSelection = $window.rangy.saveSelection();
  								val = _compileHtml();
  								val = "<" + attrs.taDefaultWrap + ">" + val + "</" + attrs.taDefaultWrap + ">";
  								_setInnerHTML(val);
  								$window.rangy.restoreSelection(_savedSelection);
  							}
  							var triggerUndo = _lastKey !== event.keyCode && UNDO_TRIGGER_KEYS.test(event.keyCode);
  							if(_keyupTimeout) $timeout.cancel(_keyupTimeout);
  							_keyupTimeout = $timeout(function() {
  								_setViewValue(val, triggerUndo, true);
  							}, ngModelOptions.$options.debounce || 400);
  							if(!triggerUndo) _undoKeyupTimeout = $timeout(function(){ ngModel.$undoManager.push(val); }, 250);
  							_lastKey = event.keyCode;
  						}
  					});
  
  					element.on('blur', scope.events.blur = function(){
  						_focussed = false;
  						/* istanbul ignore else: if readonly don't update model */
  						if(!_isReadonly){
  							_setViewValue(undefined, undefined, true);
  						}else{
  							_skipRender = true; // don't redo the whole thing, just check the placeholder logic
  							ngModel.$render();
  						}
  					});
  
  					// Placeholders not supported on ie 8 and below
  					if(attrs.placeholder && (_browserDetect.ie > 8 || _browserDetect.ie === undefined)){
  						var rule;
  						if(attrs.id) rule = addCSSRule('#' + attrs.id + '.placeholder-text:before', 'content: "' + attrs.placeholder + '"');
  						else throw('textAngular Error: An unique ID is required for placeholders to work');
  
  						scope.$on('$destroy', function(){
  							removeCSSRule(rule);
  						});
  					}
  
  					element.on('focus', scope.events.focus = function(){
  						_focussed = true;
  						element.removeClass('placeholder-text');
  						_reApplyOnSelectorHandlers();
  					});
  
  					element.on('mouseup', scope.events.mouseup = function(){
  						var _selection = taSelection.getSelection();
  						if(_selection.start.element === element[0] && element.children().length) taSelection.setSelectionToElementStart(element.children()[0]);
  					});
  
  					// prevent propagation on mousedown in editor, see #206
  					element.on('mousedown', scope.events.mousedown = function(event, eventData){
  						/* istanbul ignore else: this is for catching the jqLite testing*/
  						if(eventData) angular.extend(event, eventData);
  						event.stopPropagation();
  					});
  				}
  			}
  
  			var selectorClickHandler = function(event){
  				// emit the element-select event, pass the element
  				scope.$emit('ta-element-select', this);
  				event.preventDefault();
  				return false;
  			};
  			var fileDropHandler = function(event, eventData){
  				/* istanbul ignore else: this is for catching the jqLite testing*/
  				if(eventData) angular.extend(event, eventData);
  				// emit the drop event, pass the element, preventing should be done elsewhere
  				if(!dropFired && !_isReadonly){
  					dropFired = true;
  					var dataTransfer;
  					if(event.originalEvent) dataTransfer = event.originalEvent.dataTransfer;
  					else dataTransfer = event.dataTransfer;
  					scope.$emit('ta-drop-event', this, event, dataTransfer);
  					$timeout(function(){
  						dropFired = false;
  						_setViewValue(undefined, undefined, true);
  					}, 100);
  				}
  			};
  
  			//used for updating when inserting wrapped elements
  			var _reApplyOnSelectorHandlers = scope['reApplyOnSelectorHandlers' + (attrs.id || '')] = function(){
  				/* istanbul ignore else */
  				if(!_isReadonly) angular.forEach(taSelectableElements, function(selector){
  						// check we don't apply the handler twice
  						element.find(selector)
  							.off('click', selectorClickHandler)
  							.on('click', selectorClickHandler);
  					});
  			};
  
  			var _setInnerHTML = function(newval){
  				element[0].innerHTML = newval;
  			};
  			var _renderTimeout;
  			var _renderInProgress = false;
  			// changes to the model variable from outside the html/text inputs
  			ngModel.$render = function(){
  				/* istanbul ignore if: Catches rogue renders, hard to replicate in tests */
  				if(_renderInProgress) return;
  				else _renderInProgress = true;
  				// catch model being null or undefined
  				var val = ngModel.$viewValue || '';
  				// if the editor isn't focused it needs to be updated, otherwise it's receiving user input
  				if(!_skipRender){
  					/* istanbul ignore else: in other cases we don't care */
  					if(_isContentEditable && _focussed){
  						// update while focussed
  						element.removeClass('placeholder-text');
  						if(_renderTimeout) $timeout.cancel(_renderTimeout);
  						_renderTimeout = $timeout(function(){
  							/* istanbul ignore if: Can't be bothered testing this... */
  							if(!_focussed){
  								element[0].focus();
  								taSelection.setSelectionToElementEnd(element.children()[element.children().length - 1]);
  							}
  							_renderTimeout = undefined;
  						}, 1);
  					}
  					if(_isContentEditable){
  						// WYSIWYG Mode
  						if(attrs.placeholder){
  							if(val === ''){
  								// blank
  								_setInnerHTML(_defaultVal);
  							}else{
  								// not-blank
  								_setInnerHTML(val);
  							}
  						}else{
  							_setInnerHTML((val === '') ? _defaultVal : val);
  						}
  						// if in WYSIWYG and readOnly we kill the use of links by clicking
  						if(!_isReadonly){
  							_reApplyOnSelectorHandlers();
  							element.on('drop', fileDropHandler);
  						}else{
  							element.off('drop', fileDropHandler);
  						}
  					}else if(element[0].tagName.toLowerCase() !== 'textarea' && element[0].tagName.toLowerCase() !== 'input'){
  						// make sure the end user can SEE the html code as a display. This is a read-only display element
  						_setInnerHTML(taApplyCustomRenderers(val));
  					}else{
  						// only for input and textarea inputs
  						element.val(val);
  					}
  				}
  				if(_isContentEditable && attrs.placeholder){
  					if(val === ''){
  						if(_focussed) element.removeClass('placeholder-text');
  						else element.addClass('placeholder-text');
  					}else{
  						element.removeClass('placeholder-text');
  					}
  				}
  				_renderInProgress = _skipRender = false;
  			};
  
  			if(attrs.taReadonly){
  				//set initial value
  				_isReadonly = scope.$eval(attrs.taReadonly);
  				if(_isReadonly){
  					element.addClass('ta-readonly');
  					// we changed to readOnly mode (taReadonly='true')
  					if(element[0].tagName.toLowerCase() === 'textarea' || element[0].tagName.toLowerCase() === 'input'){
  						element.attr('disabled', 'disabled');
  					}
  					if(element.attr('contenteditable') !== undefined && element.attr('contenteditable')){
  						element.removeAttr('contenteditable');
  					}
  				}else{
  					element.removeClass('ta-readonly');
  					// we changed to NOT readOnly mode (taReadonly='false')
  					if(element[0].tagName.toLowerCase() === 'textarea' || element[0].tagName.toLowerCase() === 'input'){
  						element.removeAttr('disabled');
  					}else if(_isContentEditable){
  						element.attr('contenteditable', 'true');
  					}
  				}
  				// taReadonly only has an effect if the taBind element is an input or textarea or has contenteditable='true' on it.
  				// Otherwise it is readonly by default
  				scope.$watch(attrs.taReadonly, function(newVal, oldVal){
  					if(oldVal === newVal) return;
  					if(newVal){
  						element.addClass('ta-readonly');
  						// we changed to readOnly mode (taReadonly='true')
  						if(element[0].tagName.toLowerCase() === 'textarea' || element[0].tagName.toLowerCase() === 'input'){
  							element.attr('disabled', 'disabled');
  						}
  						if(element.attr('contenteditable') !== undefined && element.attr('contenteditable')){
  							element.removeAttr('contenteditable');
  						}
  						// turn ON selector click handlers
  						angular.forEach(taSelectableElements, function(selector){
  							element.find(selector).on('click', selectorClickHandler);
  						});
  						element.off('drop', fileDropHandler);
  					}else{
  						element.removeClass('ta-readonly');
  						// we changed to NOT readOnly mode (taReadonly='false')
  						if(element[0].tagName.toLowerCase() === 'textarea' || element[0].tagName.toLowerCase() === 'input'){
  							element.removeAttr('disabled');
  						}else if(_isContentEditable){
  							element.attr('contenteditable', 'true');
  						}
  						// remove the selector click handlers
  						angular.forEach(taSelectableElements, function(selector){
  							element.find(selector).off('click', selectorClickHandler);
  						});
  						element.on('drop', fileDropHandler);
  					}
  					_isReadonly = newVal;
  				});
  			}
  
  			// Initialise the selectableElements
  			// if in WYSIWYG and readOnly we kill the use of links by clicking
  			if(_isContentEditable && !_isReadonly){
  				angular.forEach(taSelectableElements, function(selector){
  					element.find(selector).on('click', selectorClickHandler);
  				});
  				element.on('drop', fileDropHandler);
  				element.on('blur', function(){
  					/* istanbul ignore next: webkit fix */
  					if(_browserDetect.webkit) { // detect webkit
  						globalContentEditableBlur = true;
  					}
  				});
  			}
  		}
  	};
  }]);
  
  // this global var is used to prevent multiple fires of the drop event. Needs to be global to the textAngular file.
  var dropFired = false;
  var textAngular = angular.module("textAngular", ['ngSanitize', 'textAngularSetup', 'textAngular.factories', 'textAngular.DOM', 'textAngular.validators', 'textAngular.taBind']); //This makes ngSanitize required
  
  // setup the global contstant functions for setting up the toolbar
  
  // all tool definitions
  var taTools = {};
  /*
  	A tool definition is an object with the following key/value parameters:
  		action: [function(deferred, restoreSelection)]
  				a function that is executed on clicking on the button - this will allways be executed using ng-click and will
  				overwrite any ng-click value in the display attribute.
  				The function is passed a deferred object ($q.defer()), if this is wanted to be used `return false;` from the action and
  				manually call `deferred.resolve();` elsewhere to notify the editor that the action has finished.
  				restoreSelection is only defined if the rangy library is included and it can be called as `restoreSelection()` to restore the users
  				selection in the WYSIWYG editor.
  		display: [string]?
  				Optional, an HTML element to be displayed as the button. The `scope` of the button is the tool definition object with some additional functions
  				If set this will cause buttontext and iconclass to be ignored
  		class: [string]?
  				Optional, if set will override the taOptions.classes.toolbarButton class.
  		buttontext: [string]?
  				if this is defined it will replace the contents of the element contained in the `display` element
  		iconclass: [string]?
  				if this is defined an icon (<i>) will be appended to the `display` element with this string as it's class
  		tooltiptext: [string]?
  				Optional, a plain text description of the action, used for the title attribute of the action button in the toolbar by default.
  		activestate: [function(commonElement)]?
  				this function is called on every caret movement, if it returns true then the class taOptions.classes.toolbarButtonActive
  				will be applied to the `display` element, else the class will be removed
  		disabled: [function()]?
  				if this function returns true then the tool will have the class taOptions.classes.disabled applied to it, else it will be removed
  	Other functions available on the scope are:
  		name: [string]
  				the name of the tool, this is the first parameter passed into taRegisterTool
  		isDisabled: [function()]
  				returns true if the tool is disabled, false if it isn't
  		displayActiveToolClass: [function(boolean)]
  				returns true if the tool is 'active' in the currently focussed toolbar
  		onElementSelect: [Object]
  				This object contains the following key/value pairs and is used to trigger the ta-element-select event
  				element: [String]
  					an element name, will only trigger the onElementSelect action if the tagName of the element matches this string
  				filter: [function(element)]?
  					an optional filter that returns a boolean, if true it will trigger the onElementSelect.
  				action: [function(event, element, editorScope)]
  					the action that should be executed if the onElementSelect function runs
  */
  // name and toolDefinition to add into the tools available to be added on the toolbar
  function registerTextAngularTool(name, toolDefinition){
  	if(!name || name === '' || taTools.hasOwnProperty(name)) throw('textAngular Error: A unique name is required for a Tool Definition');
  	if(
  		(toolDefinition.display && (toolDefinition.display === '' || !validElementString(toolDefinition.display))) ||
  		(!toolDefinition.display && !toolDefinition.buttontext && !toolDefinition.iconclass)
  	)
  		throw('textAngular Error: Tool Definition for "' + name + '" does not have a valid display/iconclass/buttontext value');
  	taTools[name] = toolDefinition;
  }
  
  textAngular.constant('taRegisterTool', registerTextAngularTool);
  textAngular.value('taTools', taTools);
  
  textAngular.config([function(){
  	// clear taTools variable. Just catches testing and any other time that this config may run multiple times...
  	angular.forEach(taTools, function(value, key){ delete taTools[key];	});
  }]);
  
  textAngular.run([function(){
  	/* istanbul ignore next: not sure how to test this */
  	// Require Rangy and rangy savedSelection module.
  	if (typeof define === 'function' && define.amd) {
  		// AMD. Register as an anonymous module.
  		define(function(require) {
  			window.rangy = require('rangy');
  			window.rangy.saveSelection = require('rangy/lib/rangy-selectionsaverestore');
  		});
  	} else if (typeof require ==='function' && typeof module !== 'undefined' && typeof exports === 'object') {
  		// Node/CommonJS style
  		window.rangy = require('rangy');
  		window.rangy.saveSelection = require('rangy/lib/rangy-selectionsaverestore');
  	} else {
  		// Ensure that rangy and rangy.saveSelection exists on the window (global scope).
  		// TODO: Refactor so that the global scope is no longer used.
  		if(!window.rangy){
  			throw("rangy-core.js and rangy-selectionsaverestore.js are required for textAngular to work correctly, rangy-core is not yet loaded.");
  		}else{
  			window.rangy.init();
  			if(!window.rangy.saveSelection){
  				throw("rangy-selectionsaverestore.js is required for textAngular to work correctly.");
  			}
  		}
  	}
  }]);
  
  textAngular.directive("textAngular", [
  	'$compile', '$timeout', 'taOptions', 'taSelection', 'taExecCommand',
  	'textAngularManager', '$window', '$document', '$animate', '$log', '$q', '$parse',
  	function($compile, $timeout, taOptions, taSelection, taExecCommand,
  		textAngularManager, $window, $document, $animate, $log, $q, $parse){
  		return {
  			require: '?ngModel',
  			scope: {},
  			restrict: "EA",
  			priority: 2, // So we override validators correctly
  			link: function(scope, element, attrs, ngModel){
  				// all these vars should not be accessable outside this directive
  				var _keydown, _keyup, _keypress, _mouseup, _focusin, _focusout,
  					_originalContents, _toolbars,
  					_serial = (attrs.serial) ? attrs.serial : Math.floor(Math.random() * 10000000000000000),
  					_taExecCommand, _resizeMouseDown, _updateSelectedStylesTimeout;
  
  				scope._name = (attrs.name) ? attrs.name : 'textAngularEditor' + _serial;
  
  				var oneEvent = function(_element, event, action){
  					$timeout(function(){
  						// shim the .one till fixed
  						var _func = function(){
  							_element.off(event, _func);
  							action.apply(this, arguments);
  						};
  						_element.on(event, _func);
  					}, 100);
  				};
  				_taExecCommand = taExecCommand(attrs.taDefaultWrap);
  				// get the settings from the defaults and add our specific functions that need to be on the scope
  				angular.extend(scope, angular.copy(taOptions), {
  					// wraps the selection in the provided tag / execCommand function. Should only be called in WYSIWYG mode.
  					wrapSelection: function(command, opt, isSelectableElementTool){
  						if(command.toLowerCase() === "undo"){
  							scope['$undoTaBindtaTextElement' + _serial]();
  						}else if(command.toLowerCase() === "redo"){
  							scope['$redoTaBindtaTextElement' + _serial]();
  						}else{
  							// catch errors like FF erroring when you try to force an undo with nothing done
  							_taExecCommand(command, false, opt, scope.defaultTagAttributes);
  							if(isSelectableElementTool){
  								// re-apply the selectable tool events
  								scope['reApplyOnSelectorHandlerstaTextElement' + _serial]();
  							}
  							// refocus on the shown display element, this fixes a display bug when using :focus styles to outline the box.
  							// You still have focus on the text/html input it just doesn't show up
  							scope.displayElements.text[0].focus();
  						}
  					},
  					showHtml: scope.$eval(attrs.taShowHtml) || false
  				});
  				// setup the options from the optional attributes
  				if(attrs.taFocussedClass)			scope.classes.focussed = attrs.taFocussedClass;
  				if(attrs.taTextEditorClass)			scope.classes.textEditor = attrs.taTextEditorClass;
  				if(attrs.taHtmlEditorClass)			scope.classes.htmlEditor = attrs.taHtmlEditorClass;
  				if(attrs.taDefaultTagAttributes){
  					try	{
  						//	TODO: This should use angular.merge to enhance functionality once angular 1.4 is required
  						angular.extend(scope.defaultTagAttributes, angular.fromJson(attrs.taDefaultTagAttributes));
  					} catch (error) {
  						$log.error(error);
  					}
  				}
  				// optional setup functions
  				if(attrs.taTextEditorSetup)			scope.setup.textEditorSetup = scope.$parent.$eval(attrs.taTextEditorSetup);
  				if(attrs.taHtmlEditorSetup)			scope.setup.htmlEditorSetup = scope.$parent.$eval(attrs.taHtmlEditorSetup);
  				// optional fileDropHandler function
  				if(attrs.taFileDrop)				scope.fileDropHandler = scope.$parent.$eval(attrs.taFileDrop);
  				else								scope.fileDropHandler = scope.defaultFileDropHandler;
  
  				_originalContents = element[0].innerHTML;
  				// clear the original content
  				element[0].innerHTML = '';
  
  				// Setup the HTML elements as variable references for use later
  				scope.displayElements = {
  					// we still need the hidden input even with a textarea as the textarea may have invalid/old input in it,
  					// wheras the input will ALLWAYS have the correct value.
  					forminput: angular.element("<input type='hidden' tabindex='-1' style='display: none;'>"),
  					html: angular.element("<textarea></textarea>"),
  					text: angular.element("<div></div>"),
  					// other toolbased elements
  					scrollWindow: angular.element("<div class='ta-scroll-window'></div>"),
  					popover: angular.element('<div class="popover fade bottom" style="max-width: none; width: 305px;"></div>'),
  					popoverArrow: angular.element('<div class="arrow"></div>'),
  					popoverContainer: angular.element('<div class="popover-content"></div>'),
  					resize: {
  						overlay: angular.element('<div class="ta-resizer-handle-overlay"></div>'),
  						background: angular.element('<div class="ta-resizer-handle-background"></div>'),
  						anchors: [
  							angular.element('<div class="ta-resizer-handle-corner ta-resizer-handle-corner-tl"></div>'),
  							angular.element('<div class="ta-resizer-handle-corner ta-resizer-handle-corner-tr"></div>'),
  							angular.element('<div class="ta-resizer-handle-corner ta-resizer-handle-corner-bl"></div>'),
  							angular.element('<div class="ta-resizer-handle-corner ta-resizer-handle-corner-br"></div>')
  						],
  						info: angular.element('<div class="ta-resizer-handle-info"></div>')
  					}
  				};
  
  				// Setup the popover
  				scope.displayElements.popover.append(scope.displayElements.popoverArrow);
  				scope.displayElements.popover.append(scope.displayElements.popoverContainer);
  				scope.displayElements.scrollWindow.append(scope.displayElements.popover);
  
  				scope.displayElements.popover.on('mousedown', function(e, eventData){
  					/* istanbul ignore else: this is for catching the jqLite testing*/
  					if(eventData) angular.extend(e, eventData);
  					// this prevents focusout from firing on the editor when clicking anything in the popover
  					e.preventDefault();
  					return false;
  				});
  
  				// define the popover show and hide functions
  				scope.showPopover = function(_el){
  					scope.displayElements.popover.css('display', 'block');
  					scope.reflowPopover(_el);
  					$animate.addClass(scope.displayElements.popover, 'in');
  					oneEvent($document.find('body'), 'click keyup', function(){scope.hidePopover();});
  				};
  				scope.reflowPopover = function(_el){
  					/* istanbul ignore if: catches only if near bottom of editor */
  					if(scope.displayElements.text[0].offsetHeight - 51 > _el[0].offsetTop){
  						scope.displayElements.popover.css('top', _el[0].offsetTop + _el[0].offsetHeight + scope.displayElements.scrollWindow[0].scrollTop + 'px');
  						scope.displayElements.popover.removeClass('top').addClass('bottom');
  					}else{
  						scope.displayElements.popover.css('top', _el[0].offsetTop - 54 + scope.displayElements.scrollWindow[0].scrollTop + 'px');
  						scope.displayElements.popover.removeClass('bottom').addClass('top');
  					}
  					var _maxLeft = scope.displayElements.text[0].offsetWidth - scope.displayElements.popover[0].offsetWidth;
  					var _targetLeft = _el[0].offsetLeft + (_el[0].offsetWidth / 2.0) - (scope.displayElements.popover[0].offsetWidth / 2.0);
  					scope.displayElements.popover.css('left', Math.max(0, Math.min(_maxLeft, _targetLeft)) + 'px');
  					scope.displayElements.popoverArrow.css('margin-left', (Math.min(_targetLeft, (Math.max(0, _targetLeft - _maxLeft))) - 11) + 'px');
  				};
  				scope.hidePopover = function(){
  					scope.displayElements.popover.css('display', '');
  					scope.displayElements.popoverContainer.attr('style', '');
  					scope.displayElements.popoverContainer.attr('class', 'popover-content');
  					scope.displayElements.popover.removeClass('in');
  				};
  
  				// setup the resize overlay
  				scope.displayElements.resize.overlay.append(scope.displayElements.resize.background);
  				angular.forEach(scope.displayElements.resize.anchors, function(anchor){ scope.displayElements.resize.overlay.append(anchor);});
  				scope.displayElements.resize.overlay.append(scope.displayElements.resize.info);
  				scope.displayElements.scrollWindow.append(scope.displayElements.resize.overlay);
  
  				// define the show and hide events
  				scope.reflowResizeOverlay = function(_el){
  					_el = angular.element(_el)[0];
  					scope.displayElements.resize.overlay.css({
  						'display': 'block',
  						'left': _el.offsetLeft - 5 + 'px',
  						'top': _el.offsetTop - 5 + 'px',
  						'width': _el.offsetWidth + 10 + 'px',
  						'height': _el.offsetHeight + 10 + 'px'
  					});
  					scope.displayElements.resize.info.text(_el.offsetWidth + ' x ' + _el.offsetHeight);
  				};
  				/* istanbul ignore next: pretty sure phantomjs won't test this */
  				scope.showResizeOverlay = function(_el){
  					var _body = $document.find('body');
  					_resizeMouseDown = function(event){
  						var startPosition = {
  							width: parseInt(_el.attr('width')),
  							height: parseInt(_el.attr('height')),
  							x: event.clientX,
  							y: event.clientY
  						};
  						if(startPosition.width === undefined || isNaN(startPosition.width)) startPosition.width = _el[0].offsetWidth;
  						if(startPosition.height === undefined || isNaN(startPosition.height)) startPosition.height = _el[0].offsetHeight;
  						scope.hidePopover();
  						var ratio = startPosition.height / startPosition.width;
  						var mousemove = function(event){
  							// calculate new size
  							var pos = {
  								x: Math.max(0, startPosition.width + (event.clientX - startPosition.x)),
  								y: Math.max(0, startPosition.height + (event.clientY - startPosition.y))
  							};
  
  							// DEFAULT: the aspect ratio is not locked unless the Shift key is pressed.
  							//
  							// attribute: ta-resize-force-aspect-ratio -- locks resize into maintaing the aspect ratio
  							var bForceAspectRatio = (attrs.taResizeForceAspectRatio !== undefined);
  							// attribute: ta-resize-maintain-aspect-ratio=true causes the space ratio to remain locked
  							// unless the Shift key is pressed
  							var bFlipKeyBinding = attrs.taResizeMaintainAspectRatio;
  							var bKeepRatio =  bForceAspectRatio || (bFlipKeyBinding && !event.shiftKey);
  							if(bKeepRatio) {
  								var newRatio = pos.y / pos.x;
  								pos.x = ratio > newRatio ? pos.x : pos.y / ratio;
  								pos.y = ratio > newRatio ? pos.x * ratio : pos.y;
  							}
  							var el = angular.element(_el);
  							function roundedMaxVal(val) {
  								return Math.round(Math.max(0, val));
  							}
  							el.css('height', roundedMaxVal(pos.y) + 'px');
  							el.css('width', roundedMaxVal(pos.x) + 'px');
  
  							// reflow the popover tooltip
  							scope.reflowResizeOverlay(_el);
  						};
  						_body.on('mousemove', mousemove);
  						oneEvent(_body, 'mouseup', function(event){
  							event.preventDefault();
  							event.stopPropagation();
  							_body.off('mousemove', mousemove);
  							scope.showPopover(_el);
  						});
  						event.stopPropagation();
  						event.preventDefault();
  					};
  
  					scope.displayElements.resize.anchors[3].off('mousedown');
  					scope.displayElements.resize.anchors[3].on('mousedown', _resizeMouseDown);
  
  					scope.reflowResizeOverlay(_el);
  					oneEvent(_body, 'click', function(){scope.hideResizeOverlay();});
  				};
  				/* istanbul ignore next: pretty sure phantomjs won't test this */
  				scope.hideResizeOverlay = function(){
  					scope.displayElements.resize.anchors[3].off('mousedown', _resizeMouseDown);
  					scope.displayElements.resize.overlay.css('display', '');
  				};
  
  				// allow for insertion of custom directives on the textarea and div
  				scope.setup.htmlEditorSetup(scope.displayElements.html);
  				scope.setup.textEditorSetup(scope.displayElements.text);
  				scope.displayElements.html.attr({
  					'id': 'taHtmlElement' + _serial,
  					'ng-show': 'showHtml',
  					'ta-bind': 'ta-bind',
  					'ng-model': 'html',
  					'ng-model-options': element.attr('ng-model-options')
  				});
  				scope.displayElements.text.attr({
  					'id': 'taTextElement' + _serial,
  					'contentEditable': 'true',
  					'ta-bind': 'ta-bind',
  					'ng-model': 'html',
  					'ng-model-options': element.attr('ng-model-options')
  				});
  				scope.displayElements.scrollWindow.attr({'ng-hide': 'showHtml'});
  				if(attrs.taDefaultWrap) scope.displayElements.text.attr('ta-default-wrap', attrs.taDefaultWrap);
  
  				if(attrs.taUnsafeSanitizer){
  					scope.displayElements.text.attr('ta-unsafe-sanitizer', attrs.taUnsafeSanitizer);
  					scope.displayElements.html.attr('ta-unsafe-sanitizer', attrs.taUnsafeSanitizer);
  				}
  
  				// add the main elements to the origional element
  				scope.displayElements.scrollWindow.append(scope.displayElements.text);
  				element.append(scope.displayElements.scrollWindow);
  				element.append(scope.displayElements.html);
  
  				scope.displayElements.forminput.attr('name', scope._name);
  				element.append(scope.displayElements.forminput);
  
  				if(attrs.tabindex){
  					element.removeAttr('tabindex');
  					scope.displayElements.text.attr('tabindex', attrs.tabindex);
  					scope.displayElements.html.attr('tabindex', attrs.tabindex);
  				}
  
  				if (attrs.placeholder) {
  					scope.displayElements.text.attr('placeholder', attrs.placeholder);
  					scope.displayElements.html.attr('placeholder', attrs.placeholder);
  				}
  
  				if(attrs.taDisabled){
  					scope.displayElements.text.attr('ta-readonly', 'disabled');
  					scope.displayElements.html.attr('ta-readonly', 'disabled');
  					scope.disabled = scope.$parent.$eval(attrs.taDisabled);
  					scope.$parent.$watch(attrs.taDisabled, function(newVal){
  						scope.disabled = newVal;
  						if(scope.disabled){
  							element.addClass(scope.classes.disabled);
  						}else{
  							element.removeClass(scope.classes.disabled);
  						}
  					});
  				}
  
  				if(attrs.taPaste){
  					scope._pasteHandler = function(_html){
  						return $parse(attrs.taPaste)(scope.$parent, {$html: _html});
  					};
  					scope.displayElements.text.attr('ta-paste', '_pasteHandler($html)');
  				}
  
  				// compile the scope with the text and html elements only - if we do this with the main element it causes a compile loop
  				$compile(scope.displayElements.scrollWindow)(scope);
  				$compile(scope.displayElements.html)(scope);
  
  				scope.updateTaBindtaTextElement = scope['updateTaBindtaTextElement' + _serial];
  				scope.updateTaBindtaHtmlElement = scope['updateTaBindtaHtmlElement' + _serial];
  
  				// add the classes manually last
  				element.addClass("ta-root");
  				scope.displayElements.scrollWindow.addClass("ta-text ta-editor " + scope.classes.textEditor);
  				scope.displayElements.html.addClass("ta-html ta-editor " + scope.classes.htmlEditor);
  
  				// used in the toolbar actions
  				scope._actionRunning = false;
  				var _savedSelection = false;
  				scope.startAction = function(){
  					scope._actionRunning = true;
  					// if rangy library is loaded return a function to reload the current selection
  					_savedSelection = $window.rangy.saveSelection();
  					return function(){
  						if(_savedSelection) $window.rangy.restoreSelection(_savedSelection);
  					};
  				};
  				scope.endAction = function(){
  					scope._actionRunning = false;
  					if(_savedSelection){
  						if(scope.showHtml){
  							scope.displayElements.html[0].focus();
  						}else{
  							scope.displayElements.text[0].focus();
  						}
  						// $window.rangy.restoreSelection(_savedSelection);
  						$window.rangy.removeMarkers(_savedSelection);
  					}
  					_savedSelection = false;
  					scope.updateSelectedStyles();
  					// only update if in text or WYSIWYG mode
  					if(!scope.showHtml) scope['updateTaBindtaTextElement' + _serial]();
  				};
  
  				// note that focusout > focusin is called everytime we click a button - except bad support: http://www.quirksmode.org/dom/events/blurfocus.html
  				// cascades to displayElements.text and displayElements.html automatically.
  				_focusin = function(){
  					scope.focussed = true;
  					element.addClass(scope.classes.focussed);
  					_toolbars.focus();
  					element.triggerHandler('focus');
  				};
  				scope.displayElements.html.on('focus', _focusin);
  				scope.displayElements.text.on('focus', _focusin);
  				_focusout = function(e){
  					// if we are NOT runnig an action and have NOT focussed again on the text etc then fire the blur events
  					if(!scope._actionRunning && $document[0].activeElement !== scope.displayElements.html[0] && $document[0].activeElement !== scope.displayElements.text[0]){
  						element.removeClass(scope.classes.focussed);
  						_toolbars.unfocus();
  						// to prevent multiple apply error defer to next seems to work.
  						$timeout(function(){
  							scope._bUpdateSelectedStyles = false;
  							element.triggerHandler('blur');
  							scope.focussed = false;
  						}, 0);
  					}
  					e.preventDefault();
  					return false;
  				};
  				scope.displayElements.html.on('blur', _focusout);
  				scope.displayElements.text.on('blur', _focusout);
  
  				scope.displayElements.text.on('paste', function(event){
  					element.triggerHandler('paste', event);
  				});
  
  				// Setup the default toolbar tools, this way allows the user to add new tools like plugins.
  				// This is on the editor for future proofing if we find a better way to do this.
  				scope.queryFormatBlockState = function(command){
  					// $document[0].queryCommandValue('formatBlock') errors in Firefox if we call this when focussed on the textarea
  					return !scope.showHtml && command.toLowerCase() === $document[0].queryCommandValue('formatBlock').toLowerCase();
  				};
  				scope.queryCommandState = function(command){
  					// $document[0].queryCommandValue('formatBlock') errors in Firefox if we call this when focussed on the textarea
  					return (!scope.showHtml) ? $document[0].queryCommandState(command) : '';
  				};
  				scope.switchView = function(){
  					scope.showHtml = !scope.showHtml;
  					$animate.enabled(false, scope.displayElements.html);
  					$animate.enabled(false, scope.displayElements.text);
  					//Show the HTML view
  					if(scope.showHtml){
  						//defer until the element is visible
  						$timeout(function(){
  							$animate.enabled(true, scope.displayElements.html);
  							$animate.enabled(true, scope.displayElements.text);
  							// [0] dereferences the DOM object from the angular.element
  							return scope.displayElements.html[0].focus();
  						}, 100);
  					}else{
  						//Show the WYSIWYG view
  						//defer until the element is visible
  						$timeout(function(){
  							$animate.enabled(true, scope.displayElements.html);
  							$animate.enabled(true, scope.displayElements.text);
  							// [0] dereferences the DOM object from the angular.element
  							return scope.displayElements.text[0].focus();
  						}, 100);
  					}
  				};
  
  				// changes to the model variable from outside the html/text inputs
  				// if no ngModel, then the only input is from inside text-angular
  				if(attrs.ngModel){
  					var _firstRun = true;
  					ngModel.$render = function(){
  						if(_firstRun){
  							// we need this firstRun to set the originalContents otherwise it gets overrided by the setting of ngModel to undefined from NaN
  							_firstRun = false;
  							// if view value is null or undefined initially and there was original content, set to the original content
  							var _initialValue = scope.$parent.$eval(attrs.ngModel);
  							if((_initialValue === undefined || _initialValue === null) && (_originalContents && _originalContents !== '')){
  								// on passing through to taBind it will be sanitised
  								ngModel.$setViewValue(_originalContents);
  							}
  						}
  						scope.displayElements.forminput.val(ngModel.$viewValue);
  						// if the editors aren't focused they need to be updated, otherwise they are doing the updating
  						scope.html = ngModel.$viewValue || '';
  					};
  					// trigger the validation calls
  					if(element.attr('required')) ngModel.$validators.required = function(modelValue, viewValue) {
  						var value = modelValue || viewValue;
  						return !(!value || value.trim() === '');
  					};
  				}else{
  					// if no ngModel then update from the contents of the origional html.
  					scope.displayElements.forminput.val(_originalContents);
  					scope.html = _originalContents;
  				}
  
  				// changes from taBind back up to here
  				scope.$watch('html', function(newValue, oldValue){
  					if(newValue !== oldValue){
  						if(attrs.ngModel && ngModel.$viewValue !== newValue) ngModel.$setViewValue(newValue);
  						scope.displayElements.forminput.val(newValue);
  					}
  				});
  
  				if(attrs.taTargetToolbars) _toolbars = textAngularManager.registerEditor(scope._name, scope, attrs.taTargetToolbars.split(','));
  				else{
  					var _toolbar = angular.element('<div text-angular-toolbar name="textAngularToolbar' + _serial + '">');
  					// passthrough init of toolbar options
  					if(attrs.taToolbar)						_toolbar.attr('ta-toolbar', attrs.taToolbar);
  					if(attrs.taToolbarClass)				_toolbar.attr('ta-toolbar-class', attrs.taToolbarClass);
  					if(attrs.taToolbarGroupClass)			_toolbar.attr('ta-toolbar-group-class', attrs.taToolbarGroupClass);
  					if(attrs.taToolbarButtonClass)			_toolbar.attr('ta-toolbar-button-class', attrs.taToolbarButtonClass);
  					if(attrs.taToolbarActiveButtonClass)	_toolbar.attr('ta-toolbar-active-button-class', attrs.taToolbarActiveButtonClass);
  					if(attrs.taFocussedClass)				_toolbar.attr('ta-focussed-class', attrs.taFocussedClass);
  
  					element.prepend(_toolbar);
  					$compile(_toolbar)(scope.$parent);
  					_toolbars = textAngularManager.registerEditor(scope._name, scope, ['textAngularToolbar' + _serial]);
  				}
  
  				scope.$on('$destroy', function(){
  					textAngularManager.unregisterEditor(scope._name);
  					angular.element(window).off('blur');
  				});
  
  				// catch element select event and pass to toolbar tools
  				scope.$on('ta-element-select', function(event, element){
  					if(_toolbars.triggerElementSelect(event, element)){
  						scope['reApplyOnSelectorHandlerstaTextElement' + _serial]();
  					}
  				});
  
  				scope.$on('ta-drop-event', function(event, element, dropEvent, dataTransfer){
  					scope.displayElements.text[0].focus();
  					if(dataTransfer && dataTransfer.files && dataTransfer.files.length > 0){
  						angular.forEach(dataTransfer.files, function(file){
  							// taking advantage of boolean execution, if the fileDropHandler returns true, nothing else after it is executed
  							// If it is false then execute the defaultFileDropHandler if the fileDropHandler is NOT the default one
  							// Once one of these has been executed wrap the result as a promise, if undefined or variable update the taBind, else we should wait for the promise
  							try{
  								$q.when(scope.fileDropHandler(file, scope.wrapSelection) ||
  									(scope.fileDropHandler !== scope.defaultFileDropHandler &&
  									$q.when(scope.defaultFileDropHandler(file, scope.wrapSelection)))).then(function(){
  										scope['updateTaBindtaTextElement' + _serial]();
  									});
  							}catch(error){
  								$log.error(error);
  							}
  						});
  						dropEvent.preventDefault();
  						dropEvent.stopPropagation();
  					/* istanbul ignore else, the updates if moved text */
  					}else{
  						$timeout(function(){
  							scope['updateTaBindtaTextElement' + _serial]();
  						}, 0);
  					}
  				});
  
  				// the following is for applying the active states to the tools that support it
  				scope._bUpdateSelectedStyles = false;
  				/* istanbul ignore next: browser window/tab leave check */
  				angular.element(window).on('blur', function(){
  					scope._bUpdateSelectedStyles = false;
  					scope.focussed = false;
  				});
  				// loop through all the tools polling their activeState function if it exists
  				scope.updateSelectedStyles = function(){
  					var _selection;
  					/* istanbul ignore next: This check is to ensure multiple timeouts don't exist */
  					if(_updateSelectedStylesTimeout) $timeout.cancel(_updateSelectedStylesTimeout);
  					// test if the common element ISN'T the root ta-text node
  					if((_selection = taSelection.getSelectionElement()) !== undefined && _selection.parentNode !== scope.displayElements.text[0]){
  						_toolbars.updateSelectedStyles(angular.element(_selection));
  					}else _toolbars.updateSelectedStyles();
  					// used to update the active state when a key is held down, ie the left arrow
  					/* istanbul ignore else: browser only check */
  					if(scope._bUpdateSelectedStyles) _updateSelectedStylesTimeout = $timeout(scope.updateSelectedStyles, 200);
  				};
  				// start updating on keydown
  				_keydown = function(){
  					/* istanbul ignore next: ie catch */
  					if(!scope.focussed){
  						scope._bUpdateSelectedStyles = false;
  						return;
  					}
  					/* istanbul ignore else: don't run if already running */
  					if(!scope._bUpdateSelectedStyles){
  						scope._bUpdateSelectedStyles = true;
  						scope.$apply(function(){
  							scope.updateSelectedStyles();
  						});
  					}
  				};
  				scope.displayElements.html.on('keydown', _keydown);
  				scope.displayElements.text.on('keydown', _keydown);
  				// stop updating on key up and update the display/model
  				_keyup = function(){
  					scope._bUpdateSelectedStyles = false;
  				};
  				scope.displayElements.html.on('keyup', _keyup);
  				scope.displayElements.text.on('keyup', _keyup);
  				// stop updating on key up and update the display/model
  				_keypress = function(event, eventData){
  					/* istanbul ignore else: this is for catching the jqLite testing*/
  					if(eventData) angular.extend(event, eventData);
  					scope.$apply(function(){
  						if(_toolbars.sendKeyCommand(event)){
  							/* istanbul ignore else: don't run if already running */
  							if(!scope._bUpdateSelectedStyles){
  								scope.updateSelectedStyles();
  							}
  							event.preventDefault();
  							return false;
  						}
  					});
  				};
  				scope.displayElements.html.on('keypress', _keypress);
  				scope.displayElements.text.on('keypress', _keypress);
  				// update the toolbar active states when we click somewhere in the text/html boxed
  				_mouseup = function(){
  					// ensure only one execution of updateSelectedStyles()
  					scope._bUpdateSelectedStyles = false;
  					scope.$apply(function(){
  						scope.updateSelectedStyles();
  					});
  				};
  				scope.displayElements.html.on('mouseup', _mouseup);
  				scope.displayElements.text.on('mouseup', _mouseup);
  			}
  		};
  	}
  ]);
  textAngular.service('textAngularManager', ['taToolExecuteAction', 'taTools', 'taRegisterTool', function(taToolExecuteAction, taTools, taRegisterTool){
  	// this service is used to manage all textAngular editors and toolbars.
  	// All publicly published functions that modify/need to access the toolbar or editor scopes should be in here
  	// these contain references to all the editors and toolbars that have been initialised in this app
  	var toolbars = {}, editors = {};
  	// when we focus into a toolbar, we need to set the TOOLBAR's $parent to be the toolbars it's linked to.
  	// We also need to set the tools to be updated to be the toolbars...
  	return {
  		// register an editor and the toolbars that it is affected by
  		registerEditor: function(name, scope, targetToolbars){
  			// targetToolbars are optional, we don't require a toolbar to function
  			if(!name || name === '') throw('textAngular Error: An editor requires a name');
  			if(!scope) throw('textAngular Error: An editor requires a scope');
  			if(editors[name]) throw('textAngular Error: An Editor with name "' + name + '" already exists');
  			// _toolbars is an ARRAY of toolbar scopes
  			var _toolbars = [];
  			angular.forEach(targetToolbars, function(_name){
  				if(toolbars[_name]) _toolbars.push(toolbars[_name]);
  				// if it doesn't exist it may not have been compiled yet and it will be added later
  			});
  			editors[name] = {
  				scope: scope,
  				toolbars: targetToolbars,
  				_registerToolbar: function(toolbarScope){
  					// add to the list late
  					if(this.toolbars.indexOf(toolbarScope.name) >= 0) _toolbars.push(toolbarScope);
  				},
  				// this is a suite of functions the editor should use to update all it's linked toolbars
  				editorFunctions: {
  					disable: function(){
  						// disable all linked toolbars
  						angular.forEach(_toolbars, function(toolbarScope){ toolbarScope.disabled = true; });
  					},
  					enable: function(){
  						// enable all linked toolbars
  						angular.forEach(_toolbars, function(toolbarScope){ toolbarScope.disabled = false; });
  					},
  					focus: function(){
  						// this should be called when the editor is focussed
  						angular.forEach(_toolbars, function(toolbarScope){
  							toolbarScope._parent = scope;
  							toolbarScope.disabled = false;
  							toolbarScope.focussed = true;
  							scope.focussed = true;
  						});
  					},
  					unfocus: function(){
  						// this should be called when the editor becomes unfocussed
  						angular.forEach(_toolbars, function(toolbarScope){
  							toolbarScope.disabled = true;
  							toolbarScope.focussed = false;
  						});
  						scope.focussed = false;
  					},
  					updateSelectedStyles: function(selectedElement){
  						// update the active state of all buttons on liked toolbars
  						angular.forEach(_toolbars, function(toolbarScope){
  							angular.forEach(toolbarScope.tools, function(toolScope){
  								if(toolScope.activeState){
  									toolbarScope._parent = scope;
  									toolScope.active = toolScope.activeState(selectedElement);
  								}
  							});
  						});
  					},
  					sendKeyCommand: function(event){
  						// we return true if we applied an action, false otherwise
  						var result = false;
  						if(event.ctrlKey || event.metaKey || event.specialKey) angular.forEach(taTools, function(tool, name){
  							if(tool.commandKeyCode && (tool.commandKeyCode === event.which || tool.commandKeyCode === event.specialKey)){
  								for(var _t = 0; _t < _toolbars.length; _t++){
  									if(_toolbars[_t].tools[name] !== undefined){
  										taToolExecuteAction.call(_toolbars[_t].tools[name], scope);
  										result = true;
  										break;
  									}
  								}
  							}
  						});
  						return result;
  					},
  					triggerElementSelect: function(event, element){
  						// search through the taTools to see if a match for the tag is made.
  						// if there is, see if the tool is on a registered toolbar and not disabled.
  						// NOTE: This can trigger on MULTIPLE tools simultaneously.
  						var elementHasAttrs = function(_element, attrs){
  							var result = true;
  							for(var i = 0; i < attrs.length; i++) result = result && _element.attr(attrs[i]);
  							return result;
  						};
  						var workerTools = [];
  						var unfilteredTools = {};
  						var result = false;
  						element = angular.element(element);
  						// get all valid tools by element name, keep track if one matches the
  						var onlyWithAttrsFilter = false;
  						angular.forEach(taTools, function(tool, name){
  							if(
  								tool.onElementSelect &&
  								tool.onElementSelect.element &&
  								tool.onElementSelect.element.toLowerCase() === element[0].tagName.toLowerCase() &&
  								(!tool.onElementSelect.filter || tool.onElementSelect.filter(element))
  							){
  								// this should only end up true if the element matches the only attributes
  								onlyWithAttrsFilter = onlyWithAttrsFilter ||
  									(angular.isArray(tool.onElementSelect.onlyWithAttrs) && elementHasAttrs(element, tool.onElementSelect.onlyWithAttrs));
  								if(!tool.onElementSelect.onlyWithAttrs || elementHasAttrs(element, tool.onElementSelect.onlyWithAttrs)) unfilteredTools[name] = tool;
  							}
  						});
  						// if we matched attributes to filter on, then filter, else continue
  						if(onlyWithAttrsFilter){
  							angular.forEach(unfilteredTools, function(tool, name){
  								if(tool.onElementSelect.onlyWithAttrs && elementHasAttrs(element, tool.onElementSelect.onlyWithAttrs)) workerTools.push({'name': name, 'tool': tool});
  							});
  							// sort most specific (most attrs to find) first
  							workerTools.sort(function(a,b){
  								return b.tool.onElementSelect.onlyWithAttrs.length - a.tool.onElementSelect.onlyWithAttrs.length;
  							});
  						}else{
  							angular.forEach(unfilteredTools, function(tool, name){
  								workerTools.push({'name': name, 'tool': tool});
  							});
  						}
  						// Run the actions on the first visible filtered tool only
  						if(workerTools.length > 0){
  							for(var _i = 0; _i < workerTools.length; _i++){
  								var tool = workerTools[_i].tool;
  								var name = workerTools[_i].name;
  								for(var _t = 0; _t < _toolbars.length; _t++){
  									if(_toolbars[_t].tools[name] !== undefined){
  										tool.onElementSelect.action.call(_toolbars[_t].tools[name], event, element, scope);
  										result = true;
  										break;
  									}
  								}
  								if(result) break;
  							}
  						}
  						return result;
  					}
  				}
  			};
  			return editors[name].editorFunctions;
  		},
  		// retrieve editor by name, largely used by testing suites only
  		retrieveEditor: function(name){
  			return editors[name];
  		},
  		unregisterEditor: function(name){
  			delete editors[name];
  		},
  		// registers a toolbar such that it can be linked to editors
  		registerToolbar: function(scope){
  			if(!scope) throw('textAngular Error: A toolbar requires a scope');
  			if(!scope.name || scope.name === '') throw('textAngular Error: A toolbar requires a name');
  			if(toolbars[scope.name]) throw('textAngular Error: A toolbar with name "' + scope.name + '" already exists');
  			toolbars[scope.name] = scope;
  			angular.forEach(editors, function(_editor){
  				_editor._registerToolbar(scope);
  			});
  		},
  		// retrieve toolbar by name, largely used by testing suites only
  		retrieveToolbar: function(name){
  			return toolbars[name];
  		},
  		// retrieve toolbars by editor name, largely used by testing suites only
  		retrieveToolbarsViaEditor: function(name){
  			var result = [], _this = this;
  			angular.forEach(this.retrieveEditor(name).toolbars, function(name){
  				result.push(_this.retrieveToolbar(name));
  			});
  			return result;
  		},
  		unregisterToolbar: function(name){
  			delete toolbars[name];
  		},
  		// functions for updating the toolbar buttons display
  		updateToolsDisplay: function(newTaTools){
  			// pass a partial struct of the taTools, this allows us to update the tools on the fly, will not change the defaults.
  			var _this = this;
  			angular.forEach(newTaTools, function(_newTool, key){
  				_this.updateToolDisplay(key, _newTool);
  			});
  		},
  		// this function resets all toolbars to their default tool definitions
  		resetToolsDisplay: function(){
  			var _this = this;
  			angular.forEach(taTools, function(_newTool, key){
  				_this.resetToolDisplay(key);
  			});
  		},
  		// update a tool on all toolbars
  		updateToolDisplay: function(toolKey, _newTool){
  			var _this = this;
  			angular.forEach(toolbars, function(toolbarScope, toolbarKey){
  				_this.updateToolbarToolDisplay(toolbarKey, toolKey, _newTool);
  			});
  		},
  		// resets a tool to the default/starting state on all toolbars
  		resetToolDisplay: function(toolKey){
  			var _this = this;
  			angular.forEach(toolbars, function(toolbarScope, toolbarKey){
  				_this.resetToolbarToolDisplay(toolbarKey, toolKey);
  			});
  		},
  		// update a tool on a specific toolbar
  		updateToolbarToolDisplay: function(toolbarKey, toolKey, _newTool){
  			if(toolbars[toolbarKey]) toolbars[toolbarKey].updateToolDisplay(toolKey, _newTool);
  			else throw('textAngular Error: No Toolbar with name "' + toolbarKey + '" exists');
  		},
  		// reset a tool on a specific toolbar to it's default starting value
  		resetToolbarToolDisplay: function(toolbarKey, toolKey){
  			if(toolbars[toolbarKey]) toolbars[toolbarKey].updateToolDisplay(toolKey, taTools[toolKey], true);
  			else throw('textAngular Error: No Toolbar with name "' + toolbarKey + '" exists');
  		},
  		// removes a tool from all toolbars and it's definition
  		removeTool: function(toolKey){
  			delete taTools[toolKey];
  			angular.forEach(toolbars, function(toolbarScope){
  				delete toolbarScope.tools[toolKey];
  				for(var i = 0; i < toolbarScope.toolbar.length; i++){
  					var toolbarIndex;
  					for(var j = 0; j < toolbarScope.toolbar[i].length; j++){
  						if(toolbarScope.toolbar[i][j] === toolKey){
  							toolbarIndex = {
  								group: i,
  								index: j
  							};
  							break;
  						}
  						if(toolbarIndex !== undefined) break;
  					}
  					if(toolbarIndex !== undefined){
  						toolbarScope.toolbar[toolbarIndex.group].slice(toolbarIndex.index, 1);
  						toolbarScope._$element.children().eq(toolbarIndex.group).children().eq(toolbarIndex.index).remove();
  					}
  				}
  			});
  		},
  		// toolkey, toolDefinition are required. If group is not specified will pick the last group, if index isnt defined will append to group
  		addTool: function(toolKey, toolDefinition, group, index){
  			taRegisterTool(toolKey, toolDefinition);
  			angular.forEach(toolbars, function(toolbarScope){
  				toolbarScope.addTool(toolKey, toolDefinition, group, index);
  			});
  		},
  		// adds a Tool but only to one toolbar not all
  		addToolToToolbar: function(toolKey, toolDefinition, toolbarKey, group, index){
  			taRegisterTool(toolKey, toolDefinition);
  			toolbars[toolbarKey].addTool(toolKey, toolDefinition, group, index);
  		},
  		// this is used when externally the html of an editor has been changed and textAngular needs to be notified to update the model.
  		// this will call a $digest if not already happening
  		refreshEditor: function(name){
  			if(editors[name]){
  				editors[name].scope.updateTaBindtaTextElement();
  				/* istanbul ignore else: phase catch */
  				if(!editors[name].scope.$$phase) editors[name].scope.$digest();
  			}else throw('textAngular Error: No Editor with name "' + name + '" exists');
  		},
  		// this is used by taBind to send a key command in response to a special key event
  		sendKeyCommand: function(scope, event){
  			angular.forEach(editors, function(_editor){
  				/* istanbul ignore else: if nothing to do, do nothing */
  				if (_editor.editorFunctions.sendKeyCommand(event)){
  					/* istanbul ignore else: don't run if already running */
  					if(!scope._bUpdateSelectedStyles){
  						scope.updateSelectedStyles();
  					}
  					event.preventDefault();
  					return false;
  				}
  			});
  		}
  	};
  }]);
  textAngular.directive('textAngularToolbar', [
  	'$compile', 'textAngularManager', 'taOptions', 'taTools', 'taToolExecuteAction', '$window',
  	function($compile, textAngularManager, taOptions, taTools, taToolExecuteAction, $window){
  		return {
  			scope: {
  				name: '@' // a name IS required
  			},
  			restrict: "EA",
  			link: function(scope, element, attrs){
  				if(!scope.name || scope.name === '') throw('textAngular Error: A toolbar requires a name');
  				angular.extend(scope, angular.copy(taOptions));
  				if(attrs.taToolbar)						scope.toolbar = scope.$parent.$eval(attrs.taToolbar);
  				if(attrs.taToolbarClass)				scope.classes.toolbar = attrs.taToolbarClass;
  				if(attrs.taToolbarGroupClass)			scope.classes.toolbarGroup = attrs.taToolbarGroupClass;
  				if(attrs.taToolbarButtonClass)			scope.classes.toolbarButton = attrs.taToolbarButtonClass;
  				if(attrs.taToolbarActiveButtonClass)	scope.classes.toolbarButtonActive = attrs.taToolbarActiveButtonClass;
  				if(attrs.taFocussedClass)				scope.classes.focussed = attrs.taFocussedClass;
  
  				scope.disabled = true;
  				scope.focussed = false;
  				scope._$element = element;
  				element[0].innerHTML = '';
  				element.addClass("ta-toolbar " + scope.classes.toolbar);
  
  				scope.$watch('focussed', function(){
  					if(scope.focussed) element.addClass(scope.classes.focussed);
  					else element.removeClass(scope.classes.focussed);
  				});
  
  				var setupToolElement = function(toolDefinition, toolScope){
  					var toolElement;
  					if(toolDefinition && toolDefinition.display){
  						toolElement = angular.element(toolDefinition.display);
  					}
  					else toolElement = angular.element("<button type='button'>");
  
  					if(toolDefinition && toolDefinition["class"]) toolElement.addClass(toolDefinition["class"]);
  					else toolElement.addClass(scope.classes.toolbarButton);
  
  					toolElement.attr('name', toolScope.name);
  					// important to not take focus from the main text/html entry
  					toolElement.attr('ta-button', 'ta-button');
  					toolElement.attr('ng-disabled', 'isDisabled()');
  					toolElement.attr('tabindex', '-1');
  					toolElement.attr('ng-click', 'executeAction()');
  					toolElement.attr('ng-class', 'displayActiveToolClass(active)');
  
  					if (toolDefinition && toolDefinition.tooltiptext) {
  						toolElement.attr('title', toolDefinition.tooltiptext);
  					}
  					if(toolDefinition && !toolDefinition.display && !toolScope._display){
  						// first clear out the current contents if any
  						toolElement[0].innerHTML = '';
  						// add the buttonText
  						if(toolDefinition.buttontext) toolElement[0].innerHTML = toolDefinition.buttontext;
  						// add the icon to the front of the button if there is content
  						if(toolDefinition.iconclass){
  							var icon = angular.element('<i>'), content = toolElement[0].innerHTML;
  							icon.addClass(toolDefinition.iconclass);
  							toolElement[0].innerHTML = '';
  							toolElement.append(icon);
  							if(content && content !== '') toolElement.append('&nbsp;' + content);
  						}
  					}
  
  					toolScope._lastToolDefinition = angular.copy(toolDefinition);
  
  					return $compile(toolElement)(toolScope);
  				};
  
  				// Keep a reference for updating the active states later
  				scope.tools = {};
  				// create the tools in the toolbar
  				// default functions and values to prevent errors in testing and on init
  				scope._parent = {
  					disabled: true,
  					showHtml: false,
  					queryFormatBlockState: function(){ return false; },
  					queryCommandState: function(){ return false; }
  				};
  				var defaultChildScope = {
  					$window: $window,
  					$editor: function(){
  						// dynamically gets the editor as it is set
  						return scope._parent;
  					},
  					isDisabled: function(){
  						// to set your own disabled logic set a function or boolean on the tool called 'disabled'
  						return ( // this bracket is important as without it it just returns the first bracket and ignores the rest
  							// when the button's disabled function/value evaluates to true
  							(typeof this.$eval('disabled') !== 'function' && this.$eval('disabled')) || this.$eval('disabled()') ||
  							// all buttons except the HTML Switch button should be disabled in the showHtml (RAW html) mode
  							(this.name !== 'html' && this.$editor().showHtml) ||
  							// if the toolbar is disabled
  							this.$parent.disabled ||
  							// if the current editor is disabled
  							this.$editor().disabled
  						);
  					},
  					displayActiveToolClass: function(active){
  						return (active)? scope.classes.toolbarButtonActive : '';
  					},
  					executeAction: taToolExecuteAction
  				};
  
  				angular.forEach(scope.toolbar, function(group){
  					// setup the toolbar group
  					var groupElement = angular.element("<div>");
  					groupElement.addClass(scope.classes.toolbarGroup);
  					angular.forEach(group, function(tool){
  						// init and add the tools to the group
  						// a tool name (key name from taTools struct)
  						//creates a child scope of the main angularText scope and then extends the childScope with the functions of this particular tool
  						// reference to the scope and element kept
  						scope.tools[tool] = angular.extend(scope.$new(true), taTools[tool], defaultChildScope, {name: tool});
  						scope.tools[tool].$element = setupToolElement(taTools[tool], scope.tools[tool]);
  						// append the tool compiled with the childScope to the group element
  						groupElement.append(scope.tools[tool].$element);
  					});
  					// append the group to the toolbar
  					element.append(groupElement);
  				});
  
  				// update a tool
  				// if a value is set to null, remove from the display
  				// when forceNew is set to true it will ignore all previous settings, used to reset to taTools definition
  				// to reset to defaults pass in taTools[key] as _newTool and forceNew as true, ie `updateToolDisplay(key, taTools[key], true);`
  				scope.updateToolDisplay = function(key, _newTool, forceNew){
  					var toolInstance = scope.tools[key];
  					if(toolInstance){
  						// get the last toolDefinition, then override with the new definition
  						if(toolInstance._lastToolDefinition && !forceNew) _newTool = angular.extend({}, toolInstance._lastToolDefinition, _newTool);
  						if(_newTool.buttontext === null && _newTool.iconclass === null && _newTool.display === null)
  							throw('textAngular Error: Tool Definition for updating "' + key + '" does not have a valid display/iconclass/buttontext value');
  
  						// if tool is defined on this toolbar, update/redo the tool
  						if(_newTool.buttontext === null){
  							delete _newTool.buttontext;
  						}
  						if(_newTool.iconclass === null){
  							delete _newTool.iconclass;
  						}
  						if(_newTool.display === null){
  							delete _newTool.display;
  						}
  
  						var toolElement = setupToolElement(_newTool, toolInstance);
  						toolInstance.$element.replaceWith(toolElement);
  						toolInstance.$element = toolElement;
  					}
  				};
  
  				// we assume here that all values passed are valid and correct
  				scope.addTool = function(key, _newTool, groupIndex, index){
  					scope.tools[key] = angular.extend(scope.$new(true), taTools[key], defaultChildScope, {name: key});
  					scope.tools[key].$element = setupToolElement(taTools[key], scope.tools[key]);
  					var group;
  					if(groupIndex === undefined) groupIndex = scope.toolbar.length - 1;
  					group = angular.element(element.children()[groupIndex]);
  
  					if(index === undefined){
  						group.append(scope.tools[key].$element);
  						scope.toolbar[groupIndex][scope.toolbar[groupIndex].length - 1] = key;
  					}else{
  						group.children().eq(index).after(scope.tools[key].$element);
  						scope.toolbar[groupIndex][index] = key;
  					}
  				};
  
  				textAngularManager.registerToolbar(scope);
  
  				scope.$on('$destroy', function(){
  					textAngularManager.unregisterToolbar(scope.name);
  				});
  			}
  		};
  	}
  ]);
  })();