/*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.BPBM_MediumEditor = factory;
    }
}(this, function () {

    'use strict';

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

    BPBM_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 = BPBM_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(BPBM_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.isBPBM_MediumEditorElement(node)) {
                    return false;
                }

                var previousSibling = node.previousSibling;
                while (!previousSibling && !Util.isBPBM_MediumEditorElement(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.isBPBM_MediumEditorElement(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 (!BPBM_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.isBPBM_MediumEditorElement(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.isBPBM_MediumEditorElement(toReplace) &&
                        toReplace.parentNode &&
                        toReplace.parentNode.childNodes.length === 1 &&
                        !Util.isBPBM_MediumEditorElement(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);
                        BPBM_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(BPBM_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
                    BPBM_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.isBPBM_MediumEditorElement(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;
            },

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

            getContainerEditorElement: function (element) {
                return Util.traverseUp(element, function (node) {
                    return Util.isBPBM_MediumEditorElement(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.isBPBM_MediumEditorElement(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.isBPBM_MediumEditorElement(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) {
                    BPBM_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();
            }
        };

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

    (function () {
        'use strict';

        var Extension = function (options) {
            BPBM_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)
            BPBM_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) {
                BPBM_MediumEditor.util.extend(child.prototype, protoProps);
            }

            // todo: $super?

            return child;
        };

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

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

            /* name: [string]
         *
         * 'name' of the extension, used for retrieving the extension.
         * If not set, BPBM_MediumEditor will set this to be the key
         * used when passing the extension into BPBM_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 BPBM_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 BPBM_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 BPBM_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 BPBM_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 BPBM_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, BPBM_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. BPBM_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 BPBM_MediumEditor
             * during initialization, or are helper methods which either
             * route calls to the BPBM_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 BPBM_MediumEditor and its extensions.  This is
         * passed via the 'contentWindow' option to BPBM_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 BPBM_MediumEditor and its extensions. This is
         * passed via the 'ownerDocument' optin to BPBM_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 BPBM_MediumEditor
         */
            getEditorElements: function () {
                return this.base.elements;
            },

            /* getEditorId: [function ()]
         *
         * Helper function which returns a unique identifier
         * for this instance of BPBM_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 BPBM_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 BPBM_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);
            };
        });

        BPBM_MediumEditor.Extension = Extension;
    })();

    (function () {
        'use strict';

        function filterOnlyParentElements(node) {
            if (BPBM_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 BPBM_MediumEditor.util.traverseUp(current, testElementFunction);
            },

            getSelectionElement: function (contentWindow) {
                return this.findMatchingSelectionParent(function (el) {
                    return BPBM_MediumEditor.util.isBPBM_MediumEditorElement(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 &&
                    BPBM_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 && BPBM_MediumEditor.util.isBlockContainer(startContainer.previousSibling)) {
                    startBlock = startContainer.previousSibling;
                } else {
                    startBlock = BPBM_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(BPBM_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 (!BPBM_MediumEditor.util.isElementAtBeginningOfBlock(node)) {
                        return -1;
                    }

                    var previousSibling = BPBM_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 = BPBM_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;
            }
        };

        BPBM_MediumEditor.selection = Selection;
    }());

    (function () {
        'use strict';

        function isElementDescendantOfExtension(extensions, element) {
            if (!extensions) {
                return false;
            }

            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 BPBM_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: !BPBM_MediumEditor.util.isIE && !BPBM_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 = BPBM_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) {
                    targets = BPBM_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('bpbm-medium-editor-index') === element.getAttribute('bpbm-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('bpbm-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 BPBM_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 BPBM_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 BPBM_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('bpbm-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('bpbm-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 &&
                    (BPBM_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 && (BPBM_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 = !BPBM_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('bpbm-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 BPBM_MediumEditor
                    this.base.elements.some(function (element) {
                        if (BPBM_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 (BPBM_MediumEditor.util.isKey(event, BPBM_MediumEditor.util.keyCode.SPACE)) {
                    return this.triggerCustomEvent('editableKeydownSpace', event, event.currentTarget);
                }

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

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

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

        BPBM_MediumEditor.Events = Events;
    }());

    (function () {
        'use strict';

        var Button = BPBM_MediumEditor.Extension.extend({

            /* Button Options */

            /* action: [string]
         * The action argument to pass to BPBM_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 BPBM_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)) {
                    BPBM_MediumEditor.Extension.call(this, this.defaults[options]);
                } else {
                    BPBM_MediumEditor.Extension.call(this, options);
                }
            },

            init: function () {
                BPBM_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('bpbm-medium-editor-action');
                button.classList.add('bpbm-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) {
                    button.setAttribute('title', ariaLabel);
                    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') && BPBM_MediumEditor.extensions.button.prototype.defaults.hasOwnProperty(name);
        };

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

    (function () {
        'use strict';

        /* BPBM_MediumEditor.extensions.button.defaults: [Object]
     * Set of default config options for all of the built-in BPBM_MediumEditor buttons
     */
        BPBM_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 = BPBM_MediumEditor.extensions.button.extend({

            init: function () {
                BPBM_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: 'bpbm-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 BPBM_MediumEditor
             * during initialization, or are helper methods which either
             * route calls to the BPBM_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();
                }
            }
        });

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

        var AnchorForm = BPBM_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 () {
                BPBM_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 = BPBM_MediumEditor.selection.getSelectionRange(this.document);

                if (range.startContainer.nodeName.toLowerCase() === 'a' ||
                    range.endContainer.nodeName.toLowerCase() === 'a' ||
                    BPBM_MediumEditor.util.getClosestTag(BPBM_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 (BPBM_MediumEditor.util.isKey(event, BPBM_MediumEditor.util.keyCode.K) && BPBM_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="bpbm-medium-editor-toolbar-input" placeholder="', this.placeholderText, '">'
                ];

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

                template.push('<a href="#" class="bpbm-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="bpbm-medium-editor-toolbar-form-row">',
                        '<input type="checkbox" class="bpbm-medium-editor-toolbar-anchor-target" id="bpbm-medium-editor-toolbar-anchor-target-field-' + this.getEditorId() + '">',
                        '<label for="bpbm-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="bpbm-medium-editor-toolbar-form-row">',
                        '<input type="checkbox" class="bpbm-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 BPBM_MediumEditor.extensions.form.prototype.isDisplayed.apply(this);
            },

            hideForm: function () {
                BPBM_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();
                BPBM_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('.bpbm-medium-editor-toolbar-close'),
                    save = form.querySelector('.bpbm-medium-editor-toolbar-save'),
                    input = form.querySelector('.bpbm-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 = 'bpbm-medium-editor-toolbar-form';
                form.id = 'bpbm-medium-editor-toolbar-form-anchor-' + this.getEditorId();
                form.innerHTML = this.getTemplate();
                this.attachFormEvents(form);

                return form;
            },

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

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

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

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

                // For ESCAPE -> close the form
                if (event.keyCode === BPBM_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();
            }
        });

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

    (function () {
        'use strict';

        var AnchorPreview = BPBM_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 = 'bpbm-medium-editor-anchor-preview-' + this.getEditorId();
                el.className = 'bpbm-medium-editor-anchor-preview';
                el.innerHTML = this.getTemplate();

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

                return el;
            },

            getTemplate: function () {
                return '<div class="bpbm-medium-editor-toolbar-anchor-preview" id="bpbm-medium-editor-toolbar-anchor-preview">' +
                    '    <a class="bpbm-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('bpbm-medium-editor-anchor-preview-active');
                }
                this.activeAnchor = null;
            },

            showPreview: function (anchorEl) {
                if (this.anchorPreview.classList.contains('bpbm-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('bpbm-medium-editor-anchor-preview-active')) {
                    this.anchorPreview.classList.add('bpbm-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 = BPBM_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);
            }
        });

        BPBM_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 !BPBM_MediumEditor.util.getClosestTag(node, 'a');
        }

        var AutoLink = BPBM_MediumEditor.Extension.extend({
            init: function () {
                BPBM_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._BPBM_MediumEditors.length; i++) {
                    var editor = this.window._BPBM_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 (BPBM_MediumEditor.util.isKey(keyPressEvent, [BPBM_MediumEditor.util.keyCode.SPACE, BPBM_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 = BPBM_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 = BPBM_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 = BPBM_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
                            BPBM_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 = BPBM_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 = !!BPBM_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 = BPBM_MediumEditor.util.ensureUrlHasProtocol(href);
                var anchor = BPBM_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]);
                }
            }

        });

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

    (function () {
        'use strict';

        var CLASS_DRAG_OVER = 'bpbm-medium-editor-dragover';

        function clearClassNames(element) {
            var editable = BPBM_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 = BPBM_MediumEditor.Extension.extend({
            name: 'fileDragging',

            allowedTypes: ['image'],

            init: function () {
                BPBM_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;
                    BPBM_MediumEditor.util.insertHTMLCommand(this.document, addImageElement.outerHTML);
                }.bind(this));
            }
        });

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

    (function () {
        'use strict';

        var KeyboardCommands = BPBM_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 () {
                BPBM_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 = BPBM_MediumEditor.util.getKeyCode(event);
                if (!this.keys[keyCode]) {
                    return;
                }

                var isMeta = BPBM_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);
            }
        });

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

    (function () {
        'use strict';

        var FontNameForm = BPBM_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 () {
                BPBM_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 = 'bpbm-medium-editor-toolbar-form';
                form.id = 'bpbm-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 = 'bpbm-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 = 'bpbm-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 = 'bpbm-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.bpbm-medium-editor-toolbar-select');
            },

            clearFontName: function () {
                BPBM_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();
            }
        });

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

    (function () {
        'use strict';

        var FontSizeForm = BPBM_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 () {
                BPBM_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 = 'bpbm-medium-editor-toolbar-form';
                form.id = 'bpbm-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 = 'bpbm-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 = 'bpbm-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 = 'bpbm-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.bpbm-medium-editor-toolbar-input');
            },

            clearFontSize: function () {
                BPBM_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();
            }
        });

        BPBM_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 = BPBM_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 () {
                BPBM_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 (!pastedPlain) {
                    return;
                }

                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>' + BPBM_MediumEditor.util.htmlEntities(paragraphs[p]) + '</p>';
                            }
                        }
                    } else {
                        html = BPBM_MediumEditor.util.htmlEntities(paragraphs[0]);
                    }
                } else {
                    html = BPBM_MediumEditor.util.htmlEntities(pastedPlain);
                }
                BPBM_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 (!(BPBM_MediumEditor.util.isKey(event, BPBM_MediumEditor.util.keyCode.V) && BPBM_MediumEditor.util.isMetaCtrlKey(event))) {
                    return;
                }

                event.stopImmediatePropagation();

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

            createPasteBin: function (editable) {
                var rects,
                    range = BPBM_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 = 'bpbm-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();

                BPBM_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) {
                    BPBM_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 = BPBM_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')) {
                        BPBM_MediumEditor.util.setTargetBlank(workEl);
                    }

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

                BPBM_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 (BPBM_MediumEditor.util.traverseUp(el, isCEF)) {
                        return false;
                    }

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

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

    (function () {
        'use strict';

        var Placeholder = BPBM_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 () {
                BPBM_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 (BPBM_MediumEditor.util.isFF && el.childNodes.length === 0) {
                        el.classList.add('bpbm-medium-editor-placeholder-relative');
                        el.classList.remove('bpbm-medium-editor-placeholder');
                    } else {
                        el.classList.add('bpbm-medium-editor-placeholder');
                        el.classList.remove('bpbm-medium-editor-placeholder-relative');
                    }
                }
            },

            hidePlaceholder: function (el) {
                if (el) {
                    el.classList.remove('bpbm-medium-editor-placeholder');
                    el.classList.remove('bpbm-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);
            }
        });

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

    (function () {
        'use strict';

        var Toolbar = BPBM_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: 'bpbm-medium-editor-button-first',

            /* lastButtonClass: [string]
         * CSS class added to the last button in the toolbar.
         */
            lastButtonClass: 'bpbm-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 () {
                BPBM_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);
            },

            // Toolbar creation/deletion

            createToolbar: function () {
                var toolbar = this.document.createElement('div');

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

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

                toolbar.appendChild(this.createToolbarButtons());

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

                this.attachEventHandlers();

                return toolbar;
            },

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

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

                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 (BPBM_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('.bpbm-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 = BPBM_MediumEditor.util.throttle(function () {
                    if (this.base.isActive) {
                        this.positionToolbarIfShown();
                    }
                }.bind(this));
            },

            attachEventHandlers: function () {
                // BPBM_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) {
                // Do not trigger checkState when mouseup fires over the toolbar
                if (event &&
                    event.target &&
                    BPBM_MediumEditor.util.isDescendant(this.getToolbarElement(), event.target)) {
                    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 () {
                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('bpbm-medium-editor-toolbar-active');
            },

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

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

            isToolbarDefaultActionsDisplayed: function () {
                return this.getToolbarActionsElement().style.display === 'block';
            },

            hideToolbarDefaultActions: function () {
                if (this.isToolbarDefaultActionsDisplayed()) {
                    this.getToolbarActionsElement().style.display = 'none';
                }
            },

            showToolbarDefaultActions: function () {
                this.hideExtensionForms();

                if (!this.isToolbarDefaultActionsDisplayed()) {
                    this.getToolbarActionsElement().style.display = 'block';
                }

                // 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('<(' + BPBM_MediumEditor.util.blockContainerElementNames.join('|') + ')[^>]*>', 'g'),
                    selectionHTML = BPBM_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 = BPBM_MediumEditor.util.findAdjacentTextNodeWithContent(BPBM_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 = BPBM_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() ||
                    BPBM_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 = BPBM_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 (!BPBM_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 = BPBM_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 = BPBM_MediumEditor.selection.getSelectedParentElement(selectionRange);

                // Make sure the selection parent isn't outside of the contenteditable
                if (!this.getEditorElements().some(function (element) {
                    return BPBM_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 (BPBM_MediumEditor.util.isBPBM_MediumEditorElement(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('bpbm-medium-editor-sticky-toolbar');
                        // Stick the toolbar to the top of the window
                    } else if (scrollTop > (containerTop - toolbarHeight - this.stickyTopOffset)) {
                        toolbarElement.classList.add('bpbm-medium-editor-sticky-toolbar');
                        toolbarElement.style.top = this.stickyTopOffset + 'px';
                        // Normal static toolbar position
                    } else {
                        toolbarElement.classList.remove('bpbm-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,
                    buttonHeight = 50,
                    defaultLeft = this.diffLeft - halfOffsetWidth,
                    elementsContainer = this.getEditorOption('elementsContainer'),
                    elementsContainerAbsolute = ['absolute', 'fixed'].indexOf(window.getComputedStyle(elementsContainer).getPropertyValue('position')) > -1,
                    positions = {},
                    relativeBoundary = {},
                    middleBoundary, elementsContainerBoundary;

                // 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 < buttonHeight) {
                    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';
                } else if ((containerWidth - middleBoundary) < halfOffsetWidth) {
                    positions.left = 'auto';
                    positions.right = 0;
                } else {
                    positions.left = defaultLeft + middleBoundary;
                    positions.right = 'initial';
                }

                ['top', 'left', 'right'].forEach(function (key) {
                    toolbarElement.style[key] = positions[key] + (isNaN(positions[key]) ? '' : 'px');
                });
            }
        });

        BPBM_MediumEditor.extensions.toolbar = Toolbar;
    }());

    (function () {
        'use strict';

        var ImageDragging = BPBM_MediumEditor.Extension.extend({
            init: function () {
                BPBM_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 = 'bpbm-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 = 'bpbm-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());
                            BPBM_MediumEditor.util.insertHTMLCommand(this.document, '<img class="bpbm-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);
            }
        });

        BPBM_MediumEditor.extensions.imageDragging = ImageDragging;
    }());

    (function () {
        'use strict';

        // Event handlers that shouldn't be exposed externally

        function handleDisableExtraSpaces(event) {
            var node = BPBM_MediumEditor.selection.getSelectionStart(this.options.ownerDocument),
                textContent = node.textContent,
                caretPositions = BPBM_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 = BPBM_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 = BPBM_MediumEditor.selection.getSelectionStart(this.options.ownerDocument),
                tag = node && node.nodeName.toLowerCase();

            if (tag === 'pre') {
                event.preventDefault();
                BPBM_MediumEditor.util.insertHTMLCommand(this.options.ownerDocument, '    ');
            }

            // Tab to indent list structures!
            if (BPBM_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 = BPBM_MediumEditor.selection.getSelectionStart(this.options.ownerDocument),
                tagName = node.nodeName.toLowerCase(),
                isEmpty = /^(\s+|<br\/?>)?$/i,
                isHeader = /h\d/i;

            if (BPBM_MediumEditor.util.isKey(event, [BPBM_MediumEditor.util.keyCode.BACKSPACE, BPBM_MediumEditor.util.keyCode.ENTER]) &&
                // has a preceeding sibling
                node.previousElementSibling &&
                // in a header
                isHeader.test(tagName) &&
                // at the very end of the block
                BPBM_MediumEditor.selection.getCaretOffsets(node).left === 0) {
                if (BPBM_MediumEditor.util.isKey(event, BPBM_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 && BPBM_MediumEditor.util.isKey(event, BPBM_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 (BPBM_MediumEditor.util.isKey(event, BPBM_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
                BPBM_MediumEditor.selection.moveCursor(this.options.ownerDocument, node.nextElementSibling);

                node.previousElementSibling.parentNode.removeChild(node);

                event.preventDefault();
            } else if (BPBM_MediumEditor.util.isKey(event, BPBM_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
                BPBM_MediumEditor.selection.moveCursor(this.options.ownerDocument, p);

                // remove the list element
                node.parentElement.removeChild(node);

                event.preventDefault();
            } else if (BPBM_MediumEditor.util.isKey(event, BPBM_MediumEditor.util.keyCode.BACKSPACE) &&
                (BPBM_MediumEditor.util.getClosestTag(node, 'blockquote') !== false) &&
                BPBM_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();
                BPBM_MediumEditor.util.execFormatBlock(this.options.ownerDocument, 'p');
            } else if (BPBM_MediumEditor.util.isKey(event, BPBM_MediumEditor.util.keyCode.ENTER) &&
                (BPBM_MediumEditor.util.getClosestTag(node, 'blockquote') !== false) &&
                BPBM_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
                BPBM_MediumEditor.selection.moveCursor(this.options.ownerDocument, p);

                event.preventDefault();
            } else if (BPBM_MediumEditor.util.isKey(event, BPBM_MediumEditor.util.keyCode.BACKSPACE) &&
                BPBM_MediumEditor.util.isBPBM_MediumEditorElement(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();
                BPBM_MediumEditor.selection.moveCursor(this.options.ownerDocument, node.nextSibling);
                node.parentElement.removeChild(node);
            }
        }

        function handleKeyup(event) {
            var node = BPBM_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 (BPBM_MediumEditor.util.isBPBM_MediumEditorElement(node) && node.children.length === 0 && !BPBM_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 (BPBM_MediumEditor.util.isKey(event, BPBM_MediumEditor.util.keyCode.ENTER) &&
                !BPBM_MediumEditor.util.isListItem(node) &&
                !BPBM_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[bpbm-medium-editor-textarea-id="' + editable.getAttribute('bpbm-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._BPBM_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._BPBM_MediumEditors = [null];
            }

            // If this already has a unique id, re-use it
            if (!this.id) {
                this.id = win._BPBM_MediumEditors.length;
            }

            win._BPBM_MediumEditors[this.id] = this;
        }

        function removeFromEditors(win) {
            if (!win._BPBM_MediumEditors || !win._BPBM_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._BPBM_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 (BPBM_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 (BPBM_MediumEditor.util.isElement(el) &&
                        !el.getAttribute('data-bpbm-medium-editor-element') &&
                        !el.getAttribute('bpbm-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[bpbm-medium-editor-textarea-id="' + element.getAttribute('bpbm-medium-editor-textarea-id') + '"]');
            if (textarea) {
                // Un-hide the textarea
                textarea.classList.remove('bpbm-medium-editor-hidden');
                textarea.removeAttribute('bpbm-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 = 'bpbm-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 = 'bpbm-medium-editor-' + now;
            }

            div.className = textarea.className;
            div.id = uniqueId;
            div.innerHTML = textarea.value;

            textarea.setAttribute('bpbm-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].value);
                }
            }

            // 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('bpbm-medium-editor-hidden');
            textarea.parentNode.insertBefore(
                div,
                textarea
            );

            return div;
        }

        function initElement(element, editorId) {
            if (!element.getAttribute('data-bpbm-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 = BPBM_MediumEditor.util.guid();

                element.setAttribute('data-bpbm-medium-editor-element', true);
                element.classList.add('bpbm-medium-editor-element');
                element.setAttribute('role', 'textbox');
                element.setAttribute('aria-multiline', true);
                element.setAttribute('data-bpbm-medium-editor-editor-index', editorId);
                // TODO: Merge data-bpbm-medium-editor-element and bpbm-medium-editor-index attributes for 6.0.0
                // bpbm-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('bpbm-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 = BPBM_MediumEditor.util.extend({}, this.options.toolbar, {
                    allowMultiParagraphSelection: this.options.allowMultiParagraphSelection // deprecated
                });
                toolbarExtension = new BPBM_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) {
                        BPBM_MediumEditor.util.deprecated(pair[0], pair[1], 'v6.0.0');
                    }
                });
            }

            return BPBM_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 BPBM_MediumEditor.util.execFormatBlock(this.options.ownerDocument, match[1]);
            }

            if (action === 'fontSize') {
                // TODO: Deprecate support for opts.size in 6.0.0
                if (opts.size) {
                    BPBM_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) {
                    BPBM_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 BPBM_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 = BPBM_MediumEditor.selection.getSelectedParentElement(BPBM_MediumEditor.selection.getSelectionRange(this.options.ownerDocument));
                if (parentNode) {
                    cleanupJustifyDivFragments.call(this, BPBM_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>
                            BPBM_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 = {};

        BPBM_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 BPBM_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-bpbm-medium-editor-element');
                    element.classList.remove('bpbm-medium-editor-element');
                    element.removeAttribute('role');
                    element.removeAttribute('aria-multiline');
                    element.removeAttribute('bpbm-medium-editor-index');
                    element.removeAttribute('data-bpbm-medium-editor-editor-index');

                    // Remove any elements created for textareas
                    if (element.getAttribute('bpbm-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 = BPBM_MediumEditor.util.extend({}, this.options.anchor, opts);
                        extension = new BPBM_MediumEditor.extensions.anchor(merged);
                        break;
                    case 'anchor-preview':
                        extension = new BPBM_MediumEditor.extensions.anchorPreview(this.options.anchorPreview);
                        break;
                    case 'autoLink':
                        extension = new BPBM_MediumEditor.extensions.autoLink();
                        break;
                    case 'fileDragging':
                        extension = new BPBM_MediumEditor.extensions.fileDragging(opts);
                        break;
                    case 'fontname':
                        extension = new BPBM_MediumEditor.extensions.fontName(this.options.fontName);
                        break;
                    case 'fontsize':
                        extension = new BPBM_MediumEditor.extensions.fontSize(opts);
                        break;
                    case 'keyboardCommands':
                        extension = new BPBM_MediumEditor.extensions.keyboardCommands(this.options.keyboardCommands);
                        break;
                    case 'paste':
                        extension = new BPBM_MediumEditor.extensions.paste(this.options.paste);
                        break;
                    case 'placeholder':
                        extension = new BPBM_MediumEditor.extensions.placeholder(this.options.placeholder);
                        break;
                    default:
                        // All of the built-in buttons for BPBM_MediumEditor are extensions
                        // so check to see if the extension we're creating is a built-in button
                        if (BPBM_MediumEditor.extensions.button.isBuiltInButton(name)) {
                            if (opts) {
                                merged = BPBM_MediumEditor.util.defaults({}, opts, BPBM_MediumEditor.extensions.button.prototype.defaults[name]);
                                extension = new BPBM_MediumEditor.extensions.button(merged);
                            } else {
                                extension = new BPBM_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') {
                    BPBM_MediumEditor.util.cleanListDOM(this.options.ownerDocument, this.getSelectedParentElement());
                }

                this.checkSelection();
                return result;
            },

            getSelectedParentElement: function (range) {
                if (range === undefined) {
                    range = this.options.contentWindow.getSelection().getRangeAt(0);
                }
                return BPBM_MediumEditor.selection.getSelectedParentElement(range);
            },

            selectAllContents: function () {
                var currNode = BPBM_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) {
                BPBM_MediumEditor.selection.selectNode(element, this.options.ownerDocument);

                var selElement = BPBM_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 BPBM_MediumEditor's elements
            exportSelection: function () {
                var selectionElement = BPBM_MediumEditor.selection.getSelectionElement(this.options.contentWindow),
                    editableElementIndex = this.elements.indexOf(selectionElement),
                    selectionState = null;

                if (editableElementIndex >= 0) {
                    selectionState = BPBM_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 BPBM_MediumEditor.exportSelection
            importSelection: function (selectionState, favorLaterSelectionAnchor) {
                if (!selectionState) {
                    return;
                }

                var editableElement = this.elements[selectionState.editableElementIndex || 0];
                BPBM_MediumEditor.selection.importSelection(selectionState, editableElement, this.options.ownerDocument, favorLaterSelectionAnchor);
            },

            restoreSelection: function () {
                this.importSelection(this.selectionState);
            },

            createLink: function (opts) {
                var currentEditor = BPBM_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) {
                        BPBM_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 = BPBM_MediumEditor.util.getClosestBlockContainer(currRange.startContainer);
                            endContainerParentElement = BPBM_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
                                    BPBM_MediumEditor.selection.select(
                                        this.options.ownerDocument,
                                        parentElement.firstChild,
                                        0,
                                        parentElement.lastChild,
                                        parentElement.lastChild.nodeType === 3 ?
                                            parentElement.lastChild.nodeValue.length : parentElement.lastChild.childNodes.length
                                    );
                                } else {
                                    BPBM_MediumEditor.selection.select(
                                        this.options.ownerDocument,
                                        parentElement,
                                        0,
                                        parentElement,
                                        parentElement.childNodes.length
                                    );
                                }

                                var modifiedExportedSelection = this.exportSelection();

                                textNodes = BPBM_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
                                BPBM_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
                                BPBM_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') {
                                BPBM_MediumEditor.util.setTargetBlank(BPBM_MediumEditor.selection.getSelectionStart(this.options.ownerDocument), targetUrl);
                            } else {
                                BPBM_MediumEditor.util.removeTargetBlank(BPBM_MediumEditor.selection.getSelectionStart(this.options.ownerDocument), targetUrl);
                            }

                            if (opts.buttonClass) {
                                BPBM_MediumEditor.util.addClassToAnchors(BPBM_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 || BPBM_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 bpbm-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('bpbm-medium-editor-index')], index);
                    }
                    return;
                }

                this.elements.forEach(function (el, idx) {
                    this.setContent(initialContent[el.getAttribute('bpbm-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('bpbm-medium-editor-textarea-id') && el.parentNode) {
                            return el.parentNode.querySelector('div[bpbm-medium-editor-textarea-id="' + el.getAttribute('bpbm-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('bpbm-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);
            }
        };

        BPBM_MediumEditor.getEditorFromElement = function (element) {
            var index = element.getAttribute('data-bpbm-medium-editor-editor-index'),
                win = element && element.ownerDocument && (element.ownerDocument.defaultView || element.ownerDocument.parentWindow);
            if (win && win._BPBM_MediumEditors && win._BPBM_MediumEditors[index]) {
                return win._BPBM_MediumEditors[index];
            }
            return null;
        };
    }());

    (function () {
        // summary: The default options hash used by the Editor

        BPBM_MediumEditor.prototype.defaults = {
            activeButtonClass: 'bpbm-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
        };
    })();

    BPBM_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 : '');
            }
        };
    };

    BPBM_MediumEditor.version = BPBM_MediumEditor.parseVersionString.call(this, ({
        // grunt-bump looks for this:
        'version': '5.23.3'
    }).version);

    return BPBM_MediumEditor;
}()));