/*
 * Medium.js - Taking control of content editable
 * Copyright 2013-2015, Jacob Kelley - http://jakiestfu.com/
 * Released under the MIT Licence
 * http://opensource.org/licenses/MIT
 *
 * Github:  http://github.com/jakiestfu/Medium.js/
 * Version: master
 */

(function (w, d) {
	'use strict';

	var rangy = w['rangy'] || null,
		undo = w['Undo'] || null,
		key = w.Key = {
			'backspace': 8,
			'tab': 9,
			'enter': 13,
			'shift': 16,
			'ctrl': 17,
			'alt': 18,
			'pause': 19,
			'capsLock': 20,
			'escape': 27,
			'pageUp': 33,
			'pageDown': 34,
			'end': 35,
			'home': 36,
			'leftArrow': 37,
			'upArrow': 38,
			'rightArrow': 39,
			'downArrow': 40,
			'insert': 45,
			'delete': 46,
			'0': 48,
			'1': 49,
			'2': 50,
			'3': 51,
			'4': 52,
			'5': 53,
			'6': 54,
			'7': 55,
			'8': 56,
			'9': 57,
			'a': 65,
			'b': 66,
			'c': 67,
			'd': 68,
			'e': 69,
			'f': 70,
			'g': 71,
			'h': 72,
			'i': 73,
			'j': 74,
			'k': 75,
			'l': 76,
			'm': 77,
			'n': 78,
			'o': 79,
			'p': 80,
			'q': 81,
			'r': 82,
			's': 83,
			't': 84,
			'u': 85,
			'v': 86,
			'w': 87,
			'x': 88,
			'y': 89,
			'z': 90,
			'leftWindow': 91,
			'rightWindowKey': 92,
			'select': 93,
			'numpad0': 96,
			'numpad1': 97,
			'numpad2': 98,
			'numpad3': 99,
			'numpad4': 100,
			'numpad5': 101,
			'numpad6': 102,
			'numpad7': 103,
			'numpad8': 104,
			'numpad9': 105,
			'multiply': 106,
			'add': 107,
			'subtract': 109,
			'decimalPoint': 110,
			'divide': 111,
			'f1': 112,
			'f2': 113,
			'f3': 114,
			'f4': 115,
			'f5': 116,
			'f6': 117,
			'f7': 118,
			'f8': 119,
			'f9': 120,
			'f10': 121,
			'f11': 122,
			'f12': 123,
			'numLock': 144,
			'scrollLock': 145,
			'semiColon': 186,
			'equalSign': 187,
			'comma': 188,
			'dash': 189,
			'period': 190,
			'forwardSlash': 191,
			'graveAccent': 192,
			'openBracket': 219,
			'backSlash': 220,
			'closeBracket': 221,
			'singleQuote': 222
		},
		Medium = (function () {

			/**
 * Medium.js - Taking control of content editable
 * @constructor
 * @param {Object} [userSettings] user options
 */
var Medium = function (userSettings) {
	"use strict";

	var medium = this,
		defaultSettings = utils.deepExtend({}, Medium.defaultSettings),
		settings = this.settings = utils.deepExtend(defaultSettings, userSettings),
		cache = new Medium.Cache(),
		selection = new Medium.Selection(),
		action = new Medium.Action(this),
		cursor = new Medium.Cursor(this),
		undoable = new Medium.Undoable(this),
		el,
		newVal,
		i;

	for (i in defaultSettings) if (defaultSettings.hasOwnProperty(i)) {
		// Override defaults with data-attributes
		if (
			typeof defaultSettings[i] !== 'object'
			&& defaultSettings.hasOwnProperty(i)
			&& settings.element.getAttribute('data-medium-' + key)
		) {
			newVal = settings.element.getAttribute('data-medium-' + key);

			if (newVal.toLowerCase() === "false" || newVal.toLowerCase() === "true") {
				newVal = newVal.toLowerCase() === "true";
			}
			settings[i] = newVal;
		}
	}

	if (settings.modifiers) {
		for (i in settings.modifiers) if (settings.modifiers.hasOwnProperty(i)) {
			if (typeof(key[i]) !== 'undefined') {
				settings.modifiers[key[i]] = settings.modifiers[i];
			}
		}
	}

	if (settings.keyContext) {
		for (i in settings.keyContext) if (settings.keyContext.hasOwnProperty(i)) {
			if (typeof(key[i]) !== 'undefined') {
				settings.keyContext[key[i]] = settings.keyContext[i];
			}
		}
	}

	// Extend Settings
	el = settings.element;

	// Editable
	el.contentEditable = true;
	el.className
		+= (' ' + settings.cssClasses.editor)
	+ (' ' + settings.cssClasses.editor + '-' + settings.mode);

	settings.tags = (settings.tags || {});
	if (settings.tags.outerLevel) {
		settings.tags.outerLevel = settings.tags.outerLevel.concat([settings.tags.paragraph, settings.tags.horizontalRule]);
	}

	this.settings = settings;
	this.element = el;
	el.medium = this;

	this.action = action;
	this.cache = cache;
	this.cursor = cursor;
	this.utils = utils;
	this.selection = selection;

	// Initialize editor
	medium.clean();
	medium.placeholders();
	action.preserveElementFocus();

	this.dirty = false;
	this.undoable = undoable;
	this.makeUndoable = undoable.makeUndoable;

	if (settings.drag) {
		medium.drag = new Medium.Drag(medium);
		medium.drag.setup();
	}

	action.setup();

	// Set as initialized
	cache.initialized = true;

	this.makeUndoable(true);
};

Medium.prototype = {
	placeholders: function () {
		//in IE8, just gracefully degrade to no placeholders
		if (!w.getComputedStyle) return;

		var s = this.settings,
			placeholder = this.placeholder || (this.placeholder = d.createElement('div')),
			el = this.element,
			style = placeholder.style,
			elStyle = w.getComputedStyle(el, null),
			qStyle = function (prop) {
				return elStyle.getPropertyValue(prop)
			},
			text = utils.text(el),
			cursor = this.cursor,
			childCount = el.children.length,
			hasFocus = Medium.activeElement === el;

		el.placeholder = placeholder;

		// Empty Editor
		if (
			!hasFocus
			&& text.length < 1
			&& childCount < 2
		) {
			if (el.placeHolderActive) return;

			if (!el.innerHTML.match('<' + s.tags.paragraph)) {
				el.innerHTML = '';
			}

			// We need to add placeholders
			if (s.placeholder.length > 0) {
				if (!placeholder.setup) {
					placeholder.setup = true;

					//background & background color
					style.background = qStyle('background');
					style.backgroundColor = qStyle('background-color');

					//text size & text color
					style.fontSize = qStyle('font-size');
					style.color = elStyle.color;

					//begin box-model
					//margin
					style.marginTop = qStyle('margin-top');
					style.marginBottom = qStyle('margin-bottom');
					style.marginLeft = qStyle('margin-left');
					style.marginRight = qStyle('margin-right');

					//padding
					style.paddingTop = qStyle('padding-top');
					style.paddingBottom = qStyle('padding-bottom');
					style.paddingLeft = qStyle('padding-left');
					style.paddingRight = qStyle('padding-right');

					//border
					style.borderTopWidth = qStyle('border-top-width');
					style.borderTopColor = qStyle('border-top-color');
					style.borderTopStyle = qStyle('border-top-style');
					style.borderBottomWidth = qStyle('border-bottom-width');
					style.borderBottomColor = qStyle('border-bottom-color');
					style.borderBottomStyle = qStyle('border-bottom-style');
					style.borderLeftWidth = qStyle('border-left-width');
					style.borderLeftColor = qStyle('border-left-color');
					style.borderLeftStyle = qStyle('border-left-style');
					style.borderRightWidth = qStyle('border-right-width');
					style.borderRightColor = qStyle('border-right-color');
					style.borderRightStyle = qStyle('border-right-style');
					//end box model

					//element setup
					placeholder.className = s.cssClasses.placeholder + ' ' + s.cssClasses.placeholder + '-' + s.mode;
					placeholder.innerHTML = '<div>' + s.placeholder + '</div>';
					el.parentNode.insertBefore(placeholder, el);
				}

				el.className += ' ' + s.cssClasses.clear;

				style.display = '';
				// Add base P tag and do auto focus, give it a min height if el has one
				style.minHeight = el.clientHeight + 'px';
				style.minWidth = el.clientWidth + 'px';

				if ( s.mode !== Medium.inlineMode && s.mode !== Medium.inlineRichMode ) {
					this.setupContents();

					if (childCount === 0 && el.firstChild) {
						cursor.set(this, 0, el.firstChild);
					}
				}
			}
			el.placeHolderActive = true;
		} else if (el.placeHolderActive) {
			el.placeHolderActive = false;
			style.display = 'none';
			el.className = utils.trim(el.className.replace(s.cssClasses.clear, ''));
			this.setupContents();
		}
	},

	/**
	 * Cleans element
	 * @param {HtmlElement} [el] default is settings.element
	 */
	clean: function (el) {

		/*
		 * Deletes invalid nodes
		 * Removes Attributes
		 */
		var s = this.settings,
			placeholderClass = s.cssClasses.placeholder,
			attributesToRemove = (s.attributes || {}).remove || [],
			tags = s.tags || {},
			onlyOuter = tags.outerLevel || null,
			onlyInner = tags.innerLevel || null,
			outerSwitch = {},
			innerSwitch = {},
			paragraphTag = (tags.paragraph || '').toUpperCase(),
			html = this.html,
			attr,
			text,
			j;

		el = el || s.element;

		if (s.mode === Medium.inlineRichMode) {
			onlyOuter = s.tags.innerLevel;
		}

		if (onlyOuter !== null) {
			for (j = 0; j < onlyOuter.length; j++) {
				outerSwitch[onlyOuter[j].toUpperCase()] = true;
			}
		}

		if (onlyInner !== null) {
			for (j = 0; j < onlyInner.length; j++) {
				innerSwitch[onlyInner[j].toUpperCase()] = true;
			}
		}

		utils.traverseAll(el, {
			element: function(child, i, depth, parent) {
				var nodeName = child.nodeName,
					shouldDelete = true,
					attrValue;

				// Remove attributes
				for (j = 0; j < attributesToRemove.length; j++) {
					attr = attributesToRemove[j];
					if (child.hasAttribute(attr)) {
						attrValue = child.getAttribute(attr);
						if (attrValue !== placeholderClass && (!attrValue.match('medium-') && attr === 'class')) {
							child.removeAttribute(attr);
						}
					}
				}

				if ( onlyOuter === null && onlyInner === null ) {
					return;
				}

				if (depth  === 1 && outerSwitch[nodeName] !== undefined) {
					shouldDelete = false;
				} else if (depth > 1 && innerSwitch[nodeName] !== undefined) {
					shouldDelete = false;
				}

				// Convert tags or delete
				if (shouldDelete) {
					if (w.getComputedStyle(child, null).getPropertyValue('display') === 'block') {
						if (paragraphTag.length > 0 && paragraphTag !== nodeName) {
							utils.changeTag(child, paragraphTag);
						}

						if (depth > 1) {
							while (parent.childNodes.length > i) {
								parent.parentNode.insertBefore(parent.lastChild, parent.nextSibling);
							}
						}
					} else {
						switch (nodeName) {
							case 'BR':
								if (child === child.parentNode.lastChild) {
									if (child === child.parentNode.firstChild) {
										break;
									}
									text = d.createTextNode("");
									text.innerHTML = '&nbsp';
									child.parentNode.insertBefore(text, child);
									break;
								}
							default:
								while (child.firstChild !== null) {
									child.parentNode.insertBefore(child.firstChild, child);
								}
								utils.detachNode(child);
								break;
						}
					}
				}
			}
		});
	},
	/**
	 *
	 * @param {String|Object} html
	 * @param {Function} [callback]
	 * @param {Boolean} [skipChangeEvent]
	 * @returns {Medium}
	 */
	insertHtml: function (html, callback, skipChangeEvent) {
		var result = (new Medium.Html(this, html))
			.insert(this.settings.beforeInsertHtml),
			lastElement = result[result.length - 1];

		if (skipChangeEvent === true) {
			utils.triggerEvent(this.element, "change");
		}

		if (callback) {
			callback.apply(result);
		}

		switch (lastElement.nodeName) {
			//lists need their last child selected if it exists
			case 'UL':
			case 'OL':
			case 'DL':
				if (lastElement.lastChild !== null) {
					this.cursor.moveCursorToEnd(lastElement.lastChild);
					break;
				}
			default:
				this.cursor.moveCursorToEnd(lastElement);
		}

		return this;
	},

	addTag: function (tag, shouldFocus, isEditable, afterElement) {
		if (!this.settings.beforeAddTag(tag, shouldFocus, isEditable, afterElement)) {
			var newEl = d.createElement(tag),
				toFocus;

			if (typeof isEditable !== "undefined" && isEditable === false) {
				newEl.contentEditable = false;
			}
			if (newEl.innerHTML.length == 0) {
				newEl.innerHTML = ' ';
			}
			if (afterElement && afterElement.nextSibling) {
				afterElement.parentNode.insertBefore(newEl, afterElement.nextSibling);
				toFocus = afterElement.nextSibling;

			} else {
				this.element.appendChild(newEl);
				toFocus = this.lastChild();
			}

			if (shouldFocus) {
				this.cache.focusedElement = toFocus;
				this.cursor.set(this, 0, toFocus);
			}
			return newEl;
		}
		return null;
	},

	/**
	 *
	 * @param {String} tagName
	 * @param {Object} [attributes]
	 * @param {Boolean} [skipChangeEvent]
	 * @returns {Medium}
	 */
	invokeElement: function (tagName, attributes, skipChangeEvent) {
		var settings = this.settings,
			remove = attributes.remove || [];

		attributes = attributes || {};

		switch (settings.mode) {
			case Medium.inlineMode:
			case Medium.partialMode:
				return this;
			default:
		}

		//invoke works off class, so if it isn't there, we just add it
		if (remove.length > 0) {
			if (!utils.arrayContains(settings, 'class')) {
				remove.push('class');
			}
		}

		(new Medium.Element(this, tagName, attributes))
			.invoke(this.settings.beforeInvokeElement);

		if (skipChangeEvent === true) {
			utils.triggerEvent(this.element, "change");
		}

		return this;
	},

	/**
	 *
	 * @param {String} [value]
	 * @returns {Medium}
	 */
	value: function (value) {
		if (typeof value !== 'undefined') {
			this.element.innerHTML = value;

			this.clean();
			this.placeholders();

			this.makeUndoable();
		} else {
			return this.element.innerHTML;
		}

		return this;
	},

	/**
	 * Focus on element
	 * @returns {Medium}
	 */
	focus: function () {
		var el = this.element;
		el.focus();
		return this;
	},

	/**
	 * Select all text
	 * @returns {Medium}
	 */
	select: function () {
		utils.selectNode(Medium.activeElement = this.element);
		return this;
	},

	isActive: function () {
		return (Medium.activeElement === this.element);
	},

	setupContents: function () {
		var el = this.element,
			children = el.children,
			childNodes = el.childNodes,
			initialParagraph,
			s = this.settings;

		if (
			!s.tags.paragraph
			|| children.length > 0
			|| s.mode === Medium.inlineMode
			|| s.mode === Medium.inlineRichMode
		) {
			return Medium.Utilities;
		}

		//has content, but no children
		if (childNodes.length > 0) {
			initialParagraph = d.createElement(s.tags.paragraph);
			if (el.innerHTML.match('^[&]nbsp[;]')) {
				el.innerHTML = el.innerHTML.substring(6, el.innerHTML.length - 1);
			}
			initialParagraph.innerHTML = el.innerHTML;
			el.innerHTML = '';
			el.appendChild(initialParagraph);
			//this.cursor.set(this, initialParagraph.innerHTML.length, initialParagraph);
		} else {
			initialParagraph = d.createElement(s.tags.paragraph);
			initialParagraph.innerHTML = '&nbsp;';
			el.appendChild(initialParagraph);
			this.cursor.set(this, 0, el.firstChild);
		}

		return this;
	},

	destroy: function () {
		var el = this.element,
			settings = this.settings,
			placeholder = this.placeholder || null;

		if (placeholder !== null && placeholder.setup && placeholder.parentNode !== null) {
			//remove placeholder
			placeholder.parentNode.removeChild(placeholder);
			delete el.placeHolderActive;
		}

		//remove contenteditable
		el.removeAttribute('contenteditable');

		//remove classes
		el.className = utils.trim(el.className
			.replace(settings.cssClasses.editor, '')
			.replace(settings.cssClasses.clear, '')
			.replace(settings.cssClasses.editor + '-' + settings.mode, ''));

		//remove events
		this.action.destroy();

		if (this.settings.drag) {
			this.drag.destroy();
		}
	},

	// Clears the element and restores the placeholder
	clear: function () {
		this.element.innerHTML = '';
		this.placeholders();
	},

	/**
	 * Splits content in medium element at cursor
	 * @returns {DocumentFragment|null}
	 */
	splitAtCaret: function() {
		if (!this.isActive()) return null;

		var selector = (w.getSelection || d.selection),
			sel = selector(),
			offset = sel.focusOffset,
			node = sel.focusNode,
			el = this.element,
			range = d.createRange(),
			endRange = d.createRange(),
			contents;

		range.setStart(node, offset);
		endRange.selectNodeContents(el);
		range.setEnd(endRange.endContainer, endRange.endOffset);

		contents = range.extractContents();

		return contents;
	},

	/**
	 * Deletes selection
	 */
	deleteSelection: function() {
		if (!this.isActive()) return;

		var sel = rangy.getSelection(),
			range;

		if (sel.rangeCount > 0) {
			range = sel.getRangeAt(0);
			range.deleteContents();
		}
	},

	lastChild: function () {
		return this.element.lastChild;
	},

	bold: function () {

		switch (this.settings.mode) {
			case Medium.partialMode:
			case Medium.inlineMode:
				return this;
		}

		(new Medium.Element(this, 'bold'))
			.setClean(false)
			.invoke(this.settings.beforeInvokeElement);

		return this;
	},
	underline: function () {
		switch (this.settings.mode) {
			case Medium.partialMode:
			case Medium.inlineMode:
				return this;
		}

		(new Medium.Element(this, 'underline'))
			.setClean(false)
			.invoke(this.settings.beforeInvokeElement);

		return this;
	},
	italicize: function () {
		switch (this.settings.mode) {
			case Medium.partialMode:
			case Medium.inlineMode:
				return this;
		}

		(new Medium.Element(this, 'italic'))
			.setClean(false)
			.invoke(this.settings.beforeInvokeElement);

		return this;
	},
	quote: function () {

		return this;
	},
	/**
	 *
	 * @param {String} [text]
	 * @returns {boolean}
	 */
	paste: function (text) {
		var value = this.value(),
			length = value.length,
			totalLength,
			settings = this.settings,
			selection = this.selection,
			el = this.element,
			medium = this,
			postPaste = function(text) {
				text = text || '';
				if (text.length > 0) {
					el.focus();
					Medium.activeElement = el;
					selection.restoreSelection(sel);

					//encode the text first
					text = utils.encodeHtml(text);

					//cut down it's length
					totalLength = text.length + length;
					if (settings.maxLength > 0 && totalLength > settings.maxLength) {
						text = text.substring(0, settings.maxLength - length);
					}

					if (settings.mode !== Medium.inlineMode) {
						text = text.replace(/\n/g, '<br>');
					}

					(new Medium.Html(medium, text))
						.setClean(false)
						.insert(settings.beforeInsertHtml, true);

					medium.clean();
					medium.placeholders();
				}
			};

		medium.makeUndoable();

		if (text !== undefined) {
			postPaste(text);
		} else if (settings.pasteAsText) {
			var sel = selection.saveSelection();

			utils.pasteHook(this, postPaste);
		} else {
			setTimeout(function() {
				medium.clean();
				medium.placeholders();
			}, 20);
		}
		return true;
	},
	undo: function() {
		var undoable = this.undoable,
			stack = undoable.stack,
			can = stack.canUndo();

		if (can) {
			stack.undo();
		}

		return this;
	},
	redo: function() {
		var undoable = this.undoable,
			stack = undoable.stack,
			can = stack.canRedo();

		if (can) {
			stack.redo();
		}

		return this;
	}
};

//Modes
Medium.inlineMode = 'inline';
Medium.partialMode = 'partial';
Medium.richMode = 'rich';
Medium.inlineRichMode = 'inlineRich';
Medium.Messages = {
	pastHere: 'Paste Here'
};

Medium.defaultSettings = {
	element: null,
	modifier: 'auto',
	placeholder: "",
	autofocus: false,
	autoHR: true,
	mode: Medium.richMode,
	maxLength: -1,
	modifiers: {
		'b': 'bold',
		'i': 'italicize',
		'u': 'underline'
	},
	tags: {
		'break': 'br',
		'horizontalRule': 'hr',
		'paragraph': 'p',
		'outerLevel': ['pre', 'blockquote', 'figure'],
		'innerLevel': ['a', 'b', 'u', 'i', 'img', 'strong']
	},
	cssClasses: {
		editor: 'Medium',
		pasteHook: 'Medium-paste-hook',
		placeholder: 'Medium-placeholder',
		clear: 'Medium-clear'
	},
	attributes: {
		remove: ['style', 'class']
	},
	pasteAsText: true,
	beforeInvokeElement: function () {
		//this = Medium.Element
	},
	beforeInsertHtml: function () {
		//this = Medium.Html
	},
	maxLengthReached: function (element) {
		//element
	},
	beforeAddTag: function (tag, shouldFocus, isEditable, afterElement) {
	},
        onBlur: function() {},
        onFocus: function() {},
	keyContext: null,
	drag: false
};
(function(Medium, w, d) {
	"use strict";

	function isEditable(e) {
		if (e.hasOwnProperty('target') && e.target.getAttribute('contenteditable') === 'false') {
			utils.preventDefaultEvent(e);
			return false;
		}
		return true;
	}

	Medium.Action = function (medium) {
		this.medium = medium;

		this.handledEvents = {
			keydown: null,
			keyup: null,
			blur: null,
			focus: null,
			paste: null,
			click: null
		};

	};
	Medium.Action.prototype = {
		setup: function () {
			this
				.handleFocus()
				.handleBlur()
				.handleKeyDown()
				.handleKeyUp()
				.handlePaste()
				.handleClick();
		},
		destroy: function() {
			var el = this.medium.element;

			utils
				.removeEvent(el, 'focus', this.handledEvents.focus)
				.removeEvent(el, 'blur', this.handledEvents.blur)
				.removeEvent(el, 'keydown', this.handledEvents.keydown)
				.removeEvent(el, 'keyup', this.handledEvents.keyup)
				.removeEvent(el, 'paste', this.handledEvents.paste)
				.removeEvent(el, 'click', this.handledEvents.click);
		},
		handleFocus: function () {

			var medium = this.medium,
				el = medium.element;

			utils.addEvent(el, 'focus', this.handledEvents.focus = function(e) {
				e = e || w.event;

				if (!isEditable(e)) {
					return false;
				}

				Medium.activeElement = el;
                                medium.cache.originalVal = e.target.textContent;
                                medium.settings.onFocus(e);

				medium.placeholders();
			});

			return this;
		},
		handleBlur: function () {

			var medium = this.medium,
				el = medium.element;

			utils.addEvent(el, 'blur', this.handledEvents.blur = function(e) {
				e = e || w.event;

				if (Medium.activeElement === el) {
					Medium.activeElement = null;
				}
				
				medium.settings.onBlur(e);
				medium.placeholders();
			});

			return this;
		},
		handleKeyDown: function () {

			var action = this,
				medium = this.medium,
				settings = medium.settings,
				cache = medium.cache,
				el = medium.element;

			utils.addEvent(el, 'keydown', this.handledEvents.keydown = function(e) {
				e = e || w.event;

				if (!isEditable(e)) {
					return false;
				}

				var keepEvent = true;

				//in Chrome it sends out this event before every regular event, not sure why
				if (e.keyCode === 229) return;

				utils.isCommand(settings, e, function () {
					cache.cmd = true;
				}, function () {
					cache.cmd = false;
				});

				utils.isShift(e, function () {
					cache.shift = true;
				}, function () {
					cache.shift = false;
				});

				utils.isModifier(settings, e, function (cmd) {
					if (cache.cmd) {

						if ( (settings.mode === Medium.inlineMode) || (settings.mode === Medium.partialMode) ) {
							utils.preventDefaultEvent(e);
							return false;
						}

						var cmdType = typeof cmd;
						var fn = null;
						if (cmdType === "function") {
							fn = cmd;
						} else {
							fn = medium[cmd];
						}

						keepEvent = fn.call(medium, e);

						if (keepEvent === false || keepEvent === medium) {
							utils.preventDefaultEvent(e);
							utils.stopPropagation(e);
						}
						return true;
					}
					return false;
				});

				if (settings.maxLength !== -1) {
					var len = utils.text(el).length,
						hasSelection = false,
						selection = w.getSelection(),
						isSpecial = utils.isSpecial(e),
						isNavigational = utils.isNavigational(e);

					if (selection) {
						hasSelection = !selection.isCollapsed;
					}

					if (isSpecial || isNavigational) {
						return true;
					}

					if (len >= settings.maxLength && !hasSelection) {
						settings.maxLengthReached(el);
						utils.preventDefaultEvent(e);
						return false;
					}
				}

				switch (e.keyCode) {
                                        case key['enter']:
                                                if (action.enterKey(e) === false) {
                                                        utils.preventDefaultEvent(e);
                                                }
                                                break;
                                        case key['escape']:
                                                if (action.escKey(e) === false) {
							utils.preventDefaultEvent(e);
						}
						break;
					case key['backspace']:
					case key['delete']:
						action.backspaceOrDeleteKey(e);
						break;
				}

				return keepEvent;
			});

			return this;
		},
		handleKeyUp: function () {

			var action = this,
				medium = this.medium,
				settings = medium.settings,
				cache = medium.cache,
				cursor = medium.cursor,
				el = medium.element;

			utils.addEvent(el, 'keyup', this.handledEvents.keyup = function(e) {
				e = e || w.event;

				if (!isEditable(e)) {
					return false;
				}

				utils.isCommand(settings, e, function () {
					cache.cmd = false;
				}, function () {
					cache.cmd = true;
				});
				medium.clean();
				medium.placeholders();

				//here we have a key context, so if you need to create your own object within a specific context it is doable
				var keyContext;
				if (
					settings.keyContext !== null
					&& ( keyContext = settings.keyContext[e.keyCode] )
				) {
					var el = cursor.parent();

					if (el) {
						keyContext.call(medium, e, el);
					}
				}

				action.preserveElementFocus();
			});

			return this;
		},
		handlePaste: function() {
			var medium = this.medium,
				el = medium.element,
				text,
				i,
				max,
				data,
				cD,
				type,
				types;

			utils.addEvent(el, 'paste', this.handledEvents.paste = function(e) {
				e = e || w.event;

				if (!isEditable(e)) {
					return false;
				}

				i = 0;
				utils.preventDefaultEvent(e);
				text = '';
				cD = e.clipboardData;

				if (cD && (data = cD.getData)) {
					types = cD.types;
					max = types.length;
					for (i = 0; i < max; i++) {
						type = types[i];
						switch (type) {
							//case 'text/html':
							//	return medium.paste(cD.getData('text/html'));
							case 'text/plain':
								return medium.paste(cD.getData('text/plain'));
						}
					}
				}

				medium.paste();
			});

			return this;
		},
		handleClick: function() {
			var medium = this.medium,
				el = medium.element,
				cursor = medium.cursor;

			utils.addEvent(el, 'click', this.handledEvents.click = function(e) {
				if (!isEditable(e)) {
					cursor.caretToAfter(e.target);
				}

			});

			return this;
		},
                escKey: function (e) {
                    var medium = this.medium,
                        el = medium.element,
                        settings = medium.settings,
                        cache = medium.cache;

                    if( settings.mode === Medium.inlineMode || settings.mode === Medium.inlineRichMode ){
                        e.target.textContent = cache.originalVal;
                        
                        if (settings.element.blur) {
                            settings.element.blur();
                        } else if (settings.element.onblur) {
                            settings.element.onblur();
                        }   
                        return false;
                    }
                },
		enterKey: function (e) {
			var medium = this.medium,
				el = medium.element,
				settings = medium.settings,
				cache = medium.cache,
				cursor = medium.cursor;

			if( settings.mode === Medium.inlineMode || settings.mode === Medium.inlineRichMode ){
                            if (settings.element.blur) {
                                settings.element.blur();
                            } else if (settings.element.onblur) {
                                settings.element.onblur();
                            }	
                            return false;
			}

			if (cache.shift) {
				if (settings.tags['break']) {
					medium.addTag(settings.tags['break'], true);
					return false;
				}

			} else {

				var focusedElement = utils.atCaret(medium) || {},
					children = el.children,
					lastChild = focusedElement === el.lastChild ? el.lastChild : null,
					makeHR,
					secondToLast,
					paragraph;

				if (
					lastChild
					&& lastChild !== el.firstChild
					&& settings.autoHR
					&& settings.mode !== Medium.partialMode
					&& settings.tags.horizontalRule
				) {

					utils.preventDefaultEvent(e);

					makeHR =
						utils.text(lastChild) === ""
						&& lastChild.nodeName.toLowerCase() === settings.tags.paragraph;

					if (makeHR && children.length >= 2) {
						secondToLast = children[ children.length - 2 ];

						if (secondToLast.nodeName.toLowerCase() === settings.tags.horizontalRule) {
							makeHR = false;
						}
					}

					if (makeHR) {
						medium.addTag(settings.tags.horizontalRule, false, true, focusedElement);
						focusedElement = focusedElement.nextSibling;
					}

					if ((paragraph = medium.addTag(settings.tags.paragraph, true, null, focusedElement)) !== null) {
						paragraph.innerHTML = '';
						cursor.set(medium, 0, paragraph);
					}
				}
			}

			return true;
		},
		backspaceOrDeleteKey: function (e) {
			var medium = this.medium,
				cursor = medium.cursor,
				settings = medium.settings,
				el = medium.element;

			if (settings.onBackspaceOrDelete !== undefined) {
				var result = settings.onBackspaceOrDelete.call(medium, e, el);

				if (result) {
					return;
				}
			}

			if (el.lastChild === null) return;

			var lastChild = el.lastChild,
				beforeLastChild = lastChild.previousSibling,
				anchorNode = rangy.getSelection().anchorNode;

			if (
				lastChild
				&& settings.tags.horizontalRule
				&& lastChild.nodeName.toLocaleLowerCase() === settings.tags.horizontalRule
			) {
				el.removeChild(lastChild);
			} else if (
				lastChild
				&& beforeLastChild
				&& utils.text(lastChild).length < 1

				&& beforeLastChild.nodeName.toLowerCase() === settings.tags.horizontalRule
				&& lastChild.nodeName.toLowerCase() === settings.tags.paragraph
			) {
				el.removeChild(lastChild);
				el.removeChild(beforeLastChild);
			} else if (
				el.childNodes.length === 1
				&& lastChild
				&& !utils.text(lastChild).length
			) {
				utils.preventDefaultEvent(e);
				medium.setupContents();
			}
			else if ( anchorNode && anchorNode === el ) {
				medium.deleteSelection();
				medium.setupContents();
				cursor.set(medium, 0, el.firstChild);
			}
		},
		preserveElementFocus: function () {
			// Fetch node that has focus
			var anchorNode = w.getSelection ? w.getSelection().anchorNode : document.activeElement;
			if (anchorNode) {
				var medium = this.medium,
					cache = medium.cache,
					el = medium.element,
					s = medium.settings,
					cur = anchorNode.parentNode,
					children = el.children,
					diff = cur !== cache.focusedElement,
					elementIndex = 0,
					i;

				// anchorNode is our target if element is empty
				if (cur === s.element) {
					cur = anchorNode;
				}

				// Find our child index
				for (i = 0; i < children.length; i++) {
					if (cur === children[i]) {
						elementIndex = i;
						break;
					}
				}

				// Focused element is different
				if (diff) {
					cache.focusedElement = cur;
					cache.focusedElementIndex = elementIndex;
				}
			}
		}
	};

})(Medium, w, d);
(function(Medium) {
	"use strict";

	Medium.Cache = function () {
		this.initialized = false;
		this.cmd = false;
		this.focusedElement = null;
                this.originalVal = null;
	};

})(Medium);
(function(Medium) {
	"use strict";
	/*
	 * Handle Cursor Logic
	 */
	Medium.Cursor = function (medium) {
		this.medium = medium;
	};
	Medium.Cursor.prototype = {
		set: function (pos, el) {
			var range;

			if (d.createRange) {
				var selection = w.getSelection(),
					lastChild = this.medium.lastChild(),
					length = utils.text(lastChild).length - 1,
					toModify = el ? el : lastChild,
					theLength = ((typeof pos !== 'undefined') && (pos !== null) ? pos : length);

				range = d.createRange();
				range.setStart(toModify, theLength);
				range.collapse(true);
				selection.removeAllRanges();
				selection.addRange(range);
			} else {
				range = d.body.createTextRange();
				range.moveToElementText(el);
				range.collapse(false);
				range.select();
			}
		},
		//http://davidwalsh.name/caret-end
		moveCursorToEnd: function (el) {
			//get the browser selection object - it may or may not have a selected range
			var selection = rangy.getSelection(),

				//create a range object to set the caret positioning for
				range = rangy.createRange();


			//set the caret after the start node and at the end of the end node
			//Note: the end is set using endNode.length when the node is of the text type
			//and it is set using childNodes.length when the end node is of the element type
			range.setStartAfter(el);
			range.setEnd(el, el.length || el.childNodes.length);

			//apply this range to the selection object
			selection.removeAllRanges();
			selection.addRange(range);
		},
		moveCursorToAfter: function (el) {
			var sel = rangy.getSelection();
			if (sel.rangeCount) {
				var range = sel.getRangeAt(0);
				range.collapse(false);
				range.collapseAfter(el);
				sel.setSingleRange(range);
			}
		},
		parent: function () {
			var target = null, range;

			if (w.getSelection) {
				range = w.getSelection().getRangeAt(0);
				target = range.commonAncestorContainer;

				target = (target.nodeType === 1
					? target
					: target.parentNode
				);
			}

			else if (d.selection) {
				target = d.selection.createRange().parentElement();
			}

			if (target.tagName == 'SPAN') {
				target = target.parentNode;
			}

			return target;
		},
		caretToBeginning: function (el) {
			this.set(0, el);
		},
		caretToEnd: function (el) {
			this.moveCursorToEnd(el);
		},
		caretToAfter: function (el) {
			this.moveCursorToAfter(el);
		}
	};
})(Medium);
(function(Medium) {
	"use strict";
	Medium.Drag = function(medium) {
		this.medium = medium;

		var that = this,
			iconSrc = this.iconSrc.replace(/[{][{]([a-zA-Z]+)[}][}]/g, function(ignore, match) {
				if (that.hasOwnProperty(match)) {
					return that[match];
				}

				return ignore;
			}),
			icon = this.icon = d.createElement('img');

		icon.className = this.buttonClass;
		icon.setAttribute('contenteditable', 'false');
		icon.setAttribute('src', iconSrc);

		this.hide();
		this.element = null;
		this.protectedElement = null;
		this.handledEvents = {
			dragstart: null,
			dragend: null,
			mouseover: null,
			mouseout: null,
			mousemove: null
		};
	};
	Medium.Drag.prototype = {
		elementClass: 'Medium-focused',
		buttonClass: 'Medium-drag',

		//thank you ascii for not including a directional icon (boo!)
		//http://www.flaticon.com/free-icon/pointer-crosstree_10119
		iconSrc: 'data:image/svg+xml;utf8,\
<svg version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px" width="21.424px" height="21.424px" viewBox="0 0 21.424 21.424" style="enable-background:new 0 0 21.424 21.424;" xml:space="preserve">\
	<g>\
		<g>\
			<path style="fill:{{iconColor}};" d="M13.616,17.709L13.616,17.709h0.781l-3.686,3.715l-3.685-3.715h0.781l0,0H13.616z M13.616,17.709 M14.007,17.709 M12.555,19.566 M8.87,19.566 M7.418,17.709 M7.809,17.709 M10.712,17.709"/>\
			<path style="fill:{{iconColor}};" d="M13.616,3.715L13.616,3.715h0.781L10.712,0L7.027,3.715h0.781l0,0H13.616z M13.616,3.715 M14.007,3.715 M12.555,1.858 M8.87,1.858 M7.418,3.715 M7.809,3.715 M10.712,3.715"/>\
			<path style="fill:{{iconColor}};" d="M3.716,13.616L3.716,13.616v0.781L0,10.712l3.716-3.685v0.781l0,0V13.616z M3.716,13.616 M3.716,14.007 M1.858,12.555 M1.858,8.87 M3.716,7.417 M3.716,7.808 M3.716,10.712"/>\
			<path style="fill:{{iconColor}};" d="M17.709,13.616L17.709,13.616v0.781l3.715-3.685l-3.715-3.685v0.781l0,0V13.616z M17.709,13.616 M17.709,14.007 M19.566,12.555 M19.566,8.87 M17.709,7.417 M17.709,7.808 M17.709,10.712"/>\
		</g>\
		<path style="fill-rule:evenodd;clip-rule:evenodd;fill:{{iconColor}};" d="M10.712,6.608c2.267,0,4.104,1.838,4.104,4.104 c0,2.266-1.837,4.104-4.104,4.104c-2.266,0-4.104-1.837-4.104-4.104C6.608,8.446,8.446,6.608,10.712,6.608L10.712,6.608z M10.712,7.515c-1.765,0-3.196,1.432-3.196,3.197s1.432,3.197,3.196,3.197c1.766,0,3.197-1.432,3.197-3.197 S12.478,7.515,10.712,7.515z"/>\
	</g>\
</svg>',
		iconColor: '#231F20',
		setup: function() {
			this
				.handleDragstart()
				.handleDragend()
				.handleMouseover()
				.handleMouseout()
				.handleMousemove();
		},
		destroy: function() {
			utils
				.removeEvent(this.icon, 'dragstart', this.handledEvents.dragstart)
				.removeEvent(this.icon, 'dragend', this.handledEvents.dragend)
				.removeEvent(this.icon, 'mouseover', this.handledEvents.mouseover)
				.removeEvent(this.icon, 'mouseout', this.handledEvents.mouseout)
				.removeEvent(this.medium.element, 'mousemove', this.handledEvents.mousemove);
		},
		hide: function() {
			utils.hide(this.icon);
		},
		handleDragstart: function() {

			var me = this;

			utils.addEvent(this.icon, 'dragstart', this.handledEvents.dragstart = function(e) {
				if (me.protectedElement !== null) return;

				e = e || w.event;

				me.protectedElement = utils.detachNode(me.element);

				me.icon.style.opacity = 0.00;
			});

			return this;
		},
		handleDragend: function() {
			var me = this;

			utils.addEvent(this.icon, 'dragend',  this.handledEvents.dragend = d.body.ondragend = function(e) {
				if (me.protectedElement === null) return;

				setTimeout(function() {
					me.cleanCanvas();
					me.protectedElement = null;
				}, 1);
			});

			return this;
		},
		handleMouseover: function() {
			var me = this;

			utils.addEvent(this.icon, 'mouseover', this.handledEvents.mouseover = function(e) {
				if (me.protectedElement !== null) return;

				utils
					.stopPropagation(e)
					.addClass(me.element, me.elementClass);

			});

			return this;
		},
		handleMouseout: function() {
			var me = this;

			utils.addEvent(this.icon, 'mouseout', this.handledEvents.mouseout = function(e) {
				if (me.protectedElement !== null) return;

				utils
					.stopPropagation(e)
					.removeClass(me.element, me.elementClass);
			});
			return this;
		},
		handleMousemove: function() {
			var me = this;

			utils.addEvent(this.medium.element, 'mousemove', this.handledEvents.mousemove = function(e) {
				e = e || w.event;
				var target = e.target || {};

				if (target.getAttribute('contenteditable') === 'false') {
					me.show(target);
				}
			});

			return this;
		},
		show: function(el) {
			if (el === this.icon && this.protectedElement === null) return;

			this.element = el;

			var style = this.icon.style,
				left = el.offsetLeft,
				top = el.offsetTop;

			el.dragIcon = this.icon;
			el.parentNode.appendChild(this.icon);

			style.opacity = 1;
			style.left = left + 'px';
			style.top = top + 'px';

			utils.show(this.icon);
		},
		cleanCanvas: function() {
			var target,
				inserted = false,
				buttons = d.getElementsByClassName(this.buttonClass);

			this.icon.style.opacity = 1;

			while (buttons.length > 0) {
				if (utils.isVisible(target = buttons[0])) {
					if (!inserted) {
						target.parentNode.insertBefore(this.element, target);
						inserted = true;
					}
					utils.detachNode(target);
				}
			}
			utils.detachNode(this.icon);
		}
	};
})(Medium);
(function(Medium) {
	"use strict";

	/**
	 * @param {Medium} medium
	 * @param {String} tagName
	 * @param {Object} [attributes]
	 * @constructor
	 */
	Medium.Element = function (medium, tagName, attributes) {
		this.medium = medium;
		this.element = medium.element;

		switch (tagName.toLowerCase()) {
			case 'bold':
				this.tagName = 'b';
				break;
			case 'italic':
				this.tagName = 'i';
				break;
			case 'underline':
				this.tagName = 'u';
				break;
			default:
				this.tagName = tagName;
		}

		this.attributes = attributes || {};
		this.clean = true;
	};

	Medium.Element.prototype = {
		/**
		 * @methodOf Medium.Element
		 * @param {Function} [fn]
		 */
		invoke: function (fn) {
			if (Medium.activeElement === this.element) {
				if (fn) {
					fn.apply(this);
				}

				var
					attr = this.attributes,
					tagName = this.tagName.toLowerCase(),
					applier,
					cl;

				if (attr.className !== undefined) {
					cl = (attr.className.split[' '] || [attr.className]).shift();
					delete attr.className;
				} else {
					cl = 'medium-' + tagName;
				}

				applier = rangy.createClassApplier(cl, {
					elementTagName: tagName,
					elementAttributes: this.attributes
				});

				this.medium.makeUndoable();

				applier.toggleSelection(w);

				if (this.clean) {
					//cleanup
					this.medium.clean();
					this.medium.placeholders();
				}


			}
		},

		/**
		 *
		 * @param {Boolean} clean
		 * @returns {Medium.Element}
		 */
		setClean: function (clean) {
			this.clean = clean;
			return this;
		}
	};
})(Medium);
(function(Medium) {
	"use strict";

	/**
	 * @constructor
	 * @param {Medium} medium
	 * @param {String|HtmlElement} html
	 */
	Medium.Html = function (medium, html) {
		this.html = html;
		this.medium = medium;
		this.clean = true;
		this.injector = new Medium.Injector();
	};

	Medium.Html.prototype = {
		/**
		 * @methodOf Medium.Html
		 * @param {Function} [fn]
		 * @param {Boolean} [selectInserted]
		 * @returns {HtmlElement}
		 */
		insert: function (fn, selectInserted) {
			if (Medium.activeElement === this.medium.element) {
				if (fn) {
					fn.apply(this);
				}

				var inserted = this.injector.inject(this.html, selectInserted);

				if (this.clean) {
					//cleanup
					this.medium.clean();
					this.medium.placeholders();
				}

				this.medium.makeUndoable();

				return inserted;
			} else {
				return null;
			}
		},

		/**
		 * @methodOf Medium.Html
		 * @param clean
		 * @returns {Medium.Html}
		 */
		setClean: function (clean) {
			this.clean = clean;
			return this;
		}
	};

})(Medium);
(function(Medium) {
	"use strict";

	/**
	 *
	 * @constructor
	 */
	Medium.Injector = function () {
	};

	Medium.Injector.prototype = {
		/**
		 * @methodOf Medium.Injector
		 * @param {String|HtmlElement} htmlRaw
		 * @returns {[HtmlElement|Node]}
		 */
		inject: function (htmlRaw) {
			var nodes = [],
				html,
				isConverted = false;

			if (typeof htmlRaw === 'string') {
				var htmlConverter = d.createElement('div');
				htmlConverter.innerHTML = htmlRaw;
				html = htmlConverter.childNodes;
				isConverted = true;
			} else {
				html = htmlRaw;
			}

			this.insertHTML('<span id="Medium-wedge"></span>');

			var wedge = d.getElementById('Medium-wedge'),
				parent = wedge.parentNode,
				i = 0;

			wedge.removeAttribute('id');

			if (isConverted) {
				//make an array of elements that are about to be inserted, can't use html because they will
				while (i < html.length) {
					nodes.push(html[i]);
					i++;
				}

				while (html.length > 0) {
            parent.insertBefore(html[0], wedge);
				}
			} else {
				nodes.push(html);
				parent.insertBefore(html, wedge);
			}
			parent.removeChild(wedge);
			wedge = null;

			return nodes;
		},

		//Thank you Tim Down (super uber genius): http://stackoverflow.com/questions/6690752/insert-html-at-caret-in-a-contenteditable-div/6691294#6691294
		insertHTML: function (html, selectPastedContent) {
			var sel, range;
			if (w.getSelection) {
				// IE9 and non-IE
				sel = w.getSelection();
				if (sel.getRangeAt && sel.rangeCount) {
					range = sel.getRangeAt(0);
					range.deleteContents();

					// Range.createContextualFragment() would be useful here but is
					// only relatively recently standardized and is not supported in
					// some browsers (IE9, for one)
					var el = d.createElement("div");
					el.innerHTML = html;
					var frag = d.createDocumentFragment(), node, lastNode;
					while ((node = el.firstChild)) {
						lastNode = frag.appendChild(node);
					}
					var firstNode = frag.firstChild;
					range.insertNode(frag);

					// Preserve the selection
					if (lastNode) {
						range = range.cloneRange();
						range.setStartAfter(lastNode);
						if (selectPastedContent) {
							range.setStartBefore(firstNode);
						} else {
							range.collapse(true);
						}
						sel.removeAllRanges();
						sel.addRange(range);
					}
				}
			} else if ((sel = d.selection) && sel.type != "Control") {
				// IE < 9
				var originalRange = sel.createRange();
				originalRange.collapse(true);
				sel.createRange().pasteHTML(html);
				if (selectPastedContent) {
					range = sel.createRange();
					range.setEndPoint("StartToStart", originalRange);
					range.select();
				}
			}
		}
	};
})(Medium);
(function(Medium) {
	"use strict";

	/*
	 * Handle Selection Logic
	 */
	Medium.Selection = function () {
	};
	Medium.Selection.prototype = {
		saveSelection: function () {
			if (w.getSelection) {
				var sel = w.getSelection();
				if (sel.rangeCount > 0) {
					return sel.getRangeAt(0);
				}
			} else if (d.selection && d.selection.createRange) { // IE
				return d.selection.createRange();
			}
			return null;
		},

		restoreSelection: function (range) {
			if (range) {
				if (w.getSelection) {
					var sel = w.getSelection();
					sel.removeAllRanges();
					sel.addRange(range);
				} else if (d.selection && range.select) { // IE
					range.select();
				}
			}
		}
	};
})(Medium);(function(Medium) {
	"use strict";
	Medium.Toolbar = function(medium, buttons) {
		this.medium = medium;

		var elementCreator = d.createElement('div');

		elementCreator.innerHTML = this.html;

		this.buttons = buttons;
		this.element = elementCreator.children[0];
		d.body.appendChild(this.element);
		this.active = false;
		this.busy = true;

		this.handledEvents = {
			scroll: null,
			mouseup: null,
			keyup: null
		};
	};

	Medium.Toolbar.prototype = {
		fixedClass: 'Medium-toolbar-fixed',
		showClass: 'Medium-toolbar-show',
		hideClass: 'Medium-toolbar-hide',

		html:
			'<div class="Medium-toolbar">\
				<div class="Medium-tail-outer">\
					<div class="Medium-tail-inner"></div>\
				</div>\
				<div id="Medium-buttons"></div>\
				<table id="Medium-options">\
					<tbody>\
						<tr>\
						</tr>\
					</tbody>\
				</table>\
			</div>',

		setup: function() {
			this
				.handleScroll()
				.handleMouseup()
				.handleKeyup();

		},
		destroy: function() {
			utils
				.removeEvent(w, 'scroll', this.handledEvents.scroll)
				.removeEvent(d, 'mouseup', this.handledEvents.mouseup)
				.removeEvent(d, 'keyup', this.handledEvents.keyup);
		},
		handleScroll: function() {
			var me = this;

			utils.addEvent(w, 'scroll', this.handledEvents.scroll = function() {
				if (me.active) {
					me.goToSelection();
				}
			});

			return this;
		},
		handleMouseup: function() {
			var me = this;

			utils.addEvent(d, 'mouseup', this.handledEvents.mouseup = function() {
				if (Medium.activeElement === me.medium.element && !me.busy) {
					me.goToSelection();
				}
			});

			return this;
		},
		handleKeyup: function() {
			var me = this;

			utils.addEvent(d, 'keyup', this.handledEvents.keyup = function() {
				if (Medium.activeElement === me.medium.element && !me.busy) {
					me.goToSelection();
				}
			});

			return this;
		},
		goToSelection: function() {
			var high = this.getHighlighted(),
				y = high.boundary.top - 5,
				el = this.element,
				style = el.style;

			if (w.scrollTop > 0) {
				utils.addClass(el, this.fixedClass);
			} else {
				utils.removeClass(el, this.fixedClass);
			}

			if (high !== null) {
				if (high.range.startOffset === high.range.endOffset && !high.text) {
					utils
						.removeClass(el, this.showClass)
						.addClass(el, this.hideClass);

					this.active = false;
				} else {
					utils
						.removeClass(el, this.hideClass)
						.removeClass(el, this.showClass);

					style.opacity = 0.01;
					utils.addClass(el, this.showClass);
					style.opacity = 1;
					style.top = (y - 65) + "px";
					style.left = (
					(
					high.boundary.left + (high.boundary.width / 2)
					)
					- (el.clientWidth / 2)
					) + "px";

					this.active = true;
				}
			}
		},

		getHighlighted: function() {
			var selection = w.getSelection(),
				range = (selection.anchorNode ? selection.getRangeAt(0) : false);

			if (!range) {
				return null;
			}

			return {
				selection : selection,
				range : range,
				text : utils.trim(range.toString()),
				boundary : range.getBoundingClientRect()
			};
		}
	};
})(Medium);
(function(Medium) {
	"use strict";

	/**
	 * @param {Medium} medium
	 * @constructor
	 */
	Medium.Undoable = function (medium) {
		var me = this,
			element = medium.settings.element,
			timer,
			startValue,
			stack = new Undo.Stack(),
			EditCommand = Undo.Command.extend({
				constructor: function (oldValue, newValue) {
					this.oldValue = oldValue;
					this.newValue = newValue;
				},
				execute: function () {
				},
				undo: function () {
					element.innerHTML = this.oldValue;
					medium.canUndo = stack.canUndo();
					medium.canRedo = stack.canRedo();
					medium.dirty = stack.dirty();
				},
				redo: function () {
					element.innerHTML = this.newValue;
					medium.canUndo = stack.canUndo();
					medium.canRedo = stack.canRedo();
					medium.dirty = stack.dirty();
				}
			}),
			makeUndoable = function (isInit) {
				var newValue = element.innerHTML;

				if (isInit) {
					startValue = element.innerHTML;
					stack.execute(new EditCommand(startValue, startValue));
				}

				// ignore meta key presses
				else if (newValue != startValue) {

					if (!me.movingThroughStack) {
						// this could try and make a diff instead of storing snapshots
						stack.execute(new EditCommand(startValue, newValue));
						startValue = newValue;
						medium.dirty = stack.dirty();
					}

					utils.triggerEvent(medium.settings.element, "change");
				}
			};

		this.medium = medium;
		this.timer = timer;
		this.stack = stack;
		this.makeUndoable = makeUndoable;
		this.EditCommand = EditCommand;
		this.movingThroughStack = false;

		utils
			.addEvent(element, 'keyup', function (e) {
				if (e.ctrlKey || e.keyCode === key.z) {
					utils.preventDefaultEvent(e);
					return;
				}

				// a way too simple algorithm in place of single-character undo
				clearTimeout(timer);
				timer = setTimeout(function () {
					makeUndoable();
				}, 250);
			})

			.addEvent(element, 'keydown', function (e) {
				if (!e.ctrlKey || e.keyCode !== key.z) {
					me.movingThroughStack = false;
					return;
				}

				utils.preventDefaultEvent(e);

				me.movingThroughStack = true;

				if (e.shiftKey) {
					stack.canRedo() && stack.redo()
				} else {
					stack.canUndo() && stack.undo();
				}
			});
	};
})(Medium);
(function(Medium) {
	"use strict";

	Medium.Utilities = {
		/*
		 * Keyboard Interface events
		 */
		isCommand: function (s, e, fnTrue, fnFalse) {
			if ((s.modifier === 'ctrl' && e.ctrlKey ) ||
				(s.modifier === 'cmd' && e.metaKey ) ||
				(s.modifier === 'auto' && (e.ctrlKey || e.metaKey) )
			) {
				return fnTrue.call();
			} else {
				return fnFalse.call();
			}
		},
		isShift: function (e, fnTrue, fnFalse) {
			if (e.shiftKey) {
				return fnTrue.call();
			} else {
				return fnFalse.call();
			}
		},
		isModifier: function (settings, e, fn) {
			var cmd = settings.modifiers[e.keyCode];
			if (cmd) {
				return fn.call(null, cmd);
			}
			return false;
		},
		special: (function () {
			var special = {};

			special[key['backspace']] = true;
			special[key['shift']] = true;
			special[key['ctrl']] = true;
			special[key['alt']] = true;
			special[key['delete']] = true;
			special[key['cmd']] = true;

			return special;
		})(),
		isSpecial: function (e) {
			return typeof Medium.Utilities.special[e.keyCode] !== 'undefined';
		},
		navigational: (function () {
			var navigational = {};

			navigational[key['upArrow']] = true;
			navigational[key['downArrow']] = true;
			navigational[key['leftArrow']] = true;
			navigational[key['rightArrow']] = true;

			return navigational;
		})(),
		isNavigational: function (e) {
			return typeof Medium.Utilities.navigational[e.keyCode] !== 'undefined';
		},

		/**
		 * @param element
		 * @param eventNamesString
		 * @param func
		 * @returns Medium.Utilities
		 */
		addEvents: function(element, eventNamesString, func) {
			var i = 0,
				eventName,
				eventNames = eventNamesString.split(' '),
				max = eventNames.length,
				utils = Medium.Utilities;

			for(;i < max; i++) {
				eventName = eventNames[i];
				if (eventName.length > 0) {
					utils.addEvent(element, eventName, func);
				}
			}

			return Medium.Utilities;
		},
		/*
		 * Handle Events
		 */
		addEvent: function addEvent(element, eventName, func) {
			if (element.addEventListener) {
				element.addEventListener(eventName, func, false);
			} else if (element.attachEvent) {
				element.attachEvent("on" + eventName, func);
			} else {
				element['on' + eventName] = func;
			}

			return Medium.Utilities;
		},
		removeEvent: function removeEvent(element, eventName, func) {
			if (element.removeEventListener) {
				element.removeEventListener(eventName, func, false);
			} else if (element.detachEvent) {
				element.detachEvent("on" + eventName, func);
			} else {
				element['on' + eventName] = null;
			}

			return Medium.Utilities;
		},
		preventDefaultEvent: function (e) {
			if (e.preventDefault) {
				e.preventDefault();
			} else {
				e.returnValue = false;
			}

			return Medium.Utilities;
		},
		stopPropagation: function(e) {
			e = e || window.event;
			e.cancelBubble = true;

			if (e.stopPropagation !== undefined) {
				e.stopPropagation();
			}

			return Medium.Utilities;
		},
		isEventSupported: function (element, eventName) {
			eventName = 'on' + eventName;
			var el = d.createElement(element.tagName),
				isSupported = (eventName in el);

			if (!isSupported) {
				el.setAttribute(eventName, 'return;');
				isSupported = typeof el[eventName] == 'function';
			}
			el = null;
			return isSupported;
		},
		triggerEvent: function (element, eventName) {
			var e;
			if (d.createEvent) {
				e = d.createEvent("HTMLEvents");
				e.initEvent(eventName, true, true);
				e.eventName = eventName;
				element.dispatchEvent(e);
			} else {
				e = d.createEventObject();
				element.fireEvent("on" + eventName, e);
			}

			return Medium.Utilities;
		},

		deepExtend: function (destination, source) {
			var property,
				propertyValue;

			for (property in source) if (source.hasOwnProperty(property)) {
				propertyValue = source[property];
				if (
					propertyValue !== undefined
					&& propertyValue !== null
					&& propertyValue.constructor !== undefined
					&& propertyValue.constructor === Object
				) {
					destination[property] = destination[property] || {};
					Medium.Utilities.deepExtend(destination[property], propertyValue);
				} else {
					destination[property] = propertyValue;
				}
			}
			return destination;
		},
		/*
		 * This is a Paste Hook. When the user pastes
		 * content, this ultimately converts it into
		 * plain text before inserting the data.
		 */
		pasteHook: function (medium, fn) {
			medium.makeUndoable();

			var tempEditable = d.createElement('div'),
				el = medium.element,
				existingValue,
				existingLength,
				overallLength,
				s = medium.settings,
				value,
				body = d.body,
				bodyParent = body.parentNode,
				scrollTop = bodyParent.scrollTop,
				scrollLeft = bodyParent.scrollLeft;

			tempEditable.className = s.cssClasses.pasteHook;
			tempEditable.setAttribute('contenteditable', true);

			body.appendChild(tempEditable);
			utils.selectNode(tempEditable);

			bodyParent.scrollTop = scrollTop;
			bodyParent.scrollLeft = scrollLeft;

			setTimeout(function () {
				value = utils.text(tempEditable);
				el.focus();
				if (s.maxLength > 0) {
					existingValue = utils.text(el);
					existingLength = existingValue.length;
					overallLength = existingLength + value.length;
					if (overallLength > existingLength) {
						value = value.substring(0, s.maxLength - existingLength);
					}
				}
				utils.detachNode( tempEditable );
				bodyParent.scrollTop = scrollTop;
				bodyParent.scrollLeft = scrollLeft;
				fn(value);
			}, 0);

			return Medium.Utilities;
		},
		traverseAll: function(element, options, depth) {
			var children = element.childNodes,
				length = children.length,
				i = 0,
				node;

			depth = depth || 1;

			options = options || {};

			if (length > 0) {
				for(;i < length;i++) {
					node = children[i];
					switch (node.nodeType) {
						case 1:
							Medium.Utilities.traverseAll(node, options, depth + 1);
							if (options.element !== undefined) options.element(node, i, depth, element);
							break;
						case 3:
							if (options.fragment !== undefined) options.fragment(node, i, depth, element);
					}

					//length may change
					length = children.length;
					//if length did change, and we are at the last item, this causes infinite recursion, so if we are at the last item, then stop to prevent this
					if (node === element.lastChild) {
						i = length;
					}
				}
			}
			return Medium.Utilities;
		},
		trim: function (string) {
			return string.replace(/^[\s]+|\s+$/g, '');
		},
		arrayContains: function(array, variable) {
			var i = array.length;
			while (i--) {
				if (array[i] === variable) {
					return true;
				}
			}
			return false;
		},
		addClass: function(el, className) {
			if (el.classList)
				el.classList.add(className);
			else
				el.className += ' ' + className;

			return Medium.Utilities;
		},
		removeClass: function(el, className) {
			if (el.classList)
				el.classList.remove(className);
			else
				el.className = el.className.replace(new RegExp('(^|\\b)' + className.split(' ').join('|') + '(\\b|$)', 'gi'), ' ');
			return Medium.Utilities;
		},
		hasClass: function(el, className) {
			if (el.classList)
				return el.classList.contains(className);
			else
				return new RegExp('(^| )' + className + '( |$)', 'gi').test(el.className);
		},
		isHidden: function(el) {
			return el.offsetWidth === 0 || el.offsetHeight === 0;
		},
		isVisible: function(el) {
			return el.offsetWidth !== 0 || el.offsetHeight !== 0;
		},
		encodeHtml: function ( html ) {
			return d.createElement( 'a' ).appendChild(
				d.createTextNode( html ) ).parentNode.innerHTML;
		},
		text: function (node, val) {
			if (val !== undefined) {
				if (node === null) {
					return this;
				}
				if (node.textContent !== undefined) {
					node.textContent = val;
				} else {
					node.innerText = val;
				}

				return this;
			}
			else if (node === null) {
				return this;
			}
			else if (node.innerText !== undefined) {
				return utils.trim(node.innerText);
			}

			else if (node.textContent !== undefined) {
				return utils.trim(node.textContent);
			}
			//document fragment
			else if (node.data !== undefined) {
				return utils.trim(node.data);
			}

			//for good measure
			return '';
		},
		changeTag: function (oldNode, newTag) {
			var newNode = d.createElement(newTag),
				node,
				nextNode;

			node = oldNode.firstChild;
			while (node) {
				nextNode = node.nextSibling;
				newNode.appendChild(node);
				node = nextNode;
			}

			oldNode.parentNode.insertBefore(newNode, oldNode);
			oldNode.parentNode.removeChild(oldNode);

			return newNode;
		},
		detachNode: function (el) {
			if (el.parentNode !== null) {
				el.parentNode.removeChild(el);
			}

			return this;
		},
		selectNode: function(el) {
			var range,
				selection;

			el.focus();

			if (d.body.createTextRange) {
				range = d.body.createTextRange();
				range.moveToElementText(el);
				range.select();
			} else if (w.getSelection) {
				selection = w.getSelection();
				range = d.createRange();
				range.selectNodeContents(el);
				selection.removeAllRanges();
				selection.addRange(range);
			}

			return this;
		},
		baseAtCaret: function (medium) {
			if (!medium.isActive()) return null;

			var sel = w.getSelection ? w.getSelection() : document.selection;

			if (sel.rangeCount) {
				var selRange = sel.getRangeAt(0),
					container = selRange.endContainer;

				switch (container.nodeType) {
					case 3:
						if (container.data && container.data.length != selRange.endOffset) return false;
						break;
				}

				return container;
			}

			return null;
		},
		atCaret: function (medium) {
			var container = this.baseAtCaret(medium) || {},
				el = medium.element;

			if (container === false) return null;

			while (container && container.parentNode !== el) {
				container = container.parentNode;
			}

			if (container && container.nodeType == 1) {
				return container;
			}

			return null;
		},
		hide: function(el) {
			el.style.display = 'none';
		},
		show: function(el) {
			el.style.display = '';
		},
		hideAnim: function(el) {
			el.style.opacity = 1;
		},
		showAnim: function(el) {
			el.style.opacity = 0.01;
			el.style.display = '';
		}
	};
})(Medium);
rangy.rangePrototype.insertNodeAtEnd = function (node) {
	var range = this.cloneRange();
	range.collapse(false);
	range.insertNode(node);
	range.detach();
	this.setEndAfter(node);
};

			return Medium;
		}()),
		utils = Medium.Utilities;

	if (typeof define === 'function' && define['amd']) {
		define(function () { return Medium; });
	} else if (typeof module !== 'undefined' && module.exports) {
		module.exports = Medium;
	} else if (typeof this !== 'undefined') {
		this.Medium = Medium;
	}

}).call(this, window, document);
