Browse Source

Maybe if I add the relevant files too it is better... :P

Signed-off-by: emanuele <[email protected]>
emanuele 13 years ago
parent
commit
ebf3854b62
42 changed files with 3897 additions and 4 deletions
  1. 222 0
      Themes/default/css/jquery.sceditor.css
  2. 54 0
      Themes/default/css/jquery.sceditor.default.css
  3. BIN
      Themes/default/images/bbc/icons/arrow_redo.png
  4. BIN
      Themes/default/images/bbc/icons/arrow_undo.png
  5. BIN
      Themes/default/images/bbc/icons/color_swatch.png
  6. BIN
      Themes/default/images/bbc/icons/comment.png
  7. BIN
      Themes/default/images/bbc/icons/cut.png
  8. BIN
      Themes/default/images/bbc/icons/date_add.png
  9. BIN
      Themes/default/images/bbc/icons/email_add.png
  10. BIN
      Themes/default/images/bbc/icons/emoticon_smile.png
  11. BIN
      Themes/default/images/bbc/icons/font.png
  12. BIN
      Themes/default/images/bbc/icons/font_delete.png
  13. BIN
      Themes/default/images/bbc/icons/font_size.png
  14. BIN
      Themes/default/images/bbc/icons/grip.png
  15. BIN
      Themes/default/images/bbc/icons/image_add.png
  16. BIN
      Themes/default/images/bbc/icons/link_add.png
  17. BIN
      Themes/default/images/bbc/icons/link_break.png
  18. BIN
      Themes/default/images/bbc/icons/page_copy.png
  19. BIN
      Themes/default/images/bbc/icons/page_white_paste.png
  20. BIN
      Themes/default/images/bbc/icons/page_white_text.png
  21. BIN
      Themes/default/images/bbc/icons/paste_plain.png
  22. BIN
      Themes/default/images/bbc/icons/printer.png
  23. BIN
      Themes/default/images/bbc/icons/script_code.png
  24. BIN
      Themes/default/images/bbc/icons/table_add.png
  25. BIN
      Themes/default/images/bbc/icons/text_align_center.png
  26. BIN
      Themes/default/images/bbc/icons/text_align_justify.png
  27. BIN
      Themes/default/images/bbc/icons/text_align_left.png
  28. BIN
      Themes/default/images/bbc/icons/text_align_right.png
  29. BIN
      Themes/default/images/bbc/icons/text_bold.png
  30. BIN
      Themes/default/images/bbc/icons/text_horizontalrule.png
  31. BIN
      Themes/default/images/bbc/icons/text_italic.png
  32. BIN
      Themes/default/images/bbc/icons/text_list_bullets.png
  33. BIN
      Themes/default/images/bbc/icons/text_list_numbers.png
  34. BIN
      Themes/default/images/bbc/icons/text_strikethrough.png
  35. BIN
      Themes/default/images/bbc/icons/text_subscript.png
  36. BIN
      Themes/default/images/bbc/icons/text_superscript.png
  37. BIN
      Themes/default/images/bbc/icons/text_underline.png
  38. BIN
      Themes/default/images/bbc/icons/time_add.png
  39. BIN
      Themes/default/images/bbc/icons/youtube.png
  40. 1101 0
      Themes/default/scripts/jquery.sceditor.bbcode.js
  41. 2518 0
      Themes/default/scripts/jquery.sceditor.js
  42. 2 4
      Themes/default/scripts/smf_jquery_plugins.js

+ 222 - 0
Themes/default/css/jquery.sceditor.css

@@ -0,0 +1,222 @@
+/**
+ * SCEditor v1.3.1
+ * http://www.samclarke.com/2011/07/sceditor/
+ *
+ * Copyright (C) 2011, Sam Clarke (samclarke.com)
+ *
+ * SCEditor is dual licensed under the MIT and GPL licenses:
+ *	http://www.opensource.org/licenses/mit-license.php
+ *	http://www.gnu.org/licenses/gpl.html
+ *
+ * Icons by Mark James (http://www.famfamfam.com/lab/icons/silk/)
+ * Licensed under the Creative Commons CC-BY license (http://creativecommons.org/licenses/by/3.0/)
+ */
+
+/*
+	spritemapper.output_image = minified/jquery.sceditor.min.png
+	spritemapper.output_css = minified/jquery.sceditor.min.css
+*/
+.sceditor-container {
+	position: relative;
+	overflow: hidden;
+	border: 1px solid #aaa;
+	background: #fff;
+
+	-moz-border-radius: 4px;
+	-webkit-border-radius: 4px;
+	-opera-border-radius: 4px;
+	-khtml-border-radius: 4px;
+	border-radius: 4px;
+}
+
+.sceditor-container, .sceditor-container div,
+div.sceditor-dropdown, div.sceditor-dropdown div {
+	padding: 0;
+	margin: 0;
+	z-index: 3;
+}
+	.sceditor-container iframe, .sceditor-container textarea {
+		border: 0;
+		outline: none;
+		font-family: Verdana, Arial, Helvetica, sans-serif;
+		font-size: 13px;
+		color: #111;
+		padding: 0;
+		margin: 5px;
+		resize: none;
+	}
+	div.sceditor-resize-cover {
+		position: absolute;
+		top: 0;
+		left: 0;
+		background: #000;
+		width: 3000px;
+		height: 6000px;
+		z-index: 2;
+		filter: alpha(opacity=30);
+		opacity: 0.3;
+	}
+	div.sceditor-grip {
+		background: #000;
+		overflow: hidden;
+		width: 10px;
+		height: 10px;
+		cursor: pointer;
+		position: absolute;
+		bottom: 0;
+		right: 0;
+		z-index: 3;
+		background: url('icons/grip.png');
+	}
+	div.sceditor-dropdown {
+		position: absolute;
+		margin: 22px 0 0 1px;
+		border: 1px solid #bbb;
+		background: #fff;
+		color: #222;
+		font-size: 13px;
+		z-index: 6;
+	}
+		div.sceditor-dropdown form { margin: 0; }
+		div.sceditor-dropdown label {
+			display:block;
+			font-weight: bold;
+			font-size: .95em;
+			margin: .65em 0 .15em;
+		}
+		div.sceditor-dropdown .button {	margin: .5em 0 0; }
+		.sceditor-pastetext textarea { border: 1px solid #bbb; width: 20em; }
+		div.sceditor-inserttable, div.sceditor-insertimage,
+		div.sceditor-pastetext, div.sceditor-insertlink,
+		div.sceditor-insertemail {
+			padding: 5px;
+		}
+		.sceditor-insertemoticon img {
+			cursor: pointer;
+			margin: 2px;
+		}
+		.sceditor-more {
+			border-top: 1px solid #bbb;
+			display: block;
+			text-align: center;
+			cursor: pointer;
+			padding: 2px 0;
+		}
+		.sceditor-more:hover { background: #eee; }
+		.sceditor-fontsize-option, .sceditor-font-option {
+			display: block;
+			padding: 4px 6px;
+			cursor: pointer;
+			font-size: 14px;
+			text-decoration: none;
+			color: #222;
+		}
+		.sceditor-fontsize-option:hover, .sceditor-font-option:hover { background: #eee; }
+		.sceditor-color-column { float: left; }
+			.sceditor-color-option {
+				display: block;
+				border: 1px solid #fff;
+				height: 10px;
+				width: 10px;
+				overflow: hidden;
+			}
+			.sceditor-color-option:hover { border: 1px solid #333; }
+
+	div.sceditor-toolbar {
+		overflow: hidden;
+		zoom: 1; /* IE6 */
+		padding: 3px 5px 0 5px;
+		*padding: 3px 5px 3px 5px;
+		background: #f7f7f7;
+		border-bottom: 1px solid #aaa;
+
+		-webkit-border-radius: 4px 4px 0 0;
+	}
+
+		div.sceditor-group {
+			overflow: hidden;
+			display: inline-block;
+			zoom: 1; /* IE6 */
+			*display: inline;
+			background: #ddd;
+			margin: 1px 5px 1px 0;
+			*margin: 2px 5px 2px 0;
+			padding: 2px;
+
+			-moz-border-radius: 4px;
+			-webkit-border-radius: 4px;
+			-opera-border-radius: 4px;
+			-khtml-border-radius: 4px;
+			border-radius: 4px;
+		}
+
+		.sceditor-button {
+			float: left;
+			cursor: pointer;
+			padding: 3px 4px;
+		}
+		/*.sceditor-button.disabled {
+			background-color: #666;
+		}*/
+		.sceditor-button.disabled div {
+			filter: alpha(opacity=30);
+			opacity: 0.3;
+		}
+		.sceditor-button.disabled:hover {
+			background: inherit;
+			cursor: default;
+		}
+		.sceditor-button div, .sceditor-button {
+			display: block;
+			width: 16px;
+			height: 16px;
+		}
+		.sceditor-button div {
+			margin: 0;
+			padding: 0;
+			text-indent: -9999px;
+		}
+		.sceditor-button:hover { background: #eee; }
+			.sceditor-button-bold div { background: url('icons/text_bold.png'); }
+			.sceditor-button-italic div { background: url('icons/text_italic.png'); }
+			.sceditor-button-underline div { background: url('icons/text_underline.png'); }
+			.sceditor-button-strike div { background: url('icons/text_strikethrough.png'); }
+			.sceditor-button-subscript div { background: url('icons/text_subscript.png'); }
+			.sceditor-button-superscript div { background: url('icons/text_superscript.png'); }
+
+			.sceditor-button-left div { background: url('icons/text_align_left.png'); }
+			.sceditor-button-center div { background: url('icons/text_align_center.png'); }
+			.sceditor-button-right div { background: url('icons/text_align_right.png'); }
+			.sceditor-button-justify div { background: url('icons/text_align_justify.png'); }
+
+			.sceditor-button-font div { background: url('icons/font.png'); }
+			.sceditor-button-size div { background: url('icons/font_size.png'); }
+			.sceditor-button-color div { background: url('icons/color_swatch.png'); }
+			.sceditor-button-removeformat div { background: url('icons/font_delete.png'); }
+
+			.sceditor-button-cut div { background: url('icons/cut.png'); }
+			.sceditor-button-copy div { background: url('icons/page_copy.png'); }
+			.sceditor-button-paste div { background: url('icons/page_white_paste.png'); }
+			.sceditor-button-pastetext div { background: url('icons/paste_plain.png'); }
+
+			.sceditor-button-bulletlist div { background: url('icons/text_list_bullets.png'); }
+			.sceditor-button-orderedlist div { background: url('icons/text_list_numbers.png'); }
+
+			.sceditor-button-table div { background: url('icons/table_add.png'); }
+
+			.sceditor-button-horizontalrule div { background: url('icons/text_horizontalrule.png'); }
+			.sceditor-button-image div { background: url('icons/image_add.png'); }
+			.sceditor-button-code div { background: url('icons/script_code.png'); }
+			.sceditor-button-email div { background: url('icons/email_add.png'); }
+			.sceditor-button-link div { background: url('icons/link_add.png'); }
+			.sceditor-button-unlink div { background: url('icons/link_break.png'); }
+
+			.sceditor-button-quote div { background: url('icons/comment.png'); }
+
+			.sceditor-button-emoticon div { background: url('icons/emoticon_smile.png'); }
+			.sceditor-button-youtube div { background: url('icons/youtube.png'); }
+			.sceditor-button-date div { background: url('icons/date_add.png'); }
+			.sceditor-button-time div { background: url('icons/time_add.png'); }
+
+			.sceditor-button-print div { background: url('icons/printer.png'); }
+			.sceditor-button-source div { background: url('icons/page_white_text.png'); }

+ 54 - 0
Themes/default/css/jquery.sceditor.default.css

@@ -0,0 +1,54 @@
+html, body, p, code:before, table {
+	margin: 0;
+	padding: 0;
+	font-family: Verdana, Arial, Helvetica, sans-serif;
+	font-size: 13px;
+	color: #111;
+}
+
+ul, ol {
+	margin-top: 0;
+	margin-bottom: 0;
+	padding-top: 0;
+	padding-bottom: 0;
+}
+
+table, td {
+	border: 1px dotted #000;
+	empty-cells: show;
+}
+
+code:before {
+	position: absolute;
+	content: 'Code:';
+	top: -1.35em;
+	left: 0;
+}
+code {
+	margin-top: 1.5em;
+	position: relative;
+	background: #eee;
+	border: 1px solid #aaa;
+	white-space: pre;
+	padding: .25em;
+}
+code:before, code {
+	display: block;
+	text-align: left;
+}
+
+blockquote {
+	position: relative;
+	background: #fff6c7;
+	margin: .25em 0;
+	border: 1px solid #aaa;
+	padding: .25em;
+}
+blockquote cite {
+	font-weight: bold;
+	display: block;
+	font-size: 1em;
+	border-bottom: 1px solid #aaa;
+}
+div { min-height: 1em; /*height:auto !important; height: 1em;*/ }
+h1, h2, h3, h4, h5, h6 { padding: 0; margin: 0; }

BIN
Themes/default/images/bbc/icons/arrow_redo.png


BIN
Themes/default/images/bbc/icons/arrow_undo.png


BIN
Themes/default/images/bbc/icons/color_swatch.png


BIN
Themes/default/images/bbc/icons/comment.png


BIN
Themes/default/images/bbc/icons/cut.png


BIN
Themes/default/images/bbc/icons/date_add.png


BIN
Themes/default/images/bbc/icons/email_add.png


BIN
Themes/default/images/bbc/icons/emoticon_smile.png


BIN
Themes/default/images/bbc/icons/font.png


BIN
Themes/default/images/bbc/icons/font_delete.png


BIN
Themes/default/images/bbc/icons/font_size.png


BIN
Themes/default/images/bbc/icons/grip.png


BIN
Themes/default/images/bbc/icons/image_add.png


BIN
Themes/default/images/bbc/icons/link_add.png


BIN
Themes/default/images/bbc/icons/link_break.png


BIN
Themes/default/images/bbc/icons/page_copy.png


BIN
Themes/default/images/bbc/icons/page_white_paste.png


BIN
Themes/default/images/bbc/icons/page_white_text.png


BIN
Themes/default/images/bbc/icons/paste_plain.png


BIN
Themes/default/images/bbc/icons/printer.png


BIN
Themes/default/images/bbc/icons/script_code.png


BIN
Themes/default/images/bbc/icons/table_add.png


BIN
Themes/default/images/bbc/icons/text_align_center.png


BIN
Themes/default/images/bbc/icons/text_align_justify.png


BIN
Themes/default/images/bbc/icons/text_align_left.png


BIN
Themes/default/images/bbc/icons/text_align_right.png


BIN
Themes/default/images/bbc/icons/text_bold.png


BIN
Themes/default/images/bbc/icons/text_horizontalrule.png


BIN
Themes/default/images/bbc/icons/text_italic.png


BIN
Themes/default/images/bbc/icons/text_list_bullets.png


BIN
Themes/default/images/bbc/icons/text_list_numbers.png


BIN
Themes/default/images/bbc/icons/text_strikethrough.png


BIN
Themes/default/images/bbc/icons/text_subscript.png


BIN
Themes/default/images/bbc/icons/text_superscript.png


BIN
Themes/default/images/bbc/icons/text_underline.png


BIN
Themes/default/images/bbc/icons/time_add.png


BIN
Themes/default/images/bbc/icons/youtube.png


+ 1101 - 0
Themes/default/scripts/jquery.sceditor.bbcode.js

@@ -0,0 +1,1101 @@
+/**
+ * SCEditor BBCode Plugin v1.3.4
+ * http://www.samclarke.com/2011/07/sceditor/ 
+ *
+ * Copyright (C) 2011-2012, Sam Clarke (samclarke.com)
+ *
+ * SCEditor is dual licensed under the MIT and GPL licenses:
+ *	http://www.opensource.org/licenses/mit-license.php
+ *	http://www.gnu.org/licenses/gpl.html
+ */
+
+// ==ClosureCompiler==
+// @output_file_name jquery.sceditor.min.js
+// @compilation_level SIMPLE_OPTIMIZATIONS
+// ==/ClosureCompiler==
+
+/*jshint smarttabs: true, jquery: true, eqnull:true, curly: false */
+
+(function($) {
+	'use strict';
+
+	$.sceditorBBCodePlugin = function(element, options) {
+		var base = this;
+
+		/**
+		 * Private methods
+		 * @private
+		 */
+		var	init,
+			buildBbcodeCache,
+			handleStyles,
+			handleTags,
+			formatString,
+			getStyle,
+			wrapInDivs,
+			mergeTextModeCommands;
+
+		base.bbcodes = $.sceditorBBCodePlugin.bbcodes;
+
+
+		/**
+		 * cache of all the tags pointing to their bbcodes to enable
+		 * faster lookup of which bbcode a tag should have
+		 * @private
+		 */
+		var tagsToBbcodes = {};
+
+		/**
+		 * Same as tagsToBbcodes but instead of HTML tags it's styles
+		 * @private
+		 */
+		var stylesToBbcodes = {};
+		
+		/**
+		 * Allowed children of specific HTML tags. Empty array if no
+		 * children other than text nodes are allowed
+		 * @private
+		 */
+		var validChildren = {
+			ul: ['li'],
+			ol: ['li'],
+			table: ['tr'],
+			tr: ['td', 'th'],
+			code: ['br', 'p', 'div'],
+			youtube: []
+		};
+
+
+		/**
+		 * Initializer
+		 */
+		init = function() {
+			$.data(element, "sceditorbbcode", base);
+			
+			base.options = $.extend({}, $.sceditor.defaultOptions, options);
+
+			// build the BBCode cache
+			buildBbcodeCache();
+
+			(new $.sceditor(element,
+				$.extend({}, base.options, {
+					getHtmlHandler: base.getHtmlHandler,
+					getTextHandler: base.getTextHandler,
+					commands: mergeTextModeCommands()
+				})
+			));
+		};
+		
+		mergeTextModeCommands = function() {
+			// TODO: use selection as display text if is one.
+			// TODO: add translations of the prompts
+			var merge = {
+				bold: { txtExec: ["[b]", "[/b]"] },
+				italic: { txtExec: ["[i]", "[/i]"] },
+				underline: { txtExec: ["[u]", "[/u]"] },
+				strike: { txtExec: ["[s]", "[/s]"] },
+				subscript: { txtExec: ["[sub]", "[/sub]"] },
+				superscript: { txtExec: ["[sup]", "[/sup]"] },
+				left: { txtExec: ["[left]", "[/left]"] },
+				center: { txtExec: ["[center]", "[/center]"] },
+				right: { txtExec: ["[right]", "[/right]"] },
+				justify: { txtExec: ["[justify]", "[/justify]"] },
+				//font: { txtExec: ["[u]", "[/u]"] },
+				//size: { txtExec: ["[u]", "[/u]"] },
+				//color: { txtExec: ["[u]", "[/u]"] },
+				//bulletlist: { txtExec: ["[u]", "[/u]"] },
+				//orderedlist: { txtExec: ["[u]", "[/u]"] },
+				//table: { txtExec: ["[u]", "[/u]"] },
+				horizontalrule: { txtExec: ["[hr]"] },
+				code: { txtExec: ["[code]", "[/code]"] },
+				image: { txtExec: function() {
+					var url = prompt(this._("Enter the images URL:"));
+					
+					if(url)
+						this.textEditorInsertText("[img]" + url + "[/img]");
+				} },
+				email: { txtExec: function() {
+					var	email	= prompt(this._("Enter the e-mail address:"), "@"),
+						text	= prompt(this._("Enter the displayed text:"), email) || email;
+					
+					if(email)
+						this.textEditorInsertText("[email=" + email + "]" + text + "[/email]");
+				} },
+				link: { txtExec: function() {
+					var	url	= prompt(this._("Enter the links URL:"), "http://"),
+						text	= prompt(this._("Enter the displayed text:"), url) || url;
+					
+					if(url)
+						this.textEditorInsertText("[url=" + url + "]" + text + "[/url]");
+				} },
+				quote: { txtExec: ["[quote]", "[/quote]"] },
+				youtube: { txtExec: function() {
+					var url = prompt(this._("Enter the YouTube video URL or ID:"));
+					
+					if(url)
+					{
+						if(url.indexOf("://") > -1)
+							url = url.replace(/^[^v]+v.(.{11}).*/,"$1");
+						
+						this.textEditorInsertText("[youtube]" + url + "[/youtube]");
+					}
+				} }
+			};
+
+			return $.extend(true, {}, merge, $.sceditor.commands);
+		};
+		
+		/**
+		 * Populates tagsToBbcodes and stylesToBbcodes to enable faster lookups
+		 * 
+		 * @private
+		 */
+		buildBbcodeCache = function() {
+			$.each(base.bbcodes, function(bbcode, info) {
+				if(typeof base.bbcodes[bbcode].tags !== "undefined")
+					$.each(base.bbcodes[bbcode].tags, function(tag, values) {
+						var isBlock = !!base.bbcodes[bbcode].isBlock;
+						tagsToBbcodes[tag] = (tagsToBbcodes[tag] || {});
+						tagsToBbcodes[tag][isBlock] = (tagsToBbcodes[tag][isBlock] || {});
+						tagsToBbcodes[tag][isBlock][bbcode] = values;
+					});
+
+				if(typeof base.bbcodes[bbcode].styles !== "undefined")
+					$.each(base.bbcodes[bbcode].styles, function(style, values) {
+						var isBlock = !!base.bbcodes[bbcode].isBlock;
+						stylesToBbcodes[isBlock] = (stylesToBbcodes[isBlock] || {});
+						stylesToBbcodes[isBlock][style] = (stylesToBbcodes[isBlock][style] || {});
+						stylesToBbcodes[isBlock][style][bbcode] = values;
+					});
+			});
+		};
+		
+		getStyle = function(element, property) {
+			var	name = $.camelCase(property),
+				$elm;
+
+			// add exception for align
+			if("text-align" === property)
+			{
+				$elm = $(element);
+				
+				if($elm.parent().css(property) !== $elm.css(property) &&
+					$elm.css('display') === "block" && !$elm.is('hr') && !$elm.is('th'))
+					return $elm.css(property);
+			}
+			
+			if(element.style)
+				return element.style[name];
+			
+			return null;
+		};
+
+		/**
+		 * Checks if any bbcode styles match the elements styles
+		 * 
+		 * @private
+		 * @return string Content with any matching bbcode tags wrapped around it.
+		 */
+		handleStyles = function(element, content, blockLevel) {
+			var	elementPropVal,
+				tag = element[0].nodeName.toLowerCase();
+
+			// convert blockLevel to boolean
+			blockLevel = !!blockLevel;
+			
+			if(!stylesToBbcodes[blockLevel])
+				return content;
+			
+			$.each(stylesToBbcodes[blockLevel], function(property, bbcodes) {
+				elementPropVal = getStyle(element[0], property);
+				if(elementPropVal == null || elementPropVal === "")
+					return;
+
+				// if the parent has the same style use that instead of this one
+				// so you dont end up with [i]parent[i]child[/i][/i]
+				if(getStyle(element.parent()[0], property) === elementPropVal)
+					return;
+
+				$.each(bbcodes, function(bbcode, values) {
+					if((element[0].childNodes.length === 0 || element[0].childNodes[0].nodeName.toLowerCase() === "br") &&
+						!base.bbcodes[bbcode].allowsEmpty)
+						return;
+					
+					if(values === null || $.inArray(elementPropVal.toString(), values) > -1) {
+						if($.isFunction(base.bbcodes[bbcode].format))
+							content = base.bbcodes[bbcode].format.call(base, element, content);
+						else
+							content = formatString(base.bbcodes[bbcode].format, content);
+					}
+				});
+			});
+
+			return content;
+		};
+
+		/**
+		 * Handles a HTML tag and finds any matching bbcodes
+		 * 
+		 * @private
+		 * @param	jQuery element	element		The element to convert
+		 * @param	string			content		The Tags text content
+		 * @param	bool			blockLevel	If to convert block level tags
+		 * @return	string	Content with any matching bbcode tags wrapped around it.
+		 */
+		handleTags = function(element, content, blockLevel) {
+			var tag = element[0].nodeName.toLowerCase();
+			
+			// convert blockLevel to boolean
+			blockLevel = !!blockLevel;
+
+			if(tagsToBbcodes[tag] && tagsToBbcodes[tag][blockLevel]) {
+				// loop all bbcodes for this tag
+				$.each(tagsToBbcodes[tag][blockLevel], function(bbcode, bbcodeAttribs) {
+					if(!base.bbcodes[bbcode].allowsEmpty &&
+						(element[0].childNodes.length === 0 || (element[0].childNodes[0].nodeName.toLowerCase() === "br" && element[0].childNodes.length === 1))						)
+						return;
+					
+					// if the bbcode requires any attributes then check this has
+					// all needed
+					if(bbcodeAttribs !== null) {
+						var runBbcode = false;
+
+						// loop all the bbcode attribs
+						$.each(bbcodeAttribs, function(attrib, values)
+						{
+							// check if has the bbcodes attrib
+							if(element.attr(attrib) == null)
+								return;
+
+							// if the element has the bbcodes attribute and the bbcode attribute
+							// has values check one of the values matches
+							if(values !== null && $.inArray(element.attr(attrib), values) < 0)
+								return;
+
+							// break this loop as we have matched this bbcode
+							runBbcode = true;
+							return false;
+						});
+
+						if(!runBbcode)
+							return;
+					}
+
+					if($.isFunction(base.bbcodes[bbcode].format))
+						content = base.bbcodes[bbcode].format.call(base, element, content);
+					else
+						content = formatString(base.bbcodes[bbcode].format, content);
+				});
+			}
+			
+			// add newline after paragraph elements p and div (WebKit uses divs) and br tags
+			if(blockLevel && /^(br|div|p)$/.test(tag))
+			{
+				var parentChildren = element[0].parentNode.childNodes;
+
+				// if it's a <p><br /></p> the paragraph will put the newline so skip the br
+				if(!("br" === tag && parentChildren.length === 1) &&
+					!("br" === tag && parentChildren[parentChildren.length-1] === element[0])) {
+					content += "\n";
+				}
+
+				// needed for browsers that enter textnode then when return is pressed put the rest in a div, i.e.:
+				// text<div>line 2</div>
+				if("br" !== tag && !$.sceditor.dom.isInline(element.parent()[0]) && element[0].previousSibling &&
+					element[0].previousSibling.nodeType === 3) {
+					content = "\n" + content;
+				}
+			}
+
+			return content;
+		};
+
+		/**
+		 * Formats a string in the format
+		 * {0}, {1}, {2}, ect. with the params provided
+		 * @private
+		 * @return string
+		 */
+		formatString = function() {
+			var args = arguments;
+			return args[0].replace(/\{(\d+)\}/g, function(str, p1) {
+				return typeof args[p1-0+1] !== "undefined"? 
+						args[p1-0+1] :
+						'{' + p1 + '}';
+			});
+		};
+
+		/**
+		 * Removes any leading or trailing quotes ('")
+		 *
+		 * @return string
+		 */
+		base.stripQuotes = function(str) {
+			return str.replace(/^["']+/, "").replace(/["']+$/, "");
+		};
+
+		/**
+		 * Converts HTML to BBCode
+		 * @param string	html	Html string, this function ignores this, it works off domBody
+		 * @param HtmlElement	domBody	Editors dom body object to convert
+		 * @return string BBCode which has been converted from HTML 
+		 */
+		base.getHtmlHandler = function(html, domBody) {
+			$.sceditor.dom.removeWhiteSpace(domBody[0]);
+			
+			return $.trim(base.elementToBbcode(domBody));
+		};
+
+		/**
+		 * Converts a HTML dom element to BBCode starting from
+		 * the innermost element and working backwards
+		 * 
+		 * @private
+		 * @param HtmlElement	element		The element to convert to BBCode
+		 * @param array			vChildren	Valid child tags allowed
+		 * @return string BBCode
+		 */
+		base.elementToBbcode = function($element) {
+			return (function toBBCode(node, vChildren) {
+				var ret = '';
+
+				$.sceditor.dom.traverse(node, function(node) {
+					var	$node		= $(node),
+						curTag		= '',
+						tag		= node.nodeName.toLowerCase(),
+						vChild		= validChildren[tag],
+						isValidChild	= true;
+					
+					if(typeof vChildren === 'object')
+					{
+						isValidChild = $.inArray(tag, vChildren) > -1;
+
+						// if this tag is one of the parents allowed children
+						// then set this tags allowed children to whatever it allows,
+						// otherwise set to what the parent allows
+						if(!isValidChild)
+							vChild = vChildren;
+					}
+					
+					// 3 is text element
+					if(node.nodeType !== 3)
+					{
+						// skip ignored elments
+						if($node.hasClass("sceditor-ignore"))
+							return;
+
+						// don't loop inside iframes
+						if(tag !== 'iframe')
+							curTag = toBBCode(node, vChild);
+						
+						if(isValidChild)
+						{
+							// code tags should skip most styles
+							if(!$node.is('code'))
+							{
+								// handle inline bbcodes
+								curTag = handleStyles($node, curTag);
+								curTag = handleTags($node, curTag);
+								
+								// handle blocklevel bbcodes
+								curTag = handleStyles($node, curTag, true);
+							}
+							
+							ret += handleTags($node, curTag, true);
+						}
+						else
+							ret += curTag;
+					}
+					else if(node.wholeText && (!node.previousSibling || node.previousSibling.nodeType !== 3))
+					{
+						if($(node).parents('code').length === 0)
+							ret += node.wholeText.replace(/ +/g, " ");
+						else
+							ret += node.wholeText;
+					}
+					else if(!node.wholeText)
+						ret += node.nodeValue;
+				}, false, true);
+				
+				return ret;
+			}($element.get(0)));
+		};
+
+		/**
+		 * Converts BBCode to HTML
+		 * 
+		 * @param {String} text
+		 * @param {Bool} isPaste
+		 * @return {String} HTML
+		 */
+		base.getTextHandler = function(text, isPaste) {
+			var	oldText, replaceBBCodeFunc,
+				bbcodeRegex = /\[([^\[\s=]*?)(?:([^\[]*?))?\]((?:[\s\S(?!=\[\\\1)](?!\[\1))*?)\[\/(\1)\]/g,
+				atribsRegex = /(\S+)=((?:(?:(["'])(?:\\\3|[^\3])*?\3))|(?:[^'"\s]+))/g;
+
+			replaceBBCodeFunc = function(str, bbcode, attrs, content)
+			{
+				var	attrsMap = {},
+					matches;
+					
+				if(attrs)
+				{
+					attrs = $.trim(attrs);
+					
+					// if only one attribute then remove the = from the start and strip any quotes
+					if((attrs.charAt(0) === "=" && (attrs.split("=").length - 1) <= 1) || bbcode === 'url')
+						attrsMap.defaultAttr = base.stripQuotes(attrs.substr(1));
+					else
+					{
+						if(attrs.charAt(0) === "=")
+							attrs = "defaultAttr" + attrs;
+
+						while((matches = atribsRegex.exec(attrs)))
+							attrsMap[matches[1].toLowerCase()] = base.stripQuotes(matches[2]);
+					}
+				}
+
+				if(!base.bbcodes[bbcode])
+					return str;
+
+				if($.isFunction(base.bbcodes[bbcode].html))
+					return base.bbcodes[bbcode].html.call(base, bbcode, attrsMap, content);
+				else
+					return formatString(base.bbcodes[bbcode].html, content);
+			};
+
+			text = text.replace(/&/g, "&amp;")
+					.replace(/</g, "&lt;")
+					.replace(/>/g, "&gt;")
+					.replace(/\r/g, "")
+					.replace(/(\[\/?(?:left|center|right|justify)\])\n/g, "$1")
+					.replace(/\n/g, "<br />");
+
+			while(text !== oldText)
+			{
+				oldText = text;
+				text    = text.replace(bbcodeRegex, replaceBBCodeFunc);
+			}
+
+			// As hr is the only bbcode not to have a start and end tag it's
+			// just being replace here instead of adding support for it above.
+			text = text.replace(/\[hr\]/gi, "<hr>");
+			
+			// replace multi-spaces which are not inside tags with a non-breaking space
+			// to preserve them. Otherwise they will just be converted to 1!
+			text = text.replace(/ {2}(?=([^<\>]*?<|[^<\>]*?$))/g, " &nbsp;");
+			
+			return wrapInDivs(text, isPaste);
+		};
+		
+		/**
+		 * Wraps divs around inline HTML. Needed for IE
+		 * 
+		 * @param string html
+		 * @return string HTML
+		 */
+		wrapInDivs = function(html, excludeFirstLast)
+		{
+			var	d		= document,
+				inlineFrag	= d.createDocumentFragment(),
+				outputDiv	= d.createElement('div'),
+				tmpDiv		= d.createElement('div'),
+				div, node, next, nodeName;
+			
+			$(tmpDiv).hide().appendTo(d.body);
+			tmpDiv.innerHTML = html;
+			
+			node = tmpDiv.firstChild;
+			while(node)
+			{
+				next = node.nextSibling;
+				nodeName = node.nodeName.toLowerCase();
+
+				if((node.nodeType === 1 && !$.sceditor.dom.isInline(node)) || nodeName === "br")
+				{
+					if(inlineFrag.childNodes.length > 0 || nodeName === "br")
+					{
+						div = d.createElement('div');
+						div.appendChild(inlineFrag);
+						
+						// Putting BR in a div in IE9 causes it to do a double line break,
+						// as much as I hate browser UA sniffing, to do feature detection would
+						// be more code than it's worth for this specific bug.
+						if(nodeName === "br" && (!$.sceditor.ie || $.sceditor.ie < 9))
+							div.appendChild(d.createElement('br'));
+						
+						outputDiv.appendChild(div);
+						inlineFrag = d.createDocumentFragment();
+					}
+					
+					if(nodeName !== "br")
+						outputDiv.appendChild(node);
+				}
+				else
+					inlineFrag.appendChild(node);
+					
+				node = next;
+			}
+			
+			if(inlineFrag.childNodes.length > 0)
+			{
+				div = d.createElement('div');
+				div.appendChild(inlineFrag);
+				outputDiv.appendChild(div);
+			}
+			
+			// needed for paste, the first shouldn't be wrapped in a div
+			if(excludeFirstLast)
+			{
+				node = outputDiv.firstChild;
+				if(node && node.nodeName.toLowerCase() === "div")
+				{
+					while((next = node.firstChild))
+						outputDiv.insertBefore(next, node);
+					
+					if($.sceditor.ie >= 9)
+						outputDiv.insertBefore(d.createElement('br'), node);
+					
+					outputDiv.removeChild(node);
+				}
+				
+				node = outputDiv.lastChild;
+				if(node && node.nodeName.toLowerCase() === "div")
+				{
+					while((next = node.firstChild))
+						outputDiv.insertBefore(next, node);
+
+					if($.sceditor.ie >= 9)
+						outputDiv.insertBefore(d.createElement('br'), node);
+					
+					outputDiv.removeChild(node);
+				}
+			}
+
+			$(tmpDiv).remove();
+			return outputDiv.innerHTML;
+		};
+
+		init();
+	};
+	
+	$.sceditorBBCodePlugin.bbcodes = {
+		// START_COMMAND: Bold
+		b: {
+			tags: {
+				b: null,
+				strong: null
+			},
+			styles: {
+				// 401 is for FF 3.5
+				"font-weight": ["bold", "bolder", "401", "700", "800", "900"]
+			},
+			format: "[b]{0}[/b]",
+			html: '<strong>{0}</strong>'
+		},
+		// END_COMMAND
+
+		// START_COMMAND: Italic
+		i: {
+			tags: {
+				i: null,
+				em: null
+			},
+			styles: {
+				"font-style": ["italic", "oblique"]
+			},
+			format: "[i]{0}[/i]",
+			html: '<em>{0}</em>'
+		},
+		// END_COMMAND
+
+		// START_COMMAND: Underline
+		u: {
+			tags: {
+				u: null
+			},
+			styles: {
+				"text-decoration": ["underline"]
+			},
+			format: "[u]{0}[/u]",
+			html: '<u>{0}</u>'
+		},
+		// END_COMMAND
+
+		// START_COMMAND: Strikethrough
+		s: {
+			tags: {
+				s: null,
+				strike: null
+			},
+			styles: {
+				"text-decoration": ["line-through"]
+			},
+			format: "[s]{0}[/s]",
+			html: '<s>{0}</s>'
+		},
+		// END_COMMAND
+
+		// START_COMMAND: Subscript
+		sub: {
+			tags: {
+				sub: null
+			},
+			format: "[sub]{0}[/sub]",
+			html: '<sub>{0}</sub>'
+		},
+		// END_COMMAND
+
+		// START_COMMAND: Superscript
+		sup: {
+			tags: {
+				sup: null
+			},
+			format: "[sup]{0}[/sup]",
+			html: '<sup>{0}</sup>'
+		},
+		// END_COMMAND
+
+		// START_COMMAND: Font
+		font: {
+			tags: {
+				font: {
+					face: null
+				}
+			},
+			styles: {
+				"font-family": null
+			},
+			format: function(element, content) {
+				if(element[0].nodeName.toLowerCase() === "font" && element.attr('face'))
+					return '[font=' + this.stripQuotes(element.attr('face')) + ']' + content + '[/font]';
+
+				return '[font=' + this.stripQuotes(element.css('font-family')) + ']' + content + '[/font]';
+			},
+			html: function(element, attrs, content) {
+				return '<font face="' + attrs.defaultAttr + '">' + content + '</font>';
+			}
+		},
+		// END_COMMAND
+
+		// START_COMMAND: Size
+		size: {
+			tags: {
+				font: {
+					size: null
+				}
+			},
+			styles: {
+				"font-size": null
+			},
+			format: function(element, content) {
+				var	fontSize = element.css('fontSize'),
+					size     = 1;
+
+				// Most browsers return px value but IE returns 1-7
+				if(fontSize.indexOf("px") > -1) {
+					// convert size to an int
+					fontSize = fontSize.replace("px", "") - 0;
+
+					if(fontSize > 12)
+						size = 2;
+					if(fontSize > 15)
+						size = 3;
+					if(fontSize > 17)
+						size = 4;
+					if(fontSize > 23)
+						size = 5;
+					if(fontSize > 31)
+						size = 6;
+					if(fontSize > 47)
+						size = 7;
+				}
+				else
+					size = fontSize;
+
+				return '[size=' + size + ']' + content + '[/size]';
+			},
+			html: function(element, attrs, content) {
+				return '<font size="' + attrs.defaultAttr + '">' + content + '</font>';
+			}
+		},
+		// END_COMMAND
+
+		// START_COMMAND: Color
+		color: {
+			tags: {
+				font: {
+					color: null
+				}
+			},
+			styles: {
+				color: null
+			},
+			format: function(element, content) {
+				/**
+				 * Converts CSS rgb value into hex
+				 * @private
+				 * @return string Hex color
+				 */
+				var rgbToHex = function(rgbStr) {
+					var m;
+		
+					function toHex(n) {
+						n = parseInt(n,10);
+						if(isNaN(n))
+							return "00";
+						n = Math.max(0,Math.min(n,255)).toString(16);
+		
+						return n.length<2 ? '0'+n : n;
+					}
+		
+					// rgb(n,n,n);
+					if((m = rgbStr.match(/rgb\((\d+),\s*?(\d+),\s*?(\d+)\)/i)))
+						return '#' + toHex(m[1]) + toHex(m[2]-0) + toHex(m[3]-0);
+		
+					// expand shorthand
+					if((m = rgbStr.match(/#([0-f])([0-f])([0-f])\s*?$/i)))
+						return '#' + m[1] + m[1] + m[2] + m[2] + m[3] + m[3];
+					
+					return rgbStr;
+				};
+		
+				var color = element.css('color');
+
+				if(element[0].nodeName.toLowerCase() === "font" && element.attr('color'))
+					color = element.attr('color');
+				
+				color = rgbToHex(color);
+
+				return '[color=' + color + ']' + content + '[/color]';
+			},
+			html: function(element, attrs, content) {
+				return '<font color="' + attrs.defaultAttr + '">' + content + '</font>';
+			}
+		},
+		// END_COMMAND
+
+		// START_COMMAND: Lists
+		ul: {
+			tags: {
+				ul: null
+			},
+			isBlock: true,
+			format: "[ul]{0}[/ul]",
+			html: '<ul>{0}</ul>'
+		},
+		list: {
+			html: '<ul>{0}</ul>'
+		},
+		ol: {
+			tags: {
+				ol: null
+			},
+			isBlock: true,
+			format: "[ol]{0}[/ol]",
+			html: '<ol>{0}</ol>'
+		},
+		li: {
+			tags: {
+				li: null
+			},
+			format: "[li]{0}[/li]",
+			html: '<li>{0}</li>'
+		},
+		"*": {
+			html: '<li>{0}</li>'
+		},
+		// END_COMMAND
+
+		// START_COMMAND: Table
+		table: {
+			tags: {
+				table: null
+			},
+			format: "[table]{0}[/table]",
+			html: '<table>{0}</table>'
+		},
+		tr: {
+			tags: {
+				tr: null
+			},
+			format: "[tr]{0}[/tr]",
+			html: '<tr>{0}</tr>'
+		},
+		th: {
+			tags: {
+				th: null
+			},
+			isBlock: true,
+			format: "[th]{0}[/th]",
+			html: '<th>{0}</th>'
+		},
+		td: {
+			tags: {
+				td: null
+			},
+			isBlock: true,
+			format: "[td]{0}[/td]",
+			html: '<td>{0}<br class="sceditor-ignore" /></td>'
+		},
+		// END_COMMAND
+
+		// START_COMMAND: Emoticons
+		emoticon: {
+			allowsEmpty: true,
+			tags: {
+				img: {
+					src: null,
+					"data-sceditor-emoticon": null
+				}
+			},
+			format: function(element, content) {
+				return element.attr('data-sceditor-emoticon') + content;
+			},
+			html: '{0}'
+		},
+		// END_COMMAND
+
+		// START_COMMAND: Horizontal Rule
+		horizontalrule: {
+			allowsEmpty: true,
+			tags: {
+				hr: null
+			},
+			format: "[hr]{0}",
+			html: "<hr />"
+		},
+		// END_COMMAND
+
+		// START_COMMAND: Image
+		img: {
+			allowsEmpty: true,
+			tags: {
+				img: {
+					src: null
+				}
+			},
+			format: function(element, content) {
+				// check if this is an emoticon image
+				if(typeof element.attr('data-sceditor-emoticon') !== "undefined")
+					return content;
+
+				var attribs = "=" + $(element).width() + "x" + $(element).height();
+
+				return '[img' + attribs + ']' + element.attr('src') + '[/img]';
+			},
+			html: function(element, attrs, content) {
+				var attribs = "", parts;
+
+				// handle [img width=340 height=240]url[/img]
+				if(typeof attrs.width !== "undefined")
+					attribs += ' width="' + attrs.width + '"';
+				if(typeof attrs.height !== "undefined")
+					attribs += ' height="' + attrs.height + '"';
+
+				// handle [img=340x240]url[/img]
+				if(typeof attrs.defaultAttr !== "undefined") {
+					parts = attrs.defaultAttr.split(/x/i);
+
+					attribs = ' width="' + parts[0] + '"' +
+						' height="' + (parts.length === 2 ? parts[1] : parts[0]) + '"';
+				}
+
+				return '<img ' + attribs + ' src="' + content + '" />';
+			}
+		},
+		// END_COMMAND
+
+		// START_COMMAND: URL
+		url: {
+			allowsEmpty: true,
+			tags: {
+				a: {
+					href: null
+				}
+			},
+			format: function(element, content) {
+				// make sure this link is not an e-mail, if it is return e-mail BBCode
+				if(element.attr('href').substr(0, 7) === 'mailto:')
+					return '[email=' + element.attr('href').substr(7) + ']' + content + '[/email]';
+
+				return '[url=' + decodeURI(element.attr('href')) + ']' + content + '[/url]';
+			},
+			html: function(element, attrs, content) {
+				if(typeof attrs.defaultAttr === "undefined" || attrs.defaultAttr.length === 0)
+					attrs.defaultAttr = content;
+
+				return '<a href="' + encodeURI(attrs.defaultAttr) + '">' + content + '</a>';
+			}
+		},
+		// END_COMMAND
+
+		// START_COMMAND: E-mail
+		email: {
+			html: function(element, attrs, content) {
+				if(typeof attrs.defaultAttr === "undefined")
+					attrs.defaultAttr = content;
+
+				return '<a href="mailto:' + attrs.defaultAttr + '">' + content + '</a>';
+			}
+		},
+		// END_COMMAND
+
+		// START_COMMAND: Quote
+		quote: {
+			tags: {
+				blockquote: null
+			},
+			isBlock: true,
+			format: function(element, content) {
+				var	attr = '';
+
+				if($(element).children("cite:first").length === 1) {
+					attr = '=' + $(element).children("cite:first").text();
+
+					content = '';
+					$(element).children("cite:first").remove();
+					content = this.elementToBbcode($(element));
+				}
+
+				return '[quote' + attr + ']' + content + '[/quote]';
+			},
+			html: function(element, attrs, content) {
+				if(typeof attrs.defaultAttr !== "undefined")
+					content = '<cite>' + attrs.defaultAttr + '</cite>' + content;
+
+				return '<blockquote>' + content + '</blockquote>';
+			}
+		},
+		// END_COMMAND
+
+		// START_COMMAND: Code
+		code: {
+			tags: {
+				code: null
+			},
+			isBlock: true,
+			format: "[code]{0}[/code]",
+			html: '<code>{0}</code>'
+		},
+		// END_COMMAND
+
+
+		// START_COMMAND: Left
+		left: {
+			styles: {
+				"text-align": ["left", "-webkit-left", "-moz-left", "-khtml-left"]
+			},
+			isBlock: true,
+			format: "[left]{0}[/left]",
+			html: '<div align="left">{0}</div>'
+		},
+		// END_COMMAND
+
+		// START_COMMAND: Centre
+		center: {
+			styles: {
+				"text-align": ["center", "-webkit-center", "-moz-center", "-khtml-center"]
+			},
+			isBlock: true,
+			format: "[center]{0}[/center]",
+			html: '<div align="center">{0}</div>'
+		},
+		// END_COMMAND
+
+		// START_COMMAND: Right
+		right: {
+			styles: {
+				"text-align": ["right", "-webkit-right", "-moz-right", "-khtml-right"]
+			},
+			isBlock: true,
+			format: "[right]{0}[/right]",
+			html: '<div align="right">{0}</div>'
+		},
+		// END_COMMAND
+
+		// START_COMMAND: Justify
+		justify: {
+			styles: {
+				"text-align": ["justify", "-webkit-justify", "-moz-justify", "-khtml-justify"]
+			},
+			isBlock: true,
+			format: "[justify]{0}[/justify]",
+			html: '<div align="justify">{0}</div>'
+		},
+		// END_COMMAND
+
+		// START_COMMAND: YouTube
+		youtube: {
+			allowsEmpty: true,
+			tags: {
+				iframe: {
+					'data-youtube-id': null
+				}
+			},
+			format: function(element, content) {
+				if(!element.attr('data-youtube-id'))
+					return content;
+
+				return '[youtube]' + element.attr('data-youtube-id') + '[/youtube]';
+			},
+			html: '<iframe width="560" height="315" src="http://www.youtube.com/embed/{0}?wmode=opaque' +
+				'" data-youtube-id="{0}" frameborder="0" allowfullscreen></iframe>'
+		},
+		// END_COMMAND
+		
+		// this is here so that commands above can be removed
+		// without having to remove the , after the last one.
+		// Needed for IE.
+		ignore: {}
+	};
+	
+	/**
+	 * Checks if a command with the specified name exists
+	 * 
+	 * @param string name
+	 * @return bool
+	 */
+	$.sceditorBBCodePlugin.commandExists = function(name) {
+		return typeof $.sceditorBBCodePlugin.bbcodes[name] !== "undefined";
+	};
+	
+	/**
+	 * Adds/updates a BBCode.
+	 * 
+	 * @param String		name		The BBCode name
+	 * @param Object		tags		Any html tags this bbcode applies to, i.e. strong for [b]
+	 * @param Object		styles		Any style properties this applies to, i.e. font-weight for [b]
+	 * @param String|Function	format		Function or string to convert the element into BBCode
+	 * @param String|Function	html		String or function to format the BBCode back into HTML.
+	 * @param BOOL			allowsEmpty	If this BBCodes is allowed to be empty, e.g. [b][/b]
+	 * @return Bool
+	 */
+	$.sceditorBBCodePlugin.setCommand = function(name, tags, styles, format, html, allowsEmpty) {
+		if(!name || !format || !html)
+			return false;
+		
+		if(!$.sceditorBBCodePlugin.commandExists(name))
+			$.sceditorBBCodePlugin.bbcodes[name] = {};
+
+		$.sceditorBBCodePlugin.bbcodes[name].format = format;
+		$.sceditorBBCodePlugin.bbcodes[name].html = html;
+		
+		if(tags)
+			$.sceditorBBCodePlugin.bbcodes[name].tags = tags;
+		
+		if(styles)
+			$.sceditorBBCodePlugin.bbcodes[name].styles = styles;
+			
+		if(allowsEmpty)
+			$.sceditorBBCodePlugin.bbcodes[name].allowsEmpty = allowsEmpty;
+		
+		return true;
+	};
+
+	$.fn.sceditorBBCodePlugin = function(options) {
+		return this.each(function() {
+			(new $.sceditorBBCodePlugin(this, options));
+		});
+	};
+})(jQuery);

+ 2518 - 0
Themes/default/scripts/jquery.sceditor.js

@@ -0,0 +1,2518 @@
+/**
+ * SCEditor v1.3.4
+ * http://www.samclarke.com/2011/07/sceditor/ 
+ *
+ * Copyright (C) 2011-2012, Sam Clarke (samclarke.com)
+ *
+ * SCEditor is dual licensed under the MIT and GPL licenses:
+ *	http://www.opensource.org/licenses/mit-license.php
+ *	http://www.gnu.org/licenses/gpl.html
+ */
+
+// ==ClosureCompiler==
+// @output_file_name jquery.sceditor.min.js
+// @compilation_level SIMPLE_OPTIMIZATIONS
+// ==/ClosureCompiler==
+
+/*jshint smarttabs: true, scripturl: true, jquery: true, devel:true, eqnull:true, curly: false */
+/*global XMLSerializer: true*/
+
+(function ($) {
+	'use strict';
+
+	$.sceditor = function (el, options) {
+		var base = this;
+
+		/**
+		 * The textarea element being replaced
+		 * @private
+		 */
+		var $textarea = $(el);
+		var textarea  = el;
+
+		/**
+		 * The div which contains the editor and toolbar
+		 * @private
+		 */
+		var editorContainer = null;
+
+		/**
+		 * The editors toolbar
+		 * @private
+		 */
+		var $toolbar = null;
+
+		/**
+		 * The editors iframe which should be in design mode
+		 * @private
+		 */
+		var $wysiwygEditor = null;
+		var wysiwygEditor  = null;
+
+		/**
+		 * The editors textarea for viewing source
+		 * @private
+		 */
+		var $textEditor = null;
+		var textEditor  = null;
+
+		/**
+		 * The current dropdown
+		 * @private
+		 */
+		var $dropdown               = null;
+		var dropdownIgnoreLastClick = false;
+
+		/**
+		 * Array of all the commands key press functions
+		 * @private
+		 */
+		var keyPressFuncs = [];
+
+		/**
+		 * Store the last cursor position. Needed for IE because it forgets
+		 * @private
+		 */
+		var lastRange = null;
+		
+		/**
+		 * The editors locale
+		 * @private
+		 */
+		var locale = null;
+
+		/**
+		 * Stores a cache of preloaded images
+		 * @private
+		 */
+		var preLoadCache = [];
+		
+		var rangeHelper = null;
+
+		var	init,
+			replaceEmoticons,
+			handleCommand,
+			saveRange,
+			handlePasteEvt,
+			handlePasteData,
+			handleKeyPress,
+			handleKeyUp,
+			handleMouseDown,
+			initEditor,
+			initToolBar,
+			initKeyPressFuncs,
+			initResize,
+			documentClickHandler,
+			formSubmitHandler,
+			preLoadEmoticons,
+			getWysiwygDoc,
+			handleWindowResize,
+			setHeight,
+			setWidth,
+			initLocale;
+
+		/**
+		 * All the commands supported by the editor
+		 */
+		base.commands = $.extend({}, (options.commands || $.sceditor.commands));
+
+		/**
+		 * Initializer. Creates the editor iframe and textarea
+		 * @private
+		 */
+		init = function () {
+			$textarea.data("sceditor", base);
+			base.options = $.extend({}, $.sceditor.defaultOptions, options);
+
+			// Load locale
+			if(base.options.locale !== null && base.options.locale !== "en")
+				initLocale();
+			
+			if(base.options.height !== null)
+				$textarea.height(base.options.height);
+			if(base.options.width !== null)
+				$textarea.width(base.options.width);
+
+			// if either width or height are % based, add the resize handler to update the editor
+			// when the window is resized
+			if((base.options.height !== null && base.options.height.toString().indexOf("%") > -1) ||
+				(base.options.width !== null && base.options.width.toString().indexOf("%") > -1))
+				$(window).resize(handleWindowResize);
+
+			editorContainer = $('<div class="sceditor-container" />')
+				.width($textarea.outerWidth())
+				.height($textarea.outerHeight());
+			$textarea.after(editorContainer);
+
+			// create the editor 
+			initToolBar();
+			initEditor();
+			initKeyPressFuncs();
+
+			if(base.options.resizeEnabled)
+				initResize();
+
+			$(document).click(documentClickHandler);
+
+			(textarea.form ? $(textarea.form) : $textarea.parents("form"))
+				.attr('novalidate','novalidate')
+				.submit(formSubmitHandler);
+			
+			// prefix emoticon root to emoticon urls
+			if(base.options.emoticonsRoot && base.options.emoticons)
+			{
+				$.each(base.options.emoticons, function (idx, emoticons) {
+					$.each(emoticons, function (key, url) {
+						base.options.emoticons[idx][key] = base.options.emoticonsRoot + url;
+					});
+				});
+			}
+
+			// load any textarea value into the editor
+			var val = $textarea.hide().val();
+
+			// Pass the value though the getTextHandler if it is set so that
+			// BBCode, ect. can be converted
+			if(base.options.getTextHandler)
+				val = base.options.getTextHandler(val);
+
+			base.setWysiwygEditorValue(val);
+			if(base.options.toolbar.indexOf('emoticon') !== -1)
+				preLoadEmoticons();
+		};
+
+		/**
+		 * Creates the editor iframe and textarea
+		 * @private
+		 */
+		initEditor = function () {
+			var	contentEditable = $('<div contenteditable="true">')[0].contentEditable,
+				contentEditableSupported = typeof contentEditable !== 'undefined' && contentEditable !== 'inherit',
+				$doc, $body;
+
+			$textEditor = $('<textarea></textarea>').hide();
+			$wysiwygEditor = $('<iframe frameborder="0"></iframe>');
+
+			if(window.location.protocol === "https:")
+				$wysiwygEditor.attr("src", "javascript:false");
+
+			// add the editor to the HTML and store the editors element
+			editorContainer.append($wysiwygEditor).append($textEditor);
+			wysiwygEditor	= $wysiwygEditor[0];
+			textEditor	= $textEditor[0];
+
+			setWidth($textarea.width());
+			setHeight($textarea.height());
+
+			// turn on design mode if contenteditable not supported
+			if(!contentEditableSupported)
+				getWysiwygDoc().designMode = 'On';
+			
+			getWysiwygDoc().open();
+			getWysiwygDoc().write(
+				'<html><head><!--[if gte IE 9]><style>* {min-height: auto !important}</style><![endif]-->' +
+				'<meta http-equiv="Content-Type" content="text/html;charset=' + base.options.charset + '" />' +
+				'<link rel="stylesheet" type="text/css" href="' + base.options.style + '" />' +
+				'</head><body contenteditable="true"></body></html>'
+			);
+			getWysiwygDoc().close();
+			
+			// turn on design mode if contenteditable not supported
+			if(!contentEditableSupported)
+				getWysiwygDoc().designMode = 'On';
+
+			$doc = $(getWysiwygDoc());
+			$body = $doc.find("body");
+			// set the key press event
+			$body.keypress(handleKeyPress);
+			$body.keyup(handleKeyUp);
+			$doc.keypress(handleKeyPress);
+			$doc.keyup(handleKeyUp);
+			$doc.mousedown(handleMouseDown);
+			$doc.bind("beforedeactivate keyup", saveRange);
+			$doc.focus(function() {
+				lastRange = null;
+			});
+			
+			if(base.options.enablePasteFiltering)
+				$body.bind("paste", handlePasteEvt);
+
+			rangeHelper = new $.sceditor.rangeHelper(wysiwygEditor.contentWindow);
+		};
+
+		/**
+		 * Creates the toolbar and appends it to the container
+		 * @private
+		 */
+		initToolBar = function () {
+			var buttonClick = function (e) {
+				handleCommand($(this), base.commands[$(this).data("sceditor-command")]);
+				e.preventDefault();
+			};
+
+			$toolbar   = $('<div class="sceditor-toolbar" />');
+			var	groups = base.options.toolbar.split("|"),
+				group, buttons, accessibilityName, button, i;
+
+			for (i=0; i < groups.length; i++) {
+				group   = $('<div class="sceditor-group" />');
+				buttons = groups[i].split(",");
+
+				for (var x=0; x < buttons.length; x++) {
+					// the button must be a valid command otherwise ignore it
+					if(!base.commands.hasOwnProperty(buttons[x]))
+						continue;
+
+					accessibilityName = base.commands[buttons[x]].tooltip ? base._(base.commands[buttons[x]].tooltip) : buttons[x];
+					
+					button = $('<a class="sceditor-button sceditor-button-' + buttons[x] +
+						' " unselectable="on"><div unselectable="on">' + accessibilityName + '</div></a>');
+
+					if(base.commands[buttons[x]].hasOwnProperty("tooltip"))
+						button.attr('title', base._(base.commands[buttons[x]].tooltip));
+						
+					if(base.commands[buttons[x]].exec)
+						button.data('sceditor-wysiwygmode', true);
+					else
+						button.addClass('disabled');
+						
+					if(base.commands[buttons[x]].txtExec)
+						button.data('sceditor-txtmode', true);
+
+					// add the click handler for the button
+					button.data("sceditor-command", buttons[x]);
+					button.click(buttonClick);
+
+					group.append(button);
+				}
+
+				$toolbar.append(group);
+			}
+
+			// append the toolbar to the toolbarContainer option if given
+			if(base.options.toolbarContainer === null)
+				editorContainer.append($toolbar);
+			else
+				$(base.options.toolbarContainer).append($toolbar);
+		};
+
+		/**
+		 * Creates an array of all the key press functions
+		 * like emoticons, ect.
+		 * @private
+		 */
+		initKeyPressFuncs = function () {
+			$.each(base.commands, function (command, values) {
+				if(typeof values.keyPress !== "undefined")
+					keyPressFuncs.push(values.keyPress);
+			});
+		};
+
+		setWidth = function (width) {
+			editorContainer.width(width);
+
+			// fix the height and width of the textarea/iframe
+			$wysiwygEditor.width(width);
+			$wysiwygEditor.width(width + (width - $wysiwygEditor.outerWidth(true)));
+
+			$textEditor.width(width);
+			$textEditor.width(width + (width - $textEditor.outerWidth(true)));
+		};
+
+		setHeight = function (height) {
+			editorContainer.height(height);
+
+			height = height - (base.options.toolbarContainer === null?$toolbar.outerHeight():0);
+
+			// fix the height and width of the textarea/iframe
+			$wysiwygEditor.height(height);
+			$wysiwygEditor.height(height + (height - $wysiwygEditor.outerHeight(true)));
+
+			$textEditor.height(height);
+			$textEditor.height(height + (height - $textEditor.outerHeight(true)));
+		};
+
+		/**
+		 * Creates the resizer.
+		 * @private
+		 */
+		initResize = function () {
+			var	$grip		= $('<div class="sceditor-grip" />'),
+				// cover is used to cover the editor iframe so document still gets mouse move events
+				$cover		= $('<div class="sceditor-resize-cover" />'),
+				startX		= 0,
+				startY		= 0,
+				startWidth	= 0,
+				startHeight	= 0,
+				origWidth	= editorContainer.width(),
+				origHeight	= editorContainer.height(),
+				dragging	= false,
+				minHeight, maxHeight, minWidth, maxWidth, mouseMoveFunc;
+
+			minHeight = (base.options.resizeMinHeight == null ?
+					origHeight / 2 :
+					base.options.resizeMinHeight);
+
+			maxHeight = (base.options.resizeMaxHeight == null ?
+					origHeight * 2 :
+					base.options.resizeMaxHeight);
+
+			minWidth = (base.options.resizeMinWidth == null ?
+					origWidth / 2 :
+					base.options.resizeMinWidth);
+
+			maxWidth = (base.options.resizeMaxWidth == null ?
+					origWidth * 2 :
+					base.options.resizeMaxWidth);
+
+			mouseMoveFunc = function (e) {
+				var	newHeight = startHeight + (e.pageY - startY),
+					newWidth  = startWidth  + (e.pageX - startX);
+
+				if (newWidth >= minWidth && (maxWidth < 0 || newWidth <= maxWidth))
+					setWidth(newWidth);
+
+				if (newHeight >= minHeight && (maxHeight < 0 || newHeight <= maxHeight))
+					setHeight(newHeight);
+
+				e.preventDefault();
+			};
+
+			editorContainer.append($grip);
+			editorContainer.append($cover.hide());
+
+			$grip.mousedown(function (e) {
+				startX		= e.pageX;
+				startY		= e.pageY;
+				startWidth	= editorContainer.width();
+				startHeight	= editorContainer.height();
+				dragging	= true;
+
+				$cover.show();
+				$(document).bind('mousemove', mouseMoveFunc);
+				e.preventDefault();
+			});
+
+			$(document).mouseup(function (e) {
+				if(!dragging)
+					return;
+
+				dragging = false;
+				$cover.hide();
+
+				$(document).unbind('mousemove', mouseMoveFunc);
+				e.preventDefault();
+			});
+		};
+		
+		formSubmitHandler = function(e) {
+			base.updateTextareaValue();
+			
+			$(this).removeAttr('novalidate');
+			
+			if(this.checkValidity && !this.checkValidity())
+				e.preventDefault();
+			
+			$(this).attr('novalidate','novalidate');
+		};
+		
+		/**
+		 * Destroys the editor, removing all elements and
+		 * event handlers.
+		 */
+		base.destory = function () {
+			$(document).unbind('click', documentClickHandler);
+			$textarea.removeAttr('novalidate').unbind('submit', formSubmitHandler);
+			$(window).unbind('resize', handleWindowResize);
+			
+			editorContainer.remove();
+			editorContainer = null;
+			
+			$textarea.removeData("sceditor").show();
+		};
+
+		/**
+		 * Preloads the emoticon images
+		 * Idea from: http://engineeredweb.com/blog/09/12/preloading-images-jquery-and-javascript
+		 * @private
+		 */
+		preLoadEmoticons = function () {
+			var	emoticons = $.extend({}, base.options.emoticons.more, base.options.emoticons.dropdown, base.options.emoticons.hidden),
+				emoticon;
+
+			$.each(emoticons, function (key, url) {
+				emoticon	= document.createElement('img');
+				emoticon.src	= url;
+				preLoadCache.push(emoticon);
+			});
+		};
+
+		/**
+		 * Creates a menu item drop down
+		 * @param HTMLElement	menuItem	The button to align the drop down with
+		 * @param string	dropDownName	Used for styling the dropown, will be a class sceditor-dropDownName
+		 * @param string	content			The HTML content of the dropdown
+		 * @param bool		ieUnselectable	If to add the unselectable attribute to all the contents elements. Stops
+		 *									IE from deselecting the text in the editor
+		 */
+		base.createDropDown = function (menuItem, dropDownName, content, ieUnselectable) {
+			base.closeDropDown();
+			
+			// IE needs unselectable attr to stop it from unselecting the text in the editor.
+			// The editor can cope if IE does unselect the text it's just not nice.
+			if(ieUnselectable !== false) {
+				content = $(content);
+				content.find(':not(input,textarea)').filter(function() { return this.nodeType===1; }).attr('unselectable', 'on');
+			}
+			
+			var o_css = {
+				top: menuItem.offset().top,
+				left: menuItem.offset().left
+			};
+
+			$.extend(o_css, base.options.dropDownCss);
+
+			$dropdown = $('<div class="sceditor-dropdown sceditor-' + dropDownName + '" />').css(o_css).append(content);
+
+			//editorContainer.after($dropdown);
+			$dropdown.appendTo($('body'));
+			dropdownIgnoreLastClick = true;
+
+			// stop clicks within the dropdown from being handled
+			$dropdown.click(function (e) {
+				e.stopPropagation();
+			});
+		};
+
+		/**
+		 * Handles any document click and closes the dropdown if open
+		 * @private
+		 */
+		documentClickHandler = function (e) {
+			// ignore right clicks
+			if(!dropdownIgnoreLastClick && e.which !== 3)
+				base.closeDropDown();
+
+			dropdownIgnoreLastClick = false;
+		};
+		
+		handlePasteEvt = function(e) {
+			var	elm		= getWysiwygDoc().body,
+				checkCount	= 0,
+				pastearea	= elm.ownerDocument.createElement('div'),
+				prePasteContent	= elm.ownerDocument.createDocumentFragment();
+
+			rangeHelper.saveRange();
+			document.body.appendChild(pastearea);
+
+			if (e && e.clipboardData && e.clipboardData.getData)
+			{
+				var html, handled=true;
+		
+				if ((html = e.clipboardData.getData('text/html')) || (html = e.clipboardData.getData('text/plain')))
+					pastearea.innerHTML = html;
+				else
+					handled = false;
+		
+				if(handled)
+				{
+					handlePasteData(elm, pastearea);
+		
+					if (e.preventDefault)
+					{
+						e.stopPropagation();
+						e.preventDefault();
+					}
+		
+					return false;
+				}
+			}
+			
+			while(elm.firstChild)
+				prePasteContent.appendChild(elm.firstChild);
+			
+			function handlePaste(elm, pastearea) {
+				if (elm.childNodes.length > 0)
+				{
+					while(elm.firstChild)
+						pastearea.appendChild(elm.firstChild);
+					
+					while(prePasteContent.firstChild)
+						elm.appendChild(prePasteContent.firstChild);
+					
+					handlePasteData(elm, pastearea);
+				}
+				else
+				{
+					// Allow max 25 checks before giving up.
+					// Needed inscase empty input is posted or
+					// something gose wrong.
+					if(checkCount > 25)
+					{
+						while(prePasteContent.firstChild)
+							elm.appendChild(prePasteContent.firstChild);
+						
+						return;
+					}
+
+					++checkCount;
+					setTimeout(function () {
+						handlePaste(elm, pastearea);
+					}, 20);
+				}
+			}
+			handlePaste(elm, pastearea);
+			
+			base.focus();
+			
+			return true;
+		};
+		
+		/**
+		 * @param {Element} elm
+		 * @param {Element} pastearea
+		 */
+		handlePasteData = function(elm, pastearea) {
+			// fix any invalid nesting
+			$.sceditor.dom.fixNesting(pastearea);
+			
+			var pasteddata = pastearea.innerHTML;
+			
+			if(base.options.getHtmlHandler)
+				pasteddata = base.options.getHtmlHandler(pasteddata, $(pastearea));
+
+			pastearea.parentNode.removeChild(pastearea);
+
+			if(base.options.getTextHandler)
+				pasteddata = base.options.getTextHandler(pasteddata, true);
+
+			rangeHelper.restoreRange();
+			rangeHelper.insertHTML(pasteddata);
+		};
+
+		/**
+		 * Closes the current drop down
+		 * 
+		 * @param bool focus If to focus the editor on close
+		 */
+		base.closeDropDown = function (focus) {
+			if($dropdown !== null) {
+				$dropdown.remove();
+				$dropdown = null;
+			}
+			
+			if(focus === true)
+				base.focus();
+		};
+
+		/**
+		 * Gets the WYSIWYG editors document
+		 */
+		getWysiwygDoc = function () {
+			if (wysiwygEditor.contentDocument)
+				return wysiwygEditor.contentDocument;
+
+			if (wysiwygEditor.contentWindow && wysiwygEditor.contentWindow.document)
+				return wysiwygEditor.contentWindow.document;
+
+			if (wysiwygEditor.document)
+				return wysiwygEditor.document;
+
+			return null;
+		};
+
+
+		/**
+		 * Inserts HTML into WYSIWYG editor. If endHtml is defined and some text is selected the
+		 * selected text will be put inbetween html and endHtml. If endHtml isn't defined and some
+		 * text is selected it will be replaced by the HTML
+		 * 
+		 * The HTML can have only one root node, if it has more than one only the first will be used.
+		 * e.g. with: <b>test</b><i>test2</i> only <b>test</b> will be inserted. To fix this you could
+		 * do: <span><b>test</b><i>test2</i></span>
+		 * 
+		 * @param string html		The HTML to insert
+		 * @param string endHtml	If specified instead of the inserted HTML replacing the selected text the selected text
+		 *                          will be placed between html and endHtml. If there is no selected text html and endHtml will
+		 *                          be concated together.
+		 */
+		base.wysiwygEditorInsertHtml = function (html, endHtml, overrideCodeBlocking) {
+			base.focus();
+			
+			// don't apply to code elements
+			if(!overrideCodeBlocking && ($(rangeHelper.parentNode()).is('code') ||
+				$(rangeHelper.parentNode()).parents('code').length !== 0))
+				return;
+			
+			rangeHelper.insertHTML(html, endHtml);
+		};
+
+		/**
+		 * Like wysiwygEditorInsertHtml except it converts any HTML to text
+		 * @private
+		 */
+		base.wysiwygEditorInsertText = function (text) {
+			text = text.replace(/&/g, "&amp;")
+					.replace(/</g, "&lt;")
+					.replace(/>/g, "&gt;")
+					.replace(/ /g, "&nbsp;")
+					.replace(/\r\n|\r/g, "\n")
+					.replace(/\n/g, "<br />");
+			
+			base.wysiwygEditorInsertHtml(text);
+		};
+		
+		/**
+		 * Like wysiwygEditorInsertHtml but works on the
+		 * text editor instead
+		 * 
+		 * @param {String} text
+		 * @param {String} endText
+		 */
+		base.textEditorInsertText = function (text, endText) {
+			var range, start, end, txtLen;
+			
+			textEditor.focus();
+			
+			if(textEditor.selectionStart != null)
+			{
+				start = textEditor.selectionStart;
+				end = textEditor.selectionEnd;
+				txtLen = text.length;
+				
+				if(endText)
+					text += textEditor.value.substring(start, end) + endText;
+				
+				textEditor.value = textEditor.value.substring(0, start) + text + textEditor.value.substring(end, textEditor.value.length);
+				
+				if(endText)
+					textEditor.selectionStart = (start + text.length) - endText.length;
+				else
+					textEditor.selectionStart = start + text.length;
+				
+				textEditor.selectionEnd = textEditor.selectionStart;
+			}
+			else if(document.selection.createRange)
+			{
+				range = document.selection.createRange();
+				
+				if(endText)
+					text += range.text + endText;
+				
+				range.text = text;
+			}
+			else
+				textEditor.value += text + endText;
+			
+			textEditor.focus();
+		};
+		
+		/**
+		 * Gets the current rangeHelper instance
+		 */
+		base.getRangeHelper = function () {
+			return rangeHelper;
+		};
+
+		/**
+		 * Gets the WYSIWYG editors HTML which is between the body tags
+		 */
+		base.getWysiwygEditorValue = function (filter) {
+			var	$body = $wysiwygEditor.contents().find("body"),
+				html;
+			
+			// fix any invalid nesting
+			$.sceditor.dom.fixNesting($body.get(0));
+			html = $body.html();
+			
+			if(filter !== false && base.options.getHtmlHandler)
+				html = base.options.getHtmlHandler(html, $body);
+
+			return html;
+		};
+
+		/**
+		 * Gets the text editor value
+		 * @param bool filter If to run the returned string through the filter or if to return the raw value. Defaults to filter.
+		 */
+		base.getTextareaValue = function (filter) {
+			var val = $textEditor.val();
+
+			if(filter !== false && base.options.getTextHandler)
+				val = base.options.getTextHandler(val);
+
+			return val;
+		};
+
+		/**
+		 * Sets the WYSIWYG HTML editor value. Should only be the HTML contained within the body tags
+		 * @param bool filter If to run the returned string through the filter or if to return the raw value. Defaults to filter.
+		 */
+		base.setWysiwygEditorValue = function (value) {
+			getWysiwygDoc().body.innerHTML = replaceEmoticons(value);
+		};
+
+		/**
+		 * Sets the text editor value
+		 */
+		base.setTextareaValue = function (value) {
+			$textEditor.val(value);
+		};
+
+		/**
+		 * Updates the forms textarea value
+		 */
+		base.updateTextareaValue = function () {
+			if(base.inSourceMode())
+				$textarea.val(base.getTextareaValue(false));
+			else
+				$textarea.val(base.getWysiwygEditorValue());
+		};
+
+		/**
+		 * Replaces any emoticon codes in the passed HTML with their emoticon images
+		 * @private
+		 */
+		replaceEmoticons = function (html) {
+			if(base.options.toolbar.indexOf('emoticon') === -1)
+				return html;
+			
+			var emoticons = $.extend({}, base.options.emoticons.more, base.options.emoticons.dropdown, base.options.emoticons.hidden);
+
+			$.each(emoticons, function (key, url) {
+				// escape the key before using it as a regex
+				// and append the regex to only find emoticons outside
+				// of HTML tags
+				var	reg = $.sceditor.regexEscape(key) + "(?=([^\\<\\>]*?<(?!/code)|[^\\<\\>]*?$))",
+					group = '';
+				
+				// Make sure the emoticon is surrounded by whitespace or is at the start/end of a string or html tag
+				if(base.options.emoticonsCompat)
+				{
+					reg = "((>|^|\\s|\xA0|\u2002|\u2003|\u2009|&nbsp;))" + reg + "(?=(\\s|$|<|\xA0|\u2002|\u2003|\u2009|&nbsp;))";
+					group = '$1';
+				}
+
+				html = html.replace(
+					new RegExp(reg, 'gm'),
+					group + '<img src="' + url + '" data-sceditor-emoticon="' + key + '" alt="' + key + '" />'
+				);
+			});
+
+			return html;
+		};
+		
+		/**
+		 * If the editor is in source code mode
+		 * @return boolean
+		 */
+		base.inSourceMode = function () {
+			return $textEditor.is(':visible');
+		};
+
+		/**
+		 * Switches between the WYSIWYG and plain text modes
+		 */
+		base.toggleTextMode = function () {
+			if(base.inSourceMode())
+				base.setWysiwygEditorValue(base.getTextareaValue());
+			else
+				base.setTextareaValue(base.getWysiwygEditorValue());
+			
+			// enable all the buttons
+			$toolbar.find('.sceditor-button').removeClass('disabled');
+
+			lastRange = null;
+			$textEditor.toggle();
+			$wysiwygEditor.toggle();
+			
+			// diable any buttons that are not allowed for this mode
+			$toolbar.find('.sceditor-button').each(function () {
+				var button = $(this);
+				
+				if(base.inSourceMode() && !button.data('sceditor-txtmode'))
+					button.addClass('disabled');
+				else if (!base.inSourceMode() && !button.data('sceditor-wysiwygmode'))
+					button.addClass('disabled');
+			});
+		};
+
+		/**
+		 * Handles the passed command
+		 * @private
+		 */
+		handleCommand = function (caller, command) {
+			// check if in text mode and handle text commands
+			if(base.inSourceMode())
+			{
+				if(command.txtExec)
+				{
+					if($.isArray(command.txtExec))
+						base.textEditorInsertText.apply(base, command.txtExec);
+					else
+						command.txtExec.call(base, caller);
+				}
+				
+				return;
+			}
+			
+			if(!command.hasOwnProperty("exec"))
+				return;
+			
+			if($.isFunction(command.exec))
+				command.exec.call(base, caller);
+			else
+				base.execCommand (command.exec, command.hasOwnProperty("execParam") ? command.execParam : null);
+		};
+
+		/**
+		 * Fucuses the editors input area
+		 */
+		base.focus = function () {
+			if(!base.inSourceMode())
+			{
+				wysiwygEditor.contentWindow.focus();
+				
+				// Needed for IE < 9
+				if(lastRange !== null) {
+					rangeHelper.selectRange(lastRange);
+					
+					// remove the stored range after being set.
+					// If the editor loses focus it should be
+					// saved again.
+					lastRange = null;
+				}
+			}
+			else
+				textEditor.focus();
+		};
+
+		/**
+		 * Saves the current range. Needed for IE because it forgets
+		 * where the cursor was and what was selected
+		 * @private
+		 */
+		saveRange = function () {
+			/* this is only needed for IE */
+			if(!$.browser.msie)
+				return;
+
+			lastRange = rangeHelper.selectedRange();
+		};
+
+		/**
+		 * Executes a command on the WYSIWYG editor
+		 * 
+		 * @param string|function command
+		 * @param mixed param
+		 */
+		base.execCommand = function (command, param) {
+			var executed = false;
+			base.focus();
+
+			// don't apply any comannds to code elements
+			if($(rangeHelper.parentNode()).is('code') ||
+				$(rangeHelper.parentNode()).parents('code').length !== 0)
+				return;
+
+			if(getWysiwygDoc())
+			{
+				try
+				{
+					executed = getWysiwygDoc().execCommand(command, false, param);
+				}
+				catch (e){}
+			}
+
+			// show error if execution failed and an error message exists
+			if(!executed && typeof base.commands[command] !== "undefined" &&
+				typeof base.commands[command].errorMessage !== "undefined")
+				alert(base._(base.commands[command].errorMessage));
+		};
+		
+		/**
+		 * Handles any key press in the WYSIWYG editor
+		 * 
+		 * @private
+		 */
+		handleKeyPress = function (e) {
+			base.closeDropDown();
+			
+			var	selectedContainer = rangeHelper.parentNode(),
+				$selectedContainer = $(selectedContainer);
+
+			// "Fix" (ok it's a hack) for blocklevel elements being duplicated in some browsers when
+			// enter is pressed instead of inserting a newline
+			if(e.which === 13)
+			{
+				if($selectedContainer.is('code, blockquote') || $selectedContainer.parents('code, blockquote').length !== 0)
+				{
+					lastRange = null;
+					base.wysiwygEditorInsertHtml('<br />', null, true);
+					return false;
+				}
+			}
+
+			// make sure there is always a newline after code or quote tags
+			var d = getWysiwygDoc();
+			$.sceditor.dom.rTraverse(d.body, function(node) {
+				if((node.nodeType === 3 && node.nodeValue !== "") ||
+					node.nodeName.toLowerCase() === 'br') {
+					// this is the last text or br node, if its in a code or quote tag
+					// then add a newline after it
+					if($(node).parents('code, blockquote').length > 0)
+						$(d.body).append(d.createElement('br'));
+
+					return false;
+				}
+			}, true);
+
+			// don't apply to code elements
+			if($selectedContainer.is('code') || $selectedContainer.parents('code').length !== 0)
+				return;
+			
+			var i = keyPressFuncs.length;
+			while(i--)
+				keyPressFuncs[i].call(base, e, wysiwygEditor, $textEditor);
+		};
+		
+		handleKeyUp = function (e) {
+		};
+
+		/**
+		 * Handles any mousedown press in the WYSIWYG editor
+		 * @private
+		 */
+		handleMouseDown = function (e) {
+			base.closeDropDown();
+			lastRange = null;
+		};
+
+		/**
+		 * Handles the window resize event. Needed to resize then editor
+		 * when the window size changes in fluid deisgns.
+		 */
+		handleWindowResize = function () {
+			if(base.options.height !== null && base.options.height.toString().indexOf("%") > -1)
+				setHeight(editorContainer.parent().height() *
+					(parseFloat(base.options.height) / 100));
+
+			if(base.options.width !== null && base.options.width.toString().indexOf("%") > -1)
+				setWidth(editorContainer.parent().width() *
+					(parseFloat(base.options.width) / 100));
+		};
+		
+		/**
+		 * Translates the string into the locale language.
+		 * 
+		 * Replaces any {0}, {1}, {2}, ect. with the params provided.
+
+		 * @public
+		 * @return string
+		 */
+		base._ = function() {
+			var args = arguments;
+			
+			if(locale !== null && locale[args[0]])
+				args[0] = locale[args[0]];
+			
+			return args[0].replace(/\{(\d+)\}/g, function(str, p1) {
+				return typeof args[p1-0+1] !== 'undefined'? 
+						args[p1-0+1] :
+						'{' + p1 + '}';
+			});
+		};
+		
+		/**
+		 * Init the locale variable with the specified locale if possible
+		 * @private
+		 * @return void
+		 */
+		initLocale = function () {
+			if($.sceditor.locale[base.options.locale])
+				locale = $.sceditor.locale[base.options.locale];
+			else
+			{
+				var lang = base.options.locale.split("-");
+				
+				if($.sceditor.locale[lang[0]])
+					locale = $.sceditor.locale[lang[0]];
+			}
+			
+			if(locale !== null && locale.dateFormat)
+				base.options.dateFormat = locale.dateFormat;
+		};
+
+		// run the initializer
+		init();
+	};
+	
+	// ----------------------------------------------------------
+	// A short snippet for detecting versions of IE in JavaScript
+	// without resorting to user-agent sniffing
+	// ----------------------------------------------------------
+	// 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 ...
+	// ie < 9 // Anything less than IE9
+	// ----------------------------------------------------------
+	// UPDATE: Now using Live NodeList idea from @jdalton
+	// Source: https://gist.github.com/527683
+	$.sceditor.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;
+	
+	}());
+	
+	/**
+	 * Escapes a string so it's safe to use in regex
+	 * @param string str The strong to escape
+	 * @return string
+	 */
+	$.sceditor.regexEscape = function (str) {
+		return str.replace(/[\$\?\[\]\.\*\(\)\|]/g, "\\$&")
+			.replace("<", "&lt;")
+			.replace(">", "&gt;");
+	};
+
+	$.sceditor.locale = {};
+
+	$.sceditor.commands = {
+		// START_COMMAND: Bold
+		bold: {
+			exec: "bold",
+			tooltip: "Bold"
+		},
+		// END_COMMAND
+		// START_COMMAND: Italic
+		italic: {
+			exec: "italic",
+			tooltip: "Italic"
+		},
+		// END_COMMAND
+		// START_COMMAND: Underline
+		underline: {
+			exec: "underline",
+			tooltip: "Underline"
+		},
+		// END_COMMAND
+		// START_COMMAND: Strikethrough
+		strike: {
+			exec: "strikethrough",
+			tooltip: "Strikethrough"
+		},
+		// END_COMMAND
+		// START_COMMAND: Subscript
+		subscript: {
+			exec: "subscript",
+			tooltip: "Subscript"
+		},
+		// END_COMMAND
+		// START_COMMAND: Superscript
+		superscript: {
+			exec: "superscript",
+			tooltip: "Superscript"
+		},
+		// END_COMMAND
+
+		// START_COMMAND: Left
+		left: {
+			exec: "justifyleft",
+			tooltip: "Align left"
+		},
+		// END_COMMAND
+		// START_COMMAND: Centre
+		center: {
+			exec: "justifycenter",
+			tooltip: "Center"
+		},
+		// END_COMMAND
+		// START_COMMAND: Right
+		right: {
+			exec: "justifyright",
+			tooltip: "Align right"
+		},
+		// END_COMMAND
+		// START_COMMAND: Justify
+		justify: {
+			exec: "justifyfull",
+			tooltip: "Justify"
+		},
+		// END_COMMAND
+
+		// START_COMMAND: Font
+		font: {
+			exec: function (caller) {
+				var	editor  = this,
+					fonts   = editor.options.fonts.split(","),
+					content = $("<div />"),
+					clickFunc = function (e) {
+						editor.execCommand("fontname", $(this).data('sceditor-font'));
+						editor.closeDropDown(true);
+						e.preventDefault();
+					};
+
+				for (var i=0; i < fonts.length; i++) {
+					content.append(
+						$('<a class="sceditor-font-option" href="#"><font face="' + fonts[i] + '">' + fonts[i] + '</font></a>')
+							.data('sceditor-font', fonts[i])
+							.click(clickFunc));
+				}
+
+				editor.createDropDown(caller, "font-picker", content);
+			},
+			tooltip: "Font Name"
+		},
+		// END_COMMAND
+		// START_COMMAND: Size
+		size: {
+			exec: function (caller) {
+				var	editor    = this,
+					content   = $("<div />"),
+					clickFunc = function (e) {
+						editor.execCommand("fontsize", $(this).data('sceditor-fontsize'));
+						editor.closeDropDown(true);
+						e.preventDefault();
+					};
+
+				for (var i=1; i<= 7; i++) {
+					content.append(
+						$('<a class="sceditor-fontsize-option" href="#"><font size="' + i + '">' + i + '</font></a>')
+							.data('sceditor-fontsize', i)
+							.click(clickFunc));
+				}
+
+				editor.createDropDown(caller, "fontsize-picker", content);
+			},
+			tooltip: "Font Size"
+		},
+		// END_COMMAND
+		// START_COMMAND: Colour
+		color: {
+			exec: function (caller) {
+				var	editor			= this,
+					genColor		= {r: 255, g: 255, b: 255},
+					content			= $("<div />"),
+					colorColumns	= this.options.colors?this.options.colors.split("|"):new Array(21),
+					// IE is slow at string concation so use an array
+					html			= [],
+					htmlIndex		= 0;
+
+				for (var i=0; i < colorColumns.length; ++i) {
+					var colors = (typeof colorColumns[i] !== "undefined")?colorColumns[i].split(","):new Array(21);
+
+					html[htmlIndex++] = '<div class="sceditor-color-column">';
+
+					for (var x=0; x < colors.length; ++x) {
+						// use pre defined colour if can otherwise use the generated color
+						var color = (typeof colors[x] !== "undefined")?colors[x]:"#" + genColor.r.toString(16) + genColor.g.toString(16) + genColor.b.toString(16);
+
+						html[htmlIndex++] = '<a href="#" class="sceditor-color-option" style="background-color: '+color+'" data-color="'+color+'"></a>';
+
+						// calculate the next generated color
+						if(x%5===0)
+							genColor = {r: genColor.r, g: genColor.g-51, b: 255};
+						else
+							genColor = {r: genColor.r, g: genColor.g, b: genColor.b-51};
+					}
+
+					html[htmlIndex++] = '</div>';
+
+					// calculate the next generated color
+					if(i%5===0)
+						genColor = {r: genColor.r-51, g: 255, b: 255};
+					else
+						genColor = {r: genColor.r, g: 255, b: 255};
+				}
+
+				content.append(html.join(''))
+					.find('a')
+					.click(function (e) {
+						editor.execCommand("forecolor", $(this).attr('data-color'));
+						editor.closeDropDown(true);
+						e.preventDefault();
+					});
+
+				editor.createDropDown(caller, "color-picker", content);
+			},
+			tooltip: "Font Color"
+		},
+		// END_COMMAND
+		// START_COMMAND: Remove Format
+		removeformat: {
+			exec: "removeformat",
+			tooltip: "Remove Formatting"
+		},
+		// END_COMMAND
+
+		// START_COMMAND: Cut
+		cut: {
+			exec: "cut",
+			tooltip: "Cut",
+			errorMessage: "Your browser does not allow the cut command. Please use the keyboard shortcut Ctrl/Cmd-X"
+		},
+		// END_COMMAND
+		// START_COMMAND: Copy
+		copy: {
+			exec: "copy",
+			tooltip: "Copy",
+			errorMessage: "Your browser does not allow the copy command. Please use the keyboard shortcut Ctrl/Cmd-C"
+		},
+		// END_COMMAND
+		// START_COMMAND: Paste
+		paste: {
+			exec: "paste",
+			tooltip: "Paste",
+			errorMessage: "Your browser does not allow the paste command. Please use the keyboard shortcut Ctrl/Cmd-V"
+		},
+		// END_COMMAND
+		// START_COMMAND: Paste Text
+		pastetext: {
+			exec: function (caller) {
+				var	editor = this,
+					content = $(this._('<form><div><label for="txt">{0}</label> <textarea cols="20" rows="7" id="txt">' +
+						'</textarea></div></form>',
+						this._("Paste your text inside the following box:")
+					))
+					.submit(function () {return false;});
+
+				content.append($(this._('<div><input type="button" class="button" value="{0}" /></div>',
+					this._("Insert")
+				)).click(function (e) {
+					editor.wysiwygEditorInsertText($(this).parent("form").find("#txt").val());
+					editor.closeDropDown(true);
+					e.preventDefault();
+				}));
+
+				editor.createDropDown(caller, "pastetext", content);
+			},
+			tooltip: "Paste Text"
+		},
+		// END_COMMAND
+		// START_COMMAND: Bullet List
+		bulletlist: {
+			exec: "insertunorderedlist",
+			tooltip: "Bullet list"
+		},
+		// END_COMMAND
+		// START_COMMAND: Ordered List
+		orderedlist: {
+			exec: "insertorderedlist",
+			tooltip: "Numbered list"
+		},
+		// END_COMMAND
+
+		// START_COMMAND: Table
+		table: {
+			exec: function (caller) {
+				var	editor  = this,
+					content = $(this._(
+						'<form><div><label for="rows">{0}</label><input type="text" id="rows" value="2" /></div>' +
+							'<div><label for="cols">{1}</label><input type="text" id="cols" value="2" /></div></form>',
+						this._("Rows:"),
+						this._("Cols:")
+					))
+					.submit(function () {return false;});
+
+				content.append($(this._('<div><input type="button" class="button" value="{0}" /></div>',
+					this._("Insert")
+				)).click(function (e) {
+					var rows = $(this).parent("form").find("#rows").val() - 0,
+						cols = $(this).parent("form").find("#cols").val() - 0,
+						html = '<table>';
+
+					if(rows < 1 || cols < 1)
+						return;
+					
+					for (var row=0; row < rows; row++) {
+						html += '<tr>';
+						for (var col=0; col < cols; col++) {
+							if($.browser.msie)
+								html += '<td></td>';
+							else
+								html += '<td><br class="sceditor-ignore" /></td>';
+						}
+						html += '</tr>';
+					}
+					html += '</table>';
+
+					editor.wysiwygEditorInsertHtml(html);
+
+					editor.closeDropDown(true);
+					e.preventDefault();
+				}));
+
+				editor.createDropDown(caller, "inserttable", content);
+			},
+			tooltip: "Insert a table"
+		},
+		// END_COMMAND
+
+		// START_COMMAND: Horizontal Rule
+		horizontalrule: {
+			exec: "inserthorizontalrule",
+			tooltip: "Insert a horizontal rule"
+		},
+		// END_COMMAND
+
+		// START_COMMAND: Code
+		code: {
+			exec: function () {
+				this.wysiwygEditorInsertHtml('<code>', '<br /></code>');
+			},
+			tooltip: "Code"
+		},
+		// END_COMMAND
+
+		// START_COMMAND: Image
+		image: {
+			exec: function (caller) {
+				var	editor  = this,
+					content = $(this._('<form><div><label for="link">{0}</label> <input type="text" id="image" value="http://" /></div>' +
+						'<div><label for="width">{1}</label> <input type="text" id="width" size="2" /></div>' +
+						'<div><label for="height">{2}</label> <input type="text" id="height" size="2" /></div></form>',
+							this._("URL:"),
+							this._("Width (optional):"),
+							this._("Height (optional):")
+						))
+					.submit(function () {return false;});
+
+				content.append($(this._('<div><input type="button" class="button" value="Insert" /></div>',
+						this._("Insert")
+					)).click(function (e) {
+					var $form	= $(this).parent("form"),
+						val		= $form.find("#image").val(),
+						attrs	= '',
+						width,
+						height;
+
+					if((width = $form.find("#width").val()))
+						attrs += ' width="' + width + '"';
+					if((height = $form.find("#height").val()))
+						attrs += ' height="' + height + '"';
+
+					if(val && val !== "http://")
+						editor.wysiwygEditorInsertHtml('<img' + attrs + ' src="' + val + '" />');
+
+					editor.closeDropDown(true);
+					e.preventDefault();
+				}));
+
+				editor.createDropDown(caller, "insertimage", content);
+			},
+			tooltip: "Insert an image"
+		},
+		// END_COMMAND
+
+		// START_COMMAND: E-mail
+		email: {
+			exec: function (caller) {
+				var	editor  = this,
+					content = $(this._('<form><div><label for="email">{0}</label> <input type="text" id="email" value="" /></div></form>',
+						this._("E-mail:")
+					))
+					.submit(function () {return false;});
+
+				content.append($('<div><input type="button" class="button" value="Insert" /></div>').click(function (e) {
+					var val = $(this).parent("form").find("#email").val();
+
+					if(val)
+					{
+						// needed for IE to reset the last range
+						editor.focus();
+
+						if(!editor.getRangeHelper().selectedHtml())
+							editor.wysiwygEditorInsertHtml('<a href="' + 'mailto:' + val + '">' + val + '</a>');
+						else
+							editor.execCommand("createlink", 'mailto:' + val);
+					}
+
+					editor.closeDropDown(true);
+					e.preventDefault();
+				}));
+
+				editor.createDropDown(caller, "insertemail", content);
+			},
+			tooltip: "Insert an email"
+		},
+		// END_COMMAND
+
+		// START_COMMAND: Link
+		link: {
+			exec: function (caller) {
+				var	editor  = this,
+					content = $(this._('<form><div><label for="link">{0}</label> <input type="text" id="link" value="http://" /></div>' +
+							'<div><label for="des">{1}</label> <input type="text" id="des" value="" /></div></form>',
+						this._("URL:"),
+						this._("Description (optional):")
+					))
+					.submit(function () {return false;});
+
+				content.append($(
+					this._('<div><input type="button" class="button" value="{0}" /></div>',
+						this._("Insert")
+					)).click(function (e) {
+					var val = $(this).parent("form").find("#link").val(),
+						description = $(this).parent("form").find("#des").val();
+
+					if(val !== "" && val !== "http://") {
+						// needed for IE to reset the last range
+						editor.focus();
+
+						if(!editor.getRangeHelper().selectedHtml() || description)
+						{
+							if(!description)
+								description = val;
+							
+							editor.wysiwygEditorInsertHtml('<a href="' + val + '">' + description + '</a>');
+						}
+						else
+							editor.execCommand("createlink", val);
+					}
+
+					editor.closeDropDown(true);
+					e.preventDefault();
+				}));
+
+				editor.createDropDown(caller, "insertlink", content);
+			},
+			tooltip: "Insert a link"
+		},
+		// END_COMMAND
+
+		// START_COMMAND: Unlink
+		unlink: {
+			exec: "unlink",
+			tooltip: "Unlink"
+		},
+		// END_COMMAND
+
+
+		// START_COMMAND: Quote
+		quote: {
+			exec: function (caller, html, author) {
+				var	before	= '<blockquote>',
+					end	= '</blockquote>';
+
+				// if there is HTML passed set end to null so any selected
+				// text is replaced
+				if(html)
+				{
+					author = (author ? '<cite>' + author + '</cite>' : '');
+					
+					before = before + author + html + end + '<br />';
+					end = null;
+				}
+				// if not add a newline to the end of the inserted quote
+				else if(this.getRangeHelper().selectedHtml() === "")
+					end = '<br />' + end;
+				
+				this.wysiwygEditorInsertHtml(before, end);
+			},
+			tooltip: "Insert a Quote"
+		},
+		// END_COMMAND
+
+		// START_COMMAND: Emoticons
+		emoticon: {
+			exec: function (caller) {
+				var	appendEmoticon,
+					editor  = this,
+					content = $('<div />'),
+					line    = $('<div />');
+
+				appendEmoticon = function (code, emoticon) {
+					line.append($('<img />')
+							.attr({
+								src: emoticon,
+								alt: code
+							})
+							.click(function (e) {
+								var	start = '', end = '';
+								
+								if(editor.options.emoticonsCompat)
+								{
+									start = '<span> ';
+									end   = ' </span>';
+								}
+								
+								editor.wysiwygEditorInsertHtml(start + '<img src="' + $(this).attr("src") +
+									'" data-sceditor-emoticon="' + $(this).attr('alt') + '" />' + end);
+
+								editor.closeDropDown(true);
+								e.preventDefault();
+							})
+						);
+
+					if(line.children().length > 3) {
+						content.append(line);
+						line = $('<div />');
+					}
+				};
+
+				$.each(editor.options.emoticons.dropdown, appendEmoticon);
+
+				if(line.children().length > 0)
+					content.append(line);
+
+				if(typeof editor.options.emoticons.more !== "undefined") {
+					var more = $(this._('<a class="sceditor-more">{0}</a>', this._("More"))).click(function () {
+						var	emoticons	= $.extend({}, editor.options.emoticons.dropdown, editor.options.emoticons.more);
+							content		= $('<div />');
+							line		= $('<div />');
+
+						$.each(emoticons, appendEmoticon);
+
+						if(line.children().length > 0)
+							content.append(line);
+
+						editor.createDropDown(caller, "insertemoticon", content);
+					});
+
+					content.append(more);
+				}
+
+				editor.createDropDown(caller, "insertemoticon", content);
+			},
+			keyPress: function (e, wysiwygEditor)
+			{
+				// make sure emoticons command is in the toolbar before running
+				if(this.options.toolbar.indexOf('emoticon') === -1)
+					return;
+				
+				var	editor = this,
+					pos = 0,
+					curChar = String.fromCharCode(e.which);
+					
+				if(!editor.EmoticonsCache) {
+					editor.EmoticonsCache = [];
+					
+					$.each($.extend({}, editor.options.emoticons.more, editor.options.emoticons.dropdown, editor.options.emoticons.hidden), function(key, url) {
+						editor.EmoticonsCache[pos++] = [
+							key,
+							'<img src="' + url + '" data-sceditor-emoticon="' + key + '" alt="' + key + '" />'
+						];
+					});
+
+					editor.EmoticonsCache.sort(function(a, b){
+						return a[0].length - b[0].length;
+					});
+				}
+				
+				if(!editor.longestEmoticonCode)
+					editor.longestEmoticonCode = editor.EmoticonsCache[editor.EmoticonsCache.length - 1][0].length;
+				
+				if(editor.getRangeHelper().raplaceKeyword(editor.EmoticonsCache, true, true, editor.longestEmoticonCode, editor.options.emoticonsCompat, curChar))
+				{
+					if(/^\s$/.test(curChar) && editor.options.emoticonsCompat)
+						return true;
+					
+					e.preventDefault();
+					e.stopPropagation();
+					return false;
+				}
+			},
+			tooltip: "Insert an emoticon"
+		},
+		// END_COMMAND
+
+		// START_COMMAND: YouTube
+		youtube: {
+			exec: function (caller) {
+				var editor  = this;
+				var content = $(
+					this._('<form><div><label for="link">{0}</label> <input type="text" id="link" value="http://" /></div></form>',
+						this._("Video URL:")
+					))
+					.submit(function () {return false;});
+
+				content.append(
+					$(this._('<div><input type="button" class="button" value="{0}" /></div>',
+						this._("Insert")
+					))
+					.click(function (e) {
+						var val = $(this).parent("form").find("#link").val();
+	
+						if(val !== "" && val !== "http://") {
+							// See http://www.abovetopsecret.com/forum/thread270269/pg1
+							val = val.replace(/^[^v]+v.(.{11}).*/,"$1"); 
+							editor.wysiwygEditorInsertHtml('<iframe width="560" height="315" src="http://www.youtube.com/embed/' + val +
+								'?wmode=opaque" data-youtube-id="' + val + '" frameborder="0" allowfullscreen></iframe>');
+						}
+	
+						editor.closeDropDown(true);
+						e.preventDefault();
+					}));
+
+				editor.createDropDown(caller, "insertlink", content);
+			},
+			tooltip: "Insert a YouTube video"
+		},
+		// END_COMMAND
+
+		// START_COMMAND: Date
+		date: {
+			exec: function () {
+				var	now   = new Date(),
+					year  = now.getYear(),
+					month = now.getMonth()+1,
+					day   = now.getDate();
+
+				if(year < 2000)
+					year = 1900 + year;
+				if(month < 10)
+					month = "0" + month;
+				if(day < 10)
+					day = "0" + day;
+
+				this.wysiwygEditorInsertHtml('<span>' +
+					this.options.dateFormat.replace(/year/i, year).replace(/month/i, month).replace(/day/i, day) + 
+					'</span>');
+			},
+			tooltip: "Insert current date"
+		},
+		// END_COMMAND
+
+		// START_COMMAND: Time
+		time: {
+			exec: function () {
+				var	now   = new Date(),
+					hours = now.getHours(),
+					mins  = now.getMinutes(),
+					secs  = now.getSeconds();
+
+				if(hours < 10)
+					hours = "0" + hours;
+				if(mins < 10)
+					mins = "0" + mins;
+				if(secs < 10)
+					secs = "0" + secs;
+
+				this.wysiwygEditorInsertHtml('<span>' + hours + ':' + mins + ':' + secs + '</span>');
+			},
+			tooltip: "Insert current time"
+		},
+		// END_COMMAND
+
+
+		// START_COMMAND: Print
+		print: {
+			exec: "print",
+			tooltip: "Print"
+		},
+		// END_COMMAND
+
+		// START_COMMAND: Source
+		source: {
+			exec: function () {
+				this.toggleTextMode();
+			},
+			txtExec: function () {
+				this.toggleTextMode();
+			},
+			tooltip: "View source"
+		},
+		// END_COMMAND
+		
+		// this is here so that commands above can be removed
+		// without having to remove the , after the last one.
+		// Needed for IE.
+		ignore: {}
+	};
+	
+	/**
+	 * Range helper class
+	 */
+	$.sceditor.rangeHelper = function(w, d) {
+		var	win, doc,
+			isW3C		= true,
+			startMarker	= "sceditor-start-marker",
+			endMarker	= "sceditor-end-marker",
+			base		= this,
+			init, _createMarker, _getOuterText, _selectOuterText;
+	
+		/**
+		 * @constructor
+		 * @param Window window
+		 * @param Document document
+		 * @private
+		 */
+		init = function (window, document) {
+			doc	= document || window.contentDocument || window.document;
+			win	= window;
+			isW3C	= !!window.getSelection;
+		}(w, d);
+	
+		/**
+		 * Inserts HTML.
+		 *
+		 * If endHTML is specified the selected contents will be put between
+		 * html and endHTML.
+		 * @param string html
+		 * @param string endHTML
+		 */
+		base.insertHTML = function(html, endHTML) {
+			var node, endNode, div;
+	
+			if(endHTML)
+				html += base.selectedHtml() + endHTML;
+			
+			if(isW3C)
+			{
+				div		= doc.createElement('div');
+				node		= doc.createDocumentFragment();
+				div.innerHTML	= html;
+	
+				while(div.firstChild)
+					node.appendChild(div.firstChild);
+	
+				base.insertNode(node);
+			}
+			else
+				base.selectedRange().pasteHTML(html);
+		};
+	
+		/**
+		 * Inserts a DOM node.
+		 *
+		 * If endNode is specified the selected contents will be put between
+		 * node and endNode.
+		 * @param Node node
+		 * @param Node endNode
+		 */
+		base.insertNode = function(node, endNode) {
+			if(isW3C)
+			{
+				var	toInsert	= doc.createDocumentFragment(),
+					range		= base.selectedRange(),
+					selection, selectAfter;
+	
+				toInsert.appendChild(node);
+	
+				if(endNode)
+				{
+					toInsert.appendChild(range.extractContents());
+					toInsert.appendChild(endNode);
+				}
+	
+				selectAfter = toInsert.lastChild;
+				range.deleteContents();
+				range.insertNode(toInsert);
+	
+				selection = doc.createRange();
+				selection.setStartAfter(selectAfter);
+				base.selectRange(selection);
+			}
+			else
+				base.insertHTML(node.outerHTML, endNode?endNode.outerHTML:null);
+		};
+	
+		/**
+		 * Clones the selected Range
+		 * @return Range|TextRange
+		 */
+		base.cloneSelected = function() {
+			if(!isW3C)
+				return base.selectedRange().duplicate();
+	
+			return base.selectedRange().cloneRange();
+		};
+	
+		/**
+		 * Gets the selected Range
+		 * @return Range|TextRange
+		 */
+		base.selectedRange = function() {
+			var sel;
+	
+			if(win.getSelection)
+				sel = win.getSelection();
+			else
+				sel = doc.selection;
+	
+			if(sel.getRangeAt && sel.rangeCount <= 0)
+				sel.addRange(doc.createRange());
+	
+			if(!isW3C)
+				return sel.createRange();
+	
+			return sel.getRangeAt(0);
+		};
+	
+		/**
+		 * Gets the selected HTML
+		 * @return string
+		 */
+		base.selectedHtml = function() {
+			var range = base.selectedRange();
+	
+			if(!range)
+				return '';
+	
+			// IE9+ and all other browsers
+			if (window.XMLSerializer)
+				return new XMLSerializer().serializeToString(range.cloneContents());
+	
+			// IE < 9
+			if(!isW3C)
+			{
+				if(range.text !== '' && range.htmlText)
+					return range.htmlText;
+			}
+	
+			return '';
+		};
+		
+		base.parentNode = function() {
+			var range = base.selectedRange();
+			
+			if(isW3C)
+				return range.commonAncestorContainer;
+			else
+				return range.parentElement();
+		};
+	
+		/**
+		 * Inserts a node at either the start or end of the current selection
+		 * @param Bool start
+		 * @param Node node
+		 */
+		base.insertNodeAt = function(start, node) {
+			var range = base.cloneSelected();
+	
+			range.collapse(start);
+	
+			if(range.insertNode)
+				range.insertNode(node);
+			else
+				range.pasteHTML(node.outerHTML);
+		};
+	
+		/**
+		 * Creates a marker node
+		 * @param String id
+		 * @return Node
+		 */
+		_createMarker = function(id) {
+			base.removeMarker(id);
+	
+			var marker = doc.createElement("span");
+			marker.id = id;
+			marker.style.lineHeight	= "0";
+			marker.style.display	= "none";
+			marker.className	= "sceditor-selection";
+	
+			return marker;
+		};
+	
+		/**
+		 * Inserts start/end markers for the current selection
+		 */
+		base.insertMarkers = function() {
+			base.insertNodeAt(true, _createMarker(startMarker));
+			base.insertNodeAt(false, _createMarker(endMarker));
+		};
+	
+		/**
+		 * Gets the marker with the specified ID
+		 * @param String id
+		 * @return Node
+		 */
+		base.getMarker = function(id) {
+			return doc.getElementById(id);
+		};
+	
+		/**
+		 * Removes the marker with the specified ID
+		 * @param String id
+		 */
+		base.removeMarker = function(id) {
+			var marker = base.getMarker(id);
+	
+			if(marker)
+				marker.parentNode.removeChild(marker);
+		};
+	
+		/**
+		 * Removes the start/end markers
+		 */
+		base.removeMarkers = function() {
+			base.removeMarker(startMarker);
+			base.removeMarker(endMarker);
+		};
+	
+		/**
+		 * Saves the current range location
+		 */
+		base.saveRange = function() {
+			base.insertMarkers();
+		};
+	
+		/**
+		 * Selected the specified range
+		 * @param Range|TextRange range
+		 */
+		base.selectRange = function(range) {
+			if(!isW3C)
+				range.select();
+			else
+			{
+				win.getSelection().removeAllRanges();
+				win.getSelection().addRange(range);
+			}
+		};
+	
+		/**
+		 * Restores the last saved range if possible
+		 */
+		base.restoreRange = function() {
+			var	range	= base.selectedRange(),
+				start	= base.getMarker(startMarker),
+				end	= base.getMarker(endMarker);
+	
+			if(!start || !end)
+				return false;
+	
+			if(!isW3C)
+			{
+				range = doc.body.createTextRange();
+				var marker = doc.body.createTextRange();
+
+				marker.moveToElementText(start);
+				range.setEndPoint('StartToStart', marker);
+				range.moveStart('character', 0);
+
+				marker.moveToElementText(end);
+				range.setEndPoint('EndToStart', marker);
+				range.moveEnd('character', 0);
+	
+				base.selectRange(range);
+			}
+			else
+			{
+				range = doc.createRange();
+				range.setStartBefore(start);
+				range.setEndAfter(end);
+	
+				base.selectRange(range);
+			}
+	
+			base.removeMarkers();
+		};
+	
+		/**
+		 * Selects the text left and right of the current selection
+		 * @param int left
+		 * @param int right
+		 * @private
+		 */
+		_selectOuterText = function(left, right) {
+			var range = base.cloneSelected();
+
+			range.collapse(false);
+			if(!isW3C)
+			{
+				range.moveStart('character', 0-left);
+				range.moveEnd('character', right);
+			}
+			else
+			{
+				range.setStart(range.startContainer, range.startOffset-left);
+				range.setEnd(range.endContainer, range.endOffset+right);
+				//range.deleteContents();
+			}
+	
+			base.selectRange(range);
+		};
+	
+		/**
+		 * Gets the text left or right of the current selection
+		 * @param bool before
+		 * @param int length
+		 * @private
+		 */
+		_getOuterText = function(before, length) {
+			var	ret	= "",
+				range	= base.cloneSelected(),
+				node;
+	
+			range.collapse(false);
+			if(before)
+			{
+				if(!isW3C)
+				{
+					range.moveStart('character', 0-length);
+					ret = range.text;
+				}
+				else
+				{
+					ret = range.startContainer.textContent.substr(0, range.startOffset);
+					ret = ret.substr(Math.max(0, ret.length - length));
+				}
+			}
+			else
+			{
+				if(!isW3C)
+				{
+					range.moveEnd('character', length);
+					ret = range.text;
+				}
+				else
+					ret = range.startContainer.textContent.substr(range.startOffset, length);
+			}
+
+			return ret;
+		};
+	
+		/**
+		 * Replaces keys with values based on the current range
+		 * @param Array rep
+		 * @param Bool includePrev If to include text before or just text after
+		 * @param Bool repSorted If the keys array is pre sorted
+		 * @param Int longestKey Length of the longest key
+		 * @param Bool requireWhiteSpace If the key must be surrounded by whitespace
+		 */
+		base.raplaceKeyword = function(rep, includeAfter, repSorted, longestKey, requireWhiteSpace, curChar) {
+			if(!repSorted)
+				rep.sort(function(a, b){
+					return a.length - b.length;
+				});
+
+			var	maxKeyLen = longestKey || rep[rep.length-1][0].length,
+				before, after, str, i, start, left, pat, lookStart;
+
+			before = after = str = "";
+
+			if(requireWhiteSpace)
+			{
+				// forcing spaces around doesn't work with textRanges as they will select text
+				// on the other side of an image causing space-img-key to be returned as
+				// space-key which would be valid when it's not.
+				if(!isW3C)
+					return false;
+				
+				++maxKeyLen;
+			}
+
+			before = _getOuterText(true, maxKeyLen);
+	
+			if(includeAfter)
+				after	= _getOuterText(false, maxKeyLen);
+			
+			str	= before + (curChar!=null?curChar:"") + after;
+			i	= rep.length;
+			while(i--)
+			{
+				//pat = new RegExp("(?:^|\\s)" + $.sceditor.regexEscape(rep[i][0]) + "(?=\\s|$)");
+				pat = new RegExp("(?:[\\s\xA0\u2002\u2003\u2009])" + $.sceditor.regexEscape(rep[i][0]) + "(?=[\\s\xA0\u2002\u2003\u2009])");
+				lookStart = before.length - 1 - rep[i][0].length;
+				
+				if(requireWhiteSpace)
+					--lookStart;
+					
+				lookStart = Math.max(0, lookStart);
+
+				if((!requireWhiteSpace && (start = str.indexOf(rep[i][0], lookStart)) > -1) ||
+					(requireWhiteSpace && (start = str.substr(lookStart).search(pat)) > -1)) 
+				{
+					if(requireWhiteSpace)
+						start += lookStart + 1;
+					
+					// make sure the substr is between before and after not entierly in one
+					// or the other
+					if(start > before.length || start+rep[i][0].length + (requireWhiteSpace?1:0) < before.length)
+						continue;
+
+					left = before.length - start;
+					_selectOuterText(left, rep[i][0].length-left-(curChar!=null&&/^\S/.test(curChar)?1:0));
+					base.insertHTML(rep[i][1]);
+					return true;
+				}
+			}
+			
+			return false;
+		};
+	};
+
+	/**
+	 * Static DOM helper class
+	 */
+	$.sceditor.dom = {
+		/**
+		 * Loop all child nodes of the passed node
+		 * 
+		 * The function should accept 1 parameter being the node.
+		 * If the function returns false the loop will be exited.
+		 * 
+		 * @param HTMLElement	node
+		 * @param function		func			Function that is called for every node, should accept 1 param for the node
+		 * @param bool			innermostFirst	If the innermost node should be passed to the function before it's parents
+		 * @param bool			siblingsOnly	If to only traverse the nodes siblings
+		 * @param bool			reverse			If to traverse the nodes in reverse
+		 */
+		traverse: function(node, func, innermostFirst, siblingsOnly, reverse) {
+			if(node)
+			{
+				node = reverse ? node.lastChild : node.firstChild;
+				
+				while(node != null)
+				{
+					if(!innermostFirst && func(node) === false)
+						return false;
+					
+					// traverse all children
+					if(!siblingsOnly && this.traverse(node, func, innermostFirst, siblingsOnly, reverse) === false)
+						return false;
+					
+					if(innermostFirst && func(node) === false)
+						return false;
+					
+					// move to next child
+					node = reverse ? node.previousSibling : node.nextSibling;
+				}
+			}
+		},
+		
+		rTraverse: function(node, func, innermostFirst, siblingsOnly) {
+			this.traverse(node, func, innermostFirst, siblingsOnly, true);
+		},
+		
+		/**
+		 * Checks if an element is inline
+		 * 
+		 * @param bool includeInlineBlock If passed inline-block will count as an inline element instead of block
+		 * @return bool
+		 */
+		isInline: function(elm, includeInlineBlock) {
+			if(elm == null || elm.nodeType !== 1)
+				return true;
+			
+			var d = (window.getComputedStyle ? window.getComputedStyle(elm) : elm.currentStyle).display;
+
+			if(includeInlineBlock)
+				return d !== "block"; 
+			
+			return d === "inline"; 
+		},
+		
+		/**
+		 * Gets the next node. If the node has no siblings
+		 * it gets the parents next sibling, and so on untill
+		 * another element is found. If none are found
+		 * it returns null.
+		 * 
+		 * @param HTMLElement node
+		 * @return HTMLElement
+		 */
+		/*getNext: function(node) {
+			if(!node)
+				return null;
+			
+			var n = node.nextSibling;
+			if(n)
+				return n;
+			
+			return getNext(node.parentNode);
+		},*/
+		
+		copyCSS: function(from, to) {
+			to.style.cssText = from.style.cssText;
+		},
+		
+		/**
+		 * Fixes block level elements in inline elements.
+		 * 
+		 * @param HTMLElement The node to fix
+		 */
+		fixNesting: function(node) {
+			var	base = this,
+				getLastInlineParent = function(node) {
+					while(base.isInline(node.parentNode))
+						node = node.parentNode;
+					
+					return node;
+				};
+			
+			base.traverse(node, function(node) {
+				// if node is an element, and is blocklevel and the parent isn't block level
+				// then it needs fixing
+				if(node.nodeType === 1 && !base.isInline(node) && base.isInline(node.parentNode))
+				{
+					var	parent	= getLastInlineParent(node),
+						rParent	= parent.parentNode,
+						before	= base.extractContents(parent, node),
+						middle	= node;
+					
+					// copy current styling so when moved out of the parent
+					// it still has the same styling
+					base.copyCSS(middle, middle);
+					
+					rParent.insertBefore(before, parent);
+					rParent.insertBefore(middle, parent);
+				}
+			});
+		},
+		
+		/**
+		 * Finds the common parent of two nodes
+		 * 
+		 * @param HTMLElement node1
+		 * @param HTMLElement node2
+		 * @return HTMLElement
+		 */
+		findCommonAncestor: function(node1, node2) {
+			// not as fast as making two arrays of parents and comparing
+			// but is a lot smaller and as it's currently only used with
+			// fixing invalid nesting it doesn't need to be very fast
+			return $(node1).parents().has($(node2)).first();
+		},
+		
+		/**
+		 * Removes unused whitespace from the root and it's children
+		 * 
+		 * @param HTMLElement root
+		 * @return void
+		 */
+		removeWhiteSpace: function(root) {
+			// 00A0 is non-breaking space which should not be striped
+			var regex = /[^\S|\u00A0]+/g;
+
+			this.traverse(root, function(node) {
+				if(node.nodeType === 3 && $(node).parents('code, pre').length === 0)
+				{
+					if(!/\S|\u00A0/.test(node.nodeValue))
+						node.nodeValue = " ";
+					else if(regex.test(node.nodeValue))
+						node.nodeValue = node.nodeValue.replace(regex, " ");
+				}
+			});
+		},
+		
+		/**
+		 * Extracts all the nodes between the start and end nodes
+		 * 
+		 * @param HTMLElement startNode The node to start extracting at
+		 * @param HTMLElement endNode The node to stop extracting at
+		 * @return DocumentFragment
+		 */
+		extractContents: function(startNode, endNode) {
+			var	base		= this,
+				$commonAncestor	= base.findCommonAncestor(startNode, endNode),
+				commonAncestor	= $commonAncestor===null?null:$commonAncestor.get(0),
+				startReached	= false,
+				endReached	= false;
+
+			return (function extract(root) {
+				var df = startNode.ownerDocument.createDocumentFragment();
+				
+				base.traverse(root, function(node) {
+					// if end has been reached exit loop
+					if(endReached || (node === endNode && startReached))
+					{
+						endReached = true;
+						return false;
+					}
+
+					if(node === startNode)
+						startReached = true;
+
+					var c, n;
+					if(startReached)
+					{
+						// if the start has been reached and this elm contains
+						// the end node then clone it
+						if(jQuery.contains(node, endNode) && node.nodeType === 1)
+						{
+							c = extract(node);
+							n = node.cloneNode(false);
+
+							n.appendChild(c);
+							df.appendChild(n);
+						}
+						// otherwise just move it
+						else
+							df.appendChild(node);
+					}
+					// if this node contains the start node then add it
+					else if(jQuery.contains(node, startNode) && node.nodeType === 1)
+					{
+						c = extract(node);
+						n = node.cloneNode(false);
+
+						n.appendChild(c);
+						df.appendChild(n);
+					}
+				});
+
+				return df;
+			}(commonAncestor));
+		}
+	};
+	
+	/**
+	 * Checks if a command with the specified name exists
+	 * 
+	 * @param {String} name
+	 * @return Bool
+	 */
+	$.sceditor.commandExists = function(name) {
+		return typeof $.sceditor.commands[name] !== "undefined";
+	};
+	
+	/**
+	 * Adds/updates a command.
+	 * 
+	 * Only name and exec are required. Exec is only required if
+	 * the command dose not already exist.
+	 *  
+	 * @param {String}		name		The commands name
+	 * @param {String|Function}	exec		The commands exec function or string for the native execCommand
+	 * @param {String}		tooltip		The commands tooltip text
+	 * @param {Function}		keypress	Function that gets called every time a key is pressed
+	 * @param {Function|Array}	txtExec		Called when the command is executed in source mode or array containing prepend and optional append
+	 * @return Bool
+	 */
+	$.sceditor.setCommand = function(name, exec, tooltip, keypress, txtExec) {
+		if(!name || !($.sceditor.commandExists(name) || exec))
+			return false;
+
+		if(!$.sceditor.commandExists(name))
+			$.sceditor.commands[name] = {};
+
+		$.sceditor.commands[name].exec = exec;
+
+		if(tooltip)
+			$.sceditor.commands[name].tooltip = tooltip;
+
+		if(keypress)
+			$.sceditor.commands[name].keyPress = keypress;
+		
+		if(txtExec)
+			$.sceditor.commands[name].txtExec = txtExec;
+
+		return true;
+	};
+	
+	$.sceditor.defaultOptions = {
+		// Toolbar buttons order and groups. Should be comma seperated and have a bar | to seperate groups
+		toolbar:	"bold,italic,underline,strike,subscript,superscript|left,center,right,justify|" +
+				"font,size,color,removeformat|cut,copy,paste,pastetext|bulletlist,orderedlist|" +
+				"table|code,quote|horizontalrule,image,email,link,unlink|emoticon,youtube,date,time|" +
+				"print,source",
+
+		// Stylesheet to include in the WYSIWYG editor. Will style the WYSIWYG elements
+		style: "jquery.sceditor.default.css",
+
+		// Comma seperated list of fonts for the font selector
+		fonts: "Arial,Arial Black,Comic Sans MS,Courier New,Georgia,Impact,Sans-serif,Serif,Times New Roman,Trebuchet MS,Verdana",
+
+		// Colors should be comma seperated and have a bar | to signal a new column. If null the colors will be auto generated.
+		colors: null,
+		
+		locale: "en",
+		
+		charset: "utf-8",
+
+		// compatibility mode for if you have emoticons such as :/ This mode requires
+		// emoticons to be surrounded by whitespace or end of line chars. This mode
+		// has limited As You Type emoticon converstion support (end of line chars)
+		// are not accepted as whitespace so only emoticons surrounded by whitespace
+		// will work
+		emoticonsCompat: false,
+		emoticonsRoot: '',
+		emoticons:	{
+					dropdown: {
+						":)": "emoticons/smile.png",
+						":angel:": "emoticons/angel.png",
+						":angry:": "emoticons/angry.png",
+						"8-)": "emoticons/cool.png",
+						":'(": "emoticons/cwy.png",
+						":ermm:": "emoticons/ermm.png",
+						":D": "emoticons/grin.png",
+						"<3": "emoticons/heart.png",
+						":(": "emoticons/sad.png",
+						":O": "emoticons/shocked.png",
+						":P": "emoticons/tongue.png",
+						";)": "emoticons/wink.png"
+					},
+					more: {
+						":alien:": "emoticons/alien.png",
+						":blink:": "emoticons/blink.png",
+						":blush:": "emoticons/blush.png",
+						":cheerful:": "emoticons/cheerful.png",
+						":devil:": "emoticons/devil.png",
+						":dizzy:": "emoticons/dizzy.png",
+						":getlost:": "emoticons/getlost.png",
+						":happy:": "emoticons/happy.png",
+						":kissing:": "emoticons/kissing.png",
+						":ninja:": "emoticons/ninja.png",
+						":pinch:": "emoticons/pinch.png",
+						":pouty:": "emoticons/pouty.png",
+						":sick:": "emoticons/sick.png",
+						":sideways:": "emoticons/sideways.png",
+						":silly:": "emoticons/silly.png",
+						":sleeping:": "emoticons/sleeping.png",
+						":unsure:": "emoticons/unsure.png",
+						":woot:": "emoticons/w00t.png",
+						":wassat:": "emoticons/wassat.png"
+					},
+					hidden: {
+						":whistling:": "emoticons/whistling.png",
+						":love:": "emoticons/wub.png"
+					}
+				},
+
+		// Width of the editor. Set to null for automatic with
+		width: null,
+
+		// Height of the editor including toolbat. Set to null for automatic height
+		height: null,
+
+		// If to allow the editor to be resized
+		resizeEnabled: true,
+
+		// Min resize to width, set to null for half textarea width or -1 for unlimited
+		resizeMinWidth: null,
+		// Min resize to height, set to null for half textarea height or -1 for unlimited
+		resizeMinHeight: null,
+		// Max resize to height, set to null for double textarea height or -1 for unlimited
+		resizeMaxHeight: null,
+		// Max resize to width, set to null for double textarea width or -1 for unlimited
+		resizeMaxWidth: null,
+
+		getHtmlHandler: null,
+		getTextHandler: null,
+		
+		// date format. year, month and day will be replaced with the users current year, month and day.
+		dateFormat: "year-month-day",
+
+		toolbarContainer: null,
+		
+		enablePasteFiltering: false,
+
+	        //add css to dropdown menu (eg. z-index)
+	        dropDownCss: { }
+	};
+
+	$.fn.sceditor = function (options) {
+		return this.each(function () {
+			(new $.sceditor(this, options));
+		});
+	};
+})(jQuery);

+ 2 - 4
Themes/default/scripts/smf_jquery_plugins.js

@@ -180,8 +180,7 @@
  * <http://cherne.net/brian/resources/jquery.hoverIntent.html>
  * 
  * hoverIntent is currently available for use in all personal or commercial 
- * projects under both MIT and GPL licenses. This means that you can choose 
- * the license that best suits your project, and use it accordingly.
+ * projects under MIT license.
  * 
  * // basic usage (just like .hover) receives onMouseOver and onMouseOut functions
  * $("ul li").hoverIntent( showNav , hideNav );
@@ -303,9 +302,8 @@
  * Superfish v1.4.8 - jQuery menu widget
  * Copyright (c) 2008 Joel Birch
  *
- * Dual licensed under the MIT and GPL licenses:
+ * Licensed under the MIT license:
  * 	http://www.opensource.org/licenses/mit-license.php
- * 	http://www.gnu.org/licenses/gpl.html
  *
  * CHANGELOG: http://users.tpg.com.au/j_birch/plugins/superfish/changelog.txt
  */