/*global self, document, DOMException */

/*! @source http://purl.eligrey.com/github/classList.js/blob/master/classList.js */

// Full polyfill for browsers with no classList support
if (!("classList" in document.createElement("_"))) {
	(function(view) {

		"use strict";

		if (!('Element' in view)) return;

		var
			classListProp = "classList",
			protoProp = "prototype",
			elemCtrProto = view.Element[protoProp],
			objCtr = Object,
			strTrim = String[protoProp].trim || function() {
				return this.replace(/^\s+|\s+$/g, "");
			},
			arrIndexOf = Array[protoProp].indexOf || function(item) {
				var
					i = 0,
					len = this.length;
				for (; i < len; i++) {
					if (i in this && this[i] === item) {
						return i;
					}
				}
				return -1;
			}
			// Vendors: please allow content code to instantiate DOMExceptions
			,
			DOMEx = function(type, message) {
				this.name = type;
				this.code = DOMException[type];
				this.message = message;
			},
			checkTokenAndGetIndex = function(classList, token) {
				if (token === "") {
					throw new DOMEx(
						"SYNTAX_ERR", "An invalid or illegal string was specified"
					);
				}
				if (/\s/.test(token)) {
					throw new DOMEx(
						"INVALID_CHARACTER_ERR", "String contains an invalid character"
					);
				}
				return arrIndexOf.call(classList, token);
			},
			ClassList = function(elem) {
				var
					trimmedClasses = strTrim.call(elem.getAttribute("class") || ""),
					classes = trimmedClasses ? trimmedClasses.split(/\s+/) : [],
					i = 0,
					len = classes.length;
				for (; i < len; i++) {
					this.push(classes[i]);
				}
				this._updateClassName = function() {
					elem.setAttribute("class", this.toString());
				};
			},
			classListProto = ClassList[protoProp] = [],
			classListGetter = function() {
				return new ClassList(this);
			};
		// Most DOMException implementations don't allow calling DOMException's toString()
		// on non-DOMExceptions. Error's toString() is sufficient here.
		DOMEx[protoProp] = Error[protoProp];
		classListProto.item = function(i) {
			return this[i] || null;
		};
		classListProto.contains = function(token) {
			token += "";
			return checkTokenAndGetIndex(this, token) !== -1;
		};
		classListProto.add = function() {
			var
				tokens = arguments,
				i = 0,
				l = tokens.length,
				token, updated = false;
			do {
				token = tokens[i] + "";
				if (checkTokenAndGetIndex(this, token) === -1) {
					this.push(token);
					updated = true;
				}
			}
			while (++i < l);

			if (updated) {
				this._updateClassName();
			}
		};
		classListProto.remove = function() {
			var
				tokens = arguments,
				i = 0,
				l = tokens.length,
				token, updated = false,
				index;
			do {
				token = tokens[i] + "";
				index = checkTokenAndGetIndex(this, token);
				while (index !== -1) {
					this.splice(index, 1);
					updated = true;
					index = checkTokenAndGetIndex(this, token);
				}
			}
			while (++i < l);

			if (updated) {
				this._updateClassName();
			}
		};
		classListProto.toggle = function(token, force) {
			token += "";

			var
				result = this.contains(token),
				method = result ?
				force !== true && "remove" :
				force !== false && "add";

			if (method) {
				this[method](token);
			}

			if (force === true || force === false) {
				return force;
			} else {
				return !result;
			}
		};
		classListProto.toString = function() {
			return this.join(" ");
		};

		if (objCtr.defineProperty) {
			var classListPropDesc = {
				get: classListGetter,
				enumerable: true,
				configurable: true
			};
			try {
				objCtr.defineProperty(elemCtrProto, classListProp, classListPropDesc);
			} catch (ex) { // IE 8 doesn't support enumerable:true
				if (ex.number === -0x7FF5EC54) {
					classListPropDesc.enumerable = false;
					objCtr.defineProperty(elemCtrProto, classListProp, classListPropDesc);
				}
			}
		} else if (objCtr[protoProp].__defineGetter__) {
			elemCtrProto.__defineGetter__(classListProp, classListGetter);
		}

	}(self));
}

/* Blob.js
 * A Blob implementation.
 * 2014-07-24
 *
 * By Eli Grey, http://eligrey.com
 * By Devin Samarin, https://github.com/dsamarin
 * License: X11/MIT
 *   See https://github.com/eligrey/Blob.js/blob/master/LICENSE.md
 */

/*global self, unescape */
/*jslint bitwise: true, regexp: true, confusion: true, es5: true, vars: true, white: true,
  plusplus: true */

/*! @source http://purl.eligrey.com/github/Blob.js/blob/master/Blob.js */

(function(view) {
	"use strict";

	view.URL = view.URL || view.webkitURL;

	if (view.Blob && view.URL) {
		try {
			new Blob;
			return;
		} catch (e) {}
	}

	// Internally we use a BlobBuilder implementation to base Blob off of
	// in order to support older browsers that only have BlobBuilder
	var BlobBuilder = view.BlobBuilder || view.WebKitBlobBuilder || view.MozBlobBuilder || (function(view) {
		var
			get_class = function(object) {
				return Object.prototype.toString.call(object).match(/^\[object\s(.*)\]$/)[1];
			},
			FakeBlobBuilder = function BlobBuilder() {
				this.data = [];
			},
			FakeBlob = function Blob(data, type, encoding) {
				this.data = data;
				this.size = data.length;
				this.type = type;
				this.encoding = encoding;
			},
			FBB_proto = FakeBlobBuilder.prototype,
			FB_proto = FakeBlob.prototype,
			FileReaderSync = view.FileReaderSync,
			FileException = function(type) {
				this.code = this[this.name = type];
			},
			file_ex_codes = (
				"NOT_FOUND_ERR SECURITY_ERR ABORT_ERR NOT_READABLE_ERR ENCODING_ERR " +
				"NO_MODIFICATION_ALLOWED_ERR INVALID_STATE_ERR SYNTAX_ERR"
			).split(" "),
			file_ex_code = file_ex_codes.length,
			real_URL = view.URL || view.webkitURL || view,
			real_create_object_URL = real_URL.createObjectURL,
			real_revoke_object_URL = real_URL.revokeObjectURL,
			URL = real_URL,
			btoa = view.btoa,
			atob = view.atob

			,
			ArrayBuffer = view.ArrayBuffer,
			Uint8Array = view.Uint8Array

			,
			origin = /^[\w-]+:\/*\[?[\w\.:-]+\]?(?::[0-9]+)?/;
		FakeBlob.fake = FB_proto.fake = true;
		while (file_ex_code--) {
			FileException.prototype[file_ex_codes[file_ex_code]] = file_ex_code + 1;
		}
		// Polyfill URL
		if (!real_URL.createObjectURL) {
			URL = view.URL = function(uri) {
				var
					uri_info = document.createElementNS("http://www.w3.org/1999/xhtml", "a"),
					uri_origin;
				uri_info.href = uri;
				if (!("origin" in uri_info)) {
					if (uri_info.protocol.toLowerCase() === "data:") {
						uri_info.origin = null;
					} else {
						uri_origin = uri.match(origin);
						uri_info.origin = uri_origin && uri_origin[1];
					}
				}
				return uri_info;
			};
		}
		URL.createObjectURL = function(blob) {
			var
				type = blob.type,
				data_URI_header;
			if (type === null) {
				type = "application/octet-stream";
			}
			if (blob instanceof FakeBlob) {
				data_URI_header = "data:" + type;
				if (blob.encoding === "base64") {
					return data_URI_header + ";base64," + blob.data;
				} else if (blob.encoding === "URI") {
					return data_URI_header + "," + decodeURIComponent(blob.data);
				}
				if (btoa) {
					return data_URI_header + ";base64," + btoa(blob.data);
				} else {
					return data_URI_header + "," + encodeURIComponent(blob.data);
				}
			} else if (real_create_object_URL) {
				return real_create_object_URL.call(real_URL, blob);
			}
		};
		URL.revokeObjectURL = function(object_URL) {
			if (object_URL.substring(0, 5) !== "data:" && real_revoke_object_URL) {
				real_revoke_object_URL.call(real_URL, object_URL);
			}
		};
		FBB_proto.append = function(data /*, endings*/ ) {
			var bb = this.data;
			// decode data to a binary string
			if (Uint8Array && (data instanceof ArrayBuffer || data instanceof Uint8Array)) {
				var
					str = "",
					buf = new Uint8Array(data),
					i = 0,
					buf_len = buf.length;
				for (; i < buf_len; i++) {
					str += String.fromCharCode(buf[i]);
				}
				bb.push(str);
			} else if (get_class(data) === "Blob" || get_class(data) === "File") {
				if (FileReaderSync) {
					var fr = new FileReaderSync;
					bb.push(fr.readAsBinaryString(data));
				} else {
					// async FileReader won't work as BlobBuilder is sync
					throw new FileException("NOT_READABLE_ERR");
				}
			} else if (data instanceof FakeBlob) {
				if (data.encoding === "base64" && atob) {
					bb.push(atob(data.data));
				} else if (data.encoding === "URI") {
					bb.push(decodeURIComponent(data.data));
				} else if (data.encoding === "raw") {
					bb.push(data.data);
				}
			} else {
				if (typeof data !== "string") {
					data += ""; // convert unsupported types to strings
				}
				// decode UTF-16 to binary string
				bb.push(unescape(encodeURIComponent(data)));
			}
		};
		FBB_proto.getBlob = function(type) {
			if (!arguments.length) {
				type = null;
			}
			return new FakeBlob(this.data.join(""), type, "raw");
		};
		FBB_proto.toString = function() {
			return "[object BlobBuilder]";
		};
		FB_proto.slice = function(start, end, type) {
			var args = arguments.length;
			if (args < 3) {
				type = null;
			}
			return new FakeBlob(
				this.data.slice(start, args > 1 ? end : this.data.length), type, this.encoding
			);
		};
		FB_proto.toString = function() {
			return "[object Blob]";
		};
		FB_proto.close = function() {
			this.size = 0;
			delete this.data;
		};
		return FakeBlobBuilder;
	}(view));

	view.Blob = function(blobParts, options) {
		var type = options ? (options.type || "") : "";
		var builder = new BlobBuilder();
		if (blobParts) {
			for (var i = 0, len = blobParts.length; i < len; i++) {
				if (Uint8Array && blobParts[i] instanceof Uint8Array) {
					builder.append(blobParts[i].buffer);
				} else {
					builder.append(blobParts[i]);
				}
			}
		}
		var blob = builder.getBlob(type);
		if (!blob.slice && blob.webkitSlice) {
			blob.slice = blob.webkitSlice;
		}
		return blob;
	};

	var getPrototypeOf = Object.getPrototypeOf || function(object) {
		return object.__proto__;
	};
	view.Blob.prototype = getPrototypeOf(new view.Blob());
}(typeof self !== "undefined" && self || typeof window !== "undefined" && window || this.content || this));

(function(root, factory) {
	'use strict';
	var isElectron = typeof module === 'object' && typeof process !== 'undefined' && process && process.versions && process.versions.electron;
	if (!isElectron && typeof module === 'object') {
		module.exports = factory;
	} else if (typeof define === 'function' && define.amd) {
		define(function() {
			return factory;
		});
	} else {
		root.MediumEditor = factory;
	}
}(this, function() {

	'use strict';

	function MediumEditor(elements, options) {
		'use strict';
		return this.init(elements, options);
	}

	MediumEditor.extensions = {};
	/*jshint unused: true */
	(function(window) {
		'use strict';

		function copyInto(overwrite, dest) {
			var prop,
				sources = Array.prototype.slice.call(arguments, 2);
			dest = dest || {};
			for (var i = 0; i < sources.length; i++) {
				var source = sources[i];
				if (source) {
					for (prop in source) {
						if (source.hasOwnProperty(prop) &&
							typeof source[prop] !== 'undefined' &&
							(overwrite || dest.hasOwnProperty(prop) === false)) {
							dest[prop] = source[prop];
						}
					}
				}
			}
			return dest;
		}

		// https://developer.mozilla.org/en-US/docs/Web/API/Node/contains
		// Some browsers (including phantom) don't return true for Node.contains(child)
		// if child is a text node.  Detect these cases here and use a fallback
		// for calls to Util.isDescendant()
		var nodeContainsWorksWithTextNodes = false;
		try {
			var testParent = document.createElement('div'),
				testText = document.createTextNode(' ');
			testParent.appendChild(testText);
			nodeContainsWorksWithTextNodes = testParent.contains(testText);
		} catch (exc) {}

		var Util = {

			// http://stackoverflow.com/questions/17907445/how-to-detect-ie11#comment30165888_17907562
			// by rg89
			isIE: ((navigator.appName === 'Microsoft Internet Explorer') || ((navigator.appName === 'Netscape') && (new RegExp('Trident/.*rv:([0-9]{1,}[.0-9]{0,})').exec(navigator.userAgent) !== null))),

			isEdge: (/Edge\/\d+/).exec(navigator.userAgent) !== null,

			// if firefox
			isFF: (navigator.userAgent.toLowerCase().indexOf('firefox') > -1),

			// http://stackoverflow.com/a/11752084/569101
			isMac: (window.navigator.platform.toUpperCase().indexOf('MAC') >= 0),

			// https://github.com/jashkenas/underscore
			// Lonely letter MUST USE the uppercase code
			keyCode: {
				BACKSPACE: 8,
				TAB: 9,
				ENTER: 13,
				ESCAPE: 27,
				SPACE: 32,
				DELETE: 46,
				K: 75, // K keycode, and not k
				M: 77,
				V: 86
			},

			/**
			 * Returns true if it's metaKey on Mac, or ctrlKey on non-Mac.
			 * See #591
			 */
			isMetaCtrlKey: function(event) {
				if ((Util.isMac && event.metaKey) || (!Util.isMac && event.ctrlKey)) {
					return true;
				}

				return false;
			},

			/**
			 * Returns true if the key associated to the event is inside keys array
			 *
			 * @see : https://github.com/jquery/jquery/blob/0705be475092aede1eddae01319ec931fb9c65fc/src/event.js#L473-L484
			 * @see : http://stackoverflow.com/q/4471582/569101
			 */
			isKey: function(event, keys) {
				var keyCode = Util.getKeyCode(event);

				// it's not an array let's just compare strings!
				if (false === Array.isArray(keys)) {
					return keyCode === keys;
				}

				if (-1 === keys.indexOf(keyCode)) {
					return false;
				}

				return true;
			},

			getKeyCode: function(event) {
				var keyCode = event.which;

				// getting the key code from event
				if (null === keyCode) {
					keyCode = event.charCode !== null ? event.charCode : event.keyCode;
				}

				return keyCode;
			},

			blockContainerElementNames: [
				// elements our editor generates
				'p', 'h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'blockquote', 'pre', 'ul', 'li', 'ol',
				// all other known block elements
				'address', 'article', 'aside', 'audio', 'canvas', 'dd', 'dl', 'dt', 'fieldset',
				'figcaption', 'figure', 'footer', 'form', 'header', 'hgroup', 'main', 'nav',
				'noscript', 'output', 'section', 'video',
				'table', 'thead', 'tbody', 'tfoot', 'tr', 'th', 'td'
			],

			emptyElementNames: ['br', 'col', 'colgroup', 'hr', 'img', 'input', 'source', 'wbr'],

			extend: function extend( /* dest, source1, source2, ...*/ ) {
				var args = [true].concat(Array.prototype.slice.call(arguments));
				return copyInto.apply(this, args);
			},

			defaults: function defaults( /*dest, source1, source2, ...*/ ) {
				var args = [false].concat(Array.prototype.slice.call(arguments));
				return copyInto.apply(this, args);
			},

			/*
			 * Create a link around the provided text nodes which must be adjacent to each other and all be
			 * descendants of the same closest block container. If the preconditions are not met, unexpected
			 * behavior will result.
			 */
			createLink: function(document, textNodes, href, target) {
				var anchor = document.createElement('a');
				Util.moveTextRangeIntoElement(textNodes[0], textNodes[textNodes.length - 1], anchor);
				anchor.setAttribute('href', href);
				if (target) {
					if (target === '_blank') {
						anchor.setAttribute('rel', 'noopener noreferrer');
					}
					anchor.setAttribute('target', target);
				}
				return anchor;
			},

			/*
			 * Given the provided match in the format {start: 1, end: 2} where start and end are indices into the
			 * textContent of the provided element argument, modify the DOM inside element to ensure that the text
			 * identified by the provided match can be returned as text nodes that contain exactly that text, without
			 * any additional text at the beginning or end of the returned array of adjacent text nodes.
			 *
			 * The only DOM manipulation performed by this function is splitting the text nodes, non-text nodes are
			 * not affected in any way.
			 */
			findOrCreateMatchingTextNodes: function(document, element, match) {
				var treeWalker = document.createTreeWalker(element, NodeFilter.SHOW_ALL, null, false),
					matchedNodes = [],
					currentTextIndex = 0,
					startReached = false,
					currentNode = null,
					newNode = null;

				while ((currentNode = treeWalker.nextNode()) !== null) {
					if (currentNode.nodeType > 3) {
						continue;
					} else if (currentNode.nodeType === 3) {
						if (!startReached && match.start < (currentTextIndex + currentNode.nodeValue.length)) {
							startReached = true;
							newNode = Util.splitStartNodeIfNeeded(currentNode, match.start, currentTextIndex);
						}
						if (startReached) {
							Util.splitEndNodeIfNeeded(currentNode, newNode, match.end, currentTextIndex);
						}
						if (startReached && currentTextIndex === match.end) {
							break; // Found the node(s) corresponding to the link. Break out and move on to the next.
						} else if (startReached && currentTextIndex > (match.end + 1)) {
							throw new Error('PerformLinking overshot the target!'); // should never happen...
						}

						if (startReached) {
							matchedNodes.push(newNode || currentNode);
						}

						currentTextIndex += currentNode.nodeValue.length;
						if (newNode !== null) {
							currentTextIndex += newNode.nodeValue.length;
							// Skip the newNode as we'll already have pushed it to the matches
							treeWalker.nextNode();
						}
						newNode = null;
					} else if (currentNode.tagName.toLowerCase() === 'img') {
						if (!startReached && (match.start <= currentTextIndex)) {
							startReached = true;
						}
						if (startReached) {
							matchedNodes.push(currentNode);
						}
					}
				}
				return matchedNodes;
			},

			/*
			 * Given the provided text node and text coordinates, split the text node if needed to make it align
			 * precisely with the coordinates.
			 *
			 * This function is intended to be called from Util.findOrCreateMatchingTextNodes.
			 */
			splitStartNodeIfNeeded: function(currentNode, matchStartIndex, currentTextIndex) {
				if (matchStartIndex !== currentTextIndex) {
					return currentNode.splitText(matchStartIndex - currentTextIndex);
				}
				return null;
			},

			/*
			 * Given the provided text node and text coordinates, split the text node if needed to make it align
			 * precisely with the coordinates. The newNode argument should from the result of Util.splitStartNodeIfNeeded,
			 * if that function has been called on the same currentNode.
			 *
			 * This function is intended to be called from Util.findOrCreateMatchingTextNodes.
			 */
			splitEndNodeIfNeeded: function(currentNode, newNode, matchEndIndex, currentTextIndex) {
				var textIndexOfEndOfFarthestNode,
					endSplitPoint;
				textIndexOfEndOfFarthestNode = currentTextIndex + currentNode.nodeValue.length +
					(newNode ? newNode.nodeValue.length : 0) - 1;
				endSplitPoint = matchEndIndex - currentTextIndex -
					(newNode ? currentNode.nodeValue.length : 0);
				if (textIndexOfEndOfFarthestNode >= matchEndIndex &&
					currentTextIndex !== textIndexOfEndOfFarthestNode &&
					endSplitPoint !== 0) {
					(newNode || currentNode).splitText(endSplitPoint);
				}
			},

			/*
			 * Take an element, and break up all of its text content into unique pieces such that:
			 * 1) All text content of the elements are in separate blocks. No piece of text content should span
			 *    across multiple blocks. This means no element return by this function should have
			 *    any blocks as children.
			 * 2) The union of the textcontent of all of the elements returned here covers all
			 *    of the text within the element.
			 *
			 *
			 * EXAMPLE:
			 * In the event that we have something like:
			 *
			 * <blockquote>
			 *   <p>Some Text</p>
			 *   <ol>
			 *     <li>List Item 1</li>
			 *     <li>List Item 2</li>
			 *   </ol>
			 * </blockquote>
			 *
			 * This function would return these elements as an array:
			 *   [ <p>Some Text</p>, <li>List Item 1</li>, <li>List Item 2</li> ]
			 *
			 * Since the <blockquote> and <ol> elements contain blocks within them they are not returned.
			 * Since the <p> and <li>'s don't contain block elements and cover all the text content of the
			 * <blockquote> container, they are the elements returned.
			 */
			splitByBlockElements: function(element) {
				if (element.nodeType !== 3 && element.nodeType !== 1) {
					return [];
				}

				var toRet = [],
					blockElementQuery = MediumEditor.util.blockContainerElementNames.join(',');

				if (element.nodeType === 3 || element.querySelectorAll(blockElementQuery).length === 0) {
					return [element];
				}

				for (var i = 0; i < element.childNodes.length; i++) {
					var child = element.childNodes[i];
					if (child.nodeType === 3) {
						toRet.push(child);
					} else if (child.nodeType === 1) {
						var blockElements = child.querySelectorAll(blockElementQuery);
						if (blockElements.length === 0) {
							toRet.push(child);
						} else {
							toRet = toRet.concat(MediumEditor.util.splitByBlockElements(child));
						}
					}
				}

				return toRet;
			},

			// Find the next node in the DOM tree that represents any text that is being
			// displayed directly next to the targetNode (passed as an argument)
			// Text that appears directly next to the current node can be:
			//  - A sibling text node
			//  - A descendant of a sibling element
			//  - A sibling text node of an ancestor
			//  - A descendant of a sibling element of an ancestor
			findAdjacentTextNodeWithContent: function findAdjacentTextNodeWithContent(rootNode, targetNode, ownerDocument) {
				var pastTarget = false,
					nextNode,
					nodeIterator = ownerDocument.createNodeIterator(rootNode, NodeFilter.SHOW_TEXT, null, false);

				// Use a native NodeIterator to iterate over all the text nodes that are descendants
				// of the rootNode.  Once past the targetNode, choose the first non-empty text node
				nextNode = nodeIterator.nextNode();
				while (nextNode) {
					if (nextNode === targetNode) {
						pastTarget = true;
					} else if (pastTarget) {
						if (nextNode.nodeType === 3 && nextNode.nodeValue && nextNode.nodeValue.trim().length > 0) {
							break;
						}
					}
					nextNode = nodeIterator.nextNode();
				}

				return nextNode;
			},

			// Find an element's previous sibling within a medium-editor element
			// If one doesn't exist, find the closest ancestor's previous sibling
			findPreviousSibling: function(node) {
				if (!node || Util.isMediumEditorElement(node)) {
					return false;
				}

				var previousSibling = node.previousSibling;
				while (!previousSibling && !Util.isMediumEditorElement(node.parentNode)) {
					node = node.parentNode;
					previousSibling = node.previousSibling;
				}

				return previousSibling;
			},

			isDescendant: function isDescendant(parent, child, checkEquality) {
				if (!parent || !child) {
					return false;
				}
				if (parent === child) {
					return !!checkEquality;
				}
				// If parent is not an element, it can't have any descendants
				if (parent.nodeType !== 1) {
					return false;
				}
				if (nodeContainsWorksWithTextNodes || child.nodeType !== 3) {
					return parent.contains(child);
				}
				var node = child.parentNode;
				while (node !== null) {
					if (node === parent) {
						return true;
					}
					node = node.parentNode;
				}
				return false;
			},

			// https://github.com/jashkenas/underscore
			isElement: function isElement(obj) {
				return !!(obj && obj.nodeType === 1);
			},

			// https://github.com/jashkenas/underscore
			throttle: function(func, wait) {
				var THROTTLE_INTERVAL = 50,
					context,
					args,
					result,
					timeout = null,
					previous = 0,
					later = function() {
						previous = Date.now();
						timeout = null;
						result = func.apply(context, args);
						if (!timeout) {
							context = args = null;
						}
					};

				if (!wait && wait !== 0) {
					wait = THROTTLE_INTERVAL;
				}

				return function() {
					var now = Date.now(),
						remaining = wait - (now - previous);

					context = this;
					args = arguments;
					if (remaining <= 0 || remaining > wait) {
						if (timeout) {
							clearTimeout(timeout);
							timeout = null;
						}
						previous = now;
						result = func.apply(context, args);
						if (!timeout) {
							context = args = null;
						}
					} else if (!timeout) {
						timeout = setTimeout(later, remaining);
					}
					return result;
				};
			},

			traverseUp: function(current, testElementFunction) {
				if (!current) {
					return false;
				}

				do {
					if (current.nodeType === 1) {
						if (testElementFunction(current)) {
							return current;
						}
						// do not traverse upwards past the nearest containing editor
						if (Util.isMediumEditorElement(current)) {
							return false;
						}
					}

					current = current.parentNode;
				} while (current);

				return false;
			},

			htmlEntities: function(str) {
				// converts special characters (like <) into their escaped/encoded values (like &lt;).
				// This allows you to show to display the string without the browser reading it as HTML.
				return String(str).replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;').replace(/"/g, '&quot;');
			},

			// http://stackoverflow.com/questions/6690752/insert-html-at-caret-in-a-contenteditable-div
			insertHTMLCommand: function(doc, html) {
				var selection, range, el, fragment, node, lastNode, toReplace,
					res = false,
					ecArgs = ['insertHTML', false, html];

				/* Edge's implementation of insertHTML is just buggy right now:
				 * - Doesn't allow leading white space at the beginning of an element
				 * - Found a case when a <font size="2"> tag was inserted when calling alignCenter inside a blockquote
				 *
				 * There are likely other bugs, these are just the ones we found so far.
				 * For now, let's just use the same fallback we did for IE
				 */
				if (!MediumEditor.util.isEdge && doc.queryCommandSupported('insertHTML')) {
					try {
						return doc.execCommand.apply(doc, ecArgs);
					} catch (ignore) {}
				}

				selection = doc.getSelection();
				if (selection.rangeCount) {
					range = selection.getRangeAt(0);
					toReplace = range.commonAncestorContainer;

					// https://github.com/yabwe/medium-editor/issues/748
					// If the selection is an empty editor element, create a temporary text node inside of the editor
					// and select it so that we don't delete the editor element
					if (Util.isMediumEditorElement(toReplace) && !toReplace.firstChild) {
						range.selectNode(toReplace.appendChild(doc.createTextNode('')));
					} else if ((toReplace.nodeType === 3 && range.startOffset === 0 && range.endOffset === toReplace.nodeValue.length) ||
						(toReplace.nodeType !== 3 && toReplace.innerHTML === range.toString())) {
						// Ensure range covers maximum amount of nodes as possible
						// By moving up the DOM and selecting ancestors whose only child is the range
						while (!Util.isMediumEditorElement(toReplace) &&
							toReplace.parentNode &&
							toReplace.parentNode.childNodes.length === 1 &&
							!Util.isMediumEditorElement(toReplace.parentNode)) {
							toReplace = toReplace.parentNode;
						}
						range.selectNode(toReplace);
					}
					range.deleteContents();

					el = doc.createElement('div');
					el.innerHTML = html;
					fragment = doc.createDocumentFragment();
					while (el.firstChild) {
						node = el.firstChild;
						lastNode = fragment.appendChild(node);
					}
					range.insertNode(fragment);

					// Preserve the selection:
					if (lastNode) {
						range = range.cloneRange();
						range.setStartAfter(lastNode);
						range.collapse(true);
						MediumEditor.selection.selectRange(doc, range);
					}
					res = true;
				}

				// https://github.com/yabwe/medium-editor/issues/992
				// If we're monitoring calls to execCommand, notify listeners as if a real call had happened
				if (doc.execCommand.callListeners) {
					doc.execCommand.callListeners(ecArgs, res);
				}
				return res;
			},

			execFormatBlock: function(doc, tagName) {
				// Get the top level block element that contains the selection
				var blockContainer = Util.getTopBlockContainer(MediumEditor.selection.getSelectionStart(doc)),
					childNodes;

				// Special handling for blockquote
				if (tagName === 'blockquote') {
					if (blockContainer) {
						childNodes = Array.prototype.slice.call(blockContainer.childNodes);
						// Check if the blockquote has a block element as a child (nested blocks)
						if (childNodes.some(function(childNode) {
								return Util.isBlockContainer(childNode);
							})) {
							// FF handles blockquote differently on formatBlock
							// allowing nesting, we need to use outdent
							// https://developer.mozilla.org/en-US/docs/Rich-Text_Editing_in_Mozilla
							return doc.execCommand('outdent', false, null);
						}
					}

					// When IE blockquote needs to be called as indent
					// http://stackoverflow.com/questions/1816223/rich-text-editor-with-blockquote-function/1821777#1821777
					if (Util.isIE) {
						return doc.execCommand('indent', false, tagName);
					}
				}

				// If the blockContainer is already the element type being passed in
				// treat it as 'undo' formatting and just convert it to a <p>
				if (blockContainer && tagName === blockContainer.nodeName.toLowerCase()) {
					tagName = 'p';
				}

				// When IE we need to add <> to heading elements
				// http://stackoverflow.com/questions/10741831/execcommand-formatblock-headings-in-ie
				if (Util.isIE) {
					tagName = '<' + tagName + '>';
				}

				// When FF, IE and Edge, we have to handle blockquote node seperately as 'formatblock' does not work.
				// https://developer.mozilla.org/en-US/docs/Web/API/Document/execCommand#Commands
				if (blockContainer && blockContainer.nodeName.toLowerCase() === 'blockquote') {
					// For IE, just use outdent
					if (Util.isIE && tagName === '<p>') {
						return doc.execCommand('outdent', false, tagName);
					}

					// For Firefox and Edge, make sure there's a nested block element before calling outdent
					if ((Util.isFF || Util.isEdge) && tagName === 'p') {
						childNodes = Array.prototype.slice.call(blockContainer.childNodes);
						// If there are some non-block elements we need to wrap everything in a <p> before we outdent
						if (childNodes.some(function(childNode) {
								return !Util.isBlockContainer(childNode);
							})) {
							doc.execCommand('formatBlock', false, tagName);
						}
						return doc.execCommand('outdent', false, tagName);
					}
				}

				return doc.execCommand('formatBlock', false, tagName);
			},

			/**
			 * Set target to blank on the given el element
			 *
			 * TODO: not sure if this should be here
			 *
			 * When creating a link (using core -> createLink) the selection returned by Firefox will be the parent of the created link
			 * instead of the created link itself (as it is for Chrome for example), so we retrieve all "a" children to grab the good one by
			 * using `anchorUrl` to ensure that we are adding target="_blank" on the good one.
			 * This isn't a bulletproof solution anyway ..
			 */
			setTargetBlank: function(el, anchorUrl) {
				var i, url = anchorUrl || false;
				if (el.nodeName.toLowerCase() === 'a') {
					el.target = '_blank';
					el.rel = 'noopener noreferrer';
				} else {
					el = el.getElementsByTagName('a');

					for (i = 0; i < el.length; i += 1) {
						if (false === url || url === el[i].attributes.href.value) {
							el[i].target = '_blank';
							el[i].rel = 'noopener noreferrer';
						}
					}
				}
			},

			/*
			 * this function is called to explicitly remove the target='_blank' as FF holds on to _blank value even
			 * after unchecking the checkbox on anchor form
			 */
			removeTargetBlank: function(el, anchorUrl) {
				var i;
				if (el.nodeName.toLowerCase() === 'a') {
					el.removeAttribute('target');
					el.removeAttribute('rel');
				} else {
					el = el.getElementsByTagName('a');

					for (i = 0; i < el.length; i += 1) {
						if (anchorUrl === el[i].attributes.href.value) {
							el[i].removeAttribute('target');
							el[i].removeAttribute('rel');
						}
					}
				}
			},

			/*
			 * this function adds one or several classes on an a element.
			 * if el parameter is not an a, it will look for a children of el.
			 * if no a children are found, it will look for the a parent.
			 */
			addClassToAnchors: function(el, buttonClass) {
				var classes = buttonClass.split(' '),
					i,
					j;
				if (el.nodeName.toLowerCase() === 'a') {
					for (j = 0; j < classes.length; j += 1) {
						el.classList.add(classes[j]);
					}
				} else {
					var aChildren = el.getElementsByTagName('a');
					if (aChildren.length === 0) {
						var parentAnchor = Util.getClosestTag(el, 'a');
						el = parentAnchor ? [parentAnchor] : [];
					} else {
						el = aChildren;
					}
					for (i = 0; i < el.length; i += 1) {
						for (j = 0; j < classes.length; j += 1) {
							el[i].classList.add(classes[j]);
						}
					}
				}
			},

			isListItem: function(node) {
				if (!node) {
					return false;
				}
				if (node.nodeName.toLowerCase() === 'li') {
					return true;
				}

				var parentNode = node.parentNode,
					tagName = parentNode.nodeName.toLowerCase();
				while (tagName === 'li' || (!Util.isBlockContainer(parentNode) && tagName !== 'div')) {
					if (tagName === 'li') {
						return true;
					}
					parentNode = parentNode.parentNode;
					if (parentNode) {
						tagName = parentNode.nodeName.toLowerCase();
					} else {
						return false;
					}
				}
				return false;
			},

			cleanListDOM: function(ownerDocument, element) {
				if (element.nodeName.toLowerCase() !== 'li') {
					return;
				}

				var list = element.parentElement;

				if (list.parentElement.nodeName.toLowerCase() === 'p') { // yes we need to clean up
					Util.unwrap(list.parentElement, ownerDocument);

					// move cursor at the end of the text inside the list
					// for some unknown reason, the cursor is moved to end of the "visual" line
					MediumEditor.selection.moveCursor(ownerDocument, element.firstChild, element.firstChild.textContent.length);
				}
			},

			/* splitDOMTree
			 *
			 * Given a root element some descendant element, split the root element
			 * into its own element containing the descendant element and all elements
			 * on the left or right side of the descendant ('right' is default)
			 *
			 * example:
			 *
			 *         <div>
			 *      /    |   \
			 *  <span> <span> <span>
			 *   / \    / \    / \
			 *  1   2  3   4  5   6
			 *
			 *  If I wanted to split this tree given the <div> as the root and "4" as the leaf
			 *  the result would be (the prime ' marks indicates nodes that are created as clones):
			 *
			 *   SPLITTING OFF 'RIGHT' TREE       SPLITTING OFF 'LEFT' TREE
			 *
			 *     <div>            <div>'              <div>'      <div>
			 *      / \              / \                 / \          |
			 * <span> <span>   <span>' <span>       <span> <span>   <span>
			 *   / \    |        |      / \           /\     /\       /\
			 *  1   2   3        4     5   6         1  2   3  4     5  6
			 *
			 *  The above example represents splitting off the 'right' or 'left' part of a tree, where
			 *  the <div>' would be returned as an element not appended to the DOM, and the <div>
			 *  would remain in place where it was
			 *
			 */
			splitOffDOMTree: function(rootNode, leafNode, splitLeft) {
				var splitOnNode = leafNode,
					createdNode = null,
					splitRight = !splitLeft;

				// loop until we hit the root
				while (splitOnNode !== rootNode) {
					var currParent = splitOnNode.parentNode,
						newParent = currParent.cloneNode(false),
						targetNode = (splitRight ? splitOnNode : currParent.firstChild),
						appendLast;

					// Create a new parent element which is a clone of the current parent
					if (createdNode) {
						if (splitRight) {
							// If we're splitting right, add previous created element before siblings
							newParent.appendChild(createdNode);
						} else {
							// If we're splitting left, add previous created element last
							appendLast = createdNode;
						}
					}
					createdNode = newParent;

					while (targetNode) {
						var sibling = targetNode.nextSibling;
						// Special handling for the 'splitNode'
						if (targetNode === splitOnNode) {
							if (!targetNode.hasChildNodes()) {
								targetNode.parentNode.removeChild(targetNode);
							} else {
								// For the node we're splitting on, if it has children, we need to clone it
								// and not just move it
								targetNode = targetNode.cloneNode(false);
							}
							// If the resulting split node has content, add it
							if (targetNode.textContent) {
								createdNode.appendChild(targetNode);
							}

							targetNode = (splitRight ? sibling : null);
						} else {
							// For general case, just remove the element and only
							// add it to the split tree if it contains something
							targetNode.parentNode.removeChild(targetNode);
							if (targetNode.hasChildNodes() || targetNode.textContent) {
								createdNode.appendChild(targetNode);
							}

							targetNode = sibling;
						}
					}

					// If we had an element we wanted to append at the end, do that now
					if (appendLast) {
						createdNode.appendChild(appendLast);
					}

					splitOnNode = currParent;
				}

				return createdNode;
			},

			moveTextRangeIntoElement: function(startNode, endNode, newElement) {
				if (!startNode || !endNode) {
					return false;
				}

				var rootNode = Util.findCommonRoot(startNode, endNode);
				if (!rootNode) {
					return false;
				}

				if (endNode === startNode) {
					var temp = startNode.parentNode,
						sibling = startNode.nextSibling;
					temp.removeChild(startNode);
					newElement.appendChild(startNode);
					if (sibling) {
						temp.insertBefore(newElement, sibling);
					} else {
						temp.appendChild(newElement);
					}
					return newElement.hasChildNodes();
				}

				// create rootChildren array which includes all the children
				// we care about
				var rootChildren = [],
					firstChild,
					lastChild,
					nextNode;
				for (var i = 0; i < rootNode.childNodes.length; i++) {
					nextNode = rootNode.childNodes[i];
					if (!firstChild) {
						if (Util.isDescendant(nextNode, startNode, true)) {
							firstChild = nextNode;
						}
					} else {
						if (Util.isDescendant(nextNode, endNode, true)) {
							lastChild = nextNode;
							break;
						} else {
							rootChildren.push(nextNode);
						}
					}
				}

				var afterLast = lastChild.nextSibling,
					fragment = rootNode.ownerDocument.createDocumentFragment();

				// build up fragment on startNode side of tree
				if (firstChild === startNode) {
					firstChild.parentNode.removeChild(firstChild);
					fragment.appendChild(firstChild);
				} else {
					fragment.appendChild(Util.splitOffDOMTree(firstChild, startNode));
				}

				// add any elements between firstChild & lastChild
				rootChildren.forEach(function(element) {
					element.parentNode.removeChild(element);
					fragment.appendChild(element);
				});

				// build up fragment on endNode side of the tree
				if (lastChild === endNode) {
					lastChild.parentNode.removeChild(lastChild);
					fragment.appendChild(lastChild);
				} else {
					fragment.appendChild(Util.splitOffDOMTree(lastChild, endNode, true));
				}

				// Add fragment into passed in element
				newElement.appendChild(fragment);

				if (lastChild.parentNode === rootNode) {
					// If last child is in the root, insert newElement in front of it
					rootNode.insertBefore(newElement, lastChild);
				} else if (afterLast) {
					// If last child was removed, but it had a sibling, insert in front of it
					rootNode.insertBefore(newElement, afterLast);
				} else {
					// lastChild was removed and was the last actual element just append
					rootNode.appendChild(newElement);
				}

				return newElement.hasChildNodes();
			},

			/* based on http://stackoverflow.com/a/6183069 */
			depthOfNode: function(inNode) {
				var theDepth = 0,
					node = inNode;
				while (node.parentNode !== null) {
					node = node.parentNode;
					theDepth++;
				}
				return theDepth;
			},

			findCommonRoot: function(inNode1, inNode2) {
				var depth1 = Util.depthOfNode(inNode1),
					depth2 = Util.depthOfNode(inNode2),
					node1 = inNode1,
					node2 = inNode2;

				while (depth1 !== depth2) {
					if (depth1 > depth2) {
						node1 = node1.parentNode;
						depth1 -= 1;
					} else {
						node2 = node2.parentNode;
						depth2 -= 1;
					}
				}

				while (node1 !== node2) {
					node1 = node1.parentNode;
					node2 = node2.parentNode;
				}

				return node1;
			},
			/* END - based on http://stackoverflow.com/a/6183069 */

			isElementAtBeginningOfBlock: function(node) {
				var textVal,
					sibling;
				while (!Util.isBlockContainer(node) && !Util.isMediumEditorElement(node)) {
					sibling = node;
					while (sibling = sibling.previousSibling) {
						textVal = sibling.nodeType === 3 ? sibling.nodeValue : sibling.textContent;
						if (textVal.length > 0) {
							return false;
						}
					}
					node = node.parentNode;
				}
				return true;
			},

			isMediumEditorElement: function(element) {
				return element && element.getAttribute && !!element.getAttribute('data-medium-editor-element');
			},

			getContainerEditorElement: function(element) {
				return Util.traverseUp(element, function(node) {
					return Util.isMediumEditorElement(node);
				});
			},

			isBlockContainer: function(element) {
				return element && element.nodeType !== 3 && Util.blockContainerElementNames.indexOf(element.nodeName.toLowerCase()) !== -1;
			},

			/* Finds the closest ancestor which is a block container element
			 * If element is within editor element but not within any other block element,
			 * the editor element is returned
			 */
			getClosestBlockContainer: function(node) {
				return Util.traverseUp(node, function(node) {
					return Util.isBlockContainer(node) || Util.isMediumEditorElement(node);
				});
			},

			/* Finds highest level ancestor element which is a block container element
			 * If element is within editor element but not within any other block element,
			 * the editor element is returned
			 */
			getTopBlockContainer: function(element) {
				var topBlock = Util.isBlockContainer(element) ? element : false;
				Util.traverseUp(element, function(el) {
					if (Util.isBlockContainer(el)) {
						topBlock = el;
					}
					if (!topBlock && Util.isMediumEditorElement(el)) {
						topBlock = el;
						return true;
					}
					return false;
				});
				return topBlock;
			},

			getFirstSelectableLeafNode: function(element) {
				while (element && element.firstChild) {
					element = element.firstChild;
				}

				// We don't want to set the selection to an element that can't have children, this messes up Gecko.
				element = Util.traverseUp(element, function(el) {
					return Util.emptyElementNames.indexOf(el.nodeName.toLowerCase()) === -1;
				});
				// Selecting at the beginning of a table doesn't work in PhantomJS.
				if (element.nodeName.toLowerCase() === 'table') {
					var firstCell = element.querySelector('th, td');
					if (firstCell) {
						element = firstCell;
					}
				}
				return element;
			},

			// TODO: remove getFirstTextNode AND _getFirstTextNode when jumping in 6.0.0 (no code references)
			getFirstTextNode: function(element) {
				Util.warn('getFirstTextNode is deprecated and will be removed in version 6.0.0');
				return Util._getFirstTextNode(element);
			},

			_getFirstTextNode: function(element) {
				if (element.nodeType === 3) {
					return element;
				}

				for (var i = 0; i < element.childNodes.length; i++) {
					var textNode = Util._getFirstTextNode(element.childNodes[i]);
					if (textNode !== null) {
						return textNode;
					}
				}
				return null;
			},

			ensureUrlHasProtocol: function(url) {
				if (url.indexOf('://') === -1) {
					return 'http://' + url;
				}
				return url;
			},

			warn: function() {
				if (window.console !== undefined && typeof window.console.warn === 'function') {
					window.console.warn.apply(window.console, arguments);
				}
			},

			deprecated: function(oldName, newName, version) {
				// simple deprecation warning mechanism.
				var m = oldName + ' is deprecated, please use ' + newName + ' instead.';
				if (version) {
					m += ' Will be removed in ' + version;
				}
				Util.warn(m);
			},

			deprecatedMethod: function(oldName, newName, args, version) {
				// run the replacement and warn when someone calls a deprecated method
				Util.deprecated(oldName, newName, version);
				if (typeof this[newName] === 'function') {
					this[newName].apply(this, args);
				}
			},

			cleanupAttrs: function(el, attrs) {
				attrs.forEach(function(attr) {
					el.removeAttribute(attr);
				});
			},

			cleanupTags: function(el, tags) {
				if (tags.indexOf(el.nodeName.toLowerCase()) !== -1) {
					el.parentNode.removeChild(el);
				}
			},

			unwrapTags: function(el, tags) {
				if (tags.indexOf(el.nodeName.toLowerCase()) !== -1) {
					MediumEditor.util.unwrap(el, document);
				}
			},

			// get the closest parent
			getClosestTag: function(el, tag) {
				return Util.traverseUp(el, function(element) {
					return element.nodeName.toLowerCase() === tag.toLowerCase();
				});
			},

			unwrap: function(el, doc) {
				var fragment = doc.createDocumentFragment(),
					nodes = Array.prototype.slice.call(el.childNodes);

				// cast nodeList to array since appending child
				// to a different node will alter length of el.childNodes
				for (var i = 0; i < nodes.length; i++) {
					fragment.appendChild(nodes[i]);
				}

				if (fragment.childNodes.length) {
					el.parentNode.replaceChild(fragment, el);
				} else {
					el.parentNode.removeChild(el);
				}
			},

			guid: function() {
				function _s4() {
					return Math
						.floor((1 + Math.random()) * 0x10000)
						.toString(16)
						.substring(1);
				}

				return _s4() + _s4() + '-' + _s4() + '-' + _s4() + '-' + _s4() + '-' + _s4() + _s4() + _s4();
			}
		};

		MediumEditor.util = Util;
	}(window));

	(function() {
		'use strict';

		var Extension = function(options) {
			MediumEditor.util.extend(this, options);
		};

		Extension.extend = function(protoProps) {
			// magic extender thinger. mostly borrowed from backbone/goog.inherits
			// place this function on some thing you want extend-able.
			//
			// example:
			//
			//      function Thing(args){
			//          this.options = args;
			//      }
			//
			//      Thing.prototype = { foo: "bar" };
			//      Thing.extend = extenderify;
			//
			//      var ThingTwo = Thing.extend({ foo: "baz" });
			//
			//      var thingOne = new Thing(); // foo === "bar"
			//      var thingTwo = new ThingTwo(); // foo === "baz"
			//
			//      which seems like some simply shallow copy nonsense
			//      at first, but a lot more is going on there.
			//
			//      passing a `constructor` to the extend props
			//      will cause the instance to instantiate through that
			//      instead of the parent's constructor.

			var parent = this,
				child;

			// The constructor function for the new subclass is either defined by you
			// (the "constructor" property in your `extend` definition), or defaulted
			// by us to simply call the parent's constructor.

			if (protoProps && protoProps.hasOwnProperty('constructor')) {
				child = protoProps.constructor;
			} else {
				child = function() {
					return parent.apply(this, arguments);
				};
			}

			// das statics (.extend comes over, so your subclass can have subclasses too)
			MediumEditor.util.extend(child, parent);

			// Set the prototype chain to inherit from `parent`, without calling
			// `parent`'s constructor function.
			var Surrogate = function() {
				this.constructor = child;
			};
			Surrogate.prototype = parent.prototype;
			child.prototype = new Surrogate();

			if (protoProps) {
				MediumEditor.util.extend(child.prototype, protoProps);
			}

			// todo: $super?

			return child;
		};

		Extension.prototype = {
			/* init: [function]
			 *
			 * Called by MediumEditor during initialization.
			 * The .base property will already have been set to
			 * current instance of MediumEditor when this is called.
			 * All helper methods will exist as well
			 */
			init: function() {},

			/* base: [MediumEditor instance]
			 *
			 * If not overriden, this will be set to the current instance
			 * of MediumEditor, before the init method is called
			 */
			base: undefined,

			/* name: [string]
			 *
			 * 'name' of the extension, used for retrieving the extension.
			 * If not set, MediumEditor will set this to be the key
			 * used when passing the extension into MediumEditor via the
			 * 'extensions' option
			 */
			name: undefined,

			/* checkState: [function (node)]
			 *
			 * If implemented, this function will be called one or more times
			 * the state of the editor & toolbar are updated.
			 * When the state is updated, the editor does the following:
			 *
			 * 1) Find the parent node containing the current selection
			 * 2) Call checkState on the extension, passing the node as an argument
			 * 3) Get the parent node of the previous node
			 * 4) Repeat steps #2 and #3 until we move outside the parent contenteditable
			 */
			checkState: undefined,

			/* destroy: [function ()]
			 *
			 * This method should remove any created html, custom event handlers
			 * or any other cleanup tasks that should be performed.
			 * If implemented, this function will be called when MediumEditor's
			 * destroy method has been called.
			 */
			destroy: undefined,

			/* As alternatives to checkState, these functions provide a more structured
			 * path to updating the state of an extension (usually a button) whenever
			 * the state of the editor & toolbar are updated.
			 */

			/* queryCommandState: [function ()]
			 *
			 * If implemented, this function will be called once on each extension
			 * when the state of the editor/toolbar is being updated.
			 *
			 * If this function returns a non-null value, the extension will
			 * be ignored as the code climbs the dom tree.
			 *
			 * If this function returns true, and the setActive() function is defined
			 * setActive() will be called
			 */
			queryCommandState: undefined,

			/* isActive: [function ()]
			 *
			 * If implemented, this function will be called when MediumEditor
			 * has determined that this extension is 'active' for the current selection.
			 * This may be called when the editor & toolbar are being updated,
			 * but only if queryCommandState() or isAlreadyApplied() functions
			 * are implemented, and when called, return true.
			 */
			isActive: undefined,

			/* isAlreadyApplied: [function (node)]
			 *
			 * If implemented, this function is similar to checkState() in
			 * that it will be called repeatedly as MediumEditor moves up
			 * the DOM to update the editor & toolbar after a state change.
			 *
			 * NOTE: This function will NOT be called if checkState() has
			 * been implemented. This function will NOT be called if
			 * queryCommandState() is implemented and returns a non-null
			 * value when called
			 */
			isAlreadyApplied: undefined,

			/* setActive: [function ()]
			 *
			 * If implemented, this function is called when MediumEditor knows
			 * that this extension is currently enabled.  Currently, this
			 * function is called when updating the editor & toolbar, and
			 * only if queryCommandState() or isAlreadyApplied(node) return
			 * true when called
			 */
			setActive: undefined,

			/* setInactive: [function ()]
			 *
			 * If implemented, this function is called when MediumEditor knows
			 * that this extension is currently disabled.  Curently, this
			 * is called at the beginning of each state change for
			 * the editor & toolbar. After calling this, MediumEditor
			 * will attempt to update the extension, either via checkState()
			 * or the combination of queryCommandState(), isAlreadyApplied(node),
			 * isActive(), and setActive()
			 */
			setInactive: undefined,

			/* getInteractionElements: [function ()]
			 *
			 * If the extension renders any elements that the user can interact with,
			 * this method should be implemented and return the root element or an array
			 * containing all of the root elements. MediumEditor will call this function
			 * during interaction to see if the user clicked on something outside of the editor.
			 * The elements are used to check if the target element of a click or
			 * other user event is a descendant of any extension elements.
			 * This way, the editor can also count user interaction within editor elements as
			 * interactions with the editor, and thus not trigger 'blur'
			 */
			getInteractionElements: undefined,

			/************************ Helpers ************************
			 * The following are helpers that are either set by MediumEditor
			 * during initialization, or are helper methods which either
			 * route calls to the MediumEditor instance or provide common
			 * functionality for all extensions
			 *********************************************************/

			/* window: [Window]
			 *
			 * If not overriden, this will be set to the window object
			 * to be used by MediumEditor and its extensions.  This is
			 * passed via the 'contentWindow' option to MediumEditor
			 * and is the global 'window' object by default
			 */
			'window': undefined,

			/* document: [Document]
			 *
			 * If not overriden, this will be set to the document object
			 * to be used by MediumEditor and its extensions. This is
			 * passed via the 'ownerDocument' optin to MediumEditor
			 * and is the global 'document' object by default
			 */
			'document': undefined,

			/* getEditorElements: [function ()]
			 *
			 * Helper function which returns an array containing
			 * all the contenteditable elements for this instance
			 * of MediumEditor
			 */
			getEditorElements: function() {
				return this.base.elements;
			},

			/* getEditorId: [function ()]
			 *
			 * Helper function which returns a unique identifier
			 * for this instance of MediumEditor
			 */
			getEditorId: function() {
				return this.base.id;
			},

			/* getEditorOptions: [function (option)]
			 *
			 * Helper function which returns the value of an option
			 * used to initialize this instance of MediumEditor
			 */
			getEditorOption: function(option) {
				return this.base.options[option];
			}
		};

		/* List of method names to add to the prototype of Extension
		 * Each of these methods will be defined as helpers that
		 * just call directly into the MediumEditor instance.
		 *
		 * example for 'on' method:
		 * Extension.prototype.on = function () {
		 *     return this.base.on.apply(this.base, arguments);
		 * }
		 */
		[
			// general helpers
			'execAction',

			// event handling
			'on',
			'off',
			'subscribe',
			'trigger'

		].forEach(function(helper) {
			Extension.prototype[helper] = function() {
				return this.base[helper].apply(this.base, arguments);
			};
		});

		MediumEditor.Extension = Extension;
	})();

	(function() {
		'use strict';

		function filterOnlyParentElements(node) {
			if (MediumEditor.util.isBlockContainer(node)) {
				return NodeFilter.FILTER_ACCEPT;
			} else {
				return NodeFilter.FILTER_SKIP;
			}
		}

		var Selection = {
			findMatchingSelectionParent: function(testElementFunction, contentWindow) {
				var selection = contentWindow.getSelection(),
					range,
					current;

				if (selection.rangeCount === 0) {
					return false;
				}

				range = selection.getRangeAt(0);
				current = range.commonAncestorContainer;

				return MediumEditor.util.traverseUp(current, testElementFunction);
			},

			getSelectionElement: function(contentWindow) {
				return this.findMatchingSelectionParent(function(el) {
					return MediumEditor.util.isMediumEditorElement(el);
				}, contentWindow);
			},

			// http://stackoverflow.com/questions/17678843/cant-restore-selection-after-html-modify-even-if-its-the-same-html
			// Tim Down
			exportSelection: function(root, doc) {
				if (!root) {
					return null;
				}

				var selectionState = null,
					selection = doc.getSelection();

				if (selection.rangeCount > 0) {
					var range = selection.getRangeAt(0),
						preSelectionRange = range.cloneRange(),
						start;

					preSelectionRange.selectNodeContents(root);
					preSelectionRange.setEnd(range.startContainer, range.startOffset);
					start = preSelectionRange.toString().length;

					selectionState = {
						start: start,
						end: start + range.toString().length
					};

					// Check to see if the selection starts with any images
					// if so we need to make sure the the beginning of the selection is
					// set correctly when importing selection
					if (this.doesRangeStartWithImages(range, doc)) {
						selectionState.startsWithImage = true;
					}

					// Check to see if the selection has any trailing images
					// if so, this this means we need to look for them when we import selection
					var trailingImageCount = this.getTrailingImageCount(root, selectionState, range.endContainer, range.endOffset);
					if (trailingImageCount) {
						selectionState.trailingImageCount = trailingImageCount;
					}

					// If start = 0 there may still be an empty paragraph before it, but we don't care.
					if (start !== 0) {
						var emptyBlocksIndex = this.getIndexRelativeToAdjacentEmptyBlocks(doc, root, range.startContainer, range.startOffset);
						if (emptyBlocksIndex !== -1) {
							selectionState.emptyBlocksIndex = emptyBlocksIndex;
						}
					}
				}

				return selectionState;
			},

			// http://stackoverflow.com/questions/17678843/cant-restore-selection-after-html-modify-even-if-its-the-same-html
			// Tim Down
			//
			// {object} selectionState - the selection to import
			// {DOMElement} root - the root element the selection is being restored inside of
			// {Document} doc - the document to use for managing selection
			// {boolean} [favorLaterSelectionAnchor] - defaults to false. If true, import the cursor immediately
			//      subsequent to an anchor tag if it would otherwise be placed right at the trailing edge inside the
			//      anchor. This cursor positioning, even though visually equivalent to the user, can affect behavior
			//      in MS IE.
			importSelection: function(selectionState, root, doc, favorLaterSelectionAnchor) {
				if (!selectionState || !root) {
					return;
				}

				var range = doc.createRange();
				range.setStart(root, 0);
				range.collapse(true);

				var node = root,
					nodeStack = [],
					charIndex = 0,
					foundStart = false,
					foundEnd = false,
					trailingImageCount = 0,
					stop = false,
					nextCharIndex,
					allowRangeToStartAtEndOfNode = false,
					lastTextNode = null;

				// When importing selection, the start of the selection may lie at the end of an element
				// or at the beginning of an element.  Since visually there is no difference between these 2
				// we will try to move the selection to the beginning of an element since this is generally
				// what users will expect and it's a more predictable behavior.
				//
				// However, there are some specific cases when we don't want to do this:
				//  1) We're attempting to move the cursor outside of the end of an anchor [favorLaterSelectionAnchor = true]
				//  2) The selection starts with an image, which is special since an image doesn't have any 'content'
				//     as far as selection and ranges are concerned
				//  3) The selection starts after a specified number of empty block elements (selectionState.emptyBlocksIndex)
				//
				// For these cases, we want the selection to start at a very specific location, so we should NOT
				// automatically move the cursor to the beginning of the first actual chunk of text
				if (favorLaterSelectionAnchor || selectionState.startsWithImage || typeof selectionState.emptyBlocksIndex !== 'undefined') {
					allowRangeToStartAtEndOfNode = true;
				}

				while (!stop && node) {
					// Only iterate over elements and text nodes
					if (node.nodeType > 3) {
						node = nodeStack.pop();
						continue;
					}

					// If we hit a text node, we need to add the amount of characters to the overall count
					if (node.nodeType === 3 && !foundEnd) {
						nextCharIndex = charIndex + node.length;
						// Check if we're at or beyond the start of the selection we're importing
						if (!foundStart && selectionState.start >= charIndex && selectionState.start <= nextCharIndex) {
							// NOTE: We only want to allow a selection to start at the END of an element if
							//  allowRangeToStartAtEndOfNode is true
							if (allowRangeToStartAtEndOfNode || selectionState.start < nextCharIndex) {
								range.setStart(node, selectionState.start - charIndex);
								foundStart = true;
							}
							// We're at the end of a text node where the selection could start but we shouldn't
							// make the selection start here because allowRangeToStartAtEndOfNode is false.
							// However, we should keep a reference to this node in case there aren't any more
							// text nodes after this, so that we have somewhere to import the selection to
							else {
								lastTextNode = node;
							}
						}
						// We've found the start of the selection, check if we're at or beyond the end of the selection we're importing
						if (foundStart && selectionState.end >= charIndex && selectionState.end <= nextCharIndex) {
							if (!selectionState.trailingImageCount) {
								range.setEnd(node, selectionState.end - charIndex);
								stop = true;
							} else {
								foundEnd = true;
							}
						}
						charIndex = nextCharIndex;
					} else {
						if (selectionState.trailingImageCount && foundEnd) {
							if (node.nodeName.toLowerCase() === 'img') {
								trailingImageCount++;
							}
							if (trailingImageCount === selectionState.trailingImageCount) {
								// Find which index the image is in its parent's children
								var endIndex = 0;
								while (node.parentNode.childNodes[endIndex] !== node) {
									endIndex++;
								}
								range.setEnd(node.parentNode, endIndex + 1);
								stop = true;
							}
						}

						if (!stop && node.nodeType === 1) {
							// this is an element
							// add all its children to the stack
							var i = node.childNodes.length - 1;
							while (i >= 0) {
								nodeStack.push(node.childNodes[i]);
								i -= 1;
							}
						}
					}

					if (!stop) {
						node = nodeStack.pop();
					}
				}

				// If we've gone through the entire text but didn't find the beginning of a text node
				// to make the selection start at, we should fall back to starting the selection
				// at the END of the last text node we found
				if (!foundStart && lastTextNode) {
					range.setStart(lastTextNode, lastTextNode.length);
					range.setEnd(lastTextNode, lastTextNode.length);
				}

				if (typeof selectionState.emptyBlocksIndex !== 'undefined') {
					range = this.importSelectionMoveCursorPastBlocks(doc, root, selectionState.emptyBlocksIndex, range);
				}

				// If the selection is right at the ending edge of a link, put it outside the anchor tag instead of inside.
				if (favorLaterSelectionAnchor) {
					range = this.importSelectionMoveCursorPastAnchor(selectionState, range);
				}

				this.selectRange(doc, range);
			},

			// Utility method called from importSelection only
			importSelectionMoveCursorPastAnchor: function(selectionState, range) {
				var nodeInsideAnchorTagFunction = function(node) {
					return node.nodeName.toLowerCase() === 'a';
				};
				if (selectionState.start === selectionState.end &&
					range.startContainer.nodeType === 3 &&
					range.startOffset === range.startContainer.nodeValue.length &&
					MediumEditor.util.traverseUp(range.startContainer, nodeInsideAnchorTagFunction)) {
					var prevNode = range.startContainer,
						currentNode = range.startContainer.parentNode;
					while (currentNode !== null && currentNode.nodeName.toLowerCase() !== 'a') {
						if (currentNode.childNodes[currentNode.childNodes.length - 1] !== prevNode) {
							currentNode = null;
						} else {
							prevNode = currentNode;
							currentNode = currentNode.parentNode;
						}
					}
					if (currentNode !== null && currentNode.nodeName.toLowerCase() === 'a') {
						var currentNodeIndex = null;
						for (var i = 0; currentNodeIndex === null && i < currentNode.parentNode.childNodes.length; i++) {
							if (currentNode.parentNode.childNodes[i] === currentNode) {
								currentNodeIndex = i;
							}
						}
						range.setStart(currentNode.parentNode, currentNodeIndex + 1);
						range.collapse(true);
					}
				}
				return range;
			},

			// Uses the emptyBlocksIndex calculated by getIndexRelativeToAdjacentEmptyBlocks
			// to move the cursor back to the start of the correct paragraph
			importSelectionMoveCursorPastBlocks: function(doc, root, index, range) {
				var treeWalker = doc.createTreeWalker(root, NodeFilter.SHOW_ELEMENT, filterOnlyParentElements, false),
					startContainer = range.startContainer,
					startBlock,
					targetNode,
					currIndex = 0;
				index = index || 1; // If index is 0, we still want to move to the next block

				// Chrome counts newlines and spaces that separate block elements as actual elements.
				// If the selection is inside one of these text nodes, and it has a previous sibling
				// which is a block element, we want the treewalker to start at the previous sibling
				// and NOT at the parent of the textnode
				if (startContainer.nodeType === 3 && MediumEditor.util.isBlockContainer(startContainer.previousSibling)) {
					startBlock = startContainer.previousSibling;
				} else {
					startBlock = MediumEditor.util.getClosestBlockContainer(startContainer);
				}

				// Skip over empty blocks until we hit the block we want the selection to be in
				while (treeWalker.nextNode()) {
					if (!targetNode) {
						// Loop through all blocks until we hit the starting block element
						if (startBlock === treeWalker.currentNode) {
							targetNode = treeWalker.currentNode;
						}
					} else {
						targetNode = treeWalker.currentNode;
						currIndex++;
						// We hit the target index, bail
						if (currIndex === index) {
							break;
						}
						// If we find a non-empty block, ignore the emptyBlocksIndex and just put selection here
						if (targetNode.textContent.length > 0) {
							break;
						}
					}
				}

				if (!targetNode) {
					targetNode = startBlock;
				}

				// We're selecting a high-level block node, so make sure the cursor gets moved into the deepest
				// element at the beginning of the block
				range.setStart(MediumEditor.util.getFirstSelectableLeafNode(targetNode), 0);

				return range;
			},

			// Returns -1 unless the cursor is at the beginning of a paragraph/block
			// If the paragraph/block is preceeded by empty paragraphs/block (with no text)
			// it will return the number of empty paragraphs before the cursor.
			// Otherwise, it will return 0, which indicates the cursor is at the beginning
			// of a paragraph/block, and not at the end of the paragraph/block before it
			getIndexRelativeToAdjacentEmptyBlocks: function(doc, root, cursorContainer, cursorOffset) {
				// If there is text in front of the cursor, that means there isn't only empty blocks before it
				if (cursorContainer.textContent.length > 0 && cursorOffset > 0) {
					return -1;
				}

				// Check if the block that contains the cursor has any other text in front of the cursor
				var node = cursorContainer;
				if (node.nodeType !== 3) {
					node = cursorContainer.childNodes[cursorOffset];
				}
				if (node) {
					// The element isn't at the beginning of a block, so it has content before it
					if (!MediumEditor.util.isElementAtBeginningOfBlock(node)) {
						return -1;
					}

					var previousSibling = MediumEditor.util.findPreviousSibling(node);
					// If there is no previous sibling, this is the first text element in the editor
					if (!previousSibling) {
						return -1;
					}
					// If the previous sibling has text, then there are no empty blocks before this
					else if (previousSibling.nodeValue) {
						return -1;
					}
				}

				// Walk over block elements, counting number of empty blocks between last piece of text
				// and the block the cursor is in
				var closestBlock = MediumEditor.util.getClosestBlockContainer(cursorContainer),
					treeWalker = doc.createTreeWalker(root, NodeFilter.SHOW_ELEMENT, filterOnlyParentElements, false),
					emptyBlocksCount = 0;
				while (treeWalker.nextNode()) {
					var blockIsEmpty = treeWalker.currentNode.textContent === '';
					if (blockIsEmpty || emptyBlocksCount > 0) {
						emptyBlocksCount += 1;
					}
					if (treeWalker.currentNode === closestBlock) {
						return emptyBlocksCount;
					}
					if (!blockIsEmpty) {
						emptyBlocksCount = 0;
					}
				}

				return emptyBlocksCount;
			},

			// Returns true if the selection range begins with an image tag
			// Returns false if the range starts with any non empty text nodes
			doesRangeStartWithImages: function(range, doc) {
				if (range.startOffset !== 0 || range.startContainer.nodeType !== 1) {
					return false;
				}

				if (range.startContainer.nodeName.toLowerCase() === 'img') {
					return true;
				}

				var img = range.startContainer.querySelector('img');
				if (!img) {
					return false;
				}

				var treeWalker = doc.createTreeWalker(range.startContainer, NodeFilter.SHOW_ALL, null, false);
				while (treeWalker.nextNode()) {
					var next = treeWalker.currentNode;
					// If we hit the image, then there isn't any text before the image so
					// the image is at the beginning of the range
					if (next === img) {
						break;
					}
					// If we haven't hit the iamge, but found text that contains content
					// then the range doesn't start with an image
					if (next.nodeValue) {
						return false;
					}
				}

				return true;
			},

			getTrailingImageCount: function(root, selectionState, endContainer, endOffset) {
				// If the endOffset of a range is 0, the endContainer doesn't contain images
				// If the endContainer is a text node, there are no trailing images
				if (endOffset === 0 || endContainer.nodeType !== 1) {
					return 0;
				}

				// If the endContainer isn't an image, and doesn't have an image descendants
				// there are no trailing images
				if (endContainer.nodeName.toLowerCase() !== 'img' && !endContainer.querySelector('img')) {
					return 0;
				}

				var lastNode = endContainer.childNodes[endOffset - 1];
				while (lastNode.hasChildNodes()) {
					lastNode = lastNode.lastChild;
				}

				var node = root,
					nodeStack = [],
					charIndex = 0,
					foundStart = false,
					foundEnd = false,
					stop = false,
					nextCharIndex,
					trailingImages = 0;

				while (!stop && node) {
					// Only iterate over elements and text nodes
					if (node.nodeType > 3) {
						node = nodeStack.pop();
						continue;
					}

					if (node.nodeType === 3 && !foundEnd) {
						trailingImages = 0;
						nextCharIndex = charIndex + node.length;
						if (!foundStart && selectionState.start >= charIndex && selectionState.start <= nextCharIndex) {
							foundStart = true;
						}
						if (foundStart && selectionState.end >= charIndex && selectionState.end <= nextCharIndex) {
							foundEnd = true;
						}
						charIndex = nextCharIndex;
					} else {
						if (node.nodeName.toLowerCase() === 'img') {
							trailingImages++;
						}

						if (node === lastNode) {
							stop = true;
						} else if (node.nodeType === 1) {
							// this is an element
							// add all its children to the stack
							var i = node.childNodes.length - 1;
							while (i >= 0) {
								nodeStack.push(node.childNodes[i]);
								i -= 1;
							}
						}
					}

					if (!stop) {
						node = nodeStack.pop();
					}
				}

				return trailingImages;
			},

			// determine if the current selection contains any 'content'
			// content being any non-white space text or an image
			selectionContainsContent: function(doc) {
				var sel = doc.getSelection();

				// collapsed selection or selection withour range doesn't contain content
				if (!sel || sel.isCollapsed || !sel.rangeCount) {
					return false;
				}

				// if toString() contains any text, the selection contains some content
				if (sel.toString().trim() !== '') {
					return true;
				}

				// if selection contains only image(s), it will return empty for toString()
				// so check for an image manually
				var selectionNode = this.getSelectedParentElement(sel.getRangeAt(0));
				if (selectionNode) {
					if (selectionNode.nodeName.toLowerCase() === 'img' ||
						(selectionNode.nodeType === 1 && selectionNode.querySelector('img'))) {
						return true;
					}
				}

				return false;
			},

			selectionInContentEditableFalse: function(contentWindow) {
				// determine if the current selection is exclusively inside
				// a contenteditable="false", though treat the case of an
				// explicit contenteditable="true" inside a "false" as false.
				var sawtrue,
					sawfalse = this.findMatchingSelectionParent(function(el) {
						var ce = el && el.getAttribute('contenteditable');
						if (ce === 'true') {
							sawtrue = true;
						}
						return el.nodeName !== '#text' && ce === 'false';
					}, contentWindow);

				return !sawtrue && sawfalse;
			},

			// http://stackoverflow.com/questions/4176923/html-of-selected-text
			// by Tim Down
			getSelectionHtml: function getSelectionHtml(doc) {
				var i,
					html = '',
					sel = doc.getSelection(),
					len,
					container;
				if (sel.rangeCount) {
					container = doc.createElement('div');
					for (i = 0, len = sel.rangeCount; i < len; i += 1) {
						container.appendChild(sel.getRangeAt(i).cloneContents());
					}
					html = container.innerHTML;
				}
				return html;
			},

			/**
			 *  Find the caret position within an element irrespective of any inline tags it may contain.
			 *
			 *  @param {DOMElement} An element containing the cursor to find offsets relative to.
			 *  @param {Range} A Range representing cursor position. Will window.getSelection if none is passed.
			 *  @return {Object} 'left' and 'right' attributes contain offsets from begining and end of Element
			 */
			getCaretOffsets: function getCaretOffsets(element, range) {
				var preCaretRange, postCaretRange;

				if (!range) {
					range = window.getSelection().getRangeAt(0);
				}

				preCaretRange = range.cloneRange();
				postCaretRange = range.cloneRange();

				preCaretRange.selectNodeContents(element);
				preCaretRange.setEnd(range.endContainer, range.endOffset);

				postCaretRange.selectNodeContents(element);
				postCaretRange.setStart(range.endContainer, range.endOffset);

				return {
					left: preCaretRange.toString().length,
					right: postCaretRange.toString().length
				};
			},

			// http://stackoverflow.com/questions/15867542/range-object-get-selection-parent-node-chrome-vs-firefox
			rangeSelectsSingleNode: function(range) {
				var startNode = range.startContainer;
				return startNode === range.endContainer &&
					startNode.hasChildNodes() &&
					range.endOffset === range.startOffset + 1;
			},

			getSelectedParentElement: function(range) {
				if (!range) {
					return null;
				}

				// Selection encompasses a single element
				if (this.rangeSelectsSingleNode(range) && range.startContainer.childNodes[range.startOffset].nodeType !== 3) {
					return range.startContainer.childNodes[range.startOffset];
				}

				// Selection range starts inside a text node, so get its parent
				if (range.startContainer.nodeType === 3) {
					return range.startContainer.parentNode;
				}

				// Selection starts inside an element
				return range.startContainer;
			},

			getSelectedElements: function(doc) {
				var selection = doc.getSelection(),
					range,
					toRet,
					currNode;

				if (!selection.rangeCount || selection.isCollapsed || !selection.getRangeAt(0).commonAncestorContainer) {
					return [];
				}

				range = selection.getRangeAt(0);

				if (range.commonAncestorContainer.nodeType === 3) {
					toRet = [];
					currNode = range.commonAncestorContainer;
					while (currNode.parentNode && currNode.parentNode.childNodes.length === 1) {
						toRet.push(currNode.parentNode);
						currNode = currNode.parentNode;
					}

					return toRet;
				}

				return [].filter.call(range.commonAncestorContainer.getElementsByTagName('*'), function(el) {
					return (typeof selection.containsNode === 'function') ? selection.containsNode(el, true) : true;
				});
			},

			selectNode: function(node, doc) {
				var range = doc.createRange();
				range.selectNodeContents(node);
				this.selectRange(doc, range);
			},

			select: function(doc, startNode, startOffset, endNode, endOffset) {
				var range = doc.createRange();
				range.setStart(startNode, startOffset);
				if (endNode) {
					range.setEnd(endNode, endOffset);
				} else {
					range.collapse(true);
				}
				this.selectRange(doc, range);
				return range;
			},

			/**
			 *  Clear the current highlighted selection and set the caret to the start or the end of that prior selection, defaults to end.
			 *
			 *  @param {DomDocument} doc            Current document
			 *  @param {boolean} moveCursorToStart  A boolean representing whether or not to set the caret to the beginning of the prior selection.
			 */
			clearSelection: function(doc, moveCursorToStart) {
				if (moveCursorToStart) {
					doc.getSelection().collapseToStart();
				} else {
					doc.getSelection().collapseToEnd();
				}
			},

			/**
			 * Move cursor to the given node with the given offset.
			 *
			 * @param  {DomDocument} doc     Current document
			 * @param  {DomElement}  node    Element where to jump
			 * @param  {integer}     offset  Where in the element should we jump, 0 by default
			 */
			moveCursor: function(doc, node, offset) {
				this.select(doc, node, offset);
			},

			getSelectionRange: function(ownerDocument) {
				var selection = ownerDocument.getSelection();
				if (selection.rangeCount === 0) {
					return null;
				}
				return selection.getRangeAt(0);
			},

			selectRange: function(ownerDocument, range) {
				var selection = ownerDocument.getSelection();

				selection.removeAllRanges();
				selection.addRange(range);
			},

			// http://stackoverflow.com/questions/1197401/how-can-i-get-the-element-the-caret-is-in-with-javascript-when-using-contentedi
			// by You
			getSelectionStart: function(ownerDocument) {
				var node = ownerDocument.getSelection().anchorNode,
					startNode = (node && node.nodeType === 3 ? node.parentNode : node);

				return startNode;
			}
		};

		MediumEditor.selection = Selection;
	}());

	(function() {
		'use strict';

		function isElementDescendantOfExtension(extensions, element) {
			return extensions.some(function(extension) {
				if (typeof extension.getInteractionElements !== 'function') {
					return false;
				}

				var extensionElements = extension.getInteractionElements();
				if (!extensionElements) {
					return false;
				}

				if (!Array.isArray(extensionElements)) {
					extensionElements = [extensionElements];
				}
				return extensionElements.some(function(el) {
					return MediumEditor.util.isDescendant(el, element, true);
				});
			});
		}

		var Events = function(instance) {
			this.base = instance;
			this.options = this.base.options;
			this.events = [];
			this.disabledEvents = {};
			this.customEvents = {};
			this.listeners = {};
		};

		Events.prototype = {
			InputEventOnContenteditableSupported: !MediumEditor.util.isIE && !MediumEditor.util.isEdge,

			// Helpers for event handling

			attachDOMEvent: function(targets, event, listener, useCapture) {
				var win = this.base.options.contentWindow,
					doc = this.base.options.ownerDocument;

				targets = MediumEditor.util.isElement(targets) || [win, doc].indexOf(targets) > -1 ? [targets] : targets;

				Array.prototype.forEach.call(targets, function(target) {
					target.addEventListener(event, listener, useCapture);
					this.events.push([target, event, listener, useCapture]);
				}.bind(this));
			},

			detachDOMEvent: function(targets, event, listener, useCapture) {
				var index, e,
					win = this.base.options.contentWindow,
					doc = this.base.options.ownerDocument;

				if (targets !== null) {
					targets = MediumEditor.util.isElement(targets) || [win, doc].indexOf(targets) > -1 ? [targets] : targets;

					Array.prototype.forEach.call(targets, function(target) {
						index = this.indexOfListener(target, event, listener, useCapture);
						if (index !== -1) {
							e = this.events.splice(index, 1)[0];
							e[0].removeEventListener(e[1], e[2], e[3]);
						}
					}.bind(this));
				}
			},

			indexOfListener: function(target, event, listener, useCapture) {
				var i, n, item;
				for (i = 0, n = this.events.length; i < n; i = i + 1) {
					item = this.events[i];
					if (item[0] === target && item[1] === event && item[2] === listener && item[3] === useCapture) {
						return i;
					}
				}
				return -1;
			},

			detachAllDOMEvents: function() {
				var e = this.events.pop();
				while (e) {
					e[0].removeEventListener(e[1], e[2], e[3]);
					e = this.events.pop();
				}
			},

			detachAllEventsFromElement: function(element) {
				var filtered = this.events.filter(function(e) {
					return e && e[0].getAttribute && e[0].getAttribute('medium-editor-index') === element.getAttribute('medium-editor-index');
				});

				for (var i = 0, len = filtered.length; i < len; i++) {
					var e = filtered[i];
					this.detachDOMEvent(e[0], e[1], e[2], e[3]);
				}
			},

			// Attach all existing handlers to a new element
			attachAllEventsToElement: function(element) {
				if (this.listeners['editableInput']) {
					this.contentCache[element.getAttribute('medium-editor-index')] = element.innerHTML;
				}

				if (this.eventsCache) {
					this.eventsCache.forEach(function(e) {
						this.attachDOMEvent(element, e['name'], e['handler'].bind(this));
					}, this);
				}
			},

			enableCustomEvent: function(event) {
				if (this.disabledEvents[event] !== undefined) {
					delete this.disabledEvents[event];
				}
			},

			disableCustomEvent: function(event) {
				this.disabledEvents[event] = true;
			},

			// custom events
			attachCustomEvent: function(event, listener) {
				this.setupListener(event);
				if (!this.customEvents[event]) {
					this.customEvents[event] = [];
				}
				this.customEvents[event].push(listener);
			},

			detachCustomEvent: function(event, listener) {
				var index = this.indexOfCustomListener(event, listener);
				if (index !== -1) {
					this.customEvents[event].splice(index, 1);
					// TODO: If array is empty, should detach internal listeners via destroyListener()
				}
			},

			indexOfCustomListener: function(event, listener) {
				if (!this.customEvents[event] || !this.customEvents[event].length) {
					return -1;
				}

				return this.customEvents[event].indexOf(listener);
			},

			detachAllCustomEvents: function() {
				this.customEvents = {};
				// TODO: Should detach internal listeners here via destroyListener()
			},

			triggerCustomEvent: function(name, data, editable) {
				if (this.customEvents[name] && !this.disabledEvents[name]) {
					this.customEvents[name].forEach(function(listener) {
						listener(data, editable);
					});
				}
			},

			// Cleaning up

			destroy: function() {
				this.detachAllDOMEvents();
				this.detachAllCustomEvents();
				this.detachExecCommand();

				if (this.base.elements) {
					this.base.elements.forEach(function(element) {
						element.removeAttribute('data-medium-focused');
					});
				}
			},

			// Listening to calls to document.execCommand

			// Attach a listener to be notified when document.execCommand is called
			attachToExecCommand: function() {
				if (this.execCommandListener) {
					return;
				}

				// Store an instance of the listener so:
				// 1) We only attach to execCommand once
				// 2) We can remove the listener later
				this.execCommandListener = function(execInfo) {
					this.handleDocumentExecCommand(execInfo);
				}.bind(this);

				// Ensure that execCommand has been wrapped correctly
				this.wrapExecCommand();

				// Add listener to list of execCommand listeners
				this.options.ownerDocument.execCommand.listeners.push(this.execCommandListener);
			},

			// Remove our listener for calls to document.execCommand
			detachExecCommand: function() {
				var doc = this.options.ownerDocument;
				if (!this.execCommandListener || !doc.execCommand.listeners) {
					return;
				}

				// Find the index of this listener in the array of listeners so it can be removed
				var index = doc.execCommand.listeners.indexOf(this.execCommandListener);
				if (index !== -1) {
					doc.execCommand.listeners.splice(index, 1);
				}

				// If the list of listeners is now empty, put execCommand back to its original state
				if (!doc.execCommand.listeners.length) {
					this.unwrapExecCommand();
				}
			},

			// Wrap document.execCommand in a custom method so we can listen to calls to it
			wrapExecCommand: function() {
				var doc = this.options.ownerDocument;

				// Ensure all instance of MediumEditor only wrap execCommand once
				if (doc.execCommand.listeners) {
					return;
				}

				// Helper method to call all listeners to execCommand
				var callListeners = function(args, result) {
						if (doc.execCommand.listeners) {
							doc.execCommand.listeners.forEach(function(listener) {
								listener({
									command: args[0],
									value: args[2],
									args: args,
									result: result
								});
							});
						}
					},

					// Create a wrapper method for execCommand which will:
					// 1) Call document.execCommand with the correct arguments
					// 2) Loop through any listeners and notify them that execCommand was called
					//    passing extra info on the call
					// 3) Return the result
					wrapper = function() {
						var result = doc.execCommand.orig.apply(this, arguments);

						if (!doc.execCommand.listeners) {
							return result;
						}

						var args = Array.prototype.slice.call(arguments);
						callListeners(args, result);

						return result;
					};

				// Store a reference to the original execCommand
				wrapper.orig = doc.execCommand;

				// Attach an array for storing listeners
				wrapper.listeners = [];

				// Helper for notifying listeners
				wrapper.callListeners = callListeners;

				// Overwrite execCommand
				doc.execCommand = wrapper;
			},

			// Revert document.execCommand back to its original self
			unwrapExecCommand: function() {
				var doc = this.options.ownerDocument;
				if (!doc.execCommand.orig) {
					return;
				}

				// Use the reference to the original execCommand to revert back
				doc.execCommand = doc.execCommand.orig;
			},

			// Listening to browser events to emit events medium-editor cares about
			setupListener: function(name) {
				if (this.listeners[name]) {
					return;
				}

				switch (name) {
					case 'externalInteraction':
						// Detecting when user has interacted with elements outside of MediumEditor
						this.attachDOMEvent(this.options.ownerDocument.body, 'mousedown', this.handleBodyMousedown.bind(this), true);
						this.attachDOMEvent(this.options.ownerDocument.body, 'click', this.handleBodyClick.bind(this), true);
						this.attachDOMEvent(this.options.ownerDocument.body, 'focus', this.handleBodyFocus.bind(this), true);
						break;
					case 'blur':
						// Detecting when focus is lost
						this.setupListener('externalInteraction');
						break;
					case 'focus':
						// Detecting when focus moves into some part of MediumEditor
						this.setupListener('externalInteraction');
						break;
					case 'editableInput':
						// setup cache for knowing when the content has changed
						this.contentCache = {};
						this.base.elements.forEach(function(element) {
							this.contentCache[element.getAttribute('medium-editor-index')] = element.innerHTML;
						}, this);

						// Attach to the 'oninput' event, handled correctly by most browsers
						if (this.InputEventOnContenteditableSupported) {
							this.attachToEachElement('input', this.handleInput);
						}

						// For browsers which don't support the input event on contenteditable (IE)
						// we'll attach to 'selectionchange' on the document and 'keypress' on the editables
						if (!this.InputEventOnContenteditableSupported) {
							this.setupListener('editableKeypress');
							this.keypressUpdateInput = true;
							this.attachDOMEvent(document, 'selectionchange', this.handleDocumentSelectionChange.bind(this));
							// Listen to calls to execCommand
							this.attachToExecCommand();
						}
						break;
					case 'editableClick':
						// Detecting click in the contenteditables
						this.attachToEachElement('click', this.handleClick);
						break;
					case 'editableBlur':
						// Detecting blur in the contenteditables
						this.attachToEachElement('blur', this.handleBlur);
						break;
					case 'editableKeypress':
						// Detecting keypress in the contenteditables
						this.attachToEachElement('keypress', this.handleKeypress);
						break;
					case 'editableKeyup':
						// Detecting keyup in the contenteditables
						this.attachToEachElement('keyup', this.handleKeyup);
						break;
					case 'editableKeydown':
						// Detecting keydown on the contenteditables
						this.attachToEachElement('keydown', this.handleKeydown);
						break;
					case 'editableKeydownSpace':
						// Detecting keydown for SPACE on the contenteditables
						this.setupListener('editableKeydown');
						break;
					case 'editableKeydownEnter':
						// Detecting keydown for ENTER on the contenteditables
						this.setupListener('editableKeydown');
						break;
					case 'editableKeydownTab':
						// Detecting keydown for TAB on the contenteditable
						this.setupListener('editableKeydown');
						break;
					case 'editableKeydownDelete':
						// Detecting keydown for DELETE/BACKSPACE on the contenteditables
						this.setupListener('editableKeydown');
						break;
					case 'editableMouseover':
						// Detecting mouseover on the contenteditables
						this.attachToEachElement('mouseover', this.handleMouseover);
						break;
					case 'editableDrag':
						// Detecting dragover and dragleave on the contenteditables
						this.attachToEachElement('dragover', this.handleDragging);
						this.attachToEachElement('dragleave', this.handleDragging);
						break;
					case 'editableDrop':
						// Detecting drop on the contenteditables
						this.attachToEachElement('drop', this.handleDrop);
						break;
						// TODO: We need to have a custom 'paste' event separate from 'editablePaste'
						// Need to think about the way to introduce this without breaking folks
					case 'editablePaste':
						// Detecting paste on the contenteditables
						this.attachToEachElement('paste', this.handlePaste);
						break;
				}
				this.listeners[name] = true;
			},

			attachToEachElement: function(name, handler) {
				// build our internal cache to know which element got already what handler attached
				if (!this.eventsCache) {
					this.eventsCache = [];
				}

				this.base.elements.forEach(function(element) {
					this.attachDOMEvent(element, name, handler.bind(this));
				}, this);

				this.eventsCache.push({
					'name': name,
					'handler': handler
				});
			},

			cleanupElement: function(element) {
				var index = element.getAttribute('medium-editor-index');
				if (index) {
					this.detachAllEventsFromElement(element);
					if (this.contentCache) {
						delete this.contentCache[index];
					}
				}
			},

			focusElement: function(element) {
				element.focus();
				this.updateFocus(element, {
					target: element,
					type: 'focus'
				});
			},

			updateFocus: function(target, eventObj) {
				var hadFocus = this.base.getFocusedElement(),
					toFocus;

				// For clicks, we need to know if the mousedown that caused the click happened inside the existing focused element
				// or one of the extension elements.  If so, we don't want to focus another element
				if (hadFocus &&
					eventObj.type === 'click' &&
					this.lastMousedownTarget &&
					(MediumEditor.util.isDescendant(hadFocus, this.lastMousedownTarget, true) ||
						isElementDescendantOfExtension(this.base.extensions, this.lastMousedownTarget))) {
					toFocus = hadFocus;
				}

				if (!toFocus) {
					this.base.elements.some(function(element) {
						// If the target is part of an editor element, this is the element getting focus
						if (!toFocus && (MediumEditor.util.isDescendant(element, target, true))) {
							toFocus = element;
						}

						// bail if we found an element that's getting focus
						return !!toFocus;
					}, this);
				}

				// Check if the target is external (not part of the editor, toolbar, or any other extension)
				var externalEvent = !MediumEditor.util.isDescendant(hadFocus, target, true) &&
					!isElementDescendantOfExtension(this.base.extensions, target);

				if (toFocus !== hadFocus) {
					// If element has focus, and focus is going outside of editor
					// Don't blur focused element if clicking on editor, toolbar, or anchorpreview
					if (hadFocus && externalEvent) {
						// Trigger blur on the editable that has lost focus
						hadFocus.removeAttribute('data-medium-focused');
						this.triggerCustomEvent('blur', eventObj, hadFocus);
					}

					// If focus is going into an editor element
					if (toFocus) {
						// Trigger focus on the editable that now has focus
						toFocus.setAttribute('data-medium-focused', true);
						this.triggerCustomEvent('focus', eventObj, toFocus);
					}
				}

				if (externalEvent) {
					this.triggerCustomEvent('externalInteraction', eventObj);
				}
			},

			updateInput: function(target, eventObj) {
				if (!this.contentCache) {
					return;
				}
				// An event triggered which signifies that the user may have changed someting
				// Look in our cache of input for the contenteditables to see if something changed
				var index = target.getAttribute('medium-editor-index'),
					html = target.innerHTML;

				if (html !== this.contentCache[index]) {
					// The content has changed since the last time we checked, fire the event
					this.triggerCustomEvent('editableInput', eventObj, target);
				}
				this.contentCache[index] = html;
			},

			handleDocumentSelectionChange: function(event) {
				// When selectionchange fires, target and current target are set
				// to document, since this is where the event is handled
				// However, currentTarget will have an 'activeElement' property
				// which will point to whatever element has focus.
				if (event.currentTarget && event.currentTarget.activeElement) {
					var activeElement = event.currentTarget.activeElement,
						currentTarget;
					// We can look at the 'activeElement' to determine if the selectionchange has
					// happened within a contenteditable owned by this instance of MediumEditor
					this.base.elements.some(function(element) {
						if (MediumEditor.util.isDescendant(element, activeElement, true)) {
							currentTarget = element;
							return true;
						}
						return false;
					}, this);

					// We know selectionchange fired within one of our contenteditables
					if (currentTarget) {
						this.updateInput(currentTarget, {
							target: activeElement,
							currentTarget: currentTarget
						});
					}
				}
			},

			handleDocumentExecCommand: function() {
				// document.execCommand has been called
				// If one of our contenteditables currently has focus, we should
				// attempt to trigger the 'editableInput' event
				var target = this.base.getFocusedElement();
				if (target) {
					this.updateInput(target, {
						target: target,
						currentTarget: target
					});
				}
			},

			handleBodyClick: function(event) {
				this.updateFocus(event.target, event);
			},

			handleBodyFocus: function(event) {
				this.updateFocus(event.target, event);
			},

			handleBodyMousedown: function(event) {
				this.lastMousedownTarget = event.target;
			},

			handleInput: function(event) {
				this.updateInput(event.currentTarget, event);
			},

			handleClick: function(event) {
				this.triggerCustomEvent('editableClick', event, event.currentTarget);
			},

			handleBlur: function(event) {
				this.triggerCustomEvent('editableBlur', event, event.currentTarget);
			},

			handleKeypress: function(event) {
				this.triggerCustomEvent('editableKeypress', event, event.currentTarget);

				// If we're doing manual detection of the editableInput event we need
				// to check for input changes during 'keypress'
				if (this.keypressUpdateInput) {
					var eventObj = {
						target: event.target,
						currentTarget: event.currentTarget
					};

					// In IE, we need to let the rest of the event stack complete before we detect
					// changes to input, so using setTimeout here
					setTimeout(function() {
						this.updateInput(eventObj.currentTarget, eventObj);
					}.bind(this), 0);
				}
			},

			handleKeyup: function(event) {
				this.triggerCustomEvent('editableKeyup', event, event.currentTarget);
			},

			handleMouseover: function(event) {
				this.triggerCustomEvent('editableMouseover', event, event.currentTarget);
			},

			handleDragging: function(event) {
				this.triggerCustomEvent('editableDrag', event, event.currentTarget);
			},

			handleDrop: function(event) {
				this.triggerCustomEvent('editableDrop', event, event.currentTarget);
			},

			handlePaste: function(event) {
				this.triggerCustomEvent('editablePaste', event, event.currentTarget);
			},

			handleKeydown: function(event) {

				this.triggerCustomEvent('editableKeydown', event, event.currentTarget);

				if (MediumEditor.util.isKey(event, MediumEditor.util.keyCode.SPACE)) {
					return this.triggerCustomEvent('editableKeydownSpace', event, event.currentTarget);
				}

				if (MediumEditor.util.isKey(event, MediumEditor.util.keyCode.ENTER) || (event.ctrlKey && MediumEditor.util.isKey(event, MediumEditor.util.keyCode.M))) {
					return this.triggerCustomEvent('editableKeydownEnter', event, event.currentTarget);
				}

				if (MediumEditor.util.isKey(event, MediumEditor.util.keyCode.TAB)) {
					return this.triggerCustomEvent('editableKeydownTab', event, event.currentTarget);
				}

				if (MediumEditor.util.isKey(event, [MediumEditor.util.keyCode.DELETE, MediumEditor.util.keyCode.BACKSPACE])) {
					return this.triggerCustomEvent('editableKeydownDelete', event, event.currentTarget);
				}
			}
		};

		MediumEditor.Events = Events;
	}());

	(function() {
		'use strict';

		var Button = MediumEditor.Extension.extend({

			/* Button Options */

			/* action: [string]
			 * The action argument to pass to MediumEditor.execAction()
			 * when the button is clicked
			 */
			action: undefined,

			/* aria: [string]
			 * The value to add as the aria-label attribute of the button
			 * element displayed in the toolbar.
			 * This is also used as the tooltip for the button
			 */
			aria: undefined,

			/* tagNames: [Array]
			 * NOTE: This is not used if useQueryState is set to true.
			 *
			 * Array of element tag names that would indicate that this
			 * button has already been applied. If this action has already
			 * been applied, the button will be displayed as 'active' in the toolbar
			 *
			 * Example:
			 * For 'bold', if the text is ever within a <b> or <strong>
			 * tag that indicates the text is already bold. So the array
			 * of tagNames for bold would be: ['b', 'strong']
			 */
			tagNames: undefined,

			/* style: [Object]
			 * NOTE: This is not used if useQueryState is set to true.
			 *
			 * A pair of css property & value(s) that indicate that this
			 * button has already been applied. If this action has already
			 * been applied, the button will be displayed as 'active' in the toolbar
			 * Properties of the object:
			 *   prop [String]: name of the css property
			 *   value [String]: value(s) of the css property
			 *                   multiple values can be separated by a '|'
			 *
			 * Example:
			 * For 'bold', if the text is ever within an element with a 'font-weight'
			 * style property set to '700' or 'bold', that indicates the text
			 * is already bold.  So the style object for bold would be:
			 * { prop: 'font-weight', value: '700|bold' }
			 */
			style: undefined,

			/* useQueryState: [boolean]
			 * Enables/disables whether this button should use the built-in
			 * document.queryCommandState() method to determine whether
			 * the action has already been applied.  If the action has already
			 * been applied, the button will be displayed as 'active' in the toolbar
			 *
			 * Example:
			 * For 'bold', if this is set to true, the code will call:
			 * document.queryCommandState('bold') which will return true if the
			 * browser thinks the text is already bold, and false otherwise
			 */
			useQueryState: undefined,

			/* contentDefault: [string]
			 * Default innerHTML to put inside the button
			 */
			contentDefault: undefined,

			/* contentFA: [string]
			 * The innerHTML to use for the content of the button
			 * if the `buttonLabels` option for MediumEditor is set to 'fontawesome'
			 */
			contentFA: undefined,

			/* classList: [Array]
			 * An array of classNames (strings) to be added to the button
			 */
			classList: undefined,

			/* attrs: [object]
			 * A set of key-value pairs to add to the button as custom attributes
			 */
			attrs: undefined,

			// The button constructor can optionally accept the name of a built-in button
			// (ie 'bold', 'italic', etc.)
			// When the name of a button is passed, it will initialize itself with the
			// configuration for that button
			constructor: function(options) {
				if (Button.isBuiltInButton(options)) {
					MediumEditor.Extension.call(this, this.defaults[options]);
				} else {
					MediumEditor.Extension.call(this, options);
				}
			},

			init: function() {
				MediumEditor.Extension.prototype.init.apply(this, arguments);

				this.button = this.createButton();
				this.on(this.button, 'click', this.handleClick.bind(this));
			},

			/* getButton: [function ()]
			 *
			 * If implemented, this function will be called when
			 * the toolbar is being created. The DOM Element returned
			 * by this function will be appended to the toolbar along
			 * with any other buttons.
			 */
			getButton: function() {
				return this.button;
			},

			getAction: function() {
				return (typeof this.action === 'function') ? this.action(this.base.options) : this.action;
			},

			getAria: function() {
				return (typeof this.aria === 'function') ? this.aria(this.base.options) : this.aria;
			},

			getTagNames: function() {
				return (typeof this.tagNames === 'function') ? this.tagNames(this.base.options) : this.tagNames;
			},

			createButton: function() {
				var button = this.document.createElement('button'),
					content = this.contentDefault,
					ariaLabel = this.getAria(),
					buttonLabels = this.getEditorOption('buttonLabels');
				// Add class names
				button.classList.add('medium-editor-action');
				button.classList.add('medium-editor-action-' + this.name);
				if (this.classList) {
					this.classList.forEach(function(className) {
						button.classList.add(className);
					});
				}

				// Add attributes
				button.setAttribute('data-action', this.getAction());
				if (ariaLabel) {

					// ThemeFusion edit for #361.
					button.setAttribute('aria-label', ariaLabel);
				}
				if (this.attrs) {
					Object.keys(this.attrs).forEach(function(attr) {
						button.setAttribute(attr, this.attrs[attr]);
					}, this);
				}

				if (buttonLabels === 'fontawesome' && this.contentFA) {
					content = this.contentFA;
				}
				button.innerHTML = content;
				return button;
			},

			handleClick: function(event) {
				event.preventDefault();
				event.stopPropagation();

				var action = this.getAction();

				if (action) {
					this.execAction(action);
				}
			},

			isActive: function() {
				return this.button.classList.contains(this.getEditorOption('activeButtonClass'));
			},

			setInactive: function() {
				this.button.classList.remove(this.getEditorOption('activeButtonClass'));
				delete this.knownState;
			},

			setActive: function() {
				this.button.classList.add(this.getEditorOption('activeButtonClass'));
				delete this.knownState;
			},

			queryCommandState: function() {
				var queryState = null;
				if (this.useQueryState) {
					queryState = this.base.queryCommandState(this.getAction());
				}
				return queryState;
			},

			isAlreadyApplied: function(node) {
				var isMatch = false,
					tagNames = this.getTagNames(),
					styleVals,
					computedStyle;

				if (this.knownState === false || this.knownState === true) {
					return this.knownState;
				}

				if (tagNames && tagNames.length > 0) {
					isMatch = tagNames.indexOf(node.nodeName.toLowerCase()) !== -1;
				}

				if (!isMatch && this.style) {
					styleVals = this.style.value.split('|');
					computedStyle = this.window.getComputedStyle(node, null).getPropertyValue(this.style.prop);
					styleVals.forEach(function(val) {
						if (!this.knownState) {
							isMatch = (computedStyle.indexOf(val) !== -1);
							// text-decoration is not inherited by default
							// so if the computed style for text-decoration doesn't match
							// don't write to knownState so we can fallback to other checks
							if (isMatch || this.style.prop !== 'text-decoration') {
								this.knownState = isMatch;
							}
						}
					}, this);
				}

				return isMatch;
			}
		});

		Button.isBuiltInButton = function(name) {
			return (typeof name === 'string') && MediumEditor.extensions.button.prototype.defaults.hasOwnProperty(name);
		};

		MediumEditor.extensions.button = Button;
	}());

	(function() {
		'use strict';

		/* MediumEditor.extensions.button.defaults: [Object]
		 * Set of default config options for all of the built-in MediumEditor buttons
		 */
		MediumEditor.extensions.button.prototype.defaults = {
			'bold': {
				name: 'bold',
				action: 'bold',
				aria: 'bold',
				tagNames: ['b', 'strong'],
				style: {
					prop: 'font-weight',
					value: '700|bold'
				},
				useQueryState: true,
				contentDefault: '<b>B</b>',
				contentFA: '<i class="fa fa-bold"></i>'
			},
			'italic': {
				name: 'italic',
				action: 'italic',
				aria: 'italic',
				tagNames: ['i', 'em'],
				style: {
					prop: 'font-style',
					value: 'italic'
				},
				useQueryState: true,
				contentDefault: '<b><i>I</i></b>',
				contentFA: '<i class="fa fa-italic"></i>'
			},
			'underline': {
				name: 'underline',
				action: 'underline',
				aria: 'underline',
				tagNames: ['u'],
				style: {
					prop: 'text-decoration',
					value: 'underline'
				},
				useQueryState: true,
				contentDefault: '<b><u>U</u></b>',
				contentFA: '<i class="fa fa-underline"></i>'
			},
			'strikethrough': {
				name: 'strikethrough',
				action: 'strikethrough',
				aria: 'strike through',
				tagNames: ['strike'],
				style: {
					prop: 'text-decoration',
					value: 'line-through'
				},
				useQueryState: true,
				contentDefault: '<s>A</s>',
				contentFA: '<i class="fa fa-strikethrough"></i>'
			},
			'superscript': {
				name: 'superscript',
				action: 'superscript',
				aria: 'superscript',
				tagNames: ['sup'],
				/* firefox doesn't behave the way we want it to, so we CAN'T use queryCommandState for superscript
				   https://github.com/guardian/scribe/blob/master/BROWSERINCONSISTENCIES.md#documentquerycommandstate */
				// useQueryState: true
				contentDefault: '<b>x<sup>1</sup></b>',
				contentFA: '<i class="fa fa-superscript"></i>'
			},
			'subscript': {
				name: 'subscript',
				action: 'subscript',
				aria: 'subscript',
				tagNames: ['sub'],
				/* firefox doesn't behave the way we want it to, so we CAN'T use queryCommandState for subscript
				   https://github.com/guardian/scribe/blob/master/BROWSERINCONSISTENCIES.md#documentquerycommandstate */
				// useQueryState: true
				contentDefault: '<b>x<sub>1</sub></b>',
				contentFA: '<i class="fa fa-subscript"></i>'
			},
			'image': {
				name: 'image',
				action: 'image',
				aria: 'image',
				tagNames: ['img'],
				contentDefault: '<b>image</b>',
				contentFA: '<i class="fa fa-picture-o"></i>'
			},
			'html': {
				name: 'html',
				action: 'html',
				aria: 'evaluate html',
				tagNames: ['iframe', 'object'],
				contentDefault: '<b>html</b>',
				contentFA: '<i class="fa fa-code"></i>'
			},
			'orderedlist': {
				name: 'orderedlist',
				action: 'insertorderedlist',
				aria: 'ordered list',
				tagNames: ['ol'],
				useQueryState: true,
				contentDefault: '<b>1.</b>',
				contentFA: '<i class="fa fa-list-ol"></i>'
			},
			'unorderedlist': {
				name: 'unorderedlist',
				action: 'insertunorderedlist',
				aria: 'unordered list',
				tagNames: ['ul'],
				useQueryState: true,
				contentDefault: '<b>&bull;</b>',
				contentFA: '<i class="fa fa-list-ul"></i>'
			},
			'indent': {
				name: 'indent',
				action: 'indent',
				aria: 'indent',
				tagNames: [],
				contentDefault: '<b>&rarr;</b>',
				contentFA: '<i class="fa fa-indent"></i>'
			},
			'outdent': {
				name: 'outdent',
				action: 'outdent',
				aria: 'outdent',
				tagNames: [],
				contentDefault: '<b>&larr;</b>',
				contentFA: '<i class="fa fa-outdent"></i>'
			},
			'justifyCenter': {
				name: 'justifyCenter',
				action: 'justifyCenter',
				aria: 'center justify',
				tagNames: [],
				style: {
					prop: 'text-align',
					value: 'center'
				},
				contentDefault: '<b>C</b>',
				contentFA: '<i class="fa fa-align-center"></i>'
			},
			'justifyFull': {
				name: 'justifyFull',
				action: 'justifyFull',
				aria: 'full justify',
				tagNames: [],
				style: {
					prop: 'text-align',
					value: 'justify'
				},
				contentDefault: '<b>J</b>',
				contentFA: '<i class="fa fa-align-justify"></i>'
			},
			'justifyLeft': {
				name: 'justifyLeft',
				action: 'justifyLeft',
				aria: 'left justify',
				tagNames: [],
				style: {
					prop: 'text-align',
					value: 'left'
				},
				contentDefault: '<b>L</b>',
				contentFA: '<i class="fa fa-align-left"></i>'
			},
			'justifyRight': {
				name: 'justifyRight',
				action: 'justifyRight',
				aria: 'right justify',
				tagNames: [],
				style: {
					prop: 'text-align',
					value: 'right'
				},
				contentDefault: '<b>R</b>',
				contentFA: '<i class="fa fa-align-right"></i>'
			},
			// Known inline elements that are not removed, or not removed consistantly across browsers:
			// <span>, <label>, <br>
			'removeFormat': {
				name: 'removeFormat',
				aria: 'remove formatting',
				action: 'removeFormat',
				contentDefault: '<b>X</b>',
				contentFA: '<i class="fa fa-eraser"></i>'
			},

			/***** Buttons for appending block elements (append-<element> action) *****/

			'quote': {
				name: 'quote',
				action: 'append-blockquote',
				aria: 'blockquote',
				tagNames: ['blockquote'],
				contentDefault: '<b>&ldquo;</b>',
				contentFA: '<i class="fa fa-quote-right"></i>'
			},
			'pre': {
				name: 'pre',
				action: 'append-pre',
				aria: 'preformatted text',
				tagNames: ['pre'],
				contentDefault: '<b>0101</b>',
				contentFA: '<i class="fa fa-code fa-lg"></i>'
			},
			'h1': {
				name: 'h1',
				action: 'append-h1',
				aria: 'header type one',
				tagNames: ['h1'],
				contentDefault: '<b>H1</b>',
				contentFA: '<i class="fa fa-header"><sup>1</sup>'
			},
			'h2': {
				name: 'h2',
				action: 'append-h2',
				aria: 'header type two',
				tagNames: ['h2'],
				contentDefault: '<b>H2</b>',
				contentFA: '<i class="fa fa-header"><sup>2</sup>'
			},
			'h3': {
				name: 'h3',
				action: 'append-h3',
				aria: 'header type three',
				tagNames: ['h3'],
				contentDefault: '<b>H3</b>',
				contentFA: '<i class="fa fa-header"><sup>3</sup>'
			},
			'h4': {
				name: 'h4',
				action: 'append-h4',
				aria: 'header type four',
				tagNames: ['h4'],
				contentDefault: '<b>H4</b>',
				contentFA: '<i class="fa fa-header"><sup>4</sup>'
			},
			'h5': {
				name: 'h5',
				action: 'append-h5',
				aria: 'header type five',
				tagNames: ['h5'],
				contentDefault: '<b>H5</b>',
				contentFA: '<i class="fa fa-header"><sup>5</sup>'
			},
			'h6': {
				name: 'h6',
				action: 'append-h6',
				aria: 'header type six',
				tagNames: ['h6'],
				contentDefault: '<b>H6</b>',
				contentFA: '<i class="fa fa-header"><sup>6</sup>'
			}
		};

	})();

	(function() {
		'use strict';

		/* Base functionality for an extension which will display
		 * a 'form' inside the toolbar
		 */
		var FormExtension = MediumEditor.extensions.button.extend({

			init: function() {
				MediumEditor.extensions.button.prototype.init.apply(this, arguments);
			},

			// default labels for the form buttons
			formSaveLabel: '&#10003;',
			formCloseLabel: '&times;',

			/* activeClass: [string]
			 * set class which added to shown form
			 */
			activeClass: 'medium-editor-toolbar-form-active',

			/* hasForm: [boolean]
			 *
			 * Setting this to true will cause getForm() to be called
			 * when the toolbar is created, so the form can be appended
			 * inside the toolbar container
			 */
			hasForm: true,

			/* getForm: [function ()]
			 *
			 * When hasForm is true, this function must be implemented
			 * and return a DOM Element which will be appended to
			 * the toolbar container. The form should start hidden, and
			 * the extension can choose when to hide/show it
			 */
			getForm: function() {},

			/* isDisplayed: [function ()]
			 *
			 * This function should return true/false reflecting
			 * whether the form is currently displayed
			 */
			isDisplayed: function() {
				if (this.hasForm) {
					return this.getForm().classList.contains(this.activeClass);
				}
				return false;
			},

			/* hideForm: [function ()]
			 *
			 * This function should show the form element inside
			 * the toolbar container
			 */
			showForm: function() {
				if (this.hasForm) {
					this.getForm().classList.add(this.activeClass);
				}
			},

			/* hideForm: [function ()]
			 *
			 * This function should hide the form element inside
			 * the toolbar container
			 */
			hideForm: function() {
				if (this.hasForm) {
					this.getForm().classList.remove(this.activeClass);
				}
			},

			/************************ Helpers ************************
			 * The following are helpers that are either set by MediumEditor
			 * during initialization, or are helper methods which either
			 * route calls to the MediumEditor instance or provide common
			 * functionality for all form extensions
			 *********************************************************/

			/* showToolbarDefaultActions: [function ()]
			 *
			 * Helper method which will turn back the toolbar after canceling
			 * the customized form
			 */
			showToolbarDefaultActions: function() {
				var toolbar = this.base.getExtensionByName('toolbar');
				if (toolbar) {
					toolbar.showToolbarDefaultActions();
				}
			},

			/* hideToolbarDefaultActions: [function ()]
			 *
			 * Helper function which will hide the default contents of the
			 * toolbar, but leave the toolbar container in the same state
			 * to allow a form to display its custom contents inside the toolbar
			 */
			hideToolbarDefaultActions: function() {
				var toolbar = this.base.getExtensionByName('toolbar');
				if (toolbar) {
					toolbar.hideToolbarDefaultActions();
				}
			},

			/* setToolbarPosition: [function ()]
			 *
			 * Helper function which will update the size and position
			 * of the toolbar based on the toolbar content and the current
			 * position of the user's selection
			 */
			setToolbarPosition: function() {
				var toolbar = this.base.getExtensionByName('toolbar');
				if (toolbar) {
					toolbar.setToolbarPosition();
				}
			}
		});

		MediumEditor.extensions.form = FormExtension;
	})();
	(function() {
		'use strict';

		var AnchorForm = MediumEditor.extensions.form.extend({
			/* Anchor Form Options */

			/* customClassOption: [string]  (previously options.anchorButton + options.anchorButtonClass)
			 * Custom class name the user can optionally have added to their created links (ie 'button').
			 * If passed as a non-empty string, a checkbox will be displayed allowing the user to choose
			 * whether to have the class added to the created link or not.
			 */
			customClassOption: null,

			/* customClassOptionText: [string]
			 * text to be shown in the checkbox when the __customClassOption__ is being used.
			 */
			customClassOptionText: 'Button',

			/* linkValidation: [boolean]  (previously options.checkLinkFormat)
			 * enables/disables check for common URL protocols on anchor links.
			 */
			linkValidation: false,

			/* placeholderText: [string]  (previously options.anchorInputPlaceholder)
			 * text to be shown as placeholder of the anchor input.
			 */
			placeholderText: 'Paste or type a link',

			/* targetCheckbox: [boolean]  (previously options.anchorTarget)
			 * enables/disables displaying a "Open in new window" checkbox, which when checked
			 * changes the `target` attribute of the created link.
			 */
			targetCheckbox: false,

			/* targetCheckboxText: [string]  (previously options.anchorInputCheckboxLabel)
			 * text to be shown in the checkbox enabled via the __targetCheckbox__ option.
			 */
			targetCheckboxText: 'Open in new window',

			// Options for the Button base class
			name: 'anchor',
			action: 'createLink',
			aria: 'link',
			tagNames: ['a'],
			contentDefault: '<b>#</b>',
			contentFA: '<i class="fa fa-link"></i>',

			init: function() {
				MediumEditor.extensions.form.prototype.init.apply(this, arguments);

				this.subscribe('editableKeydown', this.handleKeydown.bind(this));
			},

			// Called when the button the toolbar is clicked
			// Overrides ButtonExtension.handleClick
			handleClick: function(event) {
				event.preventDefault();
				event.stopPropagation();

				var range = MediumEditor.selection.getSelectionRange(this.document);

				if (range.startContainer.nodeName.toLowerCase() === 'a' ||
					range.endContainer.nodeName.toLowerCase() === 'a' ||
					MediumEditor.util.getClosestTag(MediumEditor.selection.getSelectedParentElement(range), 'a')) {
					return this.execAction('unlink');
				}

				if (!this.isDisplayed()) {
					this.showForm();
				}

				return false;
			},

			// Called when user hits the defined shortcut (CTRL / COMMAND + K)
			handleKeydown: function(event) {
				if (MediumEditor.util.isKey(event, MediumEditor.util.keyCode.K) && MediumEditor.util.isMetaCtrlKey(event) && !event.shiftKey) {
					this.handleClick(event);
				}
			},

			// Called by medium-editor to append form to the toolbar
			getForm: function() {
				if (!this.form) {
					this.form = this.createForm();
				}
				return this.form;
			},

			getTemplate: function() {
				var template = [
					'<input type="text" class="medium-editor-toolbar-input" placeholder="', this.placeholderText, '">'
				];

				template.push(
					'<a href="#" class="medium-editor-toolbar-save">',
					this.getEditorOption('buttonLabels') === 'fontawesome' ? '<i class="fa fa-check"></i>' : this.formSaveLabel,
					'</a>'
				);

				template.push('<a href="#" class="medium-editor-toolbar-close">',
					this.getEditorOption('buttonLabels') === 'fontawesome' ? '<i class="fa fa-times"></i>' : this.formCloseLabel,
					'</a>');

				// both of these options are slightly moot with the ability to
				// override the various form buildup/serialize functions.

				if (this.targetCheckbox) {
					// fixme: ideally, this targetCheckboxText would be a formLabel too,
					// figure out how to deprecate? also consider `fa-` icon default implcations.
					template.push(
						'<div class="medium-editor-toolbar-form-row">',
						'<input type="checkbox" class="medium-editor-toolbar-anchor-target" id="medium-editor-toolbar-anchor-target-field-' + this.getEditorId() + '">',
						'<label for="medium-editor-toolbar-anchor-target-field-' + this.getEditorId() + '">',
						this.targetCheckboxText,
						'</label>',
						'</div>'
					);
				}

				if (this.customClassOption) {
					// fixme: expose this `Button` text as a formLabel property, too
					// and provide similar access to a `fa-` icon default.
					template.push(
						'<div class="medium-editor-toolbar-form-row">',
						'<input type="checkbox" class="medium-editor-toolbar-anchor-button">',
						'<label>',
						this.customClassOptionText,
						'</label>',
						'</div>'
					);
				}

				return template.join('');

			},

			// Used by medium-editor when the default toolbar is to be displayed
			isDisplayed: function() {
				return MediumEditor.extensions.form.prototype.isDisplayed.apply(this);
			},

			hideForm: function() {
				MediumEditor.extensions.form.prototype.hideForm.apply(this);
				this.getInput().value = '';
			},

			showForm: function(opts) {
				var input = this.getInput(),
					targetCheckbox = this.getAnchorTargetCheckbox(),
					buttonCheckbox = this.getAnchorButtonCheckbox();

				opts = opts || {
					value: ''
				};
				// TODO: This is for backwards compatability
				// We don't need to support the 'string' argument in 6.0.0
				if (typeof opts === 'string') {
					opts = {
						value: opts
					};
				}

				this.base.saveSelection();
				this.hideToolbarDefaultActions();
				MediumEditor.extensions.form.prototype.showForm.apply(this);
				this.setToolbarPosition();

				input.value = opts.value;
				input.focus();

				// If we have a target checkbox, we want it to be checked/unchecked
				// based on whether the existing link has target=_blank
				if (targetCheckbox) {
					targetCheckbox.checked = opts.target === '_blank';
				}

				// If we have a custom class checkbox, we want it to be checked/unchecked
				// based on whether an existing link already has the class
				if (buttonCheckbox) {
					var classList = opts.buttonClass ? opts.buttonClass.split(' ') : [];
					buttonCheckbox.checked = (classList.indexOf(this.customClassOption) !== -1);
				}
			},

			// Called by core when tearing down medium-editor (destroy)
			destroy: function() {
				if (!this.form) {
					return false;
				}

				if (this.form.parentNode) {
					this.form.parentNode.removeChild(this.form);
				}

				delete this.form;
			},

			// core methods

			getFormOpts: function() {
				// no notion of private functions? wanted `_getFormOpts`
				var targetCheckbox = this.getAnchorTargetCheckbox(),
					buttonCheckbox = this.getAnchorButtonCheckbox(),
					opts = {
						value: this.getInput().value.trim()
					};

				if (this.linkValidation) {
					opts.value = this.checkLinkFormat(opts.value);
				}

				opts.target = '_self';
				if (targetCheckbox && targetCheckbox.checked) {
					opts.target = '_blank';
				}

				if (buttonCheckbox && buttonCheckbox.checked) {
					opts.buttonClass = this.customClassOption;
				}

				return opts;
			},

			doFormSave: function() {
				var opts = this.getFormOpts();
				this.completeFormSave(opts);
			},

			completeFormSave: function(opts) {
				this.base.restoreSelection();
				this.execAction(this.action, opts);
				this.base.checkSelection();
			},

			ensureEncodedUri: function(str) {
				return str === decodeURI(str) ? encodeURI(str) : str;
			},

			ensureEncodedUriComponent: function(str) {
				return str === decodeURIComponent(str) ? encodeURIComponent(str) : str;
			},

			ensureEncodedParam: function(param) {
				var split = param.split('='),
					key = split[0],
					val = split[1];

				return key + (val === undefined ? '' : '=' + this.ensureEncodedUriComponent(val));
			},

			ensureEncodedQuery: function(queryString) {
				return queryString.split('&').map(this.ensureEncodedParam.bind(this)).join('&');
			},

			checkLinkFormat: function(value) {
				// Matches any alphabetical characters followed by ://
				// Matches protocol relative "//"
				// Matches common external protocols "mailto:" "tel:" "maps:"
				// Matches relative hash link, begins with "#"
				var urlSchemeRegex = /^([a-z]+:)?\/\/|^(mailto|tel|maps):|^\#/i,
					hasScheme = urlSchemeRegex.test(value),
					scheme = '',
					// telRegex is a regex for checking if the string is a telephone number
					telRegex = /^\+?\s?\(?(?:\d\s?\-?\)?){3,20}$/,
					urlParts = value.match(/^(.*?)(?:\?(.*?))?(?:#(.*))?$/),
					path = urlParts[1],
					query = urlParts[2],
					fragment = urlParts[3];

				if (telRegex.test(value)) {
					return 'tel:' + value;
				}

				if (!hasScheme) {
					var host = path.split('/')[0];
					// if the host part of the path looks like a hostname
					if (host.match(/.+(\.|:).+/) || host === 'localhost') {
						scheme = 'http://';
					}
				}

				return scheme +
					// Ensure path is encoded
					this.ensureEncodedUri(path) +
					// Ensure query is encoded
					(query === undefined ? '' : '?' + this.ensureEncodedQuery(query)) +
					// Include fragment unencoded as encodeUriComponent is too
					// heavy handed for the many characters allowed in a fragment
					(fragment === undefined ? '' : '#' + fragment);
			},

			doFormCancel: function() {
				this.base.restoreSelection();
				this.base.checkSelection();
			},

			// form creation and event handling
			attachFormEvents: function(form) {
				var close = form.querySelector('.medium-editor-toolbar-close'),
					save = form.querySelector('.medium-editor-toolbar-save'),
					input = form.querySelector('.medium-editor-toolbar-input');

				// Handle clicks on the form itself
				this.on(form, 'click', this.handleFormClick.bind(this));

				// Handle typing in the textbox
				this.on(input, 'keyup', this.handleTextboxKeyup.bind(this));

				// Handle close button clicks
				this.on(close, 'click', this.handleCloseClick.bind(this));

				// Handle save button clicks (capture)
				this.on(save, 'click', this.handleSaveClick.bind(this), true);

			},

			createForm: function() {
				var doc = this.document,
					form = doc.createElement('div');

				// Anchor Form (div)
				form.className = 'medium-editor-toolbar-form';
				form.id = 'medium-editor-toolbar-form-anchor-' + this.getEditorId();
				form.innerHTML = this.getTemplate();
				this.attachFormEvents(form);

				return form;
			},

			getInput: function() {
				return this.getForm().querySelector('input.medium-editor-toolbar-input');
			},

			getAnchorTargetCheckbox: function() {
				return this.getForm().querySelector('.medium-editor-toolbar-anchor-target');
			},

			getAnchorButtonCheckbox: function() {
				return this.getForm().querySelector('.medium-editor-toolbar-anchor-button');
			},

			handleTextboxKeyup: function(event) {
				// For ENTER -> create the anchor
				if (event.keyCode === MediumEditor.util.keyCode.ENTER) {
					event.preventDefault();
					this.doFormSave();
					return;
				}

				// For ESCAPE -> close the form
				if (event.keyCode === MediumEditor.util.keyCode.ESCAPE) {
					event.preventDefault();
					this.doFormCancel();
				}
			},

			handleFormClick: function(event) {
				// make sure not to hide form when clicking inside the form
				event.stopPropagation();
			},

			handleSaveClick: function(event) {
				// Clicking Save -> create the anchor
				event.preventDefault();
				this.doFormSave();
			},

			handleCloseClick: function(event) {
				// Click Close -> close the form
				event.preventDefault();
				this.doFormCancel();
			}
		});

		MediumEditor.extensions.anchor = AnchorForm;
	}());

	(function() {
		'use strict';

		var AnchorPreview = MediumEditor.Extension.extend({
			name: 'anchor-preview',

			// Anchor Preview Options

			/* hideDelay: [number]  (previously options.anchorPreviewHideDelay)
			 * time in milliseconds to show the anchor tag preview after the mouse has left the anchor tag.
			 */
			hideDelay: 500,

			/* previewValueSelector: [string]
			 * the default selector to locate where to put the activeAnchor value in the preview
			 */
			previewValueSelector: 'a',

			/* showWhenToolbarIsVisible: [boolean]
			 * determines whether the anchor tag preview shows up when the toolbar is visible
			 */
			showWhenToolbarIsVisible: false,

			/* showOnEmptyLinks: [boolean]
			 * determines whether the anchor tag preview shows up on links with href="" or href="#something"
			 */
			showOnEmptyLinks: true,

			init: function() {
				this.anchorPreview = this.createPreview();

				this.getEditorOption('elementsContainer').appendChild(this.anchorPreview);

				this.attachToEditables();
			},

			getInteractionElements: function() {
				return this.getPreviewElement();
			},

			// TODO: Remove this function in 6.0.0
			getPreviewElement: function() {
				return this.anchorPreview;
			},

			createPreview: function() {
				var el = this.document.createElement('div');

				el.id = 'medium-editor-anchor-preview-' + this.getEditorId();
				el.className = 'medium-editor-anchor-preview';
				el.innerHTML = this.getTemplate();

				this.on(el, 'click', this.handleClick.bind(this));

				return el;
			},

			getTemplate: function() {
				return '<div class="medium-editor-toolbar-anchor-preview" id="medium-editor-toolbar-anchor-preview">' +
					'    <a class="medium-editor-toolbar-anchor-preview-inner"></a>' +
					'</div>';
			},

			destroy: function() {
				if (this.anchorPreview) {
					if (this.anchorPreview.parentNode) {
						this.anchorPreview.parentNode.removeChild(this.anchorPreview);
					}
					delete this.anchorPreview;
				}
			},

			hidePreview: function() {
				if (this.anchorPreview) {
					this.anchorPreview.classList.remove('medium-editor-anchor-preview-active');
				}
				this.activeAnchor = null;
			},

			showPreview: function(anchorEl) {
				if (this.anchorPreview.classList.contains('medium-editor-anchor-preview-active') ||
					anchorEl.getAttribute('data-disable-preview')) {
					return true;
				}

				if (this.previewValueSelector) {
					this.anchorPreview.querySelector(this.previewValueSelector).textContent = anchorEl.attributes.href.value;
					this.anchorPreview.querySelector(this.previewValueSelector).href = anchorEl.attributes.href.value;
				}

				this.anchorPreview.classList.add('medium-toolbar-arrow-over');
				this.anchorPreview.classList.remove('medium-toolbar-arrow-under');

				if (!this.anchorPreview.classList.contains('medium-editor-anchor-preview-active')) {
					this.anchorPreview.classList.add('medium-editor-anchor-preview-active');
				}

				this.activeAnchor = anchorEl;

				this.positionPreview();
				this.attachPreviewHandlers();

				return this;
			},

			positionPreview: function(activeAnchor) {
				activeAnchor = activeAnchor || this.activeAnchor;
				var containerWidth = this.window.innerWidth,
					buttonHeight = this.anchorPreview.offsetHeight,
					boundary = activeAnchor.getBoundingClientRect(),
					diffLeft = this.diffLeft,
					diffTop = this.diffTop,
					elementsContainer = this.getEditorOption('elementsContainer'),
					elementsContainerAbsolute = ['absolute', 'fixed'].indexOf(window.getComputedStyle(elementsContainer).getPropertyValue('position')) > -1,
					relativeBoundary = {},
					halfOffsetWidth, defaultLeft, middleBoundary, elementsContainerBoundary, top;

				halfOffsetWidth = this.anchorPreview.offsetWidth / 2;
				var toolbarExtension = this.base.getExtensionByName('toolbar');
				if (toolbarExtension) {
					diffLeft = toolbarExtension.diffLeft;
					diffTop = toolbarExtension.diffTop;
				}
				defaultLeft = diffLeft - halfOffsetWidth;

				// If container element is absolute / fixed, recalculate boundaries to be relative to the container
				if (elementsContainerAbsolute) {
					elementsContainerBoundary = elementsContainer.getBoundingClientRect();
					['top', 'left'].forEach(function(key) {
						relativeBoundary[key] = boundary[key] - elementsContainerBoundary[key];
					});

					relativeBoundary.width = boundary.width;
					relativeBoundary.height = boundary.height;
					boundary = relativeBoundary;

					containerWidth = elementsContainerBoundary.width;

					// Adjust top position according to container scroll position
					top = elementsContainer.scrollTop;
				} else {
					// Adjust top position according to window scroll position
					top = this.window.pageYOffset;
				}

				middleBoundary = boundary.left + boundary.width / 2;
				top += buttonHeight + boundary.top + boundary.height - diffTop - this.anchorPreview.offsetHeight;

				this.anchorPreview.style.top = Math.round(top) + 'px';
				this.anchorPreview.style.right = 'initial';
				if (middleBoundary < halfOffsetWidth) {
					this.anchorPreview.style.left = defaultLeft + halfOffsetWidth + 'px';
					this.anchorPreview.style.right = 'initial';
				} else if ((containerWidth - middleBoundary) < halfOffsetWidth) {
					this.anchorPreview.style.left = 'auto';
					this.anchorPreview.style.right = 0;
				} else {
					this.anchorPreview.style.left = defaultLeft + middleBoundary + 'px';
					this.anchorPreview.style.right = 'initial';
				}
			},

			attachToEditables: function() {
				this.subscribe('editableMouseover', this.handleEditableMouseover.bind(this));
				this.subscribe('positionedToolbar', this.handlePositionedToolbar.bind(this));
			},

			handlePositionedToolbar: function() {
				// If the toolbar is visible and positioned, we don't need to hide the preview
				// when showWhenToolbarIsVisible is true
				if (!this.showWhenToolbarIsVisible) {
					this.hidePreview();
				}
			},

			handleClick: function(event) {
				var anchorExtension = this.base.getExtensionByName('anchor'),
					activeAnchor = this.activeAnchor;

				if (anchorExtension && activeAnchor) {
					event.preventDefault();

					this.base.selectElement(this.activeAnchor);

					// Using setTimeout + delay because:
					// We may actually be displaying the anchor form, which should be controlled by delay
					this.base.delay(function() {
						if (activeAnchor) {
							var opts = {
								value: activeAnchor.attributes.href.value,
								target: activeAnchor.getAttribute('target'),
								buttonClass: activeAnchor.getAttribute('class')
							};
							anchorExtension.showForm(opts);
							activeAnchor = null;
						}
					}.bind(this));
				}

				this.hidePreview();
			},

			handleAnchorMouseout: function() {
				this.anchorToPreview = null;
				this.off(this.activeAnchor, 'mouseout', this.instanceHandleAnchorMouseout);
				this.instanceHandleAnchorMouseout = null;
			},

			handleEditableMouseover: function(event) {
				var target = MediumEditor.util.getClosestTag(event.target, 'a');

				if (false === target) {
					return;
				}

				// Detect empty href attributes
				// The browser will make href="" or href="#top"
				// into absolute urls when accessed as event.target.href, so check the html
				if (!this.showOnEmptyLinks &&
					(!/href=["']\S+["']/.test(target.outerHTML) || /href=["']#\S+["']/.test(target.outerHTML))) {
					return true;
				}

				// only show when toolbar is not present
				var toolbar = this.base.getExtensionByName('toolbar');
				if (!this.showWhenToolbarIsVisible && toolbar && toolbar.isDisplayed && toolbar.isDisplayed()) {
					return true;
				}

				// detach handler for other anchor in case we hovered multiple anchors quickly
				if (this.activeAnchor && this.activeAnchor !== target) {
					this.detachPreviewHandlers();
				}

				this.anchorToPreview = target;

				this.instanceHandleAnchorMouseout = this.handleAnchorMouseout.bind(this);
				this.on(this.anchorToPreview, 'mouseout', this.instanceHandleAnchorMouseout);
				// Using setTimeout + delay because:
				// - We're going to show the anchor preview according to the configured delay
				//   if the mouse has not left the anchor tag in that time
				this.base.delay(function() {
					if (this.anchorToPreview) {
						this.showPreview(this.anchorToPreview);
					}
				}.bind(this));
			},

			handlePreviewMouseover: function() {
				this.lastOver = (new Date()).getTime();
				this.hovering = true;
			},

			handlePreviewMouseout: function(event) {
				if (!event.relatedTarget || !/anchor-preview/.test(event.relatedTarget.className)) {
					this.hovering = false;
				}
			},

			updatePreview: function() {
				if (this.hovering) {
					return true;
				}
				var durr = (new Date()).getTime() - this.lastOver;
				if (durr > this.hideDelay) {
					// hide the preview 1/2 second after mouse leaves the link
					this.detachPreviewHandlers();
				}
			},

			detachPreviewHandlers: function() {
				// cleanup
				clearInterval(this.intervalTimer);
				if (this.instanceHandlePreviewMouseover) {
					this.off(this.anchorPreview, 'mouseover', this.instanceHandlePreviewMouseover);
					this.off(this.anchorPreview, 'mouseout', this.instanceHandlePreviewMouseout);
					if (this.activeAnchor) {
						this.off(this.activeAnchor, 'mouseover', this.instanceHandlePreviewMouseover);
						this.off(this.activeAnchor, 'mouseout', this.instanceHandlePreviewMouseout);
					}
				}

				this.hidePreview();

				this.hovering = this.instanceHandlePreviewMouseover = this.instanceHandlePreviewMouseout = null;
			},

			// TODO: break up method and extract out handlers
			attachPreviewHandlers: function() {
				this.lastOver = (new Date()).getTime();
				this.hovering = true;

				this.instanceHandlePreviewMouseover = this.handlePreviewMouseover.bind(this);
				this.instanceHandlePreviewMouseout = this.handlePreviewMouseout.bind(this);

				this.intervalTimer = setInterval(this.updatePreview.bind(this), 200);

				this.on(this.anchorPreview, 'mouseover', this.instanceHandlePreviewMouseover);
				this.on(this.anchorPreview, 'mouseout', this.instanceHandlePreviewMouseout);
				this.on(this.activeAnchor, 'mouseover', this.instanceHandlePreviewMouseover);
				this.on(this.activeAnchor, 'mouseout', this.instanceHandlePreviewMouseout);
			}
		});

		MediumEditor.extensions.anchorPreview = AnchorPreview;
	}());

	(function() {
		'use strict';

		var WHITESPACE_CHARS,
			KNOWN_TLDS_FRAGMENT,
			LINK_REGEXP_TEXT,
			KNOWN_TLDS_REGEXP,
			LINK_REGEXP;

		WHITESPACE_CHARS = [' ', '\t', '\n', '\r', '\u00A0', '\u2000', '\u2001', '\u2002', '\u2003',
			'\u2028', '\u2029'
		];
		KNOWN_TLDS_FRAGMENT = 'com|net|org|edu|gov|mil|aero|asia|biz|cat|coop|info|int|jobs|mobi|museum|name|post|pro|tel|travel|' +
			'xxx|ac|ad|ae|af|ag|ai|al|am|an|ao|aq|ar|as|at|au|aw|ax|az|ba|bb|bd|be|bf|bg|bh|bi|bj|bm|bn|bo|br|bs|bt|bv|bw|by|' +
			'bz|ca|cc|cd|cf|cg|ch|ci|ck|cl|cm|cn|co|cr|cs|cu|cv|cx|cy|cz|dd|de|dj|dk|dm|do|dz|ec|ee|eg|eh|er|es|et|eu|fi|fj|' +
			'fk|fm|fo|fr|ga|gb|gd|ge|gf|gg|gh|gi|gl|gm|gn|gp|gq|gr|gs|gt|gu|gw|gy|hk|hm|hn|hr|ht|hu|id|ie|il|im|in|io|iq|ir|' +
			'is|it|je|jm|jo|jp|ke|kg|kh|ki|km|kn|kp|kr|kw|ky|kz|la|lb|lc|li|lk|lr|ls|lt|lu|lv|ly|ma|mc|md|me|mg|mh|mk|ml|mm|' +
			'mn|mo|mp|mq|mr|ms|mt|mu|mv|mw|mx|my|mz|na|nc|ne|nf|ng|ni|nl|no|np|nr|nu|nz|om|pa|pe|pf|pg|ph|pk|pl|pm|pn|pr|ps|' +
			'pt|pw|py|qa|re|ro|rs|ru|rw|sa|sb|sc|sd|se|sg|sh|si|sj|ja|sk|sl|sm|sn|so|sr|ss|st|su|sv|sx|sy|sz|tc|td|tf|tg|th|' +
			'tj|tk|tl|tm|tn|to|tp|tr|tt|tv|tw|tz|ua|ug|uk|us|uy|uz|va|vc|ve|vg|vi|vn|vu|wf|ws|ye|yt|yu|za|zm|zw';

		LINK_REGEXP_TEXT =
			'(' +
			// Version of Gruber URL Regexp optimized for JS: http://stackoverflow.com/a/17733640
			'((?:(https?://|ftps?://|nntp://)|www\\d{0,3}[.]|[a-z0-9.\\-]+[.](' + KNOWN_TLDS_FRAGMENT + ')\\\/)\\S+(?:[^\\s`!\\[\\]{};:\'\".,?\u00AB\u00BB\u201C\u201D\u2018\u2019]))' +
			// Addition to above Regexp to support bare domains/one level subdomains with common non-i18n TLDs and without www prefix:
			')|(([a-z0-9\\-]+\\.)?[a-z0-9\\-]+\\.(' + KNOWN_TLDS_FRAGMENT + '))';

		KNOWN_TLDS_REGEXP = new RegExp('^(' + KNOWN_TLDS_FRAGMENT + ')$', 'i');

		LINK_REGEXP = new RegExp(LINK_REGEXP_TEXT, 'gi');

		function nodeIsNotInsideAnchorTag(node) {
			return !MediumEditor.util.getClosestTag(node, 'a');
		}

		var AutoLink = MediumEditor.Extension.extend({
			init: function() {
				MediumEditor.Extension.prototype.init.apply(this, arguments);

				this.disableEventHandling = false;
				this.subscribe('editableKeypress', this.onKeypress.bind(this));
				this.subscribe('editableBlur', this.onBlur.bind(this));
				// MS IE has it's own auto-URL detect feature but ours is better in some ways. Be consistent.
				this.document.execCommand('AutoUrlDetect', false, false);
			},

			isLastInstance: function() {
				var activeInstances = 0;
				for (var i = 0; i < this.window._mediumEditors.length; i++) {
					var editor = this.window._mediumEditors[i];
					if (editor !== null && editor.getExtensionByName('autoLink') !== undefined) {
						activeInstances++;
					}
				}
				return activeInstances === 1;
			},

			destroy: function() {
				// Turn AutoUrlDetect back on
				if (this.document.queryCommandSupported('AutoUrlDetect') && this.isLastInstance()) {
					this.document.execCommand('AutoUrlDetect', false, true);
				}
			},

			onBlur: function(blurEvent, editable) {
				this.performLinking(editable);
			},

			onKeypress: function(keyPressEvent) {
				if (this.disableEventHandling) {
					return;
				}

				if (MediumEditor.util.isKey(keyPressEvent, [MediumEditor.util.keyCode.SPACE, MediumEditor.util.keyCode.ENTER])) {
					clearTimeout(this.performLinkingTimeout);
					// Saving/restoring the selection in the middle of a keypress doesn't work well...
					this.performLinkingTimeout = setTimeout(function() {
						try {
							var sel = this.base.exportSelection();
							if (this.performLinking(keyPressEvent.target)) {
								// pass true for favorLaterSelectionAnchor - this is needed for links at the end of a
								// paragraph in MS IE, or MS IE causes the link to be deleted right after adding it.
								this.base.importSelection(sel, true);
							}
						} catch (e) {
							if (window.console) {
								window.console.error('Failed to perform linking', e);
							}
							this.disableEventHandling = true;
						}
					}.bind(this), 0);
				}
			},

			performLinking: function(contenteditable) {
				/*
				Perform linking on blockElement basis, blockElements are HTML elements with text content and without
				child element.

				Example:
				- HTML content
				<blockquote>
				  <p>link.</p>
				  <p>my</p>
				</blockquote>

				- blockElements
				[<p>link.</p>, <p>my</p>]

				otherwise the detection can wrongly find the end of one paragraph and the beginning of another paragraph
				to constitute a link, such as a paragraph ending "link." and the next paragraph beginning with "my" is
				interpreted into "link.my" and the code tries to create a link across blockElements - which doesn't work
				and is terrible.
				(Medium deletes the spaces/returns between P tags so the textContent ends up without paragraph spacing)
				*/
				var blockElements = MediumEditor.util.splitByBlockElements(contenteditable),
					documentModified = false;
				if (blockElements.length === 0) {
					blockElements = [contenteditable];
				}
				for (var i = 0; i < blockElements.length; i++) {
					documentModified = this.removeObsoleteAutoLinkSpans(blockElements[i]) || documentModified;
					documentModified = this.performLinkingWithinElement(blockElements[i]) || documentModified;
				}
				this.base.events.updateInput(contenteditable, {
					target: contenteditable,
					currentTarget: contenteditable
				});
				return documentModified;
			},

			removeObsoleteAutoLinkSpans: function(element) {
				if (!element || element.nodeType === 3) {
					return false;
				}

				var spans = element.querySelectorAll('span[data-auto-link="true"]'),
					documentModified = false;

				for (var i = 0; i < spans.length; i++) {
					var textContent = spans[i].textContent;
					if (textContent.indexOf('://') === -1) {
						textContent = MediumEditor.util.ensureUrlHasProtocol(textContent);
					}
					if (spans[i].getAttribute('data-href') !== textContent && nodeIsNotInsideAnchorTag(spans[i])) {
						documentModified = true;
						var trimmedTextContent = textContent.replace(/\s+$/, '');
						if (spans[i].getAttribute('data-href') === trimmedTextContent) {
							var charactersTrimmed = textContent.length - trimmedTextContent.length,
								subtree = MediumEditor.util.splitOffDOMTree(spans[i], this.splitTextBeforeEnd(spans[i], charactersTrimmed));
							spans[i].parentNode.insertBefore(subtree, spans[i].nextSibling);
						} else {
							// Some editing has happened to the span, so just remove it entirely. The user can put it back
							// around just the href content if they need to prevent it from linking
							MediumEditor.util.unwrap(spans[i], this.document);
						}
					}
				}
				return documentModified;
			},

			splitTextBeforeEnd: function(element, characterCount) {
				var treeWalker = this.document.createTreeWalker(element, NodeFilter.SHOW_TEXT, null, false),
					lastChildNotExhausted = true;

				// Start the tree walker at the last descendant of the span
				while (lastChildNotExhausted) {
					lastChildNotExhausted = treeWalker.lastChild() !== null;
				}

				var currentNode,
					currentNodeValue,
					previousNode;
				while (characterCount > 0 && previousNode !== null) {
					currentNode = treeWalker.currentNode;
					currentNodeValue = currentNode.nodeValue;
					if (currentNodeValue.length > characterCount) {
						previousNode = currentNode.splitText(currentNodeValue.length - characterCount);
						characterCount = 0;
					} else {
						previousNode = treeWalker.previousNode();
						characterCount -= currentNodeValue.length;
					}
				}
				return previousNode;
			},

			performLinkingWithinElement: function(element) {
				var matches = this.findLinkableText(element),
					linkCreated = false;

				for (var matchIndex = 0; matchIndex < matches.length; matchIndex++) {
					var matchingTextNodes = MediumEditor.util.findOrCreateMatchingTextNodes(this.document, element,
						matches[matchIndex]);
					if (this.shouldNotLink(matchingTextNodes)) {
						continue;
					}
					this.createAutoLink(matchingTextNodes, matches[matchIndex].href);
				}
				return linkCreated;
			},

			shouldNotLink: function(textNodes) {
				var shouldNotLink = false;
				for (var i = 0; i < textNodes.length && shouldNotLink === false; i++) {
					// Do not link if the text node is either inside an anchor or inside span[data-auto-link]
					shouldNotLink = !!MediumEditor.util.traverseUp(textNodes[i], function(node) {
						return node.nodeName.toLowerCase() === 'a' ||
							(node.getAttribute && node.getAttribute('data-auto-link') === 'true');
					});
				}
				return shouldNotLink;
			},

			findLinkableText: function(contenteditable) {
				var textContent = contenteditable.textContent,
					match = null,
					matches = [];

				while ((match = LINK_REGEXP.exec(textContent)) !== null) {
					var matchOk = true,
						matchEnd = match.index + match[0].length;
					// If the regexp detected something as a link that has text immediately preceding/following it, bail out.
					matchOk = (match.index === 0 || WHITESPACE_CHARS.indexOf(textContent[match.index - 1]) !== -1) &&
						(matchEnd === textContent.length || WHITESPACE_CHARS.indexOf(textContent[matchEnd]) !== -1);
					// If the regexp detected a bare domain that doesn't use one of our expected TLDs, bail out.
					matchOk = matchOk && (match[0].indexOf('/') !== -1 ||
						KNOWN_TLDS_REGEXP.test(match[0].split('.').pop().split('?').shift()));

					if (matchOk) {
						matches.push({
							href: match[0],
							start: match.index,
							end: matchEnd
						});
					}
				}
				return matches;
			},

			createAutoLink: function(textNodes, href) {
				href = MediumEditor.util.ensureUrlHasProtocol(href);
				var anchor = MediumEditor.util.createLink(this.document, textNodes, href, this.getEditorOption('targetBlank') ? '_blank' : null),
					span = this.document.createElement('span');
				span.setAttribute('data-auto-link', 'true');
				span.setAttribute('data-href', href);
				anchor.insertBefore(span, anchor.firstChild);
				while (anchor.childNodes.length > 1) {
					span.appendChild(anchor.childNodes[1]);
				}
			}

		});

		MediumEditor.extensions.autoLink = AutoLink;
	}());

	(function() {
		'use strict';

		var CLASS_DRAG_OVER = 'medium-editor-dragover';

		function clearClassNames(element) {
			var editable = MediumEditor.util.getContainerEditorElement(element),
				existing = Array.prototype.slice.call(editable.parentElement.querySelectorAll('.' + CLASS_DRAG_OVER));

			existing.forEach(function(el) {
				el.classList.remove(CLASS_DRAG_OVER);
			});
		}

		var FileDragging = MediumEditor.Extension.extend({
			name: 'fileDragging',

			allowedTypes: ['image'],

			init: function() {
				MediumEditor.Extension.prototype.init.apply(this, arguments);

				this.subscribe('editableDrag', this.handleDrag.bind(this));
				this.subscribe('editableDrop', this.handleDrop.bind(this));
			},

			handleDrag: function(event) {
				event.preventDefault();
				event.dataTransfer.dropEffect = 'copy';

				var target = event.target.classList ? event.target : event.target.parentElement;

				// Ensure the class gets removed from anything that had it before
				clearClassNames(target);

				if (event.type === 'dragover') {
					target.classList.add(CLASS_DRAG_OVER);
				}
			},

			handleDrop: function(event) {
				// Prevent file from opening in the current window
				event.preventDefault();
				event.stopPropagation();
				// Select the dropping target, and set the selection to the end of the target
				// https://github.com/yabwe/medium-editor/issues/980
				this.base.selectElement(event.target);
				var selection = this.base.exportSelection();
				selection.start = selection.end;
				this.base.importSelection(selection);
				// IE9 does not support the File API, so prevent file from opening in the window
				// but also don't try to actually get the file
				if (event.dataTransfer.files) {
					Array.prototype.slice.call(event.dataTransfer.files).forEach(function(file) {
						if (this.isAllowedFile(file)) {
							if (file.type.match('image')) {
								this.insertImageFile(file);
							}
						}
					}, this);
				}

				// Make sure we remove our class from everything
				clearClassNames(event.target);
			},

			isAllowedFile: function(file) {
				return this.allowedTypes.some(function(fileType) {
					return !!file.type.match(fileType);
				});
			},

			insertImageFile: function(file) {
				if (typeof FileReader !== 'function') {
					return;
				}
				var fileReader = new FileReader();
				fileReader.readAsDataURL(file);

				// attach the onload event handler, makes it easier to listen in with jasmine
				fileReader.addEventListener('load', function(e) {
					var addImageElement = this.document.createElement('img');
					addImageElement.src = e.target.result;
					MediumEditor.util.insertHTMLCommand(this.document, addImageElement.outerHTML);
				}.bind(this));
			}
		});

		MediumEditor.extensions.fileDragging = FileDragging;
	}());

	(function() {
		'use strict';

		var KeyboardCommands = MediumEditor.Extension.extend({
			name: 'keyboard-commands',

			/* KeyboardCommands Options */

			/* commands: [Array]
			 * Array of objects describing each command and the combination of keys that will trigger it
			 * Required for each object:
			 *   command [String] (argument passed to editor.execAction())
			 *   key [String] (keyboard character that triggers this command)
			 *   meta [boolean] (whether the ctrl/meta key has to be active or inactive)
			 *   shift [boolean] (whether the shift key has to be active or inactive)
			 *   alt [boolean] (whether the alt key has to be active or inactive)
			 */
			commands: [{
					command: 'bold',
					key: 'B',
					meta: true,
					shift: false,
					alt: false
				},
				{
					command: 'italic',
					key: 'I',
					meta: true,
					shift: false,
					alt: false
				},
				{
					command: 'underline',
					key: 'U',
					meta: true,
					shift: false,
					alt: false
				}
			],

			init: function() {
				MediumEditor.Extension.prototype.init.apply(this, arguments);

				this.subscribe('editableKeydown', this.handleKeydown.bind(this));
				this.keys = {};
				this.commands.forEach(function(command) {
					var keyCode = command.key.charCodeAt(0);
					if (!this.keys[keyCode]) {
						this.keys[keyCode] = [];
					}
					this.keys[keyCode].push(command);
				}, this);
			},

			handleKeydown: function(event) {
				var keyCode = MediumEditor.util.getKeyCode(event);
				if (!this.keys[keyCode]) {
					return;
				}

				var isMeta = MediumEditor.util.isMetaCtrlKey(event),
					isShift = !!event.shiftKey,
					isAlt = !!event.altKey;

				this.keys[keyCode].forEach(function(data) {
					if (data.meta === isMeta &&
						data.shift === isShift &&
						(data.alt === isAlt ||
							undefined === data.alt)) { // TODO deprecated: remove check for undefined === data.alt when jumping to 6.0.0
						event.preventDefault();
						event.stopPropagation();

						// command can be a function to execute
						if (typeof data.command === 'function') {
							data.command.apply(this);
						}
						// command can be false so the shortcut is just disabled
						else if (false !== data.command) {
							this.execAction(data.command);
						}
					}
				}, this);
			}
		});

		MediumEditor.extensions.keyboardCommands = KeyboardCommands;
	}());

	(function() {
		'use strict';

		var FontNameForm = MediumEditor.extensions.form.extend({

			name: 'fontname',
			action: 'fontName',
			aria: 'change font name',
			contentDefault: '&#xB1;', // ±
			contentFA: '<i class="fa fa-font"></i>',

			fonts: ['', 'Arial', 'Verdana', 'Times New Roman'],

			init: function() {
				MediumEditor.extensions.form.prototype.init.apply(this, arguments);
			},

			// Called when the button the toolbar is clicked
			// Overrides ButtonExtension.handleClick
			handleClick: function(event) {
				event.preventDefault();
				event.stopPropagation();

				if (!this.isDisplayed()) {
					// Get FontName of current selection (convert to string since IE returns this as number)
					var fontName = this.document.queryCommandValue('fontName') + '';
					this.showForm(fontName);
				}

				return false;
			},

			// Called by medium-editor to append form to the toolbar
			getForm: function() {
				if (!this.form) {
					this.form = this.createForm();
				}
				return this.form;
			},

			// Used by medium-editor when the default toolbar is to be displayed
			isDisplayed: function() {
				return this.getForm().style.display === 'block';
			},

			hideForm: function() {
				this.getForm().style.display = 'none';
				this.getSelect().value = '';
			},

			showForm: function(fontName) {
				var select = this.getSelect();

				this.base.saveSelection();
				this.hideToolbarDefaultActions();
				this.getForm().style.display = 'block';
				this.setToolbarPosition();

				select.value = fontName || '';
				select.focus();
			},

			// Called by core when tearing down medium-editor (destroy)
			destroy: function() {
				if (!this.form) {
					return false;
				}

				if (this.form.parentNode) {
					this.form.parentNode.removeChild(this.form);
				}

				delete this.form;
			},

			// core methods

			doFormSave: function() {
				this.base.restoreSelection();
				this.base.checkSelection();
			},

			doFormCancel: function() {
				this.base.restoreSelection();
				this.clearFontName();
				this.base.checkSelection();
			},

			// form creation and event handling
			createForm: function() {
				var doc = this.document,
					form = doc.createElement('div'),
					select = doc.createElement('select'),
					close = doc.createElement('a'),
					save = doc.createElement('a'),
					option;

				// Font Name Form (div)
				form.className = 'medium-editor-toolbar-form';
				form.id = 'medium-editor-toolbar-form-fontname-' + this.getEditorId();

				// Handle clicks on the form itself
				this.on(form, 'click', this.handleFormClick.bind(this));

				// Add font names
				for (var i = 0; i < this.fonts.length; i++) {
					option = doc.createElement('option');
					option.innerHTML = this.fonts[i];
					option.value = this.fonts[i];
					select.appendChild(option);
				}

				select.className = 'medium-editor-toolbar-select';
				form.appendChild(select);

				// Handle typing in the textbox
				this.on(select, 'change', this.handleFontChange.bind(this));

				// Add save buton
				save.setAttribute('href', '#');
				save.className = 'medium-editor-toobar-save';
				save.innerHTML = this.getEditorOption('buttonLabels') === 'fontawesome' ?
					'<i class="fa fa-check"></i>' :
					'&#10003;';
				form.appendChild(save);

				// Handle save button clicks (capture)
				this.on(save, 'click', this.handleSaveClick.bind(this), true);

				// Add close button
				close.setAttribute('href', '#');
				close.className = 'medium-editor-toobar-close';
				close.innerHTML = this.getEditorOption('buttonLabels') === 'fontawesome' ?
					'<i class="fa fa-times"></i>' :
					'&times;';
				form.appendChild(close);

				// Handle close button clicks
				this.on(close, 'click', this.handleCloseClick.bind(this));

				return form;
			},

			getSelect: function() {
				return this.getForm().querySelector('select.medium-editor-toolbar-select');
			},

			clearFontName: function() {
				MediumEditor.selection.getSelectedElements(this.document).forEach(function(el) {
					if (el.nodeName.toLowerCase() === 'font' && el.hasAttribute('face')) {
						el.removeAttribute('face');
					}
				});
			},

			handleFontChange: function() {
				var font = this.getSelect().value;
				if (font === '') {
					this.clearFontName();
				} else {
					this.execAction('fontName', {
						value: font
					});
				}
			},

			handleFormClick: function(event) {
				// make sure not to hide form when clicking inside the form
				event.stopPropagation();
			},

			handleSaveClick: function(event) {
				// Clicking Save -> create the font size
				event.preventDefault();
				this.doFormSave();
			},

			handleCloseClick: function(event) {
				// Click Close -> close the form
				event.preventDefault();
				this.doFormCancel();
			}
		});

		MediumEditor.extensions.fontName = FontNameForm;
	}());

	(function() {
		'use strict';

		var FontSizeForm = MediumEditor.extensions.form.extend({

			name: 'fontsize',
			action: 'fontSize',
			aria: 'increase/decrease font size',
			contentDefault: '&#xB1;', // ±
			contentFA: '<i class="fa fa-text-height"></i>',

			init: function() {
				MediumEditor.extensions.form.prototype.init.apply(this, arguments);
			},

			// Called when the button the toolbar is clicked
			// Overrides ButtonExtension.handleClick
			handleClick: function(event) {
				event.preventDefault();
				event.stopPropagation();

				if (!this.isDisplayed()) {
					// Get fontsize of current selection (convert to string since IE returns this as number)
					var fontSize = this.document.queryCommandValue('fontSize') + '';
					this.showForm(fontSize);
				}

				return false;
			},

			// Called by medium-editor to append form to the toolbar
			getForm: function() {
				if (!this.form) {
					this.form = this.createForm();
				}
				return this.form;
			},

			// Used by medium-editor when the default toolbar is to be displayed
			isDisplayed: function() {
				return this.getForm().style.display === 'block';
			},

			hideForm: function() {
				this.getForm().style.display = 'none';
				this.getInput().value = '';
			},

			showForm: function(fontSize) {
				var input = this.getInput();

				this.base.saveSelection();
				this.hideToolbarDefaultActions();
				this.getForm().style.display = 'block';
				this.setToolbarPosition();

				input.value = fontSize || '';
				input.focus();
			},

			// Called by core when tearing down medium-editor (destroy)
			destroy: function() {
				if (!this.form) {
					return false;
				}

				if (this.form.parentNode) {
					this.form.parentNode.removeChild(this.form);
				}

				delete this.form;
			},

			// core methods

			doFormSave: function() {
				this.base.restoreSelection();
				this.base.checkSelection();
			},

			doFormCancel: function() {
				this.base.restoreSelection();
				this.clearFontSize();
				this.base.checkSelection();
			},

			// form creation and event handling
			createForm: function() {
				var doc = this.document,
					form = doc.createElement('div'),
					input = doc.createElement('input'),
					close = doc.createElement('a'),
					save = doc.createElement('a');

				// Font Size Form (div)
				form.className = 'medium-editor-toolbar-form';
				form.id = 'medium-editor-toolbar-form-fontsize-' + this.getEditorId();

				// Handle clicks on the form itself
				this.on(form, 'click', this.handleFormClick.bind(this));

				// Add font size slider
				input.setAttribute('type', 'range');
				input.setAttribute('min', '1');
				input.setAttribute('max', '7');
				input.className = 'medium-editor-toolbar-input';
				form.appendChild(input);

				// Handle typing in the textbox
				this.on(input, 'change', this.handleSliderChange.bind(this));

				// Add save buton
				save.setAttribute('href', '#');
				save.className = 'medium-editor-toobar-save';
				save.innerHTML = this.getEditorOption('buttonLabels') === 'fontawesome' ?
					'<i class="fa fa-check"></i>' :
					'&#10003;';
				form.appendChild(save);

				// Handle save button clicks (capture)
				this.on(save, 'click', this.handleSaveClick.bind(this), true);

				// Add close button
				close.setAttribute('href', '#');
				close.className = 'medium-editor-toobar-close';
				close.innerHTML = this.getEditorOption('buttonLabels') === 'fontawesome' ?
					'<i class="fa fa-times"></i>' :
					'&times;';
				form.appendChild(close);

				// Handle close button clicks
				this.on(close, 'click', this.handleCloseClick.bind(this));

				return form;
			},

			getInput: function() {
				return this.getForm().querySelector('input.medium-editor-toolbar-input');
			},

			clearFontSize: function() {
				MediumEditor.selection.getSelectedElements(this.document).forEach(function(el) {
					if (el.nodeName.toLowerCase() === 'font' && el.hasAttribute('size')) {
						el.removeAttribute('size');
					}
				});
			},

			handleSliderChange: function() {
				var size = this.getInput().value;
				if (size === '4') {
					this.clearFontSize();
				} else {
					this.execAction('fontSize', {
						value: size
					});
				}
			},

			handleFormClick: function(event) {
				// make sure not to hide form when clicking inside the form
				event.stopPropagation();
			},

			handleSaveClick: function(event) {
				// Clicking Save -> create the font size
				event.preventDefault();
				this.doFormSave();
			},

			handleCloseClick: function(event) {
				// Click Close -> close the form
				event.preventDefault();
				this.doFormCancel();
			}
		});

		MediumEditor.extensions.fontSize = FontSizeForm;
	}());
	(function() {
		'use strict';

		/* Helpers and internal variables that don't need to be members of actual paste handler */

		var pasteBinDefaultContent = '%ME_PASTEBIN%',
			lastRange = null,
			keyboardPasteEditable = null,
			stopProp = function(event) {
				event.stopPropagation();
			};

		/*jslint regexp: true*/
		/*
		    jslint does not allow character negation, because the negation
		    will not match any unicode characters. In the regexes in this
		    block, negation is used specifically to match the end of an html
		    tag, and in fact unicode characters *should* be allowed.
		*/
		function createReplacements() {
			return [
				// Remove anything but the contents within the BODY element
				[new RegExp(/^[\s\S]*<body[^>]*>\s*|\s*<\/body[^>]*>[\s\S]*$/g), ''],

				// cleanup comments added by Chrome when pasting html
				[new RegExp(/<!--StartFragment-->|<!--EndFragment-->/g), ''],

				// Trailing BR elements
				[new RegExp(/<br>$/i), ''],

				// replace two bogus tags that begin pastes from google docs
				[new RegExp(/<[^>]*docs-internal-guid[^>]*>/gi), ''],
				[new RegExp(/<\/b>(<br[^>]*>)?$/gi), ''],

				// un-html spaces and newlines inserted by OS X
				[new RegExp(/<span class="Apple-converted-space">\s+<\/span>/g), ' '],
				[new RegExp(/<br class="Apple-interchange-newline">/g), '<br>'],

				// replace google docs italics+bold with a span to be replaced once the html is inserted
				[new RegExp(/<span[^>]*(font-style:italic;font-weight:(bold|700)|font-weight:(bold|700);font-style:italic)[^>]*>/gi), '<span class="replace-with italic bold">'],

				// replace google docs italics with a span to be replaced once the html is inserted
				[new RegExp(/<span[^>]*font-style:italic[^>]*>/gi), '<span class="replace-with italic">'],

				//[replace google docs bolds with a span to be replaced once the html is inserted
				[new RegExp(/<span[^>]*font-weight:(bold|700)[^>]*>/gi), '<span class="replace-with bold">'],

				// replace manually entered b/i/a tags with real ones
				[new RegExp(/&lt;(\/?)(i|b|a)&gt;/gi), '<$1$2>'],

				// replace manually a tags with real ones, converting smart-quotes from google docs
				[new RegExp(/&lt;a(?:(?!href).)+href=(?:&quot;|&rdquo;|&ldquo;|"|“|”)(((?!&quot;|&rdquo;|&ldquo;|"|“|”).)*)(?:&quot;|&rdquo;|&ldquo;|"|“|”)(?:(?!&gt;).)*&gt;/gi), '<a href="$1">'],

				// Newlines between paragraphs in html have no syntactic value,
				// but then have a tendency to accidentally become additional paragraphs down the line
				[new RegExp(/<\/p>\n+/gi), '</p>'],
				[new RegExp(/\n+<p/gi), '<p'],

				// Microsoft Word makes these odd tags, like <o:p></o:p>
				[new RegExp(/<\/?o:[a-z]*>/gi), ''],

				// Microsoft Word adds some special elements around list items
				[new RegExp(/<!\[if !supportLists\]>(((?!<!).)*)<!\[endif]\>/gi), '$1']
			];
		}
		/*jslint regexp: false*/

		/**
		 * Gets various content types out of the Clipboard API. It will also get the
		 * plain text using older IE and WebKit API.
		 *
		 * @param {event} event Event fired on paste.
		 * @param {win} reference to window
		 * @param {doc} reference to document
		 * @return {Object} Object with mime types and data for those mime types.
		 */
		function getClipboardContent(event, win, doc) {
			var dataTransfer = event.clipboardData || win.clipboardData || doc.dataTransfer,
				data = {};

			if (!dataTransfer) {
				return data;
			}

			// Use old WebKit/IE API
			if (dataTransfer.getData) {
				var legacyText = dataTransfer.getData('Text');
				if (legacyText && legacyText.length > 0) {
					data['text/plain'] = legacyText;
				}
			}

			if (dataTransfer.types) {
				for (var i = 0; i < dataTransfer.types.length; i++) {
					var contentType = dataTransfer.types[i];
					data[contentType] = dataTransfer.getData(contentType);
				}
			}

			return data;
		}

		var PasteHandler = MediumEditor.Extension.extend({
			/* Paste Options */

			/* forcePlainText: [boolean]
			 * Forces pasting as plain text.
			 */
			forcePlainText: true,

			/* cleanPastedHTML: [boolean]
			 * cleans pasted content from different sources, like google docs etc.
			 */
			cleanPastedHTML: false,

			/* preCleanReplacements: [Array]
			 * custom pairs (2 element arrays) of RegExp and replacement text to use during past when
			 * __forcePlainText__ or __cleanPastedHTML__ are `true` OR when calling `cleanPaste(text)` helper method.
			 * These replacements are executed before any medium editor defined replacements.
			 */
			preCleanReplacements: [],

			/* cleanReplacements: [Array]
			 * custom pairs (2 element arrays) of RegExp and replacement text to use during paste when
			 * __forcePlainText__ or __cleanPastedHTML__ are `true` OR when calling `cleanPaste(text)` helper method.
			 * These replacements are executed after any medium editor defined replacements.
			 */
			cleanReplacements: [],

			/* cleanAttrs:: [Array]
			 * list of element attributes to remove during paste when __cleanPastedHTML__ is `true` or when
			 * calling `cleanPaste(text)` or `pasteHTML(html, options)` helper methods.
			 */
			cleanAttrs: ['class', 'style', 'dir'],

			/* cleanTags: [Array]
			 * list of element tag names to remove during paste when __cleanPastedHTML__ is `true` or when
			 * calling `cleanPaste(text)` or `pasteHTML(html, options)` helper methods.
			 */
			cleanTags: ['meta'],

			/* unwrapTags: [Array]
			 * list of element tag names to unwrap (remove the element tag but retain its child elements)
			 * during paste when __cleanPastedHTML__ is `true` or when
			 * calling `cleanPaste(text)` or `pasteHTML(html, options)` helper methods.
			 */
			unwrapTags: [],

			init: function() {
				MediumEditor.Extension.prototype.init.apply(this, arguments);

				if (this.forcePlainText || this.cleanPastedHTML) {
					this.subscribe('editableKeydown', this.handleKeydown.bind(this));
					// We need access to the full event data in paste
					// so we can't use the editablePaste event here
					this.getEditorElements().forEach(function(element) {
						this.on(element, 'paste', this.handlePaste.bind(this));
					}, this);
					this.subscribe('addElement', this.handleAddElement.bind(this));
				}
			},

			handleAddElement: function(event, editable) {
				this.on(editable, 'paste', this.handlePaste.bind(this));
			},

			destroy: function() {
				// Make sure pastebin is destroyed in case it's still around for some reason
				if (this.forcePlainText || this.cleanPastedHTML) {
					this.removePasteBin();
				}
			},

			handlePaste: function(event, editable) {
				if (event.defaultPrevented) {
					return;
				}

				var clipboardContent = getClipboardContent(event, this.window, this.document),
					pastedHTML = clipboardContent['text/html'],
					pastedPlain = clipboardContent['text/plain'];

				if (this.window.clipboardData && event.clipboardData === undefined && !pastedHTML) {
					// If window.clipboardData exists, but event.clipboardData doesn't exist,
					// we're probably in IE. IE only has two possibilities for clipboard
					// data format: 'Text' and 'URL'.
					//
					// For IE, we'll fallback to 'Text' for text/html
					pastedHTML = pastedPlain;
				}

				if (pastedHTML || pastedPlain) {
					event.preventDefault();

					this.doPaste(pastedHTML, pastedPlain, editable);
				}
			},

			doPaste: function(pastedHTML, pastedPlain, editable) {
				var paragraphs,
					html = '',
					p;

				if (this.cleanPastedHTML && pastedHTML) {
					return this.cleanPaste(pastedHTML);
				}

				if (!(this.getEditorOption('disableReturn') || (editable && editable.getAttribute('data-disable-return')))) {
					paragraphs = pastedPlain.split(/[\r\n]+/g);
					// If there are no \r\n in data, don't wrap in <p>
					if (paragraphs.length > 1) {
						for (p = 0; p < paragraphs.length; p += 1) {
							if (paragraphs[p] !== '') {
								html += '<p>' + MediumEditor.util.htmlEntities(paragraphs[p]) + '</p>';
							}
						}
					} else {
						html = MediumEditor.util.htmlEntities(paragraphs[0]);
					}
				} else {
					html = MediumEditor.util.htmlEntities(pastedPlain);
				}
				MediumEditor.util.insertHTMLCommand(this.document, html);
			},

			handlePasteBinPaste: function(event) {
				if (event.defaultPrevented) {
					this.removePasteBin();
					return;
				}

				var clipboardContent = getClipboardContent(event, this.window, this.document),
					pastedHTML = clipboardContent['text/html'],
					pastedPlain = clipboardContent['text/plain'],
					editable = keyboardPasteEditable;

				// If we have valid html already, or we're not in cleanPastedHTML mode
				// we can ignore the paste bin and just paste now
				if (!this.cleanPastedHTML || pastedHTML) {
					event.preventDefault();
					this.removePasteBin();
					this.doPaste(pastedHTML, pastedPlain, editable);

					// The event handling code listens for paste on the editable element
					// in order to trigger the editablePaste event.  Since this paste event
					// is happening on the pastebin, the event handling code never knows about it
					// So, we have to trigger editablePaste manually
					this.trigger('editablePaste', {
						currentTarget: editable,
						target: editable
					}, editable);
					return;
				}

				// We need to look at the paste bin, so do a setTimeout to let the paste
				// fall through into the paste bin
				setTimeout(function() {
					// Only look for HTML if we're in cleanPastedHTML mode
					if (this.cleanPastedHTML) {
						// If clipboard didn't have HTML, try the paste bin
						pastedHTML = this.getPasteBinHtml();
					}

					// If we needed the paste bin, we're done with it now, remove it
					this.removePasteBin();

					// Handle the paste with the html from the paste bin
					this.doPaste(pastedHTML, pastedPlain, editable);

					// The event handling code listens for paste on the editable element
					// in order to trigger the editablePaste event.  Since this paste event
					// is happening on the pastebin, the event handling code never knows about it
					// So, we have to trigger editablePaste manually
					this.trigger('editablePaste', {
						currentTarget: editable,
						target: editable
					}, editable);
				}.bind(this), 0);
			},

			handleKeydown: function(event, editable) {
				// if it's not Ctrl+V, do nothing
				if (!(MediumEditor.util.isKey(event, MediumEditor.util.keyCode.V) && MediumEditor.util.isMetaCtrlKey(event))) {
					return;
				}

				event.stopImmediatePropagation();

				this.removePasteBin();
				this.createPasteBin(editable);
			},

			createPasteBin: function(editable) {
				var rects,
					range = MediumEditor.selection.getSelectionRange(this.document),
					top = this.window.pageYOffset;

				keyboardPasteEditable = editable;

				if (range) {
					rects = range.getClientRects();

					// on empty line, rects is empty so we grab information from the first container of the range
					if (rects.length) {
						top += rects[0].top;
					} else if (range.startContainer.getBoundingClientRect !== undefined) {
						top += range.startContainer.getBoundingClientRect().top;
					} else {
						top += range.getBoundingClientRect().top;
					}
				}

				lastRange = range;

				var pasteBinElm = this.document.createElement('div');
				pasteBinElm.id = this.pasteBinId = 'medium-editor-pastebin-' + (+Date.now());
				pasteBinElm.setAttribute('style', 'border: 1px red solid; position: absolute; top: ' + top + 'px; width: 10px; height: 10px; overflow: hidden; opacity: 0');
				pasteBinElm.setAttribute('contentEditable', true);
				pasteBinElm.innerHTML = pasteBinDefaultContent;

				this.document.body.appendChild(pasteBinElm);

				// avoid .focus() to stop other event (actually the paste event)
				this.on(pasteBinElm, 'focus', stopProp);
				this.on(pasteBinElm, 'focusin', stopProp);
				this.on(pasteBinElm, 'focusout', stopProp);

				pasteBinElm.focus();

				MediumEditor.selection.selectNode(pasteBinElm, this.document);

				if (!this.boundHandlePaste) {
					this.boundHandlePaste = this.handlePasteBinPaste.bind(this);
				}

				this.on(pasteBinElm, 'paste', this.boundHandlePaste);
			},

			removePasteBin: function() {
				if (null !== lastRange) {
					MediumEditor.selection.selectRange(this.document, lastRange);
					lastRange = null;
				}

				if (null !== keyboardPasteEditable) {
					keyboardPasteEditable = null;
				}

				var pasteBinElm = this.getPasteBin();
				if (!pasteBinElm) {
					return;
				}

				if (pasteBinElm) {
					this.off(pasteBinElm, 'focus', stopProp);
					this.off(pasteBinElm, 'focusin', stopProp);
					this.off(pasteBinElm, 'focusout', stopProp);
					this.off(pasteBinElm, 'paste', this.boundHandlePaste);
					pasteBinElm.parentElement.removeChild(pasteBinElm);
				}
			},

			getPasteBin: function() {
				return this.document.getElementById(this.pasteBinId);
			},

			getPasteBinHtml: function() {
				var pasteBinElm = this.getPasteBin();

				if (!pasteBinElm) {
					return false;
				}

				// WebKit has a nice bug where it clones the paste bin if you paste from for example notepad
				// so we need to force plain text mode in this case
				if (pasteBinElm.firstChild && pasteBinElm.firstChild.id === 'mcepastebin') {
					return false;
				}

				var pasteBinHtml = pasteBinElm.innerHTML;

				// If paste bin is empty try using plain text mode
				// since that is better than nothing right
				if (!pasteBinHtml || pasteBinHtml === pasteBinDefaultContent) {
					return false;
				}

				return pasteBinHtml;
			},

			cleanPaste: function(text) {
				var i, elList, tmp, workEl,
					multiline = /<p|<br|<div/.test(text),
					replacements = [].concat(
						this.preCleanReplacements || [],
						createReplacements(),
						this.cleanReplacements || []);

				for (i = 0; i < replacements.length; i += 1) {
					text = text.replace(replacements[i][0], replacements[i][1]);
				}

				if (!multiline) {
					return this.pasteHTML(text);
				}

				// create a temporary div to cleanup block elements
				tmp = this.document.createElement('div');

				// double br's aren't converted to p tags, but we want paragraphs.
				tmp.innerHTML = '<p>' + text.split('<br><br>').join('</p><p>') + '</p>';

				// block element cleanup
				elList = tmp.querySelectorAll('a,p,div,br');
				for (i = 0; i < elList.length; i += 1) {
					workEl = elList[i];

					// Microsoft Word replaces some spaces with newlines.
					// While newlines between block elements are meaningless, newlines within
					// elements are sometimes actually spaces.
					workEl.innerHTML = workEl.innerHTML.replace(/\n/gi, ' ');

					switch (workEl.nodeName.toLowerCase()) {
						case 'p':
						case 'div':
							this.filterCommonBlocks(workEl);
							break;
						case 'br':
							this.filterLineBreak(workEl);
							break;
					}
				}

				this.pasteHTML(tmp.innerHTML);
			},

			pasteHTML: function(html, options) {
				options = MediumEditor.util.defaults({}, options, {
					cleanAttrs: this.cleanAttrs,
					cleanTags: this.cleanTags,
					unwrapTags: this.unwrapTags
				});

				var elList, workEl, i, fragmentBody, pasteBlock = this.document.createDocumentFragment();

				pasteBlock.appendChild(this.document.createElement('body'));

				fragmentBody = pasteBlock.querySelector('body');
				fragmentBody.innerHTML = html;

				this.cleanupSpans(fragmentBody);

				elList = fragmentBody.querySelectorAll('*');
				for (i = 0; i < elList.length; i += 1) {
					workEl = elList[i];

					if ('a' === workEl.nodeName.toLowerCase() && this.getEditorOption('targetBlank')) {
						MediumEditor.util.setTargetBlank(workEl);
					}

					MediumEditor.util.cleanupAttrs(workEl, options.cleanAttrs);
					MediumEditor.util.cleanupTags(workEl, options.cleanTags);
					MediumEditor.util.unwrapTags(workEl, options.unwrapTags);
				}

				MediumEditor.util.insertHTMLCommand(this.document, fragmentBody.innerHTML.replace(/&nbsp;/g, ' '));
			},

			// TODO (6.0): Make this an internal helper instead of member of paste handler
			isCommonBlock: function(el) {
				return (el && (el.nodeName.toLowerCase() === 'p' || el.nodeName.toLowerCase() === 'div'));
			},

			// TODO (6.0): Make this an internal helper instead of member of paste handler
			filterCommonBlocks: function(el) {
				if (/^\s*$/.test(el.textContent) && el.parentNode) {
					el.parentNode.removeChild(el);
				}
			},

			// TODO (6.0): Make this an internal helper instead of member of paste handler
			filterLineBreak: function(el) {
				if (this.isCommonBlock(el.previousElementSibling)) {
					// remove stray br's following common block elements
					this.removeWithParent(el);
				} else if (this.isCommonBlock(el.parentNode) && (el.parentNode.firstChild === el || el.parentNode.lastChild === el)) {
					// remove br's just inside open or close tags of a div/p
					this.removeWithParent(el);
				} else if (el.parentNode && el.parentNode.childElementCount === 1 && el.parentNode.textContent === '') {
					// and br's that are the only child of elements other than div/p
					this.removeWithParent(el);
				}
			},

			// TODO (6.0): Make this an internal helper instead of member of paste handler
			// remove an element, including its parent, if it is the only element within its parent
			removeWithParent: function(el) {
				if (el && el.parentNode) {
					if (el.parentNode.parentNode && el.parentNode.childElementCount === 1) {
						el.parentNode.parentNode.removeChild(el.parentNode);
					} else {
						el.parentNode.removeChild(el);
					}
				}
			},

			// TODO (6.0): Make this an internal helper instead of member of paste handler
			cleanupSpans: function(containerEl) {
				var i,
					el,
					newEl,
					spans = containerEl.querySelectorAll('.replace-with'),
					isCEF = function(el) {
						return (el && el.nodeName !== '#text' && el.getAttribute('contenteditable') === 'false');
					};

				for (i = 0; i < spans.length; i += 1) {
					el = spans[i];
					newEl = this.document.createElement(el.classList.contains('bold') ? 'b' : 'i');

					if (el.classList.contains('bold') && el.classList.contains('italic')) {
						// add an i tag as well if this has both italics and bold
						newEl.innerHTML = '<i>' + el.innerHTML + '</i>';
					} else {
						newEl.innerHTML = el.innerHTML;
					}
					el.parentNode.replaceChild(newEl, el);
				}

				spans = containerEl.querySelectorAll('span');
				for (i = 0; i < spans.length; i += 1) {
					el = spans[i];

					// bail if span is in contenteditable = false
					if (MediumEditor.util.traverseUp(el, isCEF)) {
						return false;
					}

					// remove empty spans, replace others with their contents
					MediumEditor.util.unwrap(el, this.document);
				}
			}
		});

		MediumEditor.extensions.paste = PasteHandler;
	}());

	(function() {
		'use strict';

		var Placeholder = MediumEditor.Extension.extend({
			name: 'placeholder',

			/* Placeholder Options */

			/* text: [string]
			 * Text to display in the placeholder
			 */
			text: 'Type your text',

			/* hideOnClick: [boolean]
			 * Should we hide the placeholder on click (true) or when user starts typing (false)
			 */
			hideOnClick: true,

			init: function() {
				MediumEditor.Extension.prototype.init.apply(this, arguments);

				this.initPlaceholders();
				this.attachEventHandlers();
			},

			initPlaceholders: function() {
				this.getEditorElements().forEach(this.initElement, this);
			},

			handleAddElement: function(event, editable) {
				this.initElement(editable);
			},

			initElement: function(el) {
				if (!el.getAttribute('data-placeholder')) {
					el.setAttribute('data-placeholder', this.text);
				}
				this.updatePlaceholder(el);
			},

			destroy: function() {
				this.getEditorElements().forEach(this.cleanupElement, this);
			},

			handleRemoveElement: function(event, editable) {
				this.cleanupElement(editable);
			},

			cleanupElement: function(el) {
				if (el.getAttribute('data-placeholder') === this.text) {
					el.removeAttribute('data-placeholder');
				}
			},

			showPlaceholder: function(el) {
				if (el) {
					// https://github.com/yabwe/medium-editor/issues/234
					// In firefox, styling the placeholder with an absolutely positioned
					// pseudo element causes the cursor to appear in a bad location
					// when the element is completely empty, so apply a different class to
					// style it with a relatively positioned pseudo element
					if (MediumEditor.util.isFF && el.childNodes.length === 0) {
						el.classList.add('medium-editor-placeholder-relative');
						el.classList.remove('medium-editor-placeholder');
					} else {
						el.classList.add('medium-editor-placeholder');
						el.classList.remove('medium-editor-placeholder-relative');
					}
				}
			},

			hidePlaceholder: function(el) {
				if (el) {
					el.classList.remove('medium-editor-placeholder');
					el.classList.remove('medium-editor-placeholder-relative');
				}
			},

			updatePlaceholder: function(el, dontShow) {
				// If the element has content, hide the placeholder
				if (el.querySelector('img, blockquote, ul, ol, table') || (el.textContent.replace(/^\s+|\s+$/g, '') !== '')) {
					return this.hidePlaceholder(el);
				}

				if (!dontShow) {
					this.showPlaceholder(el);
				}
			},

			attachEventHandlers: function() {
				if (this.hideOnClick) {
					// For the 'hideOnClick' option, the placeholder should always be hidden on focus
					this.subscribe('focus', this.handleFocus.bind(this));
				}

				// If the editor has content, it should always hide the placeholder
				this.subscribe('editableInput', this.handleInput.bind(this));

				// When the editor loses focus, check if the placeholder should be visible
				this.subscribe('blur', this.handleBlur.bind(this));

				// Need to know when elements are added/removed from the editor
				this.subscribe('addElement', this.handleAddElement.bind(this));
				this.subscribe('removeElement', this.handleRemoveElement.bind(this));
			},

			handleInput: function(event, element) {
				// If the placeholder should be hidden on focus and the
				// element has focus, don't show the placeholder
				var dontShow = this.hideOnClick && (element === this.base.getFocusedElement());

				// Editor's content has changed, check if the placeholder should be hidden
				this.updatePlaceholder(element, dontShow);
			},

			handleFocus: function(event, element) {
				// Editor has focus, hide the placeholder
				this.hidePlaceholder(element);
			},

			handleBlur: function(event, element) {
				// Editor has lost focus, check if the placeholder should be shown
				this.updatePlaceholder(element);
			}
		});

		MediumEditor.extensions.placeholder = Placeholder;
	}());

	(function() {
		'use strict';

		var Toolbar = MediumEditor.Extension.extend({
			name: 'toolbar',

			/* Toolbar Options */

			/* align: ['left'|'center'|'right']
			 * When the __static__ option is true, this aligns the static toolbar
			 * relative to the medium-editor element.
			 */
			align: 'center',

			/* allowMultiParagraphSelection: [boolean]
			 * enables/disables whether the toolbar should be displayed when
			 * selecting multiple paragraphs/block elements
			 */
			allowMultiParagraphSelection: true,

			/* buttons: [Array]
			 * the names of the set of buttons to display on the toolbar.
			 */
			buttons: ['bold', 'italic', 'underline', 'anchor', 'h2', 'h3', 'quote'],

			/* diffLeft: [Number]
			 * value in pixels to be added to the X axis positioning of the toolbar.
			 */
			diffLeft: 0,

			/* diffTop: [Number]
			 * value in pixels to be added to the Y axis positioning of the toolbar.
			 */
			diffTop: -10,

			/* firstButtonClass: [string]
			 * CSS class added to the first button in the toolbar.
			 */
			firstButtonClass: 'medium-editor-button-first',

			/* lastButtonClass: [string]
			 * CSS class added to the last button in the toolbar.
			 */
			lastButtonClass: 'medium-editor-button-last',

			/* standardizeSelectionStart: [boolean]
			 * enables/disables standardizing how the beginning of a range is decided
			 * between browsers whenever the selected text is analyzed for updating toolbar buttons status.
			 */
			standardizeSelectionStart: false,

			/* static: [boolean]
			 * enable/disable the toolbar always displaying in the same location
			 * relative to the medium-editor element.
			 */
			static: false,

			/* sticky: [boolean]
			 * When the __static__ option is true, this enables/disables the toolbar
			 * "sticking" to the viewport and staying visible on the screen while
			 * the page scrolls.
			 */
			sticky: false,

			/* stickyTopOffset: [Number]
			 * Value in pixel of the top offset above the toolbar
			 */
			stickyTopOffset: 0,

			/* updateOnEmptySelection: [boolean]
			 * When the __static__ option is true, this enables/disables updating
			 * the state of the toolbar buttons even when the selection is collapsed
			 * (there is no selection, just a cursor).
			 */
			updateOnEmptySelection: false,

			/* relativeContainer: [node]
			 * appending the toolbar to a given node instead of body
			 */
			relativeContainer: null,

			init: function() {
				MediumEditor.Extension.prototype.init.apply(this, arguments);

				this.initThrottledMethods();

				if (!this.relativeContainer) {
					this.getEditorOption('elementsContainer').appendChild(this.getToolbarElement());
				} else {
					this.relativeContainer.appendChild(this.getToolbarElement());
				}
			},

			// Helper method to execute method for every extension, but ignoring the toolbar extension
			forEachExtension: function(iterator, context) {
				return this.base.extensions.forEach(function(command) {
					if (command === this) {
						return;
					}
					return iterator.apply(context || this, arguments);
				}, this);
			},

			// ThemeFusion edit, wrap extensions in div.
			createToolbar: function() {
				var toolbar = this.document.createElement('div'),
					extensions = this.document.createElement('div');

				toolbar.id = 'medium-editor-toolbar-' + this.getEditorId();
				toolbar.className = 'medium-editor-toolbar';

				extensions.className = 'medium-editor-extensions';

				if (this.static) {
					toolbar.className += ' static-toolbar';
				} else if (this.relativeContainer) {
					toolbar.className += ' medium-editor-relative-toolbar';
				} else {
					toolbar.className += ' medium-editor-stalker-toolbar';
				}

				toolbar.appendChild(this.createToolbarButtons());


				// Add any forms that extensions may have
				this.forEachExtension(function(extension) {
					if (extension.hasForm) {
						extensions.appendChild(extension.getForm());
					}
				});

				toolbar.appendChild( extensions );
				this.attachEventHandlers();

				return toolbar;
			},

			createToolbarButtons: function() {
				var ul = this.document.createElement('ul'),
					li,
					btn,
					buttons,
					extension,
					buttonName,
					buttonOpts;

				ul.id = 'medium-editor-toolbar-actions' + this.getEditorId();
				ul.className = 'medium-editor-toolbar-actions';

				// ThemeFusion edit.
				ul.style.display = 'flex';

				this.buttons.forEach(function(button) {
					if (typeof button === 'string') {
						buttonName = button;
						buttonOpts = null;
					} else {
						buttonName = button.name;
						buttonOpts = button;
					}

					// If the button already exists as an extension, it'll be returned
					// othwerise it'll create the default built-in button
					extension = this.base.addBuiltInExtension(buttonName, buttonOpts);

					if (extension && typeof extension.getButton === 'function') {
						btn = extension.getButton(this.base);
						li = this.document.createElement('li');
						if (MediumEditor.util.isElement(btn)) {
							li.appendChild(btn);
						} else {
							li.innerHTML = btn;
						}
						ul.appendChild(li);
					}
				}, this);

				buttons = ul.querySelectorAll('button');
				if (buttons.length > 0) {
					buttons[0].classList.add(this.firstButtonClass);
					buttons[buttons.length - 1].classList.add(this.lastButtonClass);
				}

				return ul;
			},

			destroy: function() {
				if (this.toolbar) {
					if (this.toolbar.parentNode) {
						this.toolbar.parentNode.removeChild(this.toolbar);
					}
					delete this.toolbar;
				}
			},

			// Toolbar accessors

			getInteractionElements: function() {
				return this.getToolbarElement();
			},

			getToolbarElement: function() {
				if (!this.toolbar) {
					this.toolbar = this.createToolbar();
				}

				return this.toolbar;
			},

			getToolbarActionsElement: function() {
				return this.getToolbarElement().querySelector('.medium-editor-toolbar-actions');
			},

			// Toolbar event handlers

			initThrottledMethods: function() {
				// throttledPositionToolbar is throttled because:
				// - It will be called when the browser is resizing, which can fire many times very quickly
				// - For some event (like resize) a slight lag in UI responsiveness is OK and provides performance benefits
				this.throttledPositionToolbar = MediumEditor.util.throttle(function() {
					if (this.base.isActive) {
						this.positionToolbarIfShown();
					}
				}.bind(this));
			},

			attachEventHandlers: function() {
				// MediumEditor custom events for when user beings and ends interaction with a contenteditable and its elements
				this.subscribe('blur', this.handleBlur.bind(this));
				this.subscribe('focus', this.handleFocus.bind(this));

				// Updating the state of the toolbar as things change
				this.subscribe('editableClick', this.handleEditableClick.bind(this));
				this.subscribe('editableKeyup', this.handleEditableKeyup.bind(this));

				// Handle mouseup on document for updating the selection in the toolbar
				this.on(this.document.documentElement, 'mouseup', this.handleDocumentMouseup.bind(this));

				// Add a scroll event for sticky toolbar
				if (this.static && this.sticky) {
					// On scroll (capture), re-position the toolbar
					this.on(this.window, 'scroll', this.handleWindowScroll.bind(this), true);
				}

				// On resize, re-position the toolbar
				this.on(this.window, 'resize', this.handleWindowResize.bind(this));
			},

			handleWindowScroll: function() {
				this.positionToolbarIfShown();
			},

			handleWindowResize: function() {
				this.throttledPositionToolbar();
			},

			handleDocumentMouseup: function(event) {
				var toolbarElement = this.getToolbarElement();

				// Do not trigger checkState when mouseup fires over the toolbar
				if (event &&
					event.target &&
					MediumEditor.util.isDescendant(this.getToolbarElement(), event.target)) {
					return false;
				}

				// ThemeFusion edit.
				if ( toolbarElement.querySelector( '.fusion-inline-color-picker.visible' ) ||
					toolbarElement.querySelector( '.fusion-inline-typography.visible' ) ||
					toolbarElement.querySelector( '.fusion-inline-anchor.visible' ) ) {
					return false;
				}

				this.checkState();
			},

			handleEditableClick: function() {

				// Delay the call to checkState to handle bug where selection is empty
				// immediately after clicking inside a pre-existing selection
				setTimeout(function() {
					this.checkState();
				}.bind(this), 0);
			},

			handleEditableKeyup: function( evejt ) {
				var toolbarElement = this.getToolbarElement();

				// ThemeFusion edit.
				if ( toolbarElement.querySelector( '.fusion-inline-color-picker.visible' ) ) {
					return false;
				}

				this.checkState();
			},

			handleBlur: function() {

				// Kill any previously delayed calls to hide the toolbar
				clearTimeout(this.hideTimeout);

				// Blur may fire even if we have a selection, so we want to prevent any delayed showToolbar
				// calls from happening in this specific case
				clearTimeout(this.delayShowTimeout);

				// Delay the call to hideToolbar to handle bug with multiple editors on the page at once
				this.hideTimeout = setTimeout(function() {
					this.hideToolbar();
				}.bind(this), 1);
			},

			handleFocus: function() {

				this.checkState();
			},

			// Hiding/showing toolbar

			isDisplayed: function() {
				return this.getToolbarElement().classList.contains('medium-editor-toolbar-active');
			},

			showToolbar: function() {
				clearTimeout(this.hideTimeout);
				if (!this.isDisplayed()) {
					this.getToolbarElement().classList.add('medium-editor-toolbar-active');
					this.trigger('showToolbar', {}, this.base.getFocusedElement());

					// ThemeFusion Edit. Add Class.
					jQuery( 'body' ).addClass( 'fusion-medium-editor-active' );
				}
			},

			hideToolbar: function() {
				if (this.isDisplayed()) {
					this.getToolbarElement().classList.remove('medium-editor-toolbar-active');
					this.trigger('hideToolbar', {}, this.base.getFocusedElement());

					// ThemeFusion Edit.
					this.hideExtensionForms();

					// ThemeFusion Edit. Remove Class.
					jQuery( 'body' ).removeClass( 'fusion-medium-editor-active' );
				}
			},

			isToolbarDefaultActionsDisplayed: function() {

				// ThemeFusion edit.
				return this.getToolbarActionsElement().style.display === 'flex' && this.getToolbarActionsElement().classList.contains( 'visible' );
			},

			hideToolbarDefaultActions: function() {
				var toolbar = this.getToolbarActionsElement();
				if (this.isToolbarDefaultActionsDisplayed()) {

					// ThemeFusion edit.
					toolbar.classList.add( 'hidden' );
					toolbar.classList.remove( 'visible' );
					setTimeout( function() {
						toolbar.classList.remove( 'hidden' );
					}, 400 );
				}
			},

			showToolbarDefaultActions: function() {
				var toolbar = this.getToolbarActionsElement();

				this.hideExtensionForms();

				if (!this.isToolbarDefaultActionsDisplayed()) {

					// ThemeFusion edit.
					this.getToolbarActionsElement().style.display = 'flex';

					// ThemeFusion edit.
					toolbar.classList.add( 'visible' );
					toolbar.classList.remove( 'hidden' )
				}

				// Using setTimeout + options.delay because:
				// We will actually be displaying the toolbar, which should be controlled by options.delay
				this.delayShowTimeout = this.base.delay(function() {
					this.showToolbar();
				}.bind(this));
			},

			hideExtensionForms: function() {
				// Hide all extension forms
				this.forEachExtension(function(extension) {
					if (extension.hasForm && extension.isDisplayed()) {
						extension.hideForm();
					}
				});
			},

			// Responding to changes in user selection

			// Checks for existance of multiple block elements in the current selection
			multipleBlockElementsSelected: function() {
				var regexEmptyHTMLTags = /<[^\/>][^>]*><\/[^>]+>/gim, // http://stackoverflow.com/questions/3129738/remove-empty-tags-using-regex
					regexBlockElements = new RegExp('<(' + MediumEditor.util.blockContainerElementNames.join('|') + ')[^>]*>', 'g'),
					selectionHTML = MediumEditor.selection.getSelectionHtml(this.document).replace(regexEmptyHTMLTags, ''), // Filter out empty blocks from selection
					hasMultiParagraphs = selectionHTML.match(regexBlockElements); // Find how many block elements are within the html

				return !!hasMultiParagraphs && hasMultiParagraphs.length > 1;
			},

			modifySelection: function() {
				var selection = this.window.getSelection(),
					selectionRange = selection.getRangeAt(0);

				/*
				 * In firefox, there are cases (ie doubleclick of a word) where the selectionRange start
				 * will be at the very end of an element.  In other browsers, the selectionRange start
				 * would instead be at the very beginning of an element that actually has content.
				 * example:
				 *   <span>foo</span><span>bar</span>
				 *
				 * If the text 'bar' is selected, most browsers will have the selectionRange start at the beginning
				 * of the 'bar' span.  However, there are cases where firefox will have the selectionRange start
				 * at the end of the 'foo' span.  The contenteditable behavior will be ok, but if there are any
				 * properties on the 'bar' span, they won't be reflected accurately in the toolbar
				 * (ie 'Bold' button wouldn't be active)
				 *
				 * So, for cases where the selectionRange start is at the end of an element/node, find the next
				 * adjacent text node that actually has content in it, and move the selectionRange start there.
				 */
				if (this.standardizeSelectionStart &&
					selectionRange.startContainer.nodeValue &&
					(selectionRange.startOffset === selectionRange.startContainer.nodeValue.length)) {
					var adjacentNode = MediumEditor.util.findAdjacentTextNodeWithContent(MediumEditor.selection.getSelectionElement(this.window), selectionRange.startContainer, this.document);
					if (adjacentNode) {
						var offset = 0;
						while (adjacentNode.nodeValue.substr(offset, 1).trim().length === 0) {
							offset = offset + 1;
						}
						selectionRange = MediumEditor.selection.select(this.document, adjacentNode, offset,
							selectionRange.endContainer, selectionRange.endOffset);
					}
				}
			},

			checkState: function() {

				if (this.base.preventSelectionUpdates) {
					return;
				}

				// If no editable has focus OR selection is inside contenteditable = false
				// hide toolbar
				if (!this.base.getFocusedElement() ||
					MediumEditor.selection.selectionInContentEditableFalse(this.window)) {
					return this.hideToolbar();
				}

				// If there's no selection element, selection element doesn't belong to this editor
				// or toolbar is disabled for this selection element
				// hide toolbar
				var selectionElement = MediumEditor.selection.getSelectionElement(this.window);
				if (!selectionElement ||
					this.getEditorElements().indexOf(selectionElement) === -1 ||
					selectionElement.getAttribute('data-disable-toolbar')) {
					return this.hideToolbar();
				}

				// Now we know there's a focused editable with a selection

				// If the updateOnEmptySelection option is true, show the toolbar
				if (this.updateOnEmptySelection && this.static) {
					return this.showAndUpdateToolbar();
				}

				// If we don't have a 'valid' selection -> hide toolbar
				if (!MediumEditor.selection.selectionContainsContent(this.document) ||
					(this.allowMultiParagraphSelection === false && this.multipleBlockElementsSelected())) {
					return this.hideToolbar();
				}

				this.showAndUpdateToolbar();
			},

			// Updating the toolbar

			showAndUpdateToolbar: function() {
				this.modifySelection();
				this.setToolbarButtonStates();
				this.trigger('positionToolbar', {}, this.base.getFocusedElement());
				this.showToolbarDefaultActions();
				this.setToolbarPosition();
			},

			setToolbarButtonStates: function() {
				this.forEachExtension(function(extension) {
					if (typeof extension.isActive === 'function' &&
						typeof extension.setInactive === 'function') {
						extension.setInactive();
					}
				});

				this.checkActiveButtons();
			},

			checkActiveButtons: function() {
				var manualStateChecks = [],
					queryState = null,
					selectionRange = MediumEditor.selection.getSelectionRange(this.document),
					parentNode,
					updateExtensionState = function(extension) {
						if (typeof extension.checkState === 'function') {
							extension.checkState(parentNode);
						} else if (typeof extension.isActive === 'function' &&
							typeof extension.isAlreadyApplied === 'function' &&
							typeof extension.setActive === 'function') {
							if (!extension.isActive() && extension.isAlreadyApplied(parentNode)) {
								extension.setActive();
							}
						}
					};

				if (!selectionRange) {
					return;
				}

				// Loop through all extensions
				this.forEachExtension(function(extension) {
					// For those extensions where we can use document.queryCommandState(), do so
					if (typeof extension.queryCommandState === 'function') {
						queryState = extension.queryCommandState();
						// If queryCommandState returns a valid value, we can trust the browser
						// and don't need to do our manual checks
						if (queryState !== null) {
							if (queryState && typeof extension.setActive === 'function') {
								extension.setActive();
							}
							return;
						}
					}
					// We can't use queryCommandState for this extension, so add to manualStateChecks
					manualStateChecks.push(extension);
				});

				parentNode = MediumEditor.selection.getSelectedParentElement(selectionRange);

				// Make sure the selection parent isn't outside of the contenteditable
				if (!this.getEditorElements().some(function(element) {
						return MediumEditor.util.isDescendant(element, parentNode, true);
					})) {
					return;
				}

				// Climb up the DOM and do manual checks for whether a certain extension is currently enabled for this node
				while (parentNode) {
					manualStateChecks.forEach(updateExtensionState);

					// we can abort the search upwards if we leave the contentEditable element
					if (MediumEditor.util.isMediumEditorElement(parentNode)) {
						break;
					}
					parentNode = parentNode.parentNode;
				}
			},

			// Positioning toolbar

			positionToolbarIfShown: function() {
				if (this.isDisplayed()) {
					this.setToolbarPosition();
				}
			},

			setToolbarPosition: function() {
				var container = this.base.getFocusedElement(),
					selection = this.window.getSelection();

				// If there isn't a valid selection, bail
				if (!container) {
					return this;
				}

				if (this.static || !selection.isCollapsed) {
					this.showToolbar();

					// we don't need any absolute positioning if relativeContainer is set
					if (!this.relativeContainer) {
						if (this.static) {
							this.positionStaticToolbar(container);
						} else {
							this.positionToolbar(selection);
						}
					}

					this.trigger('positionedToolbar', {}, this.base.getFocusedElement());
				}
			},

			positionStaticToolbar: function(container) {
				// position the toolbar at left 0, so we can get the real width of the toolbar
				this.getToolbarElement().style.left = '0';

				// document.documentElement for IE 9
				var scrollTop = (this.document.documentElement && this.document.documentElement.scrollTop) || this.document.body.scrollTop,
					windowWidth = this.window.innerWidth,
					toolbarElement = this.getToolbarElement(),
					containerRect = container.getBoundingClientRect(),
					containerTop = containerRect.top + scrollTop,
					containerCenter = (containerRect.left + (containerRect.width / 2)),
					toolbarHeight = toolbarElement.offsetHeight,
					toolbarWidth = toolbarElement.offsetWidth,
					halfOffsetWidth = toolbarWidth / 2,
					targetLeft;

				if (this.sticky) {
					// If it's beyond the height of the editor, position it at the bottom of the editor
					if (scrollTop > (containerTop + container.offsetHeight - toolbarHeight - this.stickyTopOffset)) {
						toolbarElement.style.top = (containerTop + container.offsetHeight - toolbarHeight) + 'px';
						toolbarElement.classList.remove('medium-editor-sticky-toolbar');
						// Stick the toolbar to the top of the window
					} else if (scrollTop > (containerTop - toolbarHeight - this.stickyTopOffset)) {
						toolbarElement.classList.add('medium-editor-sticky-toolbar');
						toolbarElement.style.top = this.stickyTopOffset + 'px';
						// Normal static toolbar position
					} else {
						toolbarElement.classList.remove('medium-editor-sticky-toolbar');
						toolbarElement.style.top = containerTop - toolbarHeight + 'px';
					}
				} else {
					toolbarElement.style.top = containerTop - toolbarHeight + 'px';
				}

				switch (this.align) {
					case 'left':
						targetLeft = containerRect.left;
						break;

					case 'right':
						targetLeft = containerRect.right - toolbarWidth;
						break;

					case 'center':
						targetLeft = containerCenter - halfOffsetWidth;
						break;
				}

				if (targetLeft < 0) {
					targetLeft = 0;
				} else if ((targetLeft + toolbarWidth) > windowWidth) {
					targetLeft = (windowWidth - Math.ceil(toolbarWidth) - 1);
				}

				toolbarElement.style.left = targetLeft + 'px';
			},

			positionToolbar: function(selection) {
				// position the toolbar at left 0, so we can get the real width of the toolbar
				this.getToolbarElement().style.left = '0';
				this.getToolbarElement().style.right = 'initial';

				var range = selection.getRangeAt(0),
					boundary = range.getBoundingClientRect();

				// Handle selections with just images
				if (!boundary || ((boundary.height === 0 && boundary.width === 0) && range.startContainer === range.endContainer)) {
					// If there's a nested image, use that for the bounding rectangle
					if (range.startContainer.nodeType === 1 && range.startContainer.querySelector('img')) {
						boundary = range.startContainer.querySelector('img').getBoundingClientRect();
					} else {
						boundary = range.startContainer.getBoundingClientRect();
					}
				}

				var containerWidth = this.window.innerWidth,
					toolbarElement = this.getToolbarElement(),
					toolbarHeight = toolbarElement.offsetHeight,
					toolbarWidth = toolbarElement.offsetWidth,
					halfOffsetWidth = toolbarWidth / 2,
					testHeight = 50,
					buttonHeight = 50,
					defaultLeft = this.diffLeft - halfOffsetWidth,
					elementsContainer = this.getEditorOption('elementsContainer'),
					elementsContainerAbsolute = ['absolute', 'fixed'].indexOf(window.getComputedStyle(elementsContainer).getPropertyValue('position')) > -1,
					positions = {},
					relativeBoundary = {},
					middleBoundary, elementsContainerBoundary;

				// ThemeFusion edit
				if ( toolbarElement.querySelector( '.medium-editor-toolbar-form.visible' ) ) {
					testHeight = toolbarElement.querySelector( '.medium-editor-toolbar-form.visible' ).offsetHeight;
				}

				// If container element is absolute / fixed, recalculate boundaries to be relative to the container
				if (elementsContainerAbsolute) {
					elementsContainerBoundary = elementsContainer.getBoundingClientRect();
					['top', 'left'].forEach(function(key) {
						relativeBoundary[key] = boundary[key] - elementsContainerBoundary[key];
					});

					relativeBoundary.width = boundary.width;
					relativeBoundary.height = boundary.height;
					boundary = relativeBoundary;

					containerWidth = elementsContainerBoundary.width;

					// Adjust top position according to container scroll position
					positions.top = elementsContainer.scrollTop;
				} else {
					// Adjust top position according to window scroll position
					positions.top = this.window.pageYOffset;
				}

				middleBoundary = boundary.left + boundary.width / 2;
				positions.top += boundary.top - toolbarHeight;

				if (boundary.top < testHeight) {
					toolbarElement.classList.add('medium-toolbar-arrow-over');
					toolbarElement.classList.remove('medium-toolbar-arrow-under');
					positions.top += buttonHeight + boundary.height - this.diffTop;
				} else {
					toolbarElement.classList.add('medium-toolbar-arrow-under');
					toolbarElement.classList.remove('medium-toolbar-arrow-over');
					positions.top += this.diffTop;
				}

				if (middleBoundary < halfOffsetWidth) {
					positions.left = defaultLeft + halfOffsetWidth;
					positions.right = 'initial';

					if ( toolbarElement.querySelector( '.fusion-inline-typography.visible' ) ) {
						if ( toolbarElement.querySelector( '.fusion-inline-typography.visible' ).classList.contains( 'fusion-stuck-to-right' ) ) {
							toolbarElement.querySelector( '.fusion-inline-typography.visible' ).classList.remove( 'fusion-stuck-to-right' );
						}
						toolbarElement.querySelector( '.fusion-inline-typography.visible' ).classList.add( 'fusion-stuck-to-left' );
					}
				} else if ((containerWidth - middleBoundary) < halfOffsetWidth) {
					positions.left = 'auto';
					positions.right = 0;

					if ( toolbarElement.querySelector( '.fusion-inline-typography.visible' ) ) {
						if ( toolbarElement.querySelector( '.fusion-inline-typography.visible' ).classList.contains( 'fusion-stuck-to-left' ) ) {
							toolbarElement.querySelector( '.fusion-inline-typography.visible' ).classList.remove( 'fusion-stuck-to-left' );
						}
						toolbarElement.querySelector( '.fusion-inline-typography.visible' ).classList.add( 'fusion-stuck-to-right' );
					}
				} else {
					positions.left = defaultLeft + middleBoundary;
					positions.right = 'initial';

					if ( toolbarElement.querySelector( '.fusion-inline-typography' ) ) {
						if ( toolbarElement.querySelector( '.fusion-inline-typography' ).classList.contains( 'fusion-stuck-to-left' ) ) {
							toolbarElement.querySelector( '.fusion-inline-typography' ).classList.remove( 'fusion-stuck-to-left' );
						}
						if ( toolbarElement.querySelector( '.fusion-inline-typography' ).classList.contains( 'fusion-stuck-to-right' ) ) {
							toolbarElement.querySelector( '.fusion-inline-typography' ).classList.remove( 'fusion-stuck-to-right' );
						}
					}
				}

				['top', 'left', 'right'].forEach(function(key) {
					toolbarElement.style[key] = positions[key] + (isNaN(positions[key]) ? '' : 'px');
				});
			}
		});

		MediumEditor.extensions.toolbar = Toolbar;
	}());

	(function() {
		'use strict';

		var ImageDragging = MediumEditor.Extension.extend({
			init: function() {
				MediumEditor.Extension.prototype.init.apply(this, arguments);

				this.subscribe('editableDrag', this.handleDrag.bind(this));
				this.subscribe('editableDrop', this.handleDrop.bind(this));
			},

			handleDrag: function(event) {
				var className = 'medium-editor-dragover';
				event.preventDefault();
				event.dataTransfer.dropEffect = 'copy';

				if (event.type === 'dragover') {
					event.target.classList.add(className);
				} else if (event.type === 'dragleave') {
					event.target.classList.remove(className);
				}
			},

			handleDrop: function(event) {
				var className = 'medium-editor-dragover',
					files;
				event.preventDefault();
				event.stopPropagation();

				// IE9 does not support the File API, so prevent file from opening in a new window
				// but also don't try to actually get the file
				if (event.dataTransfer.files) {
					files = Array.prototype.slice.call(event.dataTransfer.files, 0);
					files.some(function(file) {
						if (file.type.match('image')) {
							var fileReader, id;
							fileReader = new FileReader();
							fileReader.readAsDataURL(file);

							id = 'medium-img-' + (+new Date());
							MediumEditor.util.insertHTMLCommand(this.document, '<img class="medium-editor-image-loading" id="' + id + '" />');

							fileReader.onload = function() {
								var img = this.document.getElementById(id);
								if (img) {
									img.removeAttribute('id');
									img.removeAttribute('class');
									img.src = fileReader.result;
								}
							}.bind(this);
						}
					}.bind(this));
				}
				event.target.classList.remove(className);
			}
		});

		MediumEditor.extensions.imageDragging = ImageDragging;
	}());

	(function() {
		'use strict';

		// Event handlers that shouldn't be exposed externally

		function handleDisableExtraSpaces(event) {
			var node = MediumEditor.selection.getSelectionStart(this.options.ownerDocument),
				textContent = node.textContent,
				caretPositions = MediumEditor.selection.getCaretOffsets(node);

			if ((textContent[caretPositions.left - 1] === undefined) || (textContent[caretPositions.left - 1].trim() === '') || (textContent[caretPositions.left] !== undefined && textContent[caretPositions.left].trim() === '')) {
				event.preventDefault();
			}
		}

		function handleDisabledEnterKeydown(event, element) {
			if (this.options.disableReturn || element.getAttribute('data-disable-return')) {
				event.preventDefault();
			} else if (this.options.disableDoubleReturn || element.getAttribute('data-disable-double-return')) {
				var node = MediumEditor.selection.getSelectionStart(this.options.ownerDocument);

				// if current text selection is empty OR previous sibling text is empty OR it is not a list
				if ((node && node.textContent.trim() === '' && node.nodeName.toLowerCase() !== 'li') ||
					(node.previousElementSibling && node.previousElementSibling.nodeName.toLowerCase() !== 'br' &&
						node.previousElementSibling.textContent.trim() === '')) {
					event.preventDefault();
				}
			}
		}

		function handleTabKeydown(event) {
			// Override tab only for pre nodes
			var node = MediumEditor.selection.getSelectionStart(this.options.ownerDocument),
				tag = node && node.nodeName.toLowerCase();

			if (tag === 'pre') {
				event.preventDefault();
				MediumEditor.util.insertHTMLCommand(this.options.ownerDocument, '    ');
			}

			// Tab to indent list structures!
			if (MediumEditor.util.isListItem(node)) {
				event.preventDefault();

				// If Shift is down, outdent, otherwise indent
				if (event.shiftKey) {
					this.options.ownerDocument.execCommand('outdent', false, null);
				} else {
					this.options.ownerDocument.execCommand('indent', false, null);
				}
			}
		}

		function handleBlockDeleteKeydowns(event) {
			var p, node = MediumEditor.selection.getSelectionStart(this.options.ownerDocument),
				tagName = node.nodeName.toLowerCase(),
				isEmpty = /^(\s+|<br\/?>)?$/i,
				isHeader = /h\d/i;

			if (MediumEditor.util.isKey(event, [MediumEditor.util.keyCode.BACKSPACE, MediumEditor.util.keyCode.ENTER]) &&
				// has a preceeding sibling
				node.previousElementSibling &&
				// in a header
				isHeader.test(tagName) &&
				// at the very end of the block
				MediumEditor.selection.getCaretOffsets(node).left === 0) {
				if (MediumEditor.util.isKey(event, MediumEditor.util.keyCode.BACKSPACE) && isEmpty.test(node.previousElementSibling.innerHTML)) {
					// backspacing the begining of a header into an empty previous element will
					// change the tagName of the current node to prevent one
					// instead delete previous node and cancel the event.
					node.previousElementSibling.parentNode.removeChild(node.previousElementSibling);
					event.preventDefault();
				} else if (!this.options.disableDoubleReturn && MediumEditor.util.isKey(event, MediumEditor.util.keyCode.ENTER)) {
					// hitting return in the begining of a header will create empty header elements before the current one
					// instead, make "<p><br></p>" element, which are what happens if you hit return in an empty paragraph
					p = this.options.ownerDocument.createElement('p');
					p.innerHTML = '<br>';
					node.previousElementSibling.parentNode.insertBefore(p, node);
					event.preventDefault();
				}
			} else if (MediumEditor.util.isKey(event, MediumEditor.util.keyCode.DELETE) &&
				// between two sibling elements
				node.nextElementSibling &&
				node.previousElementSibling &&
				// not in a header
				!isHeader.test(tagName) &&
				// in an empty tag
				isEmpty.test(node.innerHTML) &&
				// when the next tag *is* a header
				isHeader.test(node.nextElementSibling.nodeName.toLowerCase())) {
				// hitting delete in an empty element preceding a header, ex:
				//  <p>[CURSOR]</p><h1>Header</h1>
				// Will cause the h1 to become a paragraph.
				// Instead, delete the paragraph node and move the cursor to the begining of the h1

				// remove node and move cursor to start of header
				MediumEditor.selection.moveCursor(this.options.ownerDocument, node.nextElementSibling);

				node.previousElementSibling.parentNode.removeChild(node);

				event.preventDefault();
			} else if (MediumEditor.util.isKey(event, MediumEditor.util.keyCode.BACKSPACE) &&
				tagName === 'li' &&
				// hitting backspace inside an empty li
				isEmpty.test(node.innerHTML) &&
				// is first element (no preceeding siblings)
				!node.previousElementSibling &&
				// parent also does not have a sibling
				!node.parentElement.previousElementSibling &&
				// is not the only li in a list
				node.nextElementSibling &&
				node.nextElementSibling.nodeName.toLowerCase() === 'li') {
				// backspacing in an empty first list element in the first list (with more elements) ex:
				//  <ul><li>[CURSOR]</li><li>List Item 2</li></ul>
				// will remove the first <li> but add some extra element before (varies based on browser)
				// Instead, this will:
				// 1) remove the list element
				// 2) create a paragraph before the list
				// 3) move the cursor into the paragraph

				// create a paragraph before the list
				p = this.options.ownerDocument.createElement('p');
				p.innerHTML = '<br>';
				node.parentElement.parentElement.insertBefore(p, node.parentElement);

				// move the cursor into the new paragraph
				MediumEditor.selection.moveCursor(this.options.ownerDocument, p);

				// remove the list element
				node.parentElement.removeChild(node);

				event.preventDefault();
			} else if (MediumEditor.util.isKey(event, MediumEditor.util.keyCode.BACKSPACE) &&
				(MediumEditor.util.getClosestTag(node, 'blockquote') !== false) &&
				MediumEditor.selection.getCaretOffsets(node).left === 0) {

				// when cursor is at the begining of the element and the element is <blockquote>
				// then pressing backspace key should change the <blockquote> to a <p> tag
				event.preventDefault();
				MediumEditor.util.execFormatBlock(this.options.ownerDocument, 'p');
			} else if (MediumEditor.util.isKey(event, MediumEditor.util.keyCode.ENTER) &&
				(MediumEditor.util.getClosestTag(node, 'blockquote') !== false) &&
				MediumEditor.selection.getCaretOffsets(node).right === 0) {

				// when cursor is at the end of <blockquote>,
				// then pressing enter key should create <p> tag, not <blockquote>
				p = this.options.ownerDocument.createElement('p');
				p.innerHTML = '<br>';
				node.parentElement.insertBefore(p, node.nextSibling);

				// move the cursor into the new paragraph
				MediumEditor.selection.moveCursor(this.options.ownerDocument, p);

				event.preventDefault();
			} else if (MediumEditor.util.isKey(event, MediumEditor.util.keyCode.BACKSPACE) &&
				MediumEditor.util.isMediumEditorElement(node.parentElement) &&
				!node.previousElementSibling &&
				node.nextElementSibling &&
				isEmpty.test(node.innerHTML)) {

				// when cursor is in the first element, it's empty and user presses backspace,
				// do delete action instead to get rid of the first element and move caret to 2nd
				event.preventDefault();
				MediumEditor.selection.moveCursor(this.options.ownerDocument, node.nextSibling);
				node.parentElement.removeChild(node);
			}
		}

		function handleKeyup(event) {
			var node = MediumEditor.selection.getSelectionStart(this.options.ownerDocument),
				tagName;

			if (!node) {
				return;
			}

			// https://github.com/yabwe/medium-editor/issues/994
			// Firefox thrown an error when calling `formatBlock` on an empty editable blockContainer that's not a <div>
			if (MediumEditor.util.isMediumEditorElement(node) && node.children.length === 0 && !MediumEditor.util.isBlockContainer(node)) {
				this.options.ownerDocument.execCommand('formatBlock', false, 'p');
			}

			// https://github.com/yabwe/medium-editor/issues/834
			// https://github.com/yabwe/medium-editor/pull/382
			// Don't call format block if this is a block element (ie h1, figCaption, etc.)
			if (MediumEditor.util.isKey(event, MediumEditor.util.keyCode.ENTER) &&
				!MediumEditor.util.isListItem(node) &&
				!MediumEditor.util.isBlockContainer(node)) {

				tagName = node.nodeName.toLowerCase();
				// For anchor tags, unlink
				if (tagName === 'a') {
					this.options.ownerDocument.execCommand('unlink', false, null);
				} else if (!event.shiftKey && !event.ctrlKey) {
					this.options.ownerDocument.execCommand('formatBlock', false, 'p');
				}
			}
		}

		function handleEditableInput(event, editable) {
			var textarea = editable.parentNode.querySelector('textarea[medium-editor-textarea-id="' + editable.getAttribute('medium-editor-textarea-id') + '"]');
			if (textarea) {
				textarea.value = editable.innerHTML.trim();
			}
		}

		// Internal helper methods which shouldn't be exposed externally

		function addToEditors(win) {
			if (!win._mediumEditors) {
				// To avoid breaking users who are assuming that the unique id on
				// medium-editor elements will start at 1, inserting a 'null' in the
				// array so the unique-id can always map to the index of the editor instance
				win._mediumEditors = [null];
			}

			// If this already has a unique id, re-use it
			if (!this.id) {
				this.id = win._mediumEditors.length;
			}

			win._mediumEditors[this.id] = this;
		}

		function removeFromEditors(win) {
			if (!win._mediumEditors || !win._mediumEditors[this.id]) {
				return;
			}

			/* Setting the instance to null in the array instead of deleting it allows:
			 * 1) Each instance to preserve its own unique-id, even after being destroyed
			 *    and initialized again
			 * 2) The unique-id to always correspond to an index in the array of medium-editor
			 *    instances. Thus, we will be able to look at a contenteditable, and determine
			 *    which instance it belongs to, by indexing into the global array.
			 */
			win._mediumEditors[this.id] = null;
		}

		function createElementsArray(selector, doc, filterEditorElements) {
			var elements = [];

			if (!selector) {
				selector = [];
			}
			// If string, use as query selector
			if (typeof selector === 'string') {
				selector = doc.querySelectorAll(selector);
			}
			// If element, put into array
			if (MediumEditor.util.isElement(selector)) {
				selector = [selector];
			}

			if (filterEditorElements) {
				// Remove elements that have already been initialized by the editor
				// selecotr might not be an array (ie NodeList) so use for loop
				for (var i = 0; i < selector.length; i++) {
					var el = selector[i];
					if (MediumEditor.util.isElement(el) &&
						!el.getAttribute('data-medium-editor-element') &&
						!el.getAttribute('medium-editor-textarea-id')) {
						elements.push(el);
					}
				}
			} else {
				// Convert NodeList (or other array like object) into an array
				elements = Array.prototype.slice.apply(selector);
			}

			return elements;
		}

		function cleanupTextareaElement(element) {
			var textarea = element.parentNode.querySelector('textarea[medium-editor-textarea-id="' + element.getAttribute('medium-editor-textarea-id') + '"]');
			if (textarea) {
				// Un-hide the textarea
				textarea.classList.remove('medium-editor-hidden');
				textarea.removeAttribute('medium-editor-textarea-id');
			}
			if (element.parentNode) {
				element.parentNode.removeChild(element);
			}
		}

		function setExtensionDefaults(extension, defaults) {
			Object.keys(defaults).forEach(function(prop) {
				if (extension[prop] === undefined) {
					extension[prop] = defaults[prop];
				}
			});
			return extension;
		}

		function initExtension(extension, name, instance) {
			var extensionDefaults = {
				'window': instance.options.contentWindow,
				'document': instance.options.ownerDocument,
				'base': instance
			};

			// Add default options into the extension
			extension = setExtensionDefaults(extension, extensionDefaults);

			// Call init on the extension
			if (typeof extension.init === 'function') {
				extension.init();
			}

			// Set extension name (if not already set)
			if (!extension.name) {
				extension.name = name;
			}
			return extension;
		}

		function isToolbarEnabled() {
			// If any of the elements don't have the toolbar disabled
			// We need a toolbar
			if (this.elements.every(function(element) {
					return !!element.getAttribute('data-disable-toolbar');
				})) {
				return false;
			}

			return this.options.toolbar !== false;
		}

		function isAnchorPreviewEnabled() {
			// If toolbar is disabled, don't add
			if (!isToolbarEnabled.call(this)) {
				return false;
			}

			return this.options.anchorPreview !== false;
		}

		function isPlaceholderEnabled() {
			return this.options.placeholder !== false;
		}

		function isAutoLinkEnabled() {
			return this.options.autoLink !== false;
		}

		function isImageDraggingEnabled() {
			return this.options.imageDragging !== false;
		}

		function isKeyboardCommandsEnabled() {
			return this.options.keyboardCommands !== false;
		}

		function shouldUseFileDraggingExtension() {
			// Since the file-dragging extension replaces the image-dragging extension,
			// we need to check if the user passed an overrided image-dragging extension.
			// If they have, to avoid breaking users, we won't use file-dragging extension.
			return !this.options.extensions['imageDragging'];
		}

		function createContentEditable(textarea) {
			var div = this.options.ownerDocument.createElement('div'),
				now = Date.now(),
				uniqueId = 'medium-editor-' + now,
				atts = textarea.attributes;

			// Some browsers can move pretty fast, since we're using a timestamp
			// to make a unique-id, ensure that the id is actually unique on the page
			while (this.options.ownerDocument.getElementById(uniqueId)) {
				now++;
				uniqueId = 'medium-editor-' + now;
			}

			div.className = textarea.className;
			div.id = uniqueId;
			div.innerHTML = textarea.value;

			textarea.setAttribute('medium-editor-textarea-id', uniqueId);

			// re-create all attributes from the textearea to the new created div
			for (var i = 0, n = atts.length; i < n; i++) {
				// do not re-create existing attributes
				if (!div.hasAttribute(atts[i].nodeName)) {
					div.setAttribute(atts[i].nodeName, atts[i].nodeValue);
				}
			}

			// If textarea has a form, listen for reset on the form to clear
			// the content of the created div
			if (textarea.form) {
				this.on(textarea.form, 'reset', function(event) {
					if (!event.defaultPrevented) {
						this.resetContent(this.options.ownerDocument.getElementById(uniqueId));
					}
				}.bind(this));
			}

			textarea.classList.add('medium-editor-hidden');
			textarea.parentNode.insertBefore(
				div,
				textarea
			);

			return div;
		}

		function initElement(element, editorId) {
			if (!element.getAttribute('data-medium-editor-element')) {
				if (element.nodeName.toLowerCase() === 'textarea') {
					element = createContentEditable.call(this, element);

					// Make sure we only attach to editableInput once for <textarea> elements
					if (!this.instanceHandleEditableInput) {
						this.instanceHandleEditableInput = handleEditableInput.bind(this);
						this.subscribe('editableInput', this.instanceHandleEditableInput);
					}
				}

				if (!this.options.disableEditing && !element.getAttribute('data-disable-editing')) {
					element.setAttribute('contentEditable', true);
					element.setAttribute('spellcheck', this.options.spellcheck);
				}

				// Make sure we only attach to editableKeydownEnter once for disable-return options
				if (!this.instanceHandleEditableKeydownEnter) {
					if (element.getAttribute('data-disable-return') || element.getAttribute('data-disable-double-return')) {
						this.instanceHandleEditableKeydownEnter = handleDisabledEnterKeydown.bind(this);
						this.subscribe('editableKeydownEnter', this.instanceHandleEditableKeydownEnter);
					}
				}

				// if we're not disabling return, add a handler to help handle cleanup
				// for certain cases when enter is pressed
				if (!this.options.disableReturn && !element.getAttribute('data-disable-return')) {
					this.on(element, 'keyup', handleKeyup.bind(this));
				}

				var elementId = MediumEditor.util.guid();

				element.setAttribute('data-medium-editor-element', true);
				element.classList.add('medium-editor-element');
				element.setAttribute('role', 'textbox');
				element.setAttribute('aria-multiline', true);
				element.setAttribute('data-medium-editor-editor-index', editorId);
				// TODO: Merge data-medium-editor-element and medium-editor-index attributes for 6.0.0
				// medium-editor-index is not named correctly anymore and can be re-purposed to signify
				// whether the element has been initialized or not
				element.setAttribute('medium-editor-index', elementId);
				initialContent[elementId] = element.innerHTML;

				this.events.attachAllEventsToElement(element);
			}

			return element;
		}

		function attachHandlers() {
			// attach to tabs
			this.subscribe('editableKeydownTab', handleTabKeydown.bind(this));

			// Bind keys which can create or destroy a block element: backspace, delete, return
			this.subscribe('editableKeydownDelete', handleBlockDeleteKeydowns.bind(this));
			this.subscribe('editableKeydownEnter', handleBlockDeleteKeydowns.bind(this));

			// Bind double space event
			if (this.options.disableExtraSpaces) {
				this.subscribe('editableKeydownSpace', handleDisableExtraSpaces.bind(this));
			}

			// Make sure we only attach to editableKeydownEnter once for disable-return options
			if (!this.instanceHandleEditableKeydownEnter) {
				// disabling return or double return
				if (this.options.disableReturn || this.options.disableDoubleReturn) {
					this.instanceHandleEditableKeydownEnter = handleDisabledEnterKeydown.bind(this);
					this.subscribe('editableKeydownEnter', this.instanceHandleEditableKeydownEnter);
				}
			}
		}

		function initExtensions() {

			this.extensions = [];

			// Passed in extensions
			Object.keys(this.options.extensions).forEach(function(name) {
				// Always save the toolbar extension for last
				if (name !== 'toolbar' && this.options.extensions[name]) {
					this.extensions.push(initExtension(this.options.extensions[name], name, this));
				}
			}, this);

			// 4 Cases for imageDragging + fileDragging extensons:
			//
			// 1. ImageDragging ON + No Custom Image Dragging Extension:
			//    * Use fileDragging extension (default options)
			// 2. ImageDragging OFF + No Custom Image Dragging Extension:
			//    * Use fileDragging extension w/ images turned off
			// 3. ImageDragging ON + Custom Image Dragging Extension:
			//    * Don't use fileDragging (could interfere with custom image dragging extension)
			// 4. ImageDragging OFF + Custom Image Dragging:
			//    * Don't use fileDragging (could interfere with custom image dragging extension)
			if (shouldUseFileDraggingExtension.call(this)) {
				var opts = this.options.fileDragging;
				if (!opts) {
					opts = {};

					// Image is in the 'allowedTypes' list by default.
					// If imageDragging is off override the 'allowedTypes' list with an empty one
					if (!isImageDraggingEnabled.call(this)) {
						opts.allowedTypes = [];
					}
				}
				this.addBuiltInExtension('fileDragging', opts);
			}

			// Built-in extensions
			var builtIns = {
				paste: true,
				'anchor-preview': isAnchorPreviewEnabled.call(this),
				autoLink: isAutoLinkEnabled.call(this),
				keyboardCommands: isKeyboardCommandsEnabled.call(this),
				placeholder: isPlaceholderEnabled.call(this)
			};
			Object.keys(builtIns).forEach(function(name) {
				if (builtIns[name]) {
					this.addBuiltInExtension(name);
				}
			}, this);

			// Users can pass in a custom toolbar extension
			// so check for that first and if it's not present
			// just create the default toolbar
			var toolbarExtension = this.options.extensions['toolbar'];
			if (!toolbarExtension && isToolbarEnabled.call(this)) {
				// Backwards compatability
				var toolbarOptions = MediumEditor.util.extend({}, this.options.toolbar, {
					allowMultiParagraphSelection: this.options.allowMultiParagraphSelection // deprecated
				});
				toolbarExtension = new MediumEditor.extensions.toolbar(toolbarOptions);
			}

			// If the toolbar is not disabled, so we actually have an extension
			// initialize it and add it to the extensions array
			if (toolbarExtension) {
				this.extensions.push(initExtension(toolbarExtension, 'toolbar', this));
			}
		}

		function mergeOptions(defaults, options) {
			var deprecatedProperties = [
				['allowMultiParagraphSelection', 'toolbar.allowMultiParagraphSelection']
			];
			// warn about using deprecated properties
			if (options) {
				deprecatedProperties.forEach(function(pair) {
					if (options.hasOwnProperty(pair[0]) && options[pair[0]] !== undefined) {
						MediumEditor.util.deprecated(pair[0], pair[1], 'v6.0.0');
					}
				});
			}

			return MediumEditor.util.defaults({}, options, defaults);
		}

		function execActionInternal(action, opts) {
			/*jslint regexp: true*/
			var appendAction = /^append-(.+)$/gi,
				justifyAction = /justify([A-Za-z]*)$/g,
				/* Detecting if is justifyCenter|Right|Left */
				match,
				cmdValueArgument;
			/*jslint regexp: false*/

			// Actions starting with 'append-' should attempt to format a block of text ('formatBlock') using a specific
			// type of block element (ie append-blockquote, append-h1, append-pre, etc.)
			match = appendAction.exec(action);
			if (match) {
				return MediumEditor.util.execFormatBlock(this.options.ownerDocument, match[1]);
			}

			if (action === 'fontSize') {
				// TODO: Deprecate support for opts.size in 6.0.0
				if (opts.size) {
					MediumEditor.util.deprecated('.size option for fontSize command', '.value', '6.0.0');
				}
				cmdValueArgument = opts.value || opts.size;
				return this.options.ownerDocument.execCommand('fontSize', false, cmdValueArgument);
			}

			if (action === 'fontName') {
				// TODO: Deprecate support for opts.name in 6.0.0
				if (opts.name) {
					MediumEditor.util.deprecated('.name option for fontName command', '.value', '6.0.0');
				}
				cmdValueArgument = opts.value || opts.name;
				return this.options.ownerDocument.execCommand('fontName', false, cmdValueArgument);
			}

			if (action === 'createLink') {
				return this.createLink(opts);
			}

			if (action === 'image') {
				var src = this.options.contentWindow.getSelection().toString().trim();
				return this.options.ownerDocument.execCommand('insertImage', false, src);
			}

			if (action === 'html') {
				var html = this.options.contentWindow.getSelection().toString().trim();
				return MediumEditor.util.insertHTMLCommand(this.options.ownerDocument, html);
			}

			/* Issue: https://github.com/yabwe/medium-editor/issues/595
			 * If the action is to justify the text */
			if (justifyAction.exec(action)) {
				var result = this.options.ownerDocument.execCommand(action, false, null),
					parentNode = MediumEditor.selection.getSelectedParentElement(MediumEditor.selection.getSelectionRange(this.options.ownerDocument));
				if (parentNode) {
					cleanupJustifyDivFragments.call(this, MediumEditor.util.getTopBlockContainer(parentNode));
				}

				return result;
			}

			cmdValueArgument = opts && opts.value;
			return this.options.ownerDocument.execCommand(action, false, cmdValueArgument);
		}

		/* If we've just justified text within a container block
		 * Chrome may have removed <br> elements and instead wrapped lines in <div> elements
		 * with a text-align property.  If so, we want to fix this
		 */
		function cleanupJustifyDivFragments(blockContainer) {
			if (!blockContainer) {
				return;
			}

			var textAlign,
				childDivs = Array.prototype.slice.call(blockContainer.childNodes).filter(function(element) {
					var isDiv = element.nodeName.toLowerCase() === 'div';
					if (isDiv && !textAlign) {
						textAlign = element.style.textAlign;
					}
					return isDiv;
				});

			/* If we found child <div> elements with text-align style attributes
			 * we should fix this by:
			 *
			 * 1) Unwrapping each <div> which has a text-align style
			 * 2) Insert a <br> element after each set of 'unwrapped' div children
			 * 3) Set the text-align style of the parent block element
			 */
			if (childDivs.length) {
				// Since we're mucking with the HTML, preserve selection
				this.saveSelection();
				childDivs.forEach(function(div) {
					if (div.style.textAlign === textAlign) {
						var lastChild = div.lastChild;
						if (lastChild) {
							// Instead of a div, extract the child elements and add a <br>
							MediumEditor.util.unwrap(div, this.options.ownerDocument);
							var br = this.options.ownerDocument.createElement('BR');
							lastChild.parentNode.insertBefore(br, lastChild.nextSibling);
						}
					}
				}, this);
				blockContainer.style.textAlign = textAlign;
				// We're done, so restore selection
				this.restoreSelection();
			}
		}

		var initialContent = {};

		MediumEditor.prototype = {
			// NOT DOCUMENTED - exposed for backwards compatability
			init: function(elements, options) {
				this.options = mergeOptions.call(this, this.defaults, options);
				this.origElements = elements;

				if (!this.options.elementsContainer) {
					this.options.elementsContainer = this.options.ownerDocument.body;
				}

				return this.setup();
			},

			setup: function() {
				if (this.isActive) {
					return;
				}

				addToEditors.call(this, this.options.contentWindow);
				this.events = new MediumEditor.Events(this);
				this.elements = [];

				this.addElements(this.origElements);

				if (this.elements.length === 0) {
					return;
				}

				this.isActive = true;

				// Call initialization helpers
				initExtensions.call(this);
				attachHandlers.call(this);
			},

			destroy: function() {
				if (!this.isActive) {
					return;
				}

				this.isActive = false;

				this.extensions.forEach(function(extension) {
					if (typeof extension.destroy === 'function') {
						extension.destroy();
					}
				}, this);

				this.events.destroy();

				this.elements.forEach(function(element) {
					// Reset elements content, fix for issue where after editor destroyed the red underlines on spelling errors are left
					if (this.options.spellcheck) {
						element.innerHTML = element.innerHTML;
					}

					// cleanup extra added attributes
					element.removeAttribute('contentEditable');
					element.removeAttribute('spellcheck');
					element.removeAttribute('data-medium-editor-element');
					element.classList.remove('medium-editor-element');
					element.removeAttribute('role');
					element.removeAttribute('aria-multiline');
					element.removeAttribute('medium-editor-index');
					element.removeAttribute('data-medium-editor-editor-index');

					// Remove any elements created for textareas
					if (element.getAttribute('medium-editor-textarea-id')) {
						cleanupTextareaElement(element);
					}
				}, this);
				this.elements = [];
				this.instanceHandleEditableKeydownEnter = null;
				this.instanceHandleEditableInput = null;

				removeFromEditors.call(this, this.options.contentWindow);
			},

			on: function(target, event, listener, useCapture) {
				this.events.attachDOMEvent(target, event, listener, useCapture);

				return this;
			},

			off: function(target, event, listener, useCapture) {
				this.events.detachDOMEvent(target, event, listener, useCapture);

				return this;
			},

			subscribe: function(event, listener) {
				this.events.attachCustomEvent(event, listener);

				return this;
			},

			unsubscribe: function(event, listener) {
				this.events.detachCustomEvent(event, listener);

				return this;
			},

			trigger: function(name, data, editable) {
				this.events.triggerCustomEvent(name, data, editable);

				return this;
			},

			delay: function(fn) {
				var self = this;
				return setTimeout(function() {
					if (self.isActive) {
						fn();
					}
				}, this.options.delay);
			},

			serialize: function() {
				var i,
					elementid,
					content = {},
					len = this.elements.length;

				for (i = 0; i < len; i += 1) {
					elementid = (this.elements[i].id !== '') ? this.elements[i].id : 'element-' + i;
					content[elementid] = {
						value: this.elements[i].innerHTML.trim()
					};
				}
				return content;
			},

			getExtensionByName: function(name) {
				var extension;
				if (this.extensions && this.extensions.length) {
					this.extensions.some(function(ext) {
						if (ext.name === name) {
							extension = ext;
							return true;
						}
						return false;
					});
				}
				return extension;
			},

			/**
			 * NOT DOCUMENTED - exposed as a helper for other extensions to use
			 */
			addBuiltInExtension: function(name, opts) {
				var extension = this.getExtensionByName(name),
					merged;
				if (extension) {
					return extension;
				}

				switch (name) {
					case 'anchor':
						merged = MediumEditor.util.extend({}, this.options.anchor, opts);
						extension = new MediumEditor.extensions.anchor(merged);
						break;
					case 'anchor-preview':
						extension = new MediumEditor.extensions.anchorPreview(this.options.anchorPreview);
						break;
					case 'autoLink':
						extension = new MediumEditor.extensions.autoLink();
						break;
					case 'fileDragging':
						extension = new MediumEditor.extensions.fileDragging(opts);
						break;
					case 'fontname':
						extension = new MediumEditor.extensions.fontName(this.options.fontName);
						break;
					case 'fontsize':
						extension = new MediumEditor.extensions.fontSize(opts);
						break;
					case 'keyboardCommands':
						extension = new MediumEditor.extensions.keyboardCommands(this.options.keyboardCommands);
						break;
					case 'paste':
						extension = new MediumEditor.extensions.paste(this.options.paste);
						break;
					case 'placeholder':
						extension = new MediumEditor.extensions.placeholder(this.options.placeholder);
						break;
					default:
						// All of the built-in buttons for MediumEditor are extensions
						// so check to see if the extension we're creating is a built-in button
						if (MediumEditor.extensions.button.isBuiltInButton(name)) {
							if (opts) {
								merged = MediumEditor.util.defaults({}, opts, MediumEditor.extensions.button.prototype.defaults[name]);
								extension = new MediumEditor.extensions.button(merged);
							} else {
								extension = new MediumEditor.extensions.button(name);
							}
						}
				}

				if (extension) {
					this.extensions.push(initExtension(extension, name, this));
				}

				return extension;
			},

			stopSelectionUpdates: function() {
				this.preventSelectionUpdates = true;
			},

			startSelectionUpdates: function() {
				this.preventSelectionUpdates = false;
			},

			checkSelection: function() {
				var toolbar = this.getExtensionByName('toolbar');
				if (toolbar) {
					toolbar.checkState();
				}
				return this;
			},

			// Wrapper around document.queryCommandState for checking whether an action has already
			// been applied to the current selection
			queryCommandState: function(action) {
				var fullAction = /^full-(.+)$/gi,
					match,
					queryState = null;

				// Actions starting with 'full-' need to be modified since this is a medium-editor concept
				match = fullAction.exec(action);
				if (match) {
					action = match[1];
				}

				try {
					queryState = this.options.ownerDocument.queryCommandState(action);
				} catch (exc) {
					queryState = null;
				}

				return queryState;
			},

			execAction: function(action, opts) {
				/*jslint regexp: true*/
				var fullAction = /^full-(.+)$/gi,
					match,
					result;
				/*jslint regexp: false*/

				// Actions starting with 'full-' should be applied to to the entire contents of the editable element
				// (ie full-bold, full-append-pre, etc.)
				match = fullAction.exec(action);
				if (match) {
					// Store the current selection to be restored after applying the action
					this.saveSelection();
					// Select all of the contents before calling the action
					this.selectAllContents();
					result = execActionInternal.call(this, match[1], opts);
					// Restore the previous selection
					this.restoreSelection();
				} else {
					result = execActionInternal.call(this, action, opts);
				}

				// do some DOM clean-up for known browser issues after the action
				if (action === 'insertunorderedlist' || action === 'insertorderedlist') {
					MediumEditor.util.cleanListDOM(this.options.ownerDocument, this.getSelectedParentElement());
				}

				// ThemeFusion Edit.
				if ('undefined' === typeof opts || 'undefined' === typeof opts.skipCheck) {
					this.checkSelection();
				}
				return result;
			},

			getSelectedParentElement: function(range) {
				if (range === undefined) {
					range = this.options.contentWindow.getSelection().getRangeAt(0);
				}
				return MediumEditor.selection.getSelectedParentElement(range);
			},

			selectAllContents: function() {
				var currNode = MediumEditor.selection.getSelectionElement(this.options.contentWindow);

				if (currNode) {
					// Move to the lowest descendant node that still selects all of the contents
					while (currNode.children.length === 1) {
						currNode = currNode.children[0];
					}

					this.selectElement(currNode);
				}
			},

			selectElement: function(element) {
				MediumEditor.selection.selectNode(element, this.options.ownerDocument);

				var selElement = MediumEditor.selection.getSelectionElement(this.options.contentWindow);
				if (selElement) {
					this.events.focusElement(selElement);
				}
			},

			getFocusedElement: function() {
				var focused;
				this.elements.some(function(element) {
					// Find the element that has focus
					if (!focused && element.getAttribute('data-medium-focused')) {
						focused = element;
					}

					// bail if we found the element that had focus
					return !!focused;
				}, this);

				return focused;
			},

			// Export the state of the selection in respect to one of this
			// instance of MediumEditor's elements
			exportSelection: function() {
				var selectionElement = MediumEditor.selection.getSelectionElement(this.options.contentWindow),
					editableElementIndex = this.elements.indexOf(selectionElement),
					selectionState = null;

				if (editableElementIndex >= 0) {
					selectionState = MediumEditor.selection.exportSelection(selectionElement, this.options.ownerDocument);
				}

				if (selectionState !== null && editableElementIndex !== 0) {
					selectionState.editableElementIndex = editableElementIndex;
				}

				return selectionState;
			},

			saveSelection: function() {
				this.selectionState = this.exportSelection();
			},

			// Restore a selection based on a selectionState returned by a call
			// to MediumEditor.exportSelection
			importSelection: function(selectionState, favorLaterSelectionAnchor) {
				if (!selectionState) {
					return;
				}

				var editableElement = this.elements[selectionState.editableElementIndex || 0];
				MediumEditor.selection.importSelection(selectionState, editableElement, this.options.ownerDocument, favorLaterSelectionAnchor);
			},

			restoreSelection: function() {
				this.importSelection(this.selectionState);
			},

			createLink: function(opts) {
				var currentEditor = MediumEditor.selection.getSelectionElement(this.options.contentWindow),
					customEvent = {},
					targetUrl;

				// Make sure the selection is within an element this editor is tracking
				if (this.elements.indexOf(currentEditor) === -1) {
					return;
				}

				try {
					this.events.disableCustomEvent('editableInput');
					// TODO: Deprecate support for opts.url in 6.0.0
					if (opts.url) {
						MediumEditor.util.deprecated('.url option for createLink', '.value', '6.0.0');
					}
					targetUrl = opts.url || opts.value;
					if (targetUrl && targetUrl.trim().length > 0) {
						var currentSelection = this.options.contentWindow.getSelection();
						if (currentSelection) {
							var currRange = currentSelection.getRangeAt(0),
								commonAncestorContainer = currRange.commonAncestorContainer,
								exportedSelection,
								startContainerParentElement,
								endContainerParentElement,
								textNodes;

							// If the selection is contained within a single text node
							// and the selection starts at the beginning of the text node,
							// MSIE still says the startContainer is the parent of the text node.
							// If the selection is contained within a single text node, we
							// want to just use the default browser 'createLink', so we need
							// to account for this case and adjust the commonAncestorContainer accordingly
							if (currRange.endContainer.nodeType === 3 &&
								currRange.startContainer.nodeType !== 3 &&
								currRange.startOffset === 0 &&
								currRange.startContainer.firstChild === currRange.endContainer) {
								commonAncestorContainer = currRange.endContainer;
							}

							startContainerParentElement = MediumEditor.util.getClosestBlockContainer(currRange.startContainer);
							endContainerParentElement = MediumEditor.util.getClosestBlockContainer(currRange.endContainer);

							// If the selection is not contained within a single text node
							// but the selection is contained within the same block element
							// we want to make sure we create a single link, and not multiple links
							// which can happen with the built in browser functionality
							if (commonAncestorContainer.nodeType !== 3 && commonAncestorContainer.textContent.length !== 0 && startContainerParentElement === endContainerParentElement) {
								var parentElement = (startContainerParentElement || currentEditor),
									fragment = this.options.ownerDocument.createDocumentFragment();

								// since we are going to create a link from an extracted text,
								// be sure that if we are updating a link, we won't let an empty link behind (see #754)
								// (Workaroung for Chrome)
								this.execAction('unlink');

								exportedSelection = this.exportSelection();
								fragment.appendChild(parentElement.cloneNode(true));

								if (currentEditor === parentElement) {
									// We have to avoid the editor itself being wiped out when it's the only block element,
									// as our reference inside this.elements gets detached from the page when insertHTML runs.
									// If we just use [parentElement, 0] and [parentElement, parentElement.childNodes.length]
									// as the range boundaries, this happens whenever parentElement === currentEditor.
									// The tradeoff to this workaround is that a orphaned tag can sometimes be left behind at
									// the end of the editor's content.
									// In Gecko:
									// as an empty <strong></strong> if parentElement.lastChild is a <strong> tag.
									// In WebKit:
									// an invented <br /> tag at the end in the same situation
									MediumEditor.selection.select(
										this.options.ownerDocument,
										parentElement.firstChild,
										0,
										parentElement.lastChild,
										parentElement.lastChild.nodeType === 3 ?
										parentElement.lastChild.nodeValue.length : parentElement.lastChild.childNodes.length
									);
								} else {
									MediumEditor.selection.select(
										this.options.ownerDocument,
										parentElement,
										0,
										parentElement,
										parentElement.childNodes.length
									);
								}

								var modifiedExportedSelection = this.exportSelection();

								textNodes = MediumEditor.util.findOrCreateMatchingTextNodes(
									this.options.ownerDocument,
									fragment, {
										start: exportedSelection.start - modifiedExportedSelection.start,
										end: exportedSelection.end - modifiedExportedSelection.start,
										editableElementIndex: exportedSelection.editableElementIndex
									}
								);
								// If textNodes are not present, when changing link on images
								// ex: <a><img src="http://image.test.com"></a>, change fragment to currRange.startContainer
								// and set textNodes array to [imageElement, imageElement]
								if (textNodes.length === 0) {
									fragment = this.options.ownerDocument.createDocumentFragment();
									fragment.appendChild(commonAncestorContainer.cloneNode(true));
									textNodes = [fragment.firstChild.firstChild, fragment.firstChild.lastChild];
								}

								// Creates the link in the document fragment
								MediumEditor.util.createLink(this.options.ownerDocument, textNodes, targetUrl.trim());

								// Chrome trims the leading whitespaces when inserting HTML, which messes up restoring the selection.
								var leadingWhitespacesCount = (fragment.firstChild.innerHTML.match(/^\s+/) || [''])[0].length;

								// Now move the created link back into the original document in a way to preserve undo/redo history
								MediumEditor.util.insertHTMLCommand(this.options.ownerDocument, fragment.firstChild.innerHTML.replace(/^\s+/, ''));
								exportedSelection.start -= leadingWhitespacesCount;
								exportedSelection.end -= leadingWhitespacesCount;

								this.importSelection(exportedSelection);
							} else {
								this.options.ownerDocument.execCommand('createLink', false, targetUrl);
							}

							if (this.options.targetBlank || opts.target === '_blank') {
								MediumEditor.util.setTargetBlank(MediumEditor.selection.getSelectionStart(this.options.ownerDocument), targetUrl);
							} else {
								MediumEditor.util.removeTargetBlank(MediumEditor.selection.getSelectionStart(this.options.ownerDocument), targetUrl);
							}

							if (opts.buttonClass) {
								MediumEditor.util.addClassToAnchors(MediumEditor.selection.getSelectionStart(this.options.ownerDocument), opts.buttonClass);
							}
						}
					}
					// Fire input event for backwards compatibility if anyone was listening directly to the DOM input event
					if (this.options.targetBlank || opts.target === '_blank' || opts.buttonClass) {
						customEvent = this.options.ownerDocument.createEvent('HTMLEvents');
						customEvent.initEvent('input', true, true, this.options.contentWindow);
						for (var i = 0, len = this.elements.length; i < len; i += 1) {
							this.elements[i].dispatchEvent(customEvent);
						}
					}
				} finally {
					this.events.enableCustomEvent('editableInput');
				}
				// Fire our custom editableInput event
				this.events.triggerCustomEvent('editableInput', customEvent, currentEditor);
			},

			cleanPaste: function(text) {
				this.getExtensionByName('paste').cleanPaste(text);
			},

			pasteHTML: function(html, options) {
				this.getExtensionByName('paste').pasteHTML(html, options);
			},

			setContent: function(html, index) {
				index = index || 0;

				if (this.elements[index]) {
					var target = this.elements[index];
					target.innerHTML = html;
					this.checkContentChanged(target);
				}
			},

			getContent: function(index) {
				index = index || 0;

				if (this.elements[index]) {
					return this.elements[index].innerHTML.trim();
				}
				return null;
			},

			checkContentChanged: function(editable) {
				editable = editable || MediumEditor.selection.getSelectionElement(this.options.contentWindow);
				this.events.updateInput(editable, {
					target: editable,
					currentTarget: editable
				});
			},

			resetContent: function(element) {
				// For all elements that exist in the this.elements array, we can assume:
				// - Its initial content has been set in the initialContent object
				// - It has a medium-editor-index attribute which is the key value in the initialContent object

				if (element) {
					var index = this.elements.indexOf(element);
					if (index !== -1) {
						this.setContent(initialContent[element.getAttribute('medium-editor-index')], index);
					}
					return;
				}

				this.elements.forEach(function(el, idx) {
					this.setContent(initialContent[el.getAttribute('medium-editor-index')], idx);
				}, this);
			},

			addElements: function(selector) {
				// Convert elements into an array
				var elements = createElementsArray(selector, this.options.ownerDocument, true);

				// Do we have elements to add now?
				if (elements.length === 0) {
					return false;
				}

				elements.forEach(function(element) {
					// Initialize all new elements (we check that in those functions don't worry)
					element = initElement.call(this, element, this.id);

					// Add new elements to our internal elements array
					this.elements.push(element);

					// Trigger event so extensions can know when an element has been added
					this.trigger('addElement', {
						target: element,
						currentTarget: element
					}, element);
				}, this);
			},

			removeElements: function(selector) {
				// Convert elements into an array
				var elements = createElementsArray(selector, this.options.ownerDocument),
					toRemove = elements.map(function(el) {
						// For textareas, make sure we're looking at the editor div and not the textarea itself
						if (el.getAttribute('medium-editor-textarea-id') && el.parentNode) {
							return el.parentNode.querySelector('div[medium-editor-textarea-id="' + el.getAttribute('medium-editor-textarea-id') + '"]');
						} else {
							return el;
						}
					});

				this.elements = this.elements.filter(function(element) {
					// If this is an element we want to remove
					if (toRemove.indexOf(element) !== -1) {
						this.events.cleanupElement(element);
						if (element.getAttribute('medium-editor-textarea-id')) {
							cleanupTextareaElement(element);
						}
						// Trigger event so extensions can clean-up elements that are being removed
						this.trigger('removeElement', {
							target: element,
							currentTarget: element
						}, element);
						return false;
					}
					return true;
				}, this);
			}
		};

		MediumEditor.getEditorFromElement = function(element) {
			var index = element.getAttribute('data-medium-editor-editor-index'),
				win = element && element.ownerDocument && (element.ownerDocument.defaultView || element.ownerDocument.parentWindow);
			if (win && win._mediumEditors && win._mediumEditors[index]) {
				return win._mediumEditors[index];
			}
			return null;
		};
	}());

	(function() {
		// summary: The default options hash used by the Editor

		MediumEditor.prototype.defaults = {
			activeButtonClass: 'medium-editor-button-active',
			buttonLabels: false,
			delay: 0,
			disableReturn: false,
			disableDoubleReturn: false,
			disableExtraSpaces: false,
			disableEditing: false,
			autoLink: false,
			elementsContainer: false,
			contentWindow: window,
			ownerDocument: document,
			targetBlank: false,
			extensions: {},
			spellcheck: true
		};
	})();

	MediumEditor.parseVersionString = function(release) {
		var split = release.split('-'),
			version = split[0].split('.'),
			preRelease = (split.length > 1) ? split[1] : '';
		return {
			major: parseInt(version[0], 10),
			minor: parseInt(version[1], 10),
			revision: parseInt(version[2], 10),
			preRelease: preRelease,
			toString: function() {
				return [version[0], version[1], version[2]].join('.') + (preRelease ? '-' + preRelease : '');
			}
		};
	};

	MediumEditor.version = MediumEditor.parseVersionString.call(this, ({
		// grunt-bump looks for this:
		'version': '5.23.2'
	}).version);

	return MediumEditor;
}()));
