[
  {
    "path": ".editorconfig",
    "content": "# EditorConfig is awesome: http://EditorConfig.org\n\nroot = true\n\n[*]\nend_of_line = lf\ninsert_final_newline = false\nindent_style = space\nindent_size = 4\n\n[*.json]\nindent_size = 2\n\n[.*rc]\nindent_size = 2\n"
  },
  {
    "path": ".github/ISSUE_TEMPLATE.md",
    "content": "### Description\n\n[Description of the bug or feature]\n\n### Steps to reproduce\n\n1. [First step]\n2. [Second step]\n3. [and so on...]\n\n**Expected behavior:** [What you expected to happen]\n\n**Actual behavior:** [What actually happened]\n\n**Link to an example:** [If you're reporting a bug that's not reproducible on our [demo page](https://yabwe.github.io/medium-editor/demo.html), please try to reproduce it on [JSFiddle](https://jsfiddle.net/), [JS Bin](https://jsbin.com), [CodePen](http://codepen.io/) or a similar service and paste a link here]\n\n### Versions\n\n- medium-editor:\n- browser:\n- OS:\n"
  },
  {
    "path": ".github/PULL_REQUEST_TEMPLATE.md",
    "content": "| Q                | A\n| ---------------- | ---\n| Bug fix?         | yes/no\n| New feature?     | yes/no\n| BC breaks?       | yes/no\n| Deprecations?    | yes/no\n| New tests added? | yes/not needed\n| Fixed tickets    | comma-separated list of tickets fixed by the PR, if any\n| License          | MIT\n\n### Description\n\n[Description of the bug or feature]\n\n--\n\n#### Please, don't submit `/dist` files with your PR!\n"
  },
  {
    "path": ".gitignore",
    "content": "*~\n*.swp\n.DS_Store\n*.swo\nnode_modules/\n.env\n.sass-cache/\nnpm-debug.log\n.grunt/\n_SpecRunner.html\nreports/\ncoverage/\n._*\nlocal.log\nbrowserstack.err\n\n# IDE\n.idea/\npackage-lock.json\n\n\n"
  },
  {
    "path": ".jscsrc",
    "content": "{\n  \"disallowEmptyBlocks\": true,\n  \"disallowKeywordsOnNewLine\": [\n    \"else\"\n  ],\n  \"disallowMixedSpacesAndTabs\": true,\n  \"disallowMultipleLineBreaks\": true,\n  \"disallowMultipleLineStrings\": true,\n  \"disallowMultipleSpaces\": true,\n  \"disallowNewlineBeforeBlockStatements\": true,\n  \"disallowSpaceAfterPrefixUnaryOperators\": [\n    \"++\",\n    \"--\",\n    \"+\",\n    \"-\",\n    \"~\",\n    \"!\"\n  ],\n  \"disallowSpaceAfterObjectKeys\": true,\n  \"disallowSpaceBeforePostfixUnaryOperators\": [\n    \"++\",\n    \"--\"\n  ],\n  \"disallowSpacesInCallExpression\": true,\n  \"disallowSpacesInFunctionDeclaration\": {\n    \"beforeOpeningRoundBrace\": true\n  },\n  \"disallowSpacesInsideArrayBrackets\": true,\n  \"disallowSpacesInsideBrackets\": true,\n  \"disallowSpacesInsideParentheses\": true,\n  \"disallowTrailingComma\": true,\n  \"disallowTrailingWhitespace\": true,\n  \"requireBlocksOnNewline\": true,\n  \"requireCamelCaseOrUpperCaseIdentifiers\": true,\n  \"requireCapitalizedConstructors\": true,\n  \"requireCommaBeforeLineBreak\": true,\n  \"requireCurlyBraces\": [\n    \"if\",\n    \"else\",\n    \"for\",\n    \"while\",\n    \"do\",\n    \"try\",\n    \"catch\"\n  ],\n  \"requireLineBreakAfterVariableAssignment\": true,\n  \"requireMultipleVarDecl\": true,\n  \"requireOperatorBeforeLineBreak\": [\n    \"?\",\n    \"=\",\n    \"+\",\n    \"-\",\n    \"/\",\n    \"*\",\n    \"==\",\n    \"===\",\n    \"!=\",\n    \"!==\",\n    \">\",\n    \">=\",\n    \"<\",\n    \"<=\"\n  ],\n  \"requireSemicolons\": true,\n  \"requireSpaceAfterBinaryOperators\": [\n    \"=\",\n    \",\",\n    \"+\",\n    \"-\",\n    \"/\",\n    \"*\",\n    \"==\",\n    \"===\",\n    \"!=\",\n    \"!==\"\n  ],\n  \"requireSpaceAfterKeywords\": [\n    \"do\",\n    \"for\",\n    \"if\",\n    \"else\",\n    \"switch\",\n    \"case\",\n    \"try\",\n    \"catch\",\n    \"void\",\n    \"while\",\n    \"with\",\n    \"return\",\n    \"typeof\",\n    \"function\"\n  ],\n  \"requireSpaceBeforeBinaryOperators\": [\n    \"=\",\n    \"+\",\n    \"-\",\n    \"/\",\n    \"*\",\n    \"==\",\n    \"===\",\n    \"!=\",\n    \"!==\"\n  ],\n  \"requireSpaceBeforeBlockStatements\": true,\n  \"requireSpaceBeforeKeywords\": [\n    \"else\",\n    \"while\",\n    \"catch\"\n  ],\n  \"requireSpaceBetweenArguments\": true,\n  \"requireSpacesInAnonymousFunctionExpression\": {\n    \"beforeOpeningRoundBrace\": true,\n    \"beforeOpeningCurlyBrace\": true\n  },\n  \"requireSpacesInConditionalExpression\": {\n    \"afterTest\": true,\n    \"beforeConsequent\": true,\n    \"afterConsequent\": true,\n    \"beforeAlternate\": true\n  },\n  \"requireSpacesInForStatement\": true,\n  \"requireSpacesInFunctionDeclaration\": {\n    \"beforeOpeningCurlyBrace\": true\n  },\n  \"requireSpacesInFunction\": {\n    \"beforeOpeningCurlyBrace\": true\n  },\n  \"requireSpacesInsideObjectBrackets\": {\n    \"allExcept\": [ \"}\", \")\" ]\n  },\n  \"validateIndentation\": 4,\n  \"validateParameterSeparator\": \", \",\n  \"validateQuoteMarks\": \"'\"\n}\n"
  },
  {
    "path": ".jshintrc",
    "content": "{\n  \"boss\": true,\n  \"browser\": true,\n  \"curly\": true,\n  \"eqeqeq\": true,\n  \"eqnull\": true,\n  \"immed\": true,\n  \"latedef\": \"nofunc\",\n  \"newcap\": false,\n  \"noarg\": true,\n  \"predef\": [ \"MediumEditor\",\n              \"afterAll\", \"afterEach\", \"beforeAll\", \"beforeEach\", \"describe\", \"expect\", \"it\", \"jasmine\", \"spyOn\",\n              \"setupTestHelpers\" ],\n  \"sub\": true,\n  \"undef\": true,\n  \"unused\": true,\n  \"validthis\": true\n}\n"
  },
  {
    "path": ".npmrc",
    "content": "save-exact=true\n"
  },
  {
    "path": ".travis.yml",
    "content": "# faster builds on new travis setup not using sudo\nsudo: false\n\n# cache vendor dirs\ncache:\n    directories:\n        - node_modules\n\nlanguage: node_js\n\nnode_js:\n    - \"12\"\n\nnotifications:\n    email: false\n    webhooks:\n        urls:\n            - https://webhooks.gitter.im/e/0913a4ced1f3322b4c40\n        on_success: change  # options: [always|never|change] default: always\n        on_failure: always  # options: [always|never|change] default: always\n        on_start: false     # default: false\n\nbefore_script:\n    - npm install -g grunt-cli\n\nscript:\n    - npm run test:ci"
  },
  {
    "path": "API.md",
    "content": "# MediumEditor Object API (v5.0.0)\n\n<!-- START doctoc generated TOC please keep comment here to allow auto update -->\n<!-- DON'T EDIT THIS SECTION, INSTEAD RE-RUN doctoc TO UPDATE -->\n\n\n- [Initialization Functions](#initialization-functions)\n  - [`MediumEditor(elements, options)`](#mediumeditorelements-options)\n  - [`destroy()`](#destroy)\n  - [`setup()`](#setup)\n  - [`addElements()`](#addelementselements)\n  - [`removeElements()`](#removeelementselements)\n- [Event Functions](#event-functions)\n  - [`on(targets, event, listener, useCapture)`](#ontargets-event-listener-usecapture)\n  - [`off(targets, event, listener, useCapture)`](#offtargets-event-listener-usecapture)\n  - [`subscribe(name, listener)`](#subscribename-listener)\n  - [`unsubscribe(name, listener)`](#unsubscribename-listener)\n  - [`trigger(name, data, editable)`](#triggername-data-editable)\n- [Selection Functions](#selection-functions)\n  - [`checkSelection()`](#checkselection)\n  - [`exportSelection()`](#exportselection)\n  - [`importSelection(selectionState, favorLaterSelectionAnchor)`](#importselectionselectionstate-favorlaterselectionanchor)\n  - [`getFocusedElement()`](#getfocusedelement)\n  - [`getSelectedParentElement(range)`](#getselectedparentelementrange)\n  - [`restoreSelection()`](#restoreselection)\n  - [`saveSelection()`](#saveselection)\n  - [`selectAllContents()`](#selectallcontents)\n  - [`selectElement(element)`](#selectelementelement)\n  - [`stopSelectionUpdates()`](#stopselectionupdates)\n  - [`startSelectionUpdates()`](#startselectionupdates)\n- [Editor Action Functions](#editor-action-functions)\n  - [`cleanPaste(text)`](#cleanpastetext)\n  - [`createLink(opts)`](#createlinkopts)\n  - [`execAction(action, opts)`](#execactionaction-opts)\n  - [`pasteHTML(html, options)`](#pastehtmlhtml-options)\n  - [`queryCommandState(action)`](#querycommandstateaction)\n- [Helper Functions](#helper-functions)\n  - [`checkContentChanged(editable)`](#checkContentChangededitable)\n  - [`delay(fn)`](#delayfn)\n  - [`getContent(index)`](#getcontentindex)\n  - [`getExtensionByName(name)`](#getextensionbynamename)\n  - [`resetContent(element)`](#resetcontentelement)\n  - [`serialize()`](#serialize)\n  - [`setContent(html, index)`](#setcontenthtml-index)\n- [Static Functions/Properties](#static-functionsproperties)\n  - [`getEditorFromElement(element)`](#geteditorfromelementelement)\n  - [`version`](#version)\n\n<!-- END doctoc generated TOC please keep comment here to allow auto update -->\n\n## Initialization Functions\n\n### `MediumEditor(elements, options)`\n\nCreating an instance of MediumEditor will:\n* Convert all passed in elements into `contenteditable` elements.\n* For any `<textarea>` elements:\n  * Hide the `<textarea>`\n  * Create a new `<div contenteditable=true>` element and add it to the elements array.\n  * Ensure the 2 elements remain sync'd.\n* Initialize any custom extensions or buttons passed in.\n* Create any additional elements needed.\n* Setup all event handling needed to monitor the editable elements.\n\n**Arguments**\n\n_**elements** (`String` | `HTMLElement` | `Array` | `NodeList` | `HTMLCollection`)_:\n\n1. `String`: If passed as a string, this is used as a selector in a call to `document.querySelectorAll()` to find elements on the page.  All results are stored in the internal list of **elements**.\n\n2. `HTMLElement`: If passed as a single element, this will be the only element in the internal list of **elements**.\n\n3. `Array`: If passed as an `Array` of `HTMLElement`s, this will be used as the internal list of **elements**.\n\n_**options** (`Object`)_:\n\nSet of [custom options](OPTIONS.md) used to initialize `MediumEditor`.\n\n***\n### `destroy()`\n\nTear down the editor if already setup by doing the following:\n* Calling the `destroy()` method on each extension within the editor. This should allow all extension to be torn down and cleaned up, including the toolbar and its elements.\n* Detaching all event listeners from the DOM\n* Detaching all references to custom event listeners\n* Remove any custom attributes from the editor **elements**\n* Unhide any `<textarea>` elements and remove any created `<div>` elements created for `<textarea>` elements.\n\n***\n### `setup()`\n\nInitialize this instance of the editor if it has been destroyed.  This will reuse the `elements` selector and `options` object passed in when the editor was instantiated.\n\n***\n### `addElements(elements)`\n\nDynamically add one or more elements to an already initialized instance of MediumEditor.\n\nPassing an elements or array of elements to `addElements(elements)` will:\n* Add the given element or array of elements to the editor **elements**\n* Ensure the element(s) are initialized with the proper attributes and event handlers as if the element had been passed during instantiation of the editor\n* For any `<textarea>` elements:\n  * Hide the `<textarea>`\n  * Create a new `<div contenteditable=true>` element and add it to the editor **elements**\n  * Ensure the 2 elements remain sync'd.\n* Be intelligent enough to run the necessary code only once per element, no matter how often you will call it\n\nSo, every element you pass to `addElements` will turn into a fully supported contenteditable too - even earlier calls to `editor.subscribe(..)`\nfor custom events will work on the newly added element(s).\n\n**Arguments**\n\n_**elements** (`String` | `HTMLElement` | `Array` | `NodeList` | `HTMLCollection`)_:\n\n1. `String`: If passed as a string, this is used as a selector in a call to `document.querySelectorAll()` to find elements on the page.\n\n2. `HTMLElement`: If passed as a single element, this will be the only element added to the editor **elements**.\n\n3. `Array` | `NodeList` | `HTMLCollection`: If passed as an `Array`-like collection of `HTMLElement`s, all of these elements will be added to the editor **elements**.\n\n***\n### `removeElements(elements)`\n\nRemove one or more elements from an already initialized instance of MediumEditor.\n\nPassing an elements or array of elements to `removeElements(elements)` will:\n* Remove the given element or array of elements from the internal `this.elements` array.\n* Remove any added event handlers or attributes (with the exception of `contenteditable`).\n* Unhide any `<textarea>` elements and remove any created `<div>` elements created for `<textarea>` elements.\n\nEach element itself will remain a contenteditable - it will just remove all event handlers and all references to it so you can safely remove it from DOM.\n\n**Arguments**\n\n_**elements** (`String` | `HTMLElement` | `Array` | `NodeList` | `HTMLCollection`)_:\n\n1. `String`: If passed as a string, this is used as a selector in a call to `document.querySelectorAll()` to find elements on the page.\n\n2. `HTMLElement`: If passed as a single element, this will be the only element removed from the editor **elements**.\n\n3. `Array` | `NodeList` | `HTMLCollection`: If passed as an `Array`-like collection of `HTMLElement`s, all of these elements will be removed from the editor **elements**.\n\n***\n## Event Functions\n\n### `on(targets, event, listener, useCapture)`\n\nAttaches an event listener to a specific element or elements via the browser's built-in `addEventListener(type, listener, useCapture)` API.  However, this helper method also ensures that when MediumEditor is destroyed, this event listener will be automatically be detached from the DOM.\n\n**Arguments**\n\n1. _**targets** (`HTMLElement` / `NodeList`)_:\n\n  * Element or elements to attach listener to via `addEventListener(type, listener, useCapture)`\n\n2. _**event** (`String`)_:\n\n  * type argument for `addEventListener(type, listener, useCapture)`\n\n3. _**listener** (`function`)_:\n\n   * listener argument for `addEventListener(type, listener, useCapture)`\n\n4. _**useCapture** (`boolean`)_:\n\n   * useCapture argument for `addEventListener(type, listener, useCapture)`\n\n***\n### `off(targets, event, listener, useCapture)`\n\nDetach an event listener from a specific element or elements via the browser's built-in `removeEventListener(type, listener, useCapture)` API.\n\n**Arguments**\n\n1. _**targets** (`HTMLElement` / `NodeList`)_:\n\n  * Element or elements to detach listener from via `removeEventListener(type, listener, useCapture)`\n\n2. _**event** (`String`)_:\n\n  * type argument for `removeEventListener(type, listener, useCapture)`\n\n3. _**listener** (`function`)_:\n\n   * listener argument for `removeEventListener(type, listener, useCapture)`\n\n4. _**useCapture** (`boolean`)_:\n\n   * useCapture argument for `removeEventListener(type, listener, useCapture)`\n\n***\n### `subscribe(name, listener)`\n\nAttaches a listener for the specified custom event name.\n\n**Arguments**\n\n1. _**name** (`String`)_:\n\n  * Name of the event to listen to.  See the list of built-in [Custom Events](CUSTOM-EVENTS.md).\n\n2. _**listener(data, editable)** (`function`)_:\n\n  * Listener method that will be called whenever the custom event is triggered.\n\n**Arguments to listener**\n\n  1. _**data** (`Event` | `object`)_\n    * For most custom events, this will be the browser's native `Event` object for the event that triggered the custom event to fire.\n    * For some custom events, this will be an object containing information describing the event (depending on which custom event it is)\n  2. _**editable** (`HTMLElement`)_\n    * A reference to the contenteditable container element that this custom event corresponds to.  This is especially useful for instances where one instance of MediumEditor contains multiple elements, or there are multiple instances of MediumEditor on the page.\n    * For example, when `blur` fires, this argument will be the `<div contenteditable=true></div>` element that is about to receive focus.\n\n***\n### `unsubscribe(name, listener)`\n\nDetaches a custom event listener for the specified custom event name.\n\n**Arguments**\n\n1. _**name** (`String`)_:\n\n  * Name of the event to detach the listener for.\n\n2. _**listener** (`function`)_:\n\n  * A reference to the listener to detach.  This must be a match by-reference and not a copy.\n\n**NOTE**\n\n  * Calling [destroy()](#destroy) on the MediumEditor object will automatically remove all custom event listeners.\n\n***\n### `trigger(name, data, editable)`\n\nManually triggers a custom event.\n\n**Arguments**\n\n1. _**name** (`String`)_:\n\n  * Name of the custom event to trigger.\n\n2. _**data** (`Event` | `object`)_:\n\n  * Native `Event` object or custom data object to pass to all the listeners to this custom event.\n\n3. _**editable** (`HTMLElement`)_:\n\n  * The `<div contenteditable=true></div>` element to pass to all of the listeners to this custom event.\n\n***\n## Selection Functions\n\n### `checkSelection()`\n\nIf the toolbar is enabled, manually forces the toolbar to update based on the user's current selection.  This includes hiding/showing the toolbar, positioning the toolbar, and updating the enabled/disable state of the toolbar buttons.\n\n***\n### `exportSelection()`\n\nReturns a data representation of the selected text, which can be applied via `importSelection(selectionState)`.  This data will include the beginning and end of the selection, as well as which of the editor **elements** the selection was within.\n\n***\n### `importSelection(selectionState, favorLaterSelectionAnchor)`\n\nRestores the selection using a data representation of previously selected text (ie value returned by `exportSelection()`).\n\n**Arguments**\n\n1. _**selectionState** (`Object`)_:\n\n  * Data representing the state of the selection to restore.\n\n2. _**favorLaterSelectionAnchor** (`boolean`)_:\n\n  * 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 Internet Explorer.\n\n***\n### `getFocusedElement()`\n\nReturns a reference to the editor **element** that currently has focus (if the editor has focus).\n\n***\n### `getSelectedParentElement(range)`\n\nReturns a reference to the editor **element** that the user's selection is currently within.\n\n**Arguments**\n\n1. _**range** (`Range`)_: _**OPTIONAL**_\n  * The `Range` to find the selection parent element within\n  * If no element is provided, the editor will use the current range within the selection of the editor's `contentWindow`\n\n***\n### `restoreSelection()`\n\nRestores the selection to what was selected the last time `saveSelection()` was called.\n\n***\n### `saveSelection()`\n\nInternally stores the user's current selection.  This can be restored by calling `restoreSelection()`.\n\n***\n### `selectAllContents()`\n\nExpands the selection to contain all text within the focused editor **element**.\n\n***\n### `selectElement(element)`\n\nChange the user's selection to select the contents of the provided element and update the toolbar to reflect this change.\n\n**Arguments**\n\n1. _**element** (`HTMLElement`)_:\n\n  * DOM Element -- which is a descendant of one of the editor's **elements** -- to select.\n\n***\n### `stopSelectionUpdates()`\n\nStop the toolbar from updating to reflect changes in the user's selection.\n\n***\n### `startSelectionUpdates()`\n\nEnable the toolbar to start updating based on the user's selection, after a call to `stopSelectionUpdates()`\n\n***\n## Editor Action Functions\n\n### `cleanPaste(text)`\n_convert text to plaintext and replace current selection with result_\n\n**Arguments**\n\n1. _**text** (`String`)_:\n\n  * Content to be pasted at the location of the current selection/cursor\n\n***\n### `createLink(opts)`\n_creates a link via the native `document.execCommand('createLink')` command_\n\n**Arguments**\n\n1. _**opts** (`Object`)_:\n\n  * Object containing additional properties needed for creating a link\n\n  **Properties of 'opts'**\n\n    1. _**value** (`String`)_ _**REQUIRED**_\n      * The url to set as the `href` of the created link.  A non-empty value must be provided for the link to be created.\n    2. _**target** (`String`)_\n      * Attribute to set as the `target` attribute of the created link.  Passing 'self' or not passing this option at all are equivalent in that they will just ensure that `target=\"_blank\"` will NOT be present on the created link.\n      * **NOTE** If the `targetBlank` option on the editor is set to true, the `target` property of opts will be ignored and `target=\"_blank\"` will be added to all created links.\n    3. _**buttonClass** (`String`)_\n      * Class (or classes) to append to the `class` attribute of the created link.\n\n##### Example\n\n```js\neditor.createLink({ value: 'https://github.com/yabwe/medium-editor', target: '_blank', buttonClass: 'medium-link' });\n```\n\n***\n### `execAction(action, opts)`\n_executes an built-in action via document.execCommand_\n\n**Arguments**\n\n1. _**action** (`String`)_:\n\n  * Action to be passed as the 'command' argument to `document.execCommand(command, showDefaultUI, value)`\n\n2. _**opts** (`Object`)_ _**OPTIONAL**_:\n\n  * Object containing additional properties for specific commands\n\n  **Properties of 'opts'**\n\n    1. _**value** (`String`)_\n      * The value to pass as the 'value' argument to `document.execCommand(command, showDefaultUI, value)`\n    2. For 'createLink', the `opts` are passed directly to [`.createLink(opts)`]((#createlinkopts)) so see that method for additional options for that command\n\n***\n### `pasteHTML(html, options)`\n_replace the current selection with html_\n\n**Arguments**\n\n1. _**html** (`String`)_:\n\n  * Content to be pasted at the location of the current selection/cursor\n\n2. _**options** (`Object`)_ _**OPTIONAL**_:\n\n  * Optional overrides for `cleanTags`, `unwrapTags`, and/or `cleanAttrs` for removing/unwrapping specific element types (`cleanTags`/`unwrapTags`),  or removing specific attributes (`cleanAttrs`) from the inserted HTML.  See [cleanTags](OPTIONS.md#cleantags), [unwrapTags](OPTIONS.md#unwraptags), and [cleanAttrs](OPTIONS.md#cleanattrs) in OPTIONS.md for more information.\n\n##### Example\n\n```js\neditor.pasteHTML('<p class=\"classy\"><strong>Some Custom HTML</strong></p>', { cleanAttrs: ['class'], cleanTags: ['strong'], unwrapTags: ['em']});\n```\n\n***\n### `queryCommandState(action)`\n_wrapper around the browser's built in `document.queryCommandState(command)` for checking whether a specific action has already been applied to the selection._\n\n**Arguments**\n\n1. _**action** (`String`)_:\n\n  * Action to be passed as the 'command' argument to `document.queryCommandState(command)`\n\n***\n## Helper Functions\n\n### `checkContentChanged(editable)`\n\nTrigger the editor to check for updates to the html, and trigger the `editableInput` event if needed.\n\n**Arguments**\n\n1. _**editable** (`HTMLElement`)_: _**OPTIONAL**_\n  * The `<div contenteditable=true></div>` element that contains the html that may have changed.\n  * If no element is provided, the editor will check the currently 'active' editor element (the element with focus).\n\n### `delay(fn)`\n\nDelay any function from being executed by the amount of time passed as the **delay** option.\n\n**Arguments**\n\n1. _**fn** (`function`)_:\n\n  * Function to delay execution for.\n\n***\n### `getContent(index)`\n\nReturns the trimmed html content for the first editor **element**, or the **element** at `index`.\n\n**Arguments**\n\n1. _**index** (`integer`)_: _**OPTIONAL**_\n  * Index of the editor **element** to retrieve the content from. Defaults to 0 when not provided (returns content of the first editor **element**).\n\n***\n### `getExtensionByName(name)`\n\nGet a reference to an extension with the specified name.\n\n**Arguments**\n\n1. _**name** (`String`)_:\n\n  * The name of the extension to retrieve (ie `toolbar`).\n\n***\n### `resetContent(element)`\n\nReset the content of all editor **elements** to their value at the time they were added to the editor.  If a specific editor **element** is provided, only the content of that element will be reset.\n\n**Arguments**\n\n1. _**element** (`DOMElement`)_: _**OPTIONAL**_\n\n  * Specific editor **element** to reset the content of.\n\n***\n### `serialize()`\n\nReturns a JSON object including the content of each of the **elements** inside the editor.\n\n***\n### `setContent(html, index)`\n\nSets the html content for the first editor **element**, or the **element** at `index`. Ensures the the `editableInput` event is triggered.\n\n**Arguments**\n\n1. _**html** (`string`)_:\n  * The content to set the element to\n\n2. _**index** (`integer`)_: _**OPTIONAL**_\n  * Index of the editor **element** to set the content of. Defaults to 0 when not provided (sets content of the first editor **element**).\n\n***\n## Static Functions/Properties\n\n### `getEditorFromElement(element)`\n\nGiven an editor **element**, retrieves the instance of MediumEditor which created/is monitoring the **element**\n\n**Arguments**\n\n1. _**element** (`DOMElement`)_:\n  * An editor **element** which is part of a MediumEditor instance\n\n### `version`\n\nObject containing data about the version of the current MediumEditor library\n\n**Properties of 'version'**\n\n1. _**major** (`Number`)_\n  * The major version number (ie the `3` in `\"3.2.1\"`)\n2. _**minor** (`Number`)_\n  * The minor version number (ie the `2` in `\"3.2.1\"`)\n3. _**revision** (`Number`)_\n  * The revision (aka \"patch\") version number (ie the `1` in `\"3.2.1\"`)\n4. _**preRelease** (`String`)_\n  * The pre-release version tag (ie the `\"rc.1\"` in `\"5.0.0-rc.1\"`)\n5. _**toString** (`Function`)_\n  * Returns the full version number as a string (ie `\"5.0.0-rc.1\"`)\n"
  },
  {
    "path": "CHANGES.md",
    "content": "5.23.3 / 2017-12-20\n==================\n* Fix medium-editor-insert plugin css fixes on beagle theme #1361\n* Update jsDelivr links #1366 & #1367\n* Fix Firefox console warning causing issues #1370\n* Do not check only for null targets or it will fail when it's undefined. #1373\n* Fix crash when 'extensions' in 'isElementDescendantOfExtension' is undefined #1362\n* Fix Jasmine Unit Test errors #1385\n* Fix null error on pastedPlain.split #1397\n* Fix broken browser tests #1396\n\n\n5.23.2 / 2017-08-02\n==================\n* Add noopener & noreferrer into targetBlank #1355\n* Add undefined check and fallback in Paste extension #1346\n\n\n5.23.1 / 2017-06-27\n==================\n* Remove src from bower ignored files #1330\n* Add label-checkbox relation in CreateLink form #1275 #1340\n\n\n5.23.0 / 2017-03-02\n==================\n* Only add schemes to URLs with hostnames #1258\n* Fix problem with addClassToAnchors #1293\n* Adding new 'html' button from #1235 #1291\n* Don't encode fragment as part of linkValidation #1257\n\n\n5.22.2 / 2017-01-19\n==================\n* Efficiency: Compile RegEx once #1230\n* Error in console at link selection #1249\n* Check for this.anchorPreview when hiding #1280\n* Save some CPU calculations #1271\n\n\n5.22.1 / 2016-09-29\n==================\n* Fix encoded urls (in linkValidaton) #1219\n* Fix CommonJS environment #1221\n\n\n5.22.0 / 2016-09-03\n==================\n* Add new extensions to extensions README #1188\n* Fix iframe div #1179\n* Fix placeholder text color in flat theme #1192\n* Add unwrapTags option to paste extension #1177\n* Remove first empty paragraph on backspace #1187\n* Update grunt-contrib-jasmine #1185\n* Added Embed Button links to README #1183\n\n\n5.21.1 / 2016-08-11\n==================\n* Make linkValidation allow hash links #1143\n* Fix toolbar in absolute container #1152\n* Fix caret issue in blockquote #1162\n* Handle new Google Docs font weights #1168\n* Add external button example #1175\n\n\n5.21.0 / 2016-06-21\n==================\n* Fix issue with electron environment #1125\n* Fix for paste and placeholder extensions & add/remove element events #1124\n* Placeholder is visible when only empty table is in Editor #1128\n\n\n5.20.2 / 2016-06-17\n==================\n(5.20.1 was skipped because of a bad release)\n* Fix test failure in Chrome 51 #1114\n* Fix slow CSS selector #1115\n* Improve documentation for toolbar.relativeContainer option #1122\n* Fix cursor rendering incorrectly in Firefox #1113\n\n\n5.20.0 / 2016-06-02\n==================\n* Fix anchor-preview bug where click preview no longer prefills href into anchor form\n* Add getEditorFromElement for retrieving an editor instance from an editor element\n* Respect form.reset for textarea elements within forms passed into the editor\n* Add getContent + resetContent helpers for retrieving/reverting content of editors\n* Add support for extensions preventing blur on editor when user interacts with extension elements\n\n\n5.19.1 / 2016-05-28\n==================\n* Add feature for toggling anchor preview for empty or # links\n* Fix keyboard paste to properly fire editablePaste\n* Standardize editablePaste to always fire with mock event object\n\n\n5.18.0 / 2016-05-21\n==================\n* Add support calling document.execCommand with arbitrary argument from execAction\n  * Also deprecate custom execAction option names in favor of standard .value\n* Fix error from addElements when initializing editor with no elements\n\n\n5.17.0 / 2016-05-17\n==================\n* Improved paste handling\n  * Includes proper support for keyboard paste in firefox\n  * More cleanup when pasting from Word\n* Introduce support for adding and removing elements from an existing editor instances\n  * New addElements and removeElements methods\n* Add checkContentChanged method for manually triggering editableInput\n* Add selection.selectRange helper method\n\n\n5.16.1 / 2016-04-14\n==================\n* Fix incorrect word breaking\n\n\n5.16.0 / 2016-04-12\n==================\n* Add support for multiple targets for attaching/detach event handlers\n* Add support for chaining calls to attach/detach events\n* Fix issue with click anchor-preview when using showWhenToolbarIsVisible\n* Fix IE issue with line-breaking within editor\n* Fix Firefox error when using elements other than divs as editor\n\n\n5.15.1 / 2016-04-05\n==================\n* Fix link validation in anchor extension\n* Improve performance when dealing with a lot of data\n* Enable functions to be used as keyboard commands\n\n\n5.15.0 / 2016-03-23\n==================\n* Use class instead of inline style for hiding/showing anchor form\n* Add helpers for hiding/showing form into form extension base class\n* Fix issue where auto-link extension re-enabled IE's built-in auto-link when other instances still existed\n* Fix anchor form to display form before attempting to position correctly\n* Add new selection.clearSelection() helper method for moving cursor to beginning/end of selection\n\n\n5.14.4 / 2016-02-25\n==================\n* editableInput event fixes\n  * Fix issue with event not triggering when dragging in images\n  * Fix issue with event not triggering on autolink\n  * Fix issue with event not triggering on insertHTML in Edge\n* Fix issue with hitting enter when directly inside figcaption and other block elements\n\n\n5.14.3 / 2016-02-22\n==================\n* Fix behaviour of \"Open in new window\" checkbox for Firefox\n* Added instruction to disable file dragging all together\n* Fix issue with image dragging and dropping at end of target\n* Fix issue with extra space when space already exists\n\n\n5.14.2 / 2016-02-10\n==================\n* Support Microsoft Edge\n  * Fallback to custom insertHTML command instead of built-in command for Edge\n  * Use shim code for detecting input on contenteditable for Edge\n  * Fix issue with converting blockquotes to paragraphs in Edge\n  * Update documentation, fix tests, and include Edge in browser testing\n\n\n5.14.1 / 2016-02-05\n==================\n* Fix issue with saving selection after newline and whitespace text nodes\n* Fix import/export selection to prefer start of nodes over end of nodes\n* Fix for getClosestBlockContainer utility function\n* Fix for getTopBlockContainer utility function\n* Deprecate getFirstTextNode utility function\n\n\n5.14.0 / 2016-01-31\n==================\n* Added pre clean replacements\n* Fixed an infinite loop\n* Handled enter event for empty h2/h3 tag\n\n\n5.13.0 / 2016-01-18\n==================\n* Added stickyTopOffset option to keep on the screen a sticky toolbar\n* Fixed misplacement of buttons when selection is near to the right edge\n* Updated dev dependencies\n* Added reference to README for who is using medium-editor\n\n\n5.12.0 / 2015-12-15\n==================\n* Fix issue with image-only selections\n* Trim src when using the image toolbar button\n* Fix auto linking with comments\n* Documented the process of releasing a new version\n\n\n5.11.0 / 2015-12-05\n==================\n* Updated table extension demo\n* Removed the carriage return character from a demo file\n* Updated checkLinkFormat function to support more schemes\n* Fixed issue with disableExtraSpaces option to allow space at the end of line\n* Use editableInput instead of input event for textarea syncing\n* Fixed style for correct positioning of placeholder\n* Allowed to remove blockquote by pressing delete at beginning of the quote\n* Fixed failing test cases in IE9 and IE10\n\n\n5.10.0 / 2015-10-30\n==================\n* Added disableExtraSpaces option for preventing errant spaces\n* Added editalbeKeydownSpace event\n* Fix issue with return key at the end of text with bad formatting\n* Added new font name extension (beta)\n* Documentation updates and cleanup\n\n\n5.9.0 / 2015-10-19\n==================\n* Add showWhenToolbarIsVisible option for displaying anchor-preview when toolbar is visible\n* Remove trailing whitespace when creating links via anchor extension\n* Fix issue with escaping list items via pressing enter\n* Fix font-awesome links in demo pages\n* Updates to documentation around creating links\n\n\n5.8.3 / 2015-10-08\n==================\n* Fix changing link on images\n\n\n5.8.2 / 2015-09-21\n==================\n* Fix type of elements which can contain auto-links\n\n\n5.8.1 / 2015-09-16\n==================\n* Fix inconsistancies and errors in isDescendant utility method\n\n\n5.8.0 / 2015-09-13\n==================\n* Added relativeContainer options for the toolbar\n* Fix issue with auto-linking across consecutive list-items\n* Added beagle theme\n\n\n5.7.0 / 2015-08-21\n==================\n* Fix backwards compatability issue with how keyboard commands extension handles 'alt'\n* Rewrite which event placeholder extension listens to for hiding/showing placeholder\n  * Fix issue where placeholder is not hidden when calling setContent()\n  * Fix issue where placeholder is displayed incorrectly when hideOnClick option is true\n\n\n5.6.3 / 2015-08-18\n==================\n* Ensure textarea ids are unique on entire page\n* Fix broken auto-link within block elements other than paragraphs\n* Fix issue with editor element being removed in IE11\n* Remove references to global variables from internal code\n\n\n5.6.2 / 2015-08-11\n==================\n* Fix a regression in the paste extension related to `pasteHTML` function\n\n\n5.6.1 / 2015-08-10\n==================\n* Fix issue with creating anchors and restoring selection at the beginning of paragraphs\n* Fix issue with creating anchors and restoring selection within list items and nested blocks\n* Ensure CTRL + M is respected as a way to insert new lines\n\n\n5.6.0 / 2015-08-07\n==================\n* Add new 'tim' theme for medium-editor toolbar\n* Fix issue Chrome generated comment tags when pasting\n* Fix issue where 'editableInput' is triggered multiple times when creating links\n\n\n5.5.4 / 2015-08-04\n==================\n* Fix issue with anchor and selection inconsitencies in IE\n\n\n5.5.3 / 2015-08-03\n==================\n* Fix issue with replacing a pre-existing link\n* Fix issue with selection after creating a link after empty paragraphs\n\n\n5.5.2 / 2015-08-02\n==================\n* Fix issue where block elements where cleaned up incorrectly when pasting\n* Fix anchor form checkboxes to reflect status of selected link\n* Fix issue with creating links in same paragraph as another link\n* Fix issue with creating links after empty paragraphs\n* Ensure all attributes are copied from textareas to divs\n\n\n5.5.1 / 2015-07-23\n==================\n* Fix appearance of anchor form when checkboxes are present\n* Fix breaking issue with standardizeSelectionStart option\n\n\n5.5.0 / 2015-07-21\n==================\n* Add setContent method into core API, which triggers editableInput\n\n\n5.4.1 / 2015-07-20\n==================\n* Fix issue where custom anchor-preview extensions weren't overriding built-in anchor preview\n* Add documentation from wiki into the source code\n\n\n5.4.0 / 2015-07-16\n==================\n* Add support for including 'alt' key in keyboard-commands\n\n\n5.3.0 / 2015-07-07\n==================\n* Fix issue with disabling image drag & drop via imageDragging option\n  * Deprecate image-dragging extension\n  * Introduce file-dragging extension\n* Ensure autolink urls respect targetBlank option\n* Expose importSelection and exportSelection as generic Selection helpers\n\n\n5.2.0 / 2015-06-29\n==================\n* Move allowMultiParagraphSelection into toolbar options\n  * Deprecate global allowMultiParagraphSelection option\n* Fix issue with allowMultiParagraphSelection option over empty elements\n* Fix issue with creating links producing multiple anchor tags\n* Fix issue where anchor preview displays while toolbar is visible\n* Add demo pages for example extension and example button\n\n\n5.1.0 / 2015-06-26\n==================\n* Add showToolbarDefaultAction helper method to form extension\n* Ensure elements generated for textareas have a unique id\n* Ensure all added attributes are removed during destroy\n* Cleanup divs generated by Chrome during justify actions\n* Add parameter to anchorPreview.positionPreview for reusability\n\n\n5.0.0 / 2015-06-18\n==================\n* All deprecated functions have been removed\n* Keyboard Shorcuts are now part of an extension and not attached to specific button/commands\n* Placeholders are now part of an extension with its own dedicated options\n* Toolbar is now an extension with its own dedicated options\n* firstHeader and secondHeader are gone you should use h1 thru h6\n* Support pre-releases\n* Buttons\n  * The array of buttons can now contain objects, for overriding any part of the button object\n    * This replaces the custom object value for the buttonLabels option\n* API\n  * Unique id for MediumEditor instance will now remain unique (regardless of how many instances are created)\n  * .statics references are gone\n  * .trigger supports triggering events without needing to declare the event\n  * .callExtensions(), .setToolbarPosition(), and .hideToolbarDefaultActions() have been removed\n* Extension\n  * .window & .document are now exposed as members of the Extension\n  * init no longer is passed MediumEditor instance as first argument\n* CSS\n  * All classes are now `medium-editor` prefixed\n* Util\n  * getProp, derives, getSelectionData, setObject & getObject are gone\n  * getSelectionRange & getSelectionStart are now in Selection\n\n\n4.12.5 / 2015-06-16\n==================\n* Fix issue with restoring selection within nested block elements\n\n\n4.12.4 / 2015-06-15\n==================\n* Ensure auto-link will never select an empty element (br, hr, input, etc.)\n\n\n4.12.3 / 2015-06-12\n==================\n* Fix bug with un-linked auto-links causing unexpected cursor positioning\n\n\n4.12.2 / 2015-06-10\n==================\n* Fix broken keyboard shortcuts\n\n\n4.12.1 / 2015-06-02\n==================\n* Fix break with updateOnEmptySelection option for static toolbars\n\n\n4.12.0 / 2015-06-01\n==================\n* Fix pasting links when targetBlank option is being used\n* Fix for spellcheck option after destroy\n* Fix over-reaching keyboard shortcuts for commands\n* Expose new 'positionToolbar' custom event\n* Add new isKey() helper in util\n* Add cleanup on destroy for auto-link and placeholder extensions\n* Base extension changes\n  * Add getEditorElements(), getEditorId(), and getEditorOption() helpers\n  * Add on(), off(), subscribe(), and execAction() helpers\n  * Introduce destroy() lifecycle method + deprecate deactivate()\n\n\n4.11.1 / 2015-05-26\n==================\n* Fix issue with auto-linked text after manually unlinking\n* Fix some incorrect TLDs for auto-link\n\n\n4.11.0 / 2015-05-26\n==================\n* Add hideToolbar and showToolbar custom events\n* Add hideOnClick option for placeholder extension\n* Fix issue with linebreaks in Safari\n* Fix issue with calling setup again after destroy\n* Add support for CDN hosting\n* Pass window and document to each extension\n* Deprecate .parent property of extensions\n\n\n4.10.2 / 2015-05-21\n==================\n* Auto-Link Fixes\n  * Don't auto-link text after it is manually unlinked\n  * Trigger auto-linking when focus is lost (ie Tab key)\n  * Fix issue where link appears and immediately disappears when hitting Enter in IE11\n  * Fix issue where hostname with more than three w's only auto-links final three w's in the name\n  * Fix issue where valid urls were not auto-linked\n  * Fix issue where some text was auto-linked when it shouldn't be\n\n\n4.10.1 / 2015-05-20\n==================\n* Fix paste issue with plain-text containing multiple paragraphs\n* Fix issue with incorrect cursor positon after creating a list\n* Fix disabledDoubleReturn option within a sentence\n* Allow for nested contenteditables\n* New style of passing options for anchor-preview and anchor\n* Introduce extensions.button + extensions.form as extendable base extensions\n* Convert anchor, fontsize, and anchor-preview to updated extensions model\n\n\n4.9.0 / 2015-05-18\n==================\n* New auto-link support for detecting urls and converting them to links\n* Fix target _blank issue for links in Firefox\n* Don't show placeholders for empty lists\n* Allow for overriding image drag and drop via extension\n\n\n4.8.1 / 2015-05-13\n==================\n* Fix error thrown when loading MediumEditor js from head\n\n\n4.8.0 / 2015-05-11\n==================\n* Expose new 'editableInput' event for monitoring changes to editor\n* Cleanup contenteditable elements created for textareas\n\n\n4.7.3 / 2015-05-07\n==================\n* Update version number in dist files\n\n\n4.7.2 / 2015-05-06\n==================\n* Add shortcut to insert a link (ctrl/cmd + k)\n* Fix `this.getAttribute is not a function` error\n\n\n4.7.1 / 2015-04-30\n==================\n* Make anchor preview wrap for long links\n* Fix issue when clean pasting spans with child nodes\n\n\n4.7.0 / 2015-04-27\n==================\n* Expose importSelection + exportSelection helper methods\n* Fix issue with initialization of MediumEditor using textarea\n* Introduce jscs\n\n4.6.0 / 2015-04-22\n==================\n* Add 'beta' version of fontSize button/form\n* Add option for enabling/disabling spellcheck\n* Add titles to toolbar buttons for tooltips\n* Use actual anchor tag in anchor preview\n* Fix anchor preview issue with tags nested inside anchors\n\n* Speed up travis builds\n* Convert paste handler into overrideable extension\n\n\n4.5.2 / 2015-04-14\n==================\n* Fix blur event detection when clicking on elements that don't clear focus\n\n\n4.5.1 / 2015-04-14\n==================\n* Fix broken 'paste.cleanPastedHtml' option and rename to 'paste.cleanPastedHTML'\n\n\n4.5.0 / 2015-04-13\n==================\n* Expose 'unsubscribe' for custom events\n* Detach custom events when editor is destroyed\n* Fix fontawesome url in demo page\n\n\n4.4.0 / 2015-04-11\n==================\n* Expose smart 'blur' and 'focus' events which account for toolbar interaction\n* Expose selectElement method for selecting text and updating toolbar\n* Fix always wrapping pasted text in a <p> tag\n\n\n4.3.0 / 2015-04-10\n==================\n* Add override options for pasteHTML and cleanPaste\n* Support overriding of scss theme variables\n* Fix for justify button states in IE\n* New helpers for manipulating nested objects\n* Internal tooling prep for options and defaults\n\n\n4.2.0 / 2015-04-05\n==================\n* Add textarea support\n\n\n4.1.1 / 2015-04-01\n==================\n* Fix .version issue\n\n\n4.1.0 / 2015-03-29\n==================\n* Expose Util and Selection methods externally via MediumEditor.util and MediumEditor.selection\n* Expose MediumEditor.version for version info\n* Add support for custom cleaning of attributes and tags for .pasteHTML\n* Move from jslint to jshint\n\n\n4.0.3 / 2015-03-27\n==================\n* Introduce 'removeFormat' button, for removing formatting from selection\n* Fix issues with focus/blur when using standardizeSelectionStart option\n\n\n4.0.2 / 2015-03-26\n==================\n* Fix bug causing toolbar to disappaer on click in safari (rollback fix from 4.0.1)\n* Break up anchor form extension logic into more overrideable parts\n\n\n4.0.1 / 2015-03-24\n==================\n* Fix issue with dragged in image sizes\n* Fix issues with focus/blur when using standardizeSelectionStart option\n\n\n4.0.0 / 2015-03-23\n==================\n* Introduced custom events (consumable externally)\n* Reduce API surface area\n  * Deprecated activate & deactivated. Exposed setup and destroy as replacements\n  * Updated documentation to reflect API changes\n* HTML standardization around list items\n* Fixed throttling\n* Added superscript & subscript css\n* Added better paste cleaning for Microsoft Word\n* Convert anchor preview into overrideable extension\n* Added disableAnchorPreview option\n\n\n3.0.9 / 2015-03-10\n==================\n* Extract toolbar\n* Extract anchor preview\n\n\n3.0.8 / 2015-02-27\n==================\n* MIT License\n* Use code from selection.js which is duplicated in core.js\n* Fix bug in paste handling + increase paste coverage\n\n\n3.0.7 / 2015-02-26\n==================\n* Ensure static toolbar won't render outside window + minimize when toolbar overflows\n* Fix flashing static-toolbar bug\n* Fix bug with sticky-toolbar when scrolling past bottom of contenteditable\n* Fix css declaration of linear-gradient\n* Fix AMD \"Uncaught TypeError: undefined is not a function\" issue\n* Account for 'full' actions when doing queryCommandState\n* Fix bugs in modified queryCommandState calls\n\n\n3.0.0 / 2015-02-23\n==================\n* Extract anchor form code from core code and convert into an extension\n* Expose onShowToolbar and onHideToolbar as options\n* Change button method names (now `setActive` and `setInactive`) to differentiate from core's `activate` and `deactivate`\n* Simplify blur check selection\n* Add Sauce Labs configuration to automate cross-browser testing\n* Add IE9 polyfill to repo\n* Let 'meta' key trigger shortcuts\n\n\n2.4.6 / 2015-02-18\n==================\n* Add basic support to keyboard shortcuts\n\n\n2.4.5 / 2015-02-17\n==================\n* Fix main file reference in npm package\n\n\n2.4.3 / 2015-02-16\n==================\n* Introduce full content actions\n\n\n2.4.2 / 2015-02-15\n==================\n* Fix disableDoubleReturn option\n\n\n2.4.1 / 2015-02-15\n==================\n* Fix isListItemChild call\n\n\n2.4.0 / 2015-02-15\n==================\n* Split source code into several files for better development flow\n* Make saveSelection and restoreSelection more consistant cross browser\n* Use document.queryCommandState for some button toolbar states\n* Add selection storage\n* Call extensions deactivate when deactivating the editor\n* Turn Anchor button into an extension\n\n\n2.3.0 / 2015-02-11\n==================\n* Fix various selection and positioning bugs\n* Introduce commands as combination of buttons and extensions\n* Update aria label so that setting secondHeader activates the toolbar\n* Don't use styles for detecting underline + strikethrough\n* Fix 'imageDragging: false' option\n* Fix list item tab identation\n* Add extension onHide command\n\n\n2.2.0 / 2015-02-05\n==================\n* Fix bug in getSelectedParentElement + Fix tests in browsers\n* Fall back to shimmed insertHTML in cases where firefox throws\n  when calling insertHTML\n* Prevent \"Argument not optional\" error\n* Prevent infinite loop after findAdjacentTextNodeWithContent\n* Remove cleanups from contenteditable false areas\n* Firefox fix: Don't modify value of input before calling execCommand()\n* Fix selection issue for clean pasted html test case in firefox\n* Add image drag and drop support\n\n\n2.1.3 / 2015-01-31\n==================\n* Fix issue with multiple elements with the same class\n  on the same editor instance\n\n\n2.1.2 / 2015-01-30\n==================\n* Specify default npm registry (`http://registry.npmjs.org`)\n\n\n2.1.1 / 2015-01-30\n==================\n* Adds support for newlines in placeholder attribute\n* Adds support and documentation for new toolbar extensions\n* Adds support for changing 'open in new window' label text\n* Fixes bug where `nodeValue` could unexpectedly be null\n* A couple of fixes to make tests a bit more reliable when run in the browser\n\n\n2.1.0 / 2015-01-27\n==================\n\n* Handles ESC key in link editor\n* Standardizes usage of setTimeout for UX delays vs debouncing vs deferring\n* Adds an optional onShowToolbar method\n* Supports enabling/disabling checkSelection updates externally\n* Standardizes where in the DOM a range begins\n* Adds ARIA role information\n* Fixes off() not removing any event listeners\n* Misc minor bug fixes and improvements\n\n\n2.0.0 / 2015-01-06\n==================\n\n* Adds static toolbar feature\n* Now uses textContent instead of innerText\n* Fixes plain text paste on IE\n* Hides placeholder on mouse click\n* Adds a 'collapse' option to 'selectElementContents' helper\n* Allows toolbar button states to change when selection is collapsed\n* In hideToolbarActions, calls an optional 'onHideToolbar' method\n* Ensures that ul.id and anchor.id are unique\n* Avoids grabbing selection on keypress for contenteditable except for spacebar\n* Supports disabling anchorForm, avoiding unnecessary event handling and element creation\n* Supports disabling placeholders, including not attaching event handlers when not needed\n* Various minor bug fixes and improvements\n\n\n1.9.13 / 2014-11-24\n===================\n\n* Adds a strikethrough option in buttonLabel\n* Now uses `options.elementsContainer` to calculate ID\n* Removes events during deactivate\n\n\n1.9.10 / 2014-11-17\n===================\n\n* Adds custom doc and win functionality, now you can specify the editor container\n* Minor bugfixes\n\n\n1.9.8 / 2014-10-21\n==================\n\n* Fixes 'this' out of scope\n\n\n1.9.7 / 2014-10-20\n==================\n\n* Adds justify buttons\n* Fix #308 by passing clipboard content through self.htmlEntities before inserting\n* Minor bug fixes\n\n\n1.9.4 / 2014-09-16\n==================\n\n* Adds support for tab based indenting and outdenting of <ul> and <ol>\n* Adds a save button to the anchor form\n* Improves toolbar positioning\n* Adds anchorButton and anchorButtonClass options\n\n\n1.9.0 / 2014-08-08\n==================\n\n* Extensions\n* Disables the toolbar when selecting within an element that has contenteditable=\"false\"\n* Fixes hidden placeholder content override\n\n\n1.8.14 / 2014-06-11\n===================\n\n* Fixes bug where if you had an empty blockquote the placeholder would still be active\n* Fixes bug that would create link without values\n* Exposes save/restoreSelection()\n* Allows customization of active/first/last button classes\n* Adds a script to run app from the cli\n* Adds protocols to checkLinkFormat regex\n\n\n1.8.8 / 2014-05-08\n==================\n\n* Fixes unlink behavior on Firefox\n* Fixes white space behavior at the end of anchors\n\n\n1.8.6 / 2014-05-03\n==================\n\n* Adds non-minified CSS files to bower.json\n\n\n1.8.5 / 2014-05-01\n==================\n\n* Changes to the element list or element selector now take effect on reactivation\n* Changed innerHTML to textContent to prevent XSS through twisted href values\n* Checks for data-disable-return on element on paste\n* Adds disableEditing and elementsContainer options\n\n\n1.8.0 / 2014-04-12\n==================\n\n* Removes anchor preview listeners on deactivate\n* Implements clean paste\n* Adds an option to validate links\n* Adds a basic extensions support\n* Misc minor fixes\n\n\n1.7.5 / 2014-03-30\n==================\n\n* Fixes isActive toggling\n* Removes anchor preview default value\n\n\n1.7.3 / 2014-03-22\n==================\n\n* Fixes activate/deactivate behavior\n\n\n1.7.2 / 2014-03-22\n==================\n\n* Removes DOM elements created by MediumEditor on deactivate\n\n\n1.7.1 / 2014-03-22\n==================\n\n* Prevents new lines with shift+enter when disableReturn is set to true\n\n\n1.7.0 / 2014-03-22\n==================\n\n* Removes compass dependency by using grunt with libsass\n* Fixes subscript button markup\n* Fixes anchor preview behavior for empty links and anchors\n* Adds a new option to disable double returns\n\n\n1.6.7 / 2014-03-13\n==================\n\n* Allows initialization with a single DOM node\n* Adds indent and outdent buttons\n\n\n1.6.5 / 2014-03-08\n==================\n\n* fixes some minor paste bugs\n* adds a delay option for anchor toolbar\n* fixes anchor toolbar initial positioning\n* fixes heading and blockquote on IE\n\n\n1.6.1 / 2014-03-04\n==================\n\n* fixes case where clicking anchor preview and then clicking into the anchorInput\n  causes hideToolbarActions to be called\n* fixes window resize when toolbar element is not created\n\n\n1.6.0 / 2014-02-27\n==================\n\n* Reorganizes CSS files\n* Removes unused method bindElementToolbarEvents\n* Adds a preview toolbar for anchors\n* Removes paste event binding on deactivate\n\n\n1.5.4 / 2014-02-12\n==================\n\n* Fixes filenames for main in bower.json\n* Removes window resize event listener on deactivate\n\n\n1.5.3 / 2014-01-22\n==================\n\n* Adds bootstrap theme\n* Adds image button that converts selected text into an image tag\n* Removes normalize.css dependency\n\n\n1.5.0 / 2014-01-16\n==================\n\n* Adds 3 new themes: Roman, Mani e Flat\n\n\n1.4.5 / 2014-01-13\n==================\n\n* Adds ability to set custom labels on buttons\n* Updates uglify\n* Fixes bug where pressing enter on formatted list item would generate\n  a new list instead of a new list item\n\n\n1.4.0 / 2013-12-13\n==================\n\n* Adds new extra buttons: pre and strikethrough\n* Fixes placeholder bug on paste\n* Various code improvements\n* Prevents returns using shift when disableReturn is set to true\n* Improves CSS to avoid conflicts\n\n\n1.3.5 / 2013-11-27\n==================\n\n* Fixes problem with text selection ending outside the container div\n* Implements serialize method\n* Adds a targetBlank option\n* Fixes Firefox box-sizing declarations\n\n\n1.3.1 / 2013-11-19\n==================\n\n* Fixes toolbar binding button issue with multi-editor mode\n\n\n1.3.0 / 2013-11-18\n==================\n\n* Fixes data-disable-return not preventing paragraph creation\n* Improves getSelectionElement() to work in any case\n* Fixes multi element selection bug\n* Fixes Issues #88 & #89\n* Improves binding for multiple editor instance, checkSelection() is called only once per instance\n* Improves allowMultiParagraphSelection filter by removing empty tags elements before counting\n* Considers header tags has a paragraph too (same as medium)\n\n\n1.2.2 / 2013-11-07\n==================\n\n* Removes blur event listener when disabling the toolbar\n* Adds a light gradient opacity to the toolbar\n* Fixes bug that would keep toolbar alive when moving out of the anchor input\n\n\n1.2.1 / 2013-11-07\n==================\n\n* Fixes empty selectionNode.el bug\n* Prevents toolbar opening when changing to selection elements\n  with the toolbar disabled\n* Adds a transition to the toolbar when moving across elements\n\n\n1.2.0 / 2013-11-06\n==================\n\n* Fixes issue on deactivation without enabled toolbar\n* Fixes checkSelection error when disableToolbar option is enabled\n* Adds new option to disable multiple paragraph selection\n* Prevents paragraph creation on paste when disableReturn is set to true\n\n\n1.1.6 / 2013-10-24\n==================\n\n* Adds extra buttons: superscript, subscript, ordered list and unordered list\n\n\n1.1.5 / 2013-10-23\n==================\n\n* Changes buttons blacklist to whitelist\n\n\n1.1.4 / 2013-10-13\n==================\n\n* Exports MediumEditor as module\n* Changes \"Underline\" button to display \"U\" instead of \"S\"\n\n\n1.1.3 / 2013-10-08\n==================\n\n* Pasted text is now wrapped into P elements\n\n\n1.1.2 / 2013-10-06\n==================\n\n* Changes the editor to use the formatBlock command to handle block elements\n* Fixes placeholder for empty elements\n\n\n1.1.1 / 2013-10-04\n==================\n\n* Normalizes styles and scripts\n* Improves bower manifest\n\n\n1.1.0 / 2013-10-03\n==================\n\n* Adds an option to disable the toolbar and maintain only the contenteditable behavior\n* Adds an option to disable returns\n* Adds an placeholder option for the contenteditable areas\n\n\n1.0.3 / 2013-10-01\n==================\n\n* Fixes toolbar positioning on screen top\n\n\n1.0.2 / 2013-09-24\n==================\n\n* Adds the possibility to pass an element list directly into initialization\n* Fixes issue with initial positioning when some actions are disabled\n* Don't rely on :last-child to style first/last element, as they may be hidden\n\n\n1.0.1 / 2013-09-20\n==================\n\n* Changes demo texto to something more friendly\n* Fixes shift+enter behavior\n\n\n1.0.0 / 2013-08-26\n==================\n\n* Initial release\n"
  },
  {
    "path": "CODE_OF_CONDUCT.md",
    "content": "# Contributor Code of Conduct\n\nAs contributors and maintainers of MediumEditor, we pledge to respect all people who contribute through reporting issues, posting feature requests, updating documentation, submitting pull requests or patches, and other activities.\n\nWe are committed to making participation in this project a harassment-free experience for everyone, regardless of level of experience, gender, gender identity and expression, sexual orientation, disability, personal appearance, body size, race, age, or religion.\n\nExamples of unacceptable behavior by participants include the use of sexual language or imagery, derogatory comments or personal attacks, trolling, public or private harassment, insults, or other unprofessional conduct.\n\nProject maintainers have the right and responsibility to remove, edit, or reject comments, commits, code, wiki edits, issues, and other contributions that are not aligned to this Code of Conduct. Project maintainers who do not follow the Code of Conduct may be removed from the project team.\n\nInstances of abusive, harassing, or otherwise unacceptable behavior may be reported by opening an issue or contacting one or more of the project maintainers.\n\nThis Code of Conduct is adapted from the [Contributor Covenant](http://contributor-covenant.org), version 1.0.0, available at [http://contributor-covenant.org/version/1/0/0/](http://contributor-covenant.org/version/1/0/0/)\n"
  },
  {
    "path": "CONTRIBUTING.md",
    "content": "# Contributing\n\n## To contribute and end up in this [list](https://github.com/yabwe/medium-editor/graphs/contributors):\n\n[Kill some bugs :)](https://github.com/yabwe/medium-editor/issues?q=is%3Aopen+is%3Aissue+label%3Abug)\n\n1. Fork it\n2. Create your feature branch (`git checkout -b my-new-feature`)\n3. Test your changes to the best of your ability.\n4. Update the documentation to reflect your changes if they add or changes current functionality.\n5. Commit your changes (`git commit -am 'Added some feature'`) **without files from the _dist_ directory**.\n6. Push to the branch (`git push origin my-new-feature`)\n7. Create new Pull Request\n\n## Code Consitency\n\nTo help create consistent looking code throughout the project, we use a few tools to help us. They have plugins for most popular editors/IDEs to make coding for our project, but you should use them in your project as well!\n\n#### JSHint\n\nWe use [JSHint](http://jshint.com/) on each build to find easy-to-catch errors and potential problems in our js.  You can find our JSHint settings in the `.jshintrc` file in the root of the project.\n\n#### jscs\n\nWe use [jscs](http://jscs.info/) on each build to enforce some code style rules we have for our project.  You can find our jscs settings in the `.jscsrc` file in the root of the project.\n\n#### EditorConfig\n\nWe use [EditorConfig](http://EditorConfig.org) to maintain consistent coding styles between various editors and IDEs.  You can find our settings in the `.editorconfig` file in the root of the project.\n\n## Easy First Bugs\n\nLooking for something simple for a first contribution? Try fixing an [easy first bug](https://github.com/yabwe/medium-editor/issues?q=is%3Aopen+is%3Aissue+label%3A%22easy+first+bug%22)!\n\n## Development\n\nMediumEditor development tasks are managed by Grunt. To install all the necessary packages, just invoke:\n\n```bash\nnpm install\n```\n\nTo run all the test and build the dist files for testing on demo pages, just invoke:\n```bash\ngrunt\n```\n\nThese are the other available grunt tasks:\n\n* __js__: runs jslint and jasmine tests and creates minified and concatenated versions of the script;\n* __css__: runs autoprefixer and csslint\n* __test__: runs jasmine tests, jslint and csslint\n* __watch__: watch for modifications on script/scss files\n* __spec__: runs a task against a specified file\n\nThe source files are located inside the __src__ directory.  Be sure to make changes to these files and not files in the dist directory.\n"
  },
  {
    "path": "CUSTOM-EVENTS.md",
    "content": "# MediumEditor Custom Events (v5.0.0)\n\nMediumEditor exposes a variety of custom events for convenience when using the editor with your web application.  You can attach and detach listeners to these custom events, as well as manually trigger any custom events including your own custom events.\n\n**NOTE:**\n\nCustom event listeners are triggered in the order that they were 'subscribed' to.  Most functionality within medium-editor uses these custom events to trigger updates, so in general, it can be assumed that most of the built-in functionality has already been completed before any of your custom event listeners will be called.\n\nIf you need to override the editor's built-in behavior, try overriding the built-in extensions with your own [custom extension](src/js/extensions).\n\n<!-- START doctoc generated TOC please keep comment here to allow auto update -->\n<!-- DON'T EDIT THIS SECTION, INSTEAD RE-RUN doctoc TO UPDATE -->\n\n- [API Methods](#api-methods)\n  - [`MediumEditor.subscribe(name, listener)`](#mediumeditorsubscribename-listener)\n  - [`MediumEditor.unsubscribe(name, listener)`](#mediumeditorunsubscribename-listener)\n  - [`MediumEditor.trigger(name, data, editable)`](#mediumeditortriggername-data-editable)\n- [Custom Events](#custom-events)\n  - [`addElement`](#addelement)\n  - [`blur`](#blur)\n  - [`editableInput`](#editableinput)\n  - [`externalInteraction`](#externalinteraction)\n  - [`focus`](#focus)\n  - [`removeElement`](#removeelement)\n- [Toolbar Custom Events](#toolbar-custom-events)\n  - [`hideToolbar`](#hidetoolbar)\n  - [`positionToolbar`](#positiontoolbar)\n  - [`positionedToolbar`](#positionedtoolbar)\n  - [`showToolbar`](#showtoolbar)\n- [Proxied Custom Events](#proxied-custom-events)\n      - [`editableClick`](#editableclick)\n      - [`editableBlur`](#editableblur)\n      - [`editableKeypress`](#editablekeypress)\n      - [`editableKeyup`](#editablekeyup)\n      - [`editableKeydown`](#editablekeydown)\n      - [`editableKeydownEnter`](#editablekeydownenter)\n      - [`editableKeydownTab`](#editablekeydowntab)\n      - [`editableKeydownDelete`](#editablekeydowndelete)\n      - [`editableKeydownSpace`](#editablekeydownspace)\n      - [`editableMouseover`](#editablemouseover)\n      - [`editableDrag`](#editabledrag)\n      - [`editableDrop`](#editabledrop)\n      - [`editablePaste`](#editablepaste)\n\n<!-- END doctoc generated TOC please keep comment here to allow auto update -->\n\n## API Methods\n\nUse the following methods of [MediumEditor](API.md) for custom event interaction:\n\n### `MediumEditor.subscribe(name, listener)`\n\nAttaches a listener for the specified custom event name.\n\n**Arguments**\n\n1. _**name** (`String`)_:\n\n  * Name of the event to listen to.  See the list of built-in [Custom Events](#custom-events) below.\n\n2. _**listener(data, editable)** (`function`)_:\n\n  * Listener method that will be called whenever the custom event is triggered.\n\n**Arguments to listener**\n\n  1. _**data** (`Event` | `object`)_\n    * For most custom events, this will be the browser's native `Event` object for the event that triggered the custom event to fire.\n    * For some custom events, this will be an object containing information describing the event (depending on which custom event it is)\n  2. _**editable** (`HTMLElement`)_\n    * A reference to the contenteditable container element that this custom event corresponds to.  This is especially useful for instances where one instance of MediumEditor contains multiple elements, or there are multiple instances of MediumEditor on the page.\n    * For example, when `blur` fires, this argument will be the `<div contenteditable=true></div>` element that is about to receive focus.\n\n***\n### `MediumEditor.unsubscribe(name, listener)`\n\nDetaches a custom event listener for the specified custom event name.\n\n**Arguments**\n\n1. _**name** (`String`)_:\n\n  * Name of the event to detach the listener for.\n\n2. _**listener** (`function`)_:\n\n  * A reference to the listener to detach.  This must be a match by-reference and not a copy.\n\n**NOTE**\n\n  * Calling [destroy()](API.md#destroy) on the MediumEditor object will automatically remove all custom event listeners.\n\n***\n### `MediumEditor.trigger(name, data, editable)`\n\nManually triggers a custom event.\n\n1. _**name** (`String`)_:\n\n  * Name of the custom event to trigger.\n\n2. _**data** (`Event` | `object`)_:\n\n  * Native `Event` object or custom data object to pass to all the listeners to this custom event.\n\n3. _**editable** (`HTMLElement`)_:\n\n  * The `<div contenteditable=true></div>` element to pass to all of the listeners to this custom event.\n\n## Custom Events\n\nThese events are custom to MediumEditor so there may be one or more native events that can trigger them.\n\n### `addElement`\n\n`addElement` is triggered whenever an element is added to the editor after the editor has been instantiated.  This custom event will be triggered **after** the element has already been initialized by the editor and added to the internal array of **elements**.  If the element being added was a `<textarea>`, the element passed to the listener will be the created `<div contenteditable=true>` element and not the root `<textarea>`.\n\n**Arguments to listener**\n\n1. _**data** (`object`)_\n  * Properties of data object\n    * `target`: element which was added to the editor\n    * `currentTarget`: element which was added to the editor\n2. _**editable** (`HTMLElement`)_\n  * element which was added to the editor\n\n***\n### `blur`\n\n`blur` is triggered whenever a `contenteditable` element within an editor has lost focus to an element other than an editor maintained element (ie Toolbar, Anchor Preview, etc).\n\nExample:\n\n1. User selects text within an editor element, causing the toolbar to appear\n2. User clicks on a toolbar button\n  * Technically focus may have been lost on the editor element, but since the user is interacting with the toolbar, `blur` is NOT fired.\n3. User hovers over a link, anchor-preview is displayed\n4. User clicks link to edit it, and the toolbar now displays a textbox to edit the url\n  * Focus will have lost here since focus is now in the url editing textbox, but again since it's within the toolbar, `blur` is NOT fired.\n5. User clicks on another part of the page which hides the toolbar and focus is no longer in the `contenteditable`\n6. `blur` is triggered\n\n***\n### `editableInput`\n\n`editableInput` is triggered whenever the content of a `contenteditable` changes, including keypresses, toolbar actions, or any other user interaction that changes the html within the element.  For non-IE browsers, this is just a proxied version of the native `input` event.  However, Internet Explorer and has never supported the `input` event on `contenteditable` elements, and Edge has some support for `input` on `contenteditable` (which may be fixed in upcoming release of Edge) so for these browsers the `editableInput` event is triggered through a combination of:\n* native `keypress` event on the element\n* native `selectionchange` event on the document\n* monitoring calls the `document.execCommand()`\n\n***\n### `externalInteraction`\n\n`externalInteraction` is triggered whenever the user interact with any element outside of the `contenteditable` element or the other elements maintained by the editor (ie Toolbar, Anchor Preview, etc.).  This event trigger regardless of whether an existing `contenteditable` element had focus or not.\n\n***\n### `focus`\n\n`focus` is triggered whenever a `contenteditable` element within an editor receives focus. If the user interacts with any editor maintained elements (ie toolbar), `blur` is NOT triggered because focus has not been lost.  Thus, `focus` will only be triggered when an `contenteditable` element (or the editor that contains it) is first interacted with.\n\n***\n### `removeElement`\n\n`removeElement` is triggered whenever an element is removed from the editor after the editor has been instantiated.  This custom event will be triggered **after** the element has already been removed from the editor and any events attached to it have already been removed.  If the element being removed was a `<div>` created to correspond to a `<textarea>`, the element will already have been removed from the DOM.\n\n**Arguments to listener**\n\n1. _**data** (`object`)_\n  * Properties of data object\n    * `target`: element which was removed from the editor\n    * `currentTarget`: element which was removed from the editor\n2. _**editable** (`HTMLElement`)_\n  * element which was removed from the editor\n\n## Toolbar Custom Events\n\nThese events are triggered by the toolbar when the toolbar extension has not been disabled.\n\n### `hideToolbar`\n\n`hideToolbar` is triggered whenever the toolbar was visible and has just been hidden.\n\n### `positionToolbar`\n`positionToolbar` is triggered each time the current selection is checked and the toolbar's position is about to be updated. This event is triggered after all of the buttons have had their state updated, but before the toolbar is moved to the correct location.  This event will be triggered even if nothing will be changed about the toolbar's appearance.\n\n### `positionedToolbar`\n`positionedToolbar` is triggered each time the current selection is checked, the toolbar is displayed, and the toolbar's position was updated. This differs from the `positionToolbar` event in that the visibility and location of the toolbar has already been changed (as opposed to the event triggering before those changes occur). This event will be triggered even if nothing was changed about the toolbar's appearance.\n\n### `showToolbar`\n`showToolbar` is triggered whenever the toolbar was hidden and has just been displayed.\n\n## Proxied Custom Events\n\nThese events are triggered whenever a native browser event is triggered for any of the `contenteditable` elements monitored by this instance of MediumEditor.\n\nFor example, the `editableClick` custom event will be triggered when a native `click` event is fired on any of the `contenteditable` elements. This provides a single event listener that can get fired for all elements, and also allows for the `contenteditable` element that triggered the event to be passed to the listener.\n\n##### `editableClick`\nnative `click` event for each element\n##### `editableBlur`\nnative `blur` event for each element.\n##### `editableKeypress`\nnative `keypress` event for each element.\n##### `editableKeyup`\nnative `keyup` event for each element.\n##### `editableKeydown`\nnative `keydown` event for each element.\n##### `editableKeydownEnter`\nnative `keydown` event for each element, but only triggered if the key is `ENTER` (keycode 13).\n##### `editableKeydownTab`\nnative `keydown` event for each element, but only triggered if the key is `TAB` (keycode 9).\n##### `editableKeydownDelete`\nnative `keydown` event for each element, but only triggered if the key is `DELETE` (keycode 46).\n##### `editableKeydownSpace`\nnative `keydown` event for each element, but only triggered if the key is `SPACE` (keycode 32).\n##### `editableMouseover`\nnative `mouseover` event for each element.\n##### `editableDrag`\nnative `drag` event for each element.\n##### `editableDrop`\nnative `drop` event for each element.\n##### `editablePaste`\nnative `paste` event for each element.\n"
  },
  {
    "path": "Gruntfile.js",
    "content": "/*global module, require, process*/\n\nmodule.exports = function (grunt) {\n    'use strict';\n\n    var autoprefixerBrowsers = ['last 3 versions', 'ie >= 9'],\n        globalConfig = {\n            src: 'src',\n            dest: 'dev'\n        },\n        gruntConfig = {\n            pkg: grunt.file.readJSON('package.json'),\n            globalConfig: globalConfig\n        },\n        srcFiles = [\n            'src/js/globals.js',\n            'src/js/util.js',\n            'src/js/extension.js',\n            'src/js/selection.js',\n            'src/js/events.js',\n            'src/js/extensions/button.js',\n            'src/js/defaults/buttons.js',\n            'src/js/extensions/form.js',\n            'src/js/extensions/anchor.js',\n            'src/js/extensions/anchor-preview.js',\n            'src/js/extensions/auto-link.js',\n            'src/js/extensions/file-dragging.js',\n            'src/js/extensions/keyboard-commands.js',\n            'src/js/extensions/fontname.js',\n            'src/js/extensions/fontsize.js',\n            'src/js/extensions/paste.js',\n            'src/js/extensions/placeholder.js',\n            'src/js/extensions/toolbar.js',\n            'src/js/extensions/deprecated/image-dragging.js',\n            'src/js/core.js',\n            'src/js/defaults/options.js',\n            'src/js/version.js'\n        ];\n    gruntConfig.connect = {\n        server: {\n            options: {\n                base: '',\n                port: 9999\n            }\n        }\n    };\n\n    // TODO: build check with debug and devel false\n    gruntConfig.jshint = {\n        options: {\n            ignores: ['src/js/polyfills.js'],\n            jshintrc: true,\n            reporter: require('jshint-stylish')\n        },\n        all: {\n            src: [\n                'src/js/**/*.js',\n                'spec/*.spec.js',\n                'Gruntfile.js'\n            ]\n        }\n    };\n\n    // TODO: \"maximumLineLength\": 120\n    gruntConfig.jscs = {\n        src: [\n            'src/js/**/*.js',\n            'spec/*.spec.js',\n            'Gruntfile.js',\n            '!src/js/polyfills.js'\n        ],\n        options: {\n            config: '.jscsrc'\n        }\n    };\n\n    gruntConfig.karma = {\n        unit: {\n            configFile: 'karma.conf.js'\n        },\n        dev: {\n            configFile: 'karma.dev.conf.js'\n        }\n    };\n\n    gruntConfig.uglify = {\n        options: {\n            report: 'gzip'\n        },\n        build: {\n            src: 'dist/js/medium-editor.js',\n            dest: 'dist/js/<%= pkg.name %>.min.js'\n        }\n    };\n\n    gruntConfig.csslint = {\n        strict: {\n            options: {\n                'box-sizing': false,\n                'compatible-vendor-prefixes': false,\n                'fallback-colors': false,\n                'gradients': false,\n                'important': false,\n                'import': 2,\n                'outline-none': false,\n                'adjoining-classes': false\n            },\n            src: 'dist/css/**/*.css'\n        }\n    };\n\n    gruntConfig.sass = {\n        dist: {\n            options: {\n                includePaths: ['src/sass/']\n            },\n            files: {\n                'dist/css/medium-editor.css': 'src/sass/medium-editor.scss',\n                'dist/css/themes/bootstrap.css': 'src/sass/themes/bootstrap.scss',\n                'dist/css/themes/default.css': 'src/sass/themes/default.scss',\n                'dist/css/themes/flat.css': 'src/sass/themes/flat.scss',\n                'dist/css/themes/mani.css': 'src/sass/themes/mani.scss',\n                'dist/css/themes/roman.css': 'src/sass/themes/roman.scss',\n                'dist/css/themes/tim.css': 'src/sass/themes/tim.scss',\n                'dist/css/themes/beagle.css': 'src/sass/themes/beagle.scss'\n            }\n        }\n    };\n\n    gruntConfig.cssmin = {\n        main: {\n            options: {\n                noAdvanced: true\n            },\n\n            expand: true,\n            cwd: 'dist/css/',\n            src: ['*.css', '!*.min.css'],\n            dest: 'dist/css/',\n            ext: '.min.css'\n        },\n        themes: {\n            options: {\n                noAdvanced: true\n            },\n\n            expand: true,\n            cwd: 'dist/css/themes/',\n            src: ['*.css', '!*.min.css'],\n            dest: 'dist/css/themes/',\n            ext: '.min.css'\n        }\n    };\n\n    gruntConfig.autoprefixer = {\n        main: {\n            expand: true,\n            cwd: 'dist/css/',\n            src: ['*.css', '!*.min.css'],\n            dest: 'dist/css/',\n            browsers: autoprefixerBrowsers\n        },\n        themes: {\n            expand: true,\n            cwd: 'dist/css/themes/',\n            src: ['*.css', '!*.min.css'],\n            dest: 'dist/css/themes/',\n            browsers: autoprefixerBrowsers\n        }\n    };\n\n    gruntConfig.watch = {\n        scripts: {\n            files: ['src/js/**/*.js', 'spec/**/*.js', 'Gruntfile.js'],\n            tasks: ['js'],\n            options: {\n                debounceDelay: 250\n            }\n        },\n        styles: {\n            files: 'src/sass/**/*.scss',\n            tasks: ['css'],\n            options: {\n                debounceDelay: 250\n            }\n        }\n    };\n\n    gruntConfig.concat = {\n        options: {\n            stripBanners: true\n        },\n        dist: {\n            src: ['src/js/polyfills.js']\n                .concat(['src/wrappers/start.js'])\n                .concat(srcFiles)\n                .concat(['src/wrappers/end.js']),\n            dest: 'dist/js/<%= pkg.name %>.js',\n            nonull: true\n        }\n    };\n\n    gruntConfig.plato = {\n        feed: {\n            files: {\n                'reports/plato': srcFiles\n            }\n        }\n    };\n\n    gruntConfig.bump = {\n        options: {\n            files: ['package.json', 'src/js/version.js'],\n            updateConfigs: [],\n            commit: false,\n            createTag: false,\n            push: false\n        }\n    };\n\n    grunt.initConfig(gruntConfig);\n\n    require('time-grunt')(grunt);\n    require('load-grunt-tasks')(grunt, {\n        pattern: [\n            'grunt-*',\n            '!grunt-template-jasmine-istanbul'\n        ]\n    });\n\n    if (parseInt(process.env.TRAVIS_PULL_REQUEST, 10) > 0) {\n        grunt.registerTask('travis', ['jshint', 'jscs', 'karma:unit', 'csslint']);\n    } else {\n        grunt.registerTask('travis', ['jshint', 'jscs', 'csslint', 'karma:unit']);\n    }\n\n    grunt.registerTask('test', ['jshint', 'jscs', 'concat', 'csslint', 'karma:dev']);\n    grunt.registerTask('js', ['jshint', 'jscs', 'concat', 'karma:dev', 'uglify']);\n    grunt.registerTask('css', ['sass', 'autoprefixer', 'cssmin', 'csslint']);\n    grunt.registerTask('default', ['js', 'css']);\n\n    grunt.registerTask('spec', 'Runs a task on a specified file', function (taskName, fileName) {\n        globalConfig.file = fileName;\n        grunt.task.run(taskName + ':spec');\n    });\n\n    grunt.registerTask('patch', ['bump', 'css', 'js']);\n    grunt.registerTask('minor', ['bump:minor', 'css', 'js']);\n    grunt.registerTask('major', ['bump:major', 'css', 'js']);\n};"
  },
  {
    "path": "LICENSE",
    "content": "Copyright Davi Ferreira, https://www.daviferreira.com/\n\nThis software consists of voluntary contributions made by many\nindividuals. For exact contribution history, see the revision history\navailable at https://github.com/yabwe/medium-editor\n\nThe following license applies to all parts of this software except as\ndocumented below:\n\n====\n\nPermission is hereby granted, free of charge, to any person obtaining\na copy of this software and associated documentation files (the\n\"Software\"), to deal in the Software without restriction, including\nwithout limitation the rights to use, copy, modify, merge, publish,\ndistribute, sublicense, and/or sell copies of the Software, and to\npermit persons to whom the Software is furnished to do so, subject to\nthe following conditions:\n\nThe above copyright notice and this permission notice shall be\nincluded in all copies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND,\nEXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF\nMERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND\nNONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE\nLIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION\nOF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION\nWITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.\n\n====\n\nAll files located in the node_modules directory are\nexternally maintained libraries used by this software which have their\nown licenses; we recommend you read them, as their terms may differ from\nthe terms above.\n"
  },
  {
    "path": "MAINTAINERS.md",
    "content": "## STEPS TO RELEASE:\n\n1. Find the last release commit in log history. Look through all the commits or PR history and see all the stuff that has happened since the last release.\n2. Add a row describing each high-level change into `CHANGES.md`. Looking at `CHANGES.md` would be a good stepping off point.\n3. Depending upon the changes, decide if it is a major/minor/patch release. _Read more about [semantic versioning](http://semver.org/)_.\n4. Depending upon the type of release, run `grunt major`, `grunt minor`, `grunt patch` to update the version number and generate all the dist files.\n5. Commit all your changes (**including `CHANGES.md`**) into your commit. Add the new release number into your commit message. And push it up to the remote master branch.\n6. Go [here](https://github.com/yabwe/medium-editor/releases) and ‘Draft a new release’. Title the release as the new release number (ex: `5.11.0`). Copy/paste the entries you made in `CHANGES.md` into the release summary. **Make sure the release is against the master branch.**\n7. Once the release is created, go back to your git and run `npm publish`.\n\n\n## RUNNING TESTS FOR FORK BRANCHES IN SAUCELABS:\n\nFor pull requests submitted from a forked version of the repo, the test suite won't run in Saucelabs so we haven't been able to know if tests fail in various browsers until after the PR is merged into master.  This is deliberate by Saucelabs as a security measure to prevent external forks from doing malicious things to the repo.\n\nThere is a workaround however, so when a PR is submitted from an external fork, follow these steps to verify the tests don't fail in Saucelabs before merging the PR into master.\n\nFor this example, let's assume there's a new pull request (#123) from a branch of an external user's fork (external-user/new-branch)\n\n1. Create a new local branch for the pull request\n  * ```git checkout -b integration-123```\n2. Add a remote that points to the external fork\n  * ```git remote add external-user git@github.com:external-user/medium-editor.git```\n3. Fetch the remote repo\n  * ```git fetch external-user```\n4. Merge the external branch into your local branch\n  * ```git merge external-user/new-branch```\n5. Push your local branch up to the main repo\n  * ```git push```\n\nThat's it.  Pushing the branch up should kick off a travis build, which will cause the tests to run in Saucelabs.  Github is smart enough to link the existing pull request to that build and reflect the status of the build (including the results from Saucelabs) on the PR summary page!\n\n\n"
  },
  {
    "path": "OPTIONS.md",
    "content": "# MediumEditor Options (v5.0.0)\n\nOptions to customize medium-editor are passed as the second argument to the [MediumEditor constructor](API.md#mediumeditorelements-options).  Example:\n\n```js\nvar editor = new MediumEditor('.editor', {\n    // options go here\n});\n```\n\n<!-- START doctoc generated TOC please keep comment here to allow auto update -->\n<!-- DON'T EDIT THIS SECTION, INSTEAD RE-RUN doctoc TO UPDATE -->\n\n\n- [Core Options](#core-options)\n    - [`activeButtonClass`](#activebuttonclass)\n    - [`buttonLabels`](#buttonlabels)\n    - [`contentWindow`](#contentwindow)\n    - [`delay`](#delay)\n    - [`disableReturn`](#disablereturn)\n    - [`disableDoubleReturn`](#disabledoublereturn)\n    - [`disableExtraSpaces`](#disableextraspaces)\n    - [`disableEditing`](#disableediting)\n    - [`elementsContainer`](#elementscontainer)\n    - [`extensions`](#extensions)\n    - [`ownerDocument`](#ownerdocument)\n    - [`spellcheck`](#spellcheck)\n    - [`targetBlank`](#targetblank)\n- [Toolbar options](#toolbar-options)\n    - [`allowMultiParagraphSelection`](#allowmultiparagraphselection)\n    - [`buttons`](#buttons)\n    - [`diffLeft`](#diffleft)\n    - [`diffTop`](#difftop)\n    - [`firstButtonClass`](#firstbuttonclass)\n    - [`lastButtonClass`](#lastbuttonclass)\n    - [`relativeContainer`](#relativecontainer)\n    - [`standardizeSelectionStart`](#standardizeselectionstart)\n    - [`static`](#static)\n  - ['static' Toolbar Options](#static-toolbar-options)\n    - [`align`](#align)\n    - [`sticky`](#sticky)\n    - [`stickyTopOffset`](#stickytopoffset)\n    - [`updateOnEmptySelection`](#updateonemptyselection)\n  - [Disabling Toolbar](#disabling-toolbar)\n- [Anchor Preview options](#anchor-preview-options)\n    - [`hideDelay`](#hidedelay)\n    - [`previewValueSelector`](#previewvalueselector)\n    - [`showOnEmptyLinks`](#showonemptylinks)\n    - [`showWhenToolbarIsVisible`](#showwhentoolbarisvisible)\n  - [Disabling Anchor Preview](#disabling-anchor-preview)\n- [Placeholder Options](#placeholder-options)\n    - [`text`](#text)\n    - [`hideOnClick`](#hideonclick)\n  - [Disabling Placeholders](#disabling-placeholders)\n- [Anchor Form options](#anchor-form-options)\n    - [`customClassOption`](#customclassoption)\n    - [`customClassOptionText`](#customclassoptiontext)\n    - [`linkValidation`](#linkvalidation)\n    - [`placeholderText`](#placeholdertext)\n    - [`targetCheckbox`](#targetcheckbox)\n    - [`targetCheckboxText`](#targetcheckboxtext)\n- [Paste Options](#paste-options)\n    - [`forcePlainText`](#forceplaintext)\n    - [`cleanPastedHTML`](#cleanpastedhtml)\n    - [`cleanReplacements`](#cleanreplacements)\n    - [`cleanAttrs`](#cleanattrs)\n    - [`cleanTags`](#cleantags)\n    - [`unwrapTags`](#unwraptags)\n  - [Disabling Paste Handling](#disabling-paste-handling)\n- [KeyboardCommands Options](#keyboardcommands-options)\n    - [`commands`](#commands)\n  - [Disabling Keyboard Commands](#disabling-keyboard-commands)\n- [Auto Link Options](#auto-link-options)\n    - [`autoLink`](#autolink)\n  - [Enabling Auto Link](#enabling-auto-link)\n- [Image Dragging Options](#image-dragging-options)\n    - [`imageDragging`](#imagedragging)\n  - [Disabling Image Dragging](#disabling-image-dragging)\n- [Options Example:](#options-example)\n\n<!-- END doctoc generated TOC please keep comment here to allow auto update -->\n\n## Core Options\n\nThese are global options that apply to the entire editor. Example:\n\n```js\nvar editor = new MediumEditor('.editable', {\n    /* These are the default options for the editor,\n        if nothing is passed this is what is used */\n    activeButtonClass: 'medium-editor-button-active',\n    allowMultiParagraphSelection: true,\n    buttonLabels: false,\n    contentWindow: window,\n    delay: 0,\n    disableReturn: false,\n    disableDoubleReturn: false,\n    disableExtraSpaces: false,\n    disableEditing: false,\n    elementsContainer: false,\n    extensions: {},\n    ownerDocument: document,\n    spellcheck: true,\n    targetBlank: false\n});\n```\n\n#### `activeButtonClass`\n**Default:** `'medium-editor-button-active'`\n\nCSS class added to active buttons in the toolbar.\n\n***\n#### `buttonLabels`\n**Default:** `false`\n\nCustom content for the toolbar buttons.\n\n**Valid Values:**\n* `false`\n  * Use default button labels\n* `'fontawesome'`\n  * Uses fontawesome icon set for all toolbar icons\n\n**NOTE**:\n\nUsing `'fontawesome'` as the buttonLabels requires version 4.1.0 of the fontawesome css to be on the page to ensure all icons will be displayed correctly.\n\n***\n#### `contentWindow`\n**Default:** `window`\n\nThe contentWindow object that contains the contenteditable element. MediumEditor will use this for attaching events, getting selection, etc.\n\n***\n#### `delay`\n**Default:** `0`\n\nTime in milliseconds to show the toolbar or anchor tag preview.\n\n***\n#### `disableReturn`\n**Default:** `false`\n\nEnables/disables the use of the return-key. You can also set specific element behavior by using setting a data-disable-return attribute.\n\n***\n#### `disableDoubleReturn`\n**Default:** `false`\n\nAllows/disallows two (or more) empty new lines. You can also set specific element behavior by using setting a data-disable-double-return attribute.\n\n***\n#### `disableExtraSpaces`\n**Default:** `false`\n\nWhen set to true, it disallows spaces at the beginning and end of the element. Also it disallows entering 2 consecutive spaces between 2 words.\n\n***\n#### `disableEditing`\n**Default:** `false`\n\nEnables/disables adding the contenteditable behavior. Useful for using the toolbar with customized buttons/actions. You can also set specific element behavior by using setting a data-disable-editing attribute.\n\n***\n#### `elementsContainer`\n**Default:** `ownerDocument.body`\n\nSpecifies a DOM node to contain MediumEditor's toolbar and anchor preview elements.\n\n***\n#### `extensions`\n**Default:** `{}`\n\nCustom extensions to use. See [Custom Buttons and Extensions](src/js/extensions) for more details on extensions.\n\n***\n#### `ownerDocument`\n**Default:** `window.document`\n\nThe ownerDocument object for the contenteditable element.  MediumEditor will use this for creating elements, getting selection, attaching events, etc.\n\n***\n#### `spellcheck`\n**Default:** `true`\n\nEnable/disable native contentEditable automatic spellcheck.\n\n***\n#### `targetBlank`\n**Default:** `false`\n\nEnables/disables automatically adding the `target=\"_blank\"` attribute to anchor tags.\n\n## Toolbar options\n\nThe toolbar for MediumEditor is implemented as a built-in extension which automatically displays whenever the user selects some text.  The toolbar can hold any set of defined built-in buttons, but can also hold any custom buttons passed in as extensions.\n\nOptions for the toolbar are passed as an object that is a member of the outer options object. Example:\n```js\nvar editor = new MediumEditor('.editable', {\n    toolbar: {\n        /* These are the default options for the toolbar,\n           if nothing is passed this is what is used */\n        allowMultiParagraphSelection: true,\n        buttons: ['bold', 'italic', 'underline', 'anchor', 'h2', 'h3', 'quote'],\n        diffLeft: 0,\n        diffTop: -10,\n        firstButtonClass: 'medium-editor-button-first',\n        lastButtonClass: 'medium-editor-button-last',\n        relativeContainer: null,\n        standardizeSelectionStart: false,\n        static: false,\n\n        /* options which only apply when static is true */\n        align: 'center',\n        sticky: false,\n        updateOnEmptySelection: false\n    }\n});\n```\n\n***\n#### `allowMultiParagraphSelection`\n**Default:** `true`\n\nenables/disables whether the toolbar should be displayed when selecting multiple paragraphs/block elements.\n\n***\n#### `buttons`\n**Default:** `['bold', 'italic', 'underline', 'anchor', 'h2', 'h3', 'quote']`\n\nThe set of buttons to display on the toolbar.\n\n***\n#### `diffLeft`\n**Default:** `0`\n\nValue in pixels to be added to the X axis positioning of the toolbar.\n\n***\n#### `diffTop`\n**Default:** `-10`\n\nValue in pixels to be added to the Y axis positioning of the toolbar.\n\n***\n#### `firstButtonClass`\n**Default:** `'medium-editor-button-first'`\n\nCSS class added to the first button in the toolbar.\n\n***\n#### `lastButtonClass`\n**Default:** `'medium-editor-button-last'`\n\nCSS class added to the last button in the toolbar.\n\n***\n#### `relativeContainer`\n**Default:** `null`\n\nDOMElement to append the toolbar to instead of the body.  When an element is passed the toolbar will also be positioned `relative` instead of `absolute`, which means the editor will not attempt to manually position the toolbar automatically.\n\n**NOTE:**\n* Using this in combination with the `static` option for toolbar is not explicitly supported and the behavior in this case is not defined.\n\n***\n#### `standardizeSelectionStart`\n**Default:** `false`\n\nEnables/disables standardizing how the beginning of a range is decided between browsers whenever the selected text is analyzed for updating toolbar buttons status.\n\n***\n#### `static`\n**Default:** `false`\n\nEnable/disable the toolbar always displaying in the same location relative to the medium-editor element.\n\n\n### 'static' Toolbar Options\n\nThese options only apply when the `static` option is being used.\n\n***\n#### `align`\n**Default:** `center`\n\nWhen the __static__ option is `true`, this aligns the static toolbar relative to the medium-editor element.\n\n**Valid Values**\n\n`'left'` | `'center'` | `'right'`\n\n***\n#### `sticky`\n**Default:** `false`\n\nWhen the __static__ option is `true`, this enables/disables the toolbar \"sticking\" to the viewport and staying visible on the screen while the page scrolls.\n\n***\n#### `stickyTopOffset`\n**Default:** `0`\n\nWhen the __sticky__ option is `true`, this set in pixel a top offset above the toolbar.\n\n***\n#### `updateOnEmptySelection`\n**Default:** `false`\n\nWhen 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).\n\n### Disabling Toolbar\n\nTo disable the toolbar (which also disables the anchor-preview extension), set the value of the `toolbar` option to `false`:\n```javascript\nvar editor = new MediumEditor('.editable', {\n    toolbar: false\n});\n```\n\n\n## Anchor Preview options\n\nThe anchor preview is a built-in extension which automatically displays a 'tooltip' when the user is hovering over a link in the editor.  The tooltip will display the `href` of the link, and when click, will open the anchor editing form in the toolbar.\n\nOptions for the anchor preview 'tooltip' are passed as an object that is a member of the outer options object. Example:\n```javascript\nvar editor = new MediumEditor('.editable', {\n    anchorPreview: {\n        /* These are the default options for anchor preview,\n           if nothing is passed this is what it used */\n        hideDelay: 500,\n        previewValueSelector: 'a'\n    }\n}\n});\n```\n\n\n***\n#### `hideDelay`\n**Default:** `500`\n\nTime in milliseconds to show the anchor tag preview after the mouse has left the anchor tag.\n\n***\n#### `previewValueSelector`\n**Default:** `'a'`\n\nThe default selector to locate where to put the activeAnchor value in the preview. You should only need to override this if you've modified the way in which the anchor-preview extension renders.\n\n***\n#### `showOnEmptyLinks`\n**Default:** `true`\n\nDetermines whether the anchor tag preview shows up on link with href as \"\" or \"#something\". You should set this value to false if you do not want the preview to show up in such use cases.\n\n***\n#### `showWhenToolbarIsVisible`\n**Default:** `false`\n\nDetermines whether the anchor tag preview shows up when the toolbar is visible. You should set this value to true if the static option for the toolbar is true and you want the preview to show at the same time.\n\n### Disabling Anchor Preview\n\nTo disable the anchor preview, set the value of the `anchorPreview` option to `false`:\n```javascript\nvar editor = new MediumEditor('.editable', {\n    anchorPreview: false\n});\n```\n\n**NOTE:**\n\n* If the toolbar is disabled (via `toolbar: false` option or `data-disable-toolbar` attribute) the anchor-preview is automatically disabled.\n* If the anchor editing form is not enabled, clicking on the anchor-preview will not allow the href of the link to be edited\n\n## Placeholder Options\n\nThe placeholder handler is a built-in extension which displays placeholder text when the editor is empty.\n\nOptions for placeholder are passed as an object that is a member of the outer options object. Example:\n```javascript\nvar editor = new MediumEditor('.editable', {\n    placeholder: {\n        /* This example includes the default options for placeholder,\n           if nothing is passed this is what it used */\n        text: 'Type your text',\n        hideOnClick: true\n    }\n});\n```\n\n\n***\n#### `text`\n**Default:** `'Type your text'`\n\nDefines the default placeholder for empty contenteditables when __placeholder__ is not set to false. You can overwrite it by setting a `data-placeholder` attribute on the editor elements.\n\n***\n#### `hideOnClick`\n**Default:** `true`\n\nCauses the placeholder to disappear as soon as the field gains focus. To hide the placeholder only after starting to type, and to show it again as soon as field is empty, set this option to `false`.\n\n\n### Disabling Placeholders\n\nTo disable the placeholder, set the value of the `placeholder` option to `false`:\n```javascript\nvar editor = new MediumEditor('.editable', {\n    placeholder: false\n});\n```\n\n## Anchor Form options\n\nThe anchor form is a built-in button extension which allows the user to add/edit/remove links from within the editor.  When 'anchor' is passed in as a button in the list of buttons, this extension will be enabled and can be triggered by clicking the corresponding button in the toolbar.\n\nOptions for the anchor form are passed as an object that is a member of the outer options object. Example:\n```javascript\nvar editor = new MediumEditor('.editable', {\n    toolbar: {\n        buttons: ['bold', 'italic', 'underline', 'anchor']\n    },\n    anchor: {\n        /* These are the default options for anchor form,\n           if nothing is passed this is what it used */\n        customClassOption: null,\n        customClassOptionText: 'Button',\n        linkValidation: false,\n        placeholderText: 'Paste or type a link',\n        targetCheckbox: false,\n        targetCheckboxText: 'Open in new window'\n    }\n}\n});\n```\n\n\n***\n#### `customClassOption`\n**Default:** `null`\n\nCustom 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.\n\n***\n#### `customClassOptionText`\n**Default:** `'Button'`\n\nText to be shown in the checkbox when the __customClassOption__ is being used.\n\n***\n#### `linkValidation`\n**Default:** `false`\n\nEnables/disables check for common URL protocols on anchor links. Converts invalid url characters (ie spaces) to valid characters using `encodeURI`\n\n***\n#### `placeholderText`\n**Default:** `'Paste or type a link'`\n\nText to be shown as placeholder of the anchor input.\n\n***\n#### `targetCheckbox`\n**Default:** `false`\n\nEnables/disables displaying a \"Open in new window\" checkbox, which when checked changes the `target` attribute of the created link.\n\n***\n#### `targetCheckboxText`\n**Default:** `'Open in new window'`\n\nText to be shown in the checkbox enabled via the __targetCheckbox__ option.\n\n## Paste Options\n\nThe paste handler is a built-in extension which attempts to filter the content when the user pastes.  How the paste handler filters is configurable via specific options.\n\nOptions for paste handling are passed as an object that is a member of the outer options object. Example:\n```javascript\nvar editor = new MediumEditor('.editable', {\n    paste: {\n        /* This example includes the default options for paste,\n           if nothing is passed this is what it used */\n        forcePlainText: true,\n        cleanPastedHTML: false,\n        cleanReplacements: [],\n        cleanAttrs: ['class', 'style', 'dir'],\n        cleanTags: ['meta']\n    }\n});\n```\n\n\n***\n#### `forcePlainText`\n**Default:** `true`\n\nForces pasting as plain text.\n\n***\n#### `cleanPastedHTML`\n**Default:** `false`\n\nCleans pasted content from different sources, like google docs etc.\n\n***\n#### `cleanReplacements`\n**Default:** `[]`\n\nCustom 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.\n\n***\n#### `cleanAttrs`\n**Default:** `['class', 'style', 'dir']`\n\nList of element attributes to remove during paste when __cleanPastedHTML__ is `true` or when calling `cleanPaste(text)` or `pasteHTML(html,options)` helper methods.\n\n***\n#### `cleanTags`\n**Default:** `['meta']`\n\nList of element tag names to remove during paste when __cleanPastedHTML__ is `true` or when calling `cleanPaste(text)` or `pasteHTML(html,options)` helper methods.\n\n***\n#### `unwrapTags`\n**Default:** `[]`\n\nList 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.\n\n***\n### Disabling Paste Handling\n\nTo disable MediumEditor manipulating pasted content, set the both the `forcePlainText` and `cleanPastedHTML` options to `false`:\n```javascript\nvar editor = new MediumEditor('.editable', {\n    paste: {\n        forcePlainText: false,\n        cleanPastedHTML: false\n    }\n});\n```\n\n## KeyboardCommands Options\n\nThe keyboard commands handler is a built-in extension for mapping key-combinations to actions to execute in the editor.\n\nOptions for KeyboardCommands are passed as an object that is a member of the outer options object. Example:\n```javascript\nvar editor = new MediumEditor('.editable', {\n    keyboardCommands: {\n        /* This example includes the default options for keyboardCommands,\n           if nothing is passed this is what it used */\n        commands: [\n            {\n                command: 'bold',\n                key: 'b',\n                meta: true,\n                shift: false\n            },\n            {\n                command: 'italic',\n                key: 'i',\n                meta: true,\n                shift: false\n            },\n            {\n                command: 'underline',\n                key: 'u',\n                meta: true,\n                shift: false\n            }\n        ],\n    }\n});\n```\n\n\n***\n#### `commands`\n**Default:** shortcuts for `bold`, `italic`, and `underline` (See above example)\n\nArray of objects describing each command and the combination of keys that will trigger it.  Required for each object:\n  * _command_: argument passed to `editor.execAction()` when key-combination is used\n  * _key_: keyboard character that triggers this command\n  * _meta_: whether the ctrl/meta key has to be active or inactive\n  * _shift_: whether the shift key has to be active or inactive\n\n### Disabling Keyboard Commands\n\nTo disable the keyboard commands, set the value of the `keyboardCommands` option to `false`:\n```javascript\nvar editor = new MediumEditor('.editable', {\n    keyboardCommands: false\n});\n```\n\n## Auto Link Options\n\n#### `autoLink`\n**Default:** `false`\n\nThe auto-link handler is a built-in extension which automatically turns URLs entered into the text field into HTML anchor tags (similar to the functionality of Markdown).  This feature is OFF by default.\n\n### Enabling Auto Link\n\nTo enable built-in auto-link support, set the value of the `autoLink` option to `true`:\n\n```javascript\nvar editor = new MediumEditor('.editable', {\n    autoLink: true\n});\n```\n\n## Image Dragging Options\n\n#### `imageDragging`\n**Default:** `true`\n\nThe image dragging handler is a built-in extension for handling dragging & dropping images into the contenteditable.  This feature is ON by default.\n\n### Disabling Image Dragging\n\nTo disable built-in image dragging, set the value of the `imageDragging` option to `false`:\n```javascript\nvar editor = new MediumEditor('.editable', {\n    imageDragging: false\n});\n```\n\n## Options Example:\n\n```javascript\nvar editor = new MediumEditor('.editable', {\n    delay: 1000,\n    targetBlank: true,\n    toolbar: {\n        buttons: ['bold', 'italic', 'quote'],\n        diffLeft: 25,\n        diffTop: 10,\n    },\n    anchor: {\n        placeholderText: 'Type a link',\n        customClassOption: 'btn',\n        customClassOptionText: 'Create Button'\n    },\n    paste: {\n        cleanPastedHTML: true,\n        cleanAttrs: ['style', 'dir'],\n        cleanTags: ['label', 'meta']\n    },\n    anchorPreview: {\n        hideDelay: 300\n    },\n    placeholder: {\n        text: 'Click to edit'\n    }\n});\n```\n"
  },
  {
    "path": "README.md",
    "content": "﻿![medium-editor needs help!](https://user-images.githubusercontent.com/2444240/56086015-c42e3000-5e1b-11e9-9692-b97816f67712.png)\n\nIf you would be interested in helping to maintain one of the most successful WYSIWYG text editors on github, let us know!  (See issue [#1503](https://github.com/yabwe/medium-editor/issues/1503))\n\n# MediumEditor\n\nThis is a clone of [medium.com](https://medium.com) inline editor toolbar.\n\nMediumEditor has been written using vanilla JavaScript, no additional frameworks required.\n\n[![screenshot](https://raw.github.com/yabwe/medium-editor/master/demo/img/medium-editor.jpg)](http://yabwe.github.io/medium-editor/)\n\n[![Join the chat at https://gitter.im/yabwe/medium-editor](https://badges.gitter.im/Join%20Chat.svg)](https://gitter.im/yabwe/medium-editor?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge)\n\n## Browser Support\n\n[![Saucelabs Build Status](https://saucelabs.com/browser-matrix/mediumeditor.svg)](https://saucelabs.com/beta/dashboard/builds)\n\n![Supported Browsers](https://cloud.githubusercontent.com/assets/2444240/12874138/d3960a04-cd9b-11e5-8cc5-8136d82cf5f6.png)\n\n[![NPM info](https://nodei.co/npm/medium-editor.png?downloads=true)](https://www.npmjs.com/package/medium-editor)\n\n[![Travis build status](https://travis-ci.org/yabwe/medium-editor.svg?branch=master)](https://travis-ci.org/yabwe/medium-editor)\n[![Dependency Status](https://david-dm.org/yabwe/medium-editor.svg)](https://david-dm.org/yabwe/medium-editor)\n[![devDependency Status](https://david-dm.org/yabwe/medium-editor/dev-status.svg)](https://david-dm.org/yabwe/medium-editor#info=devDependencies)\n[![Coverage Status](https://coveralls.io/repos/yabwe/medium-editor/badge.svg?branch=master&service=github)](https://coveralls.io/github/yabwe/medium-editor?branch=master)\n\n# Basic usage\n\n### Demo\n\n__demo__: [http://yabwe.github.io/medium-editor/](http://yabwe.github.io/medium-editor/)\n\n### Installation\n\n**Via npm:**\n\nRun in your console: `npm install medium-editor`\n\n**Via bower:**\n\n`bower install medium-editor`\n\n**Via an external CDN**\n\n* Using [jsDelivr](http://www.jsdelivr.com/#!medium-editor).\n\n For the latest version:\n\n ```html\n <script src=\"//cdn.jsdelivr.net/npm/medium-editor@latest/dist/js/medium-editor.min.js\"></script>\n <link rel=\"stylesheet\" href=\"//cdn.jsdelivr.net/npm/medium-editor@latest/dist/css/medium-editor.min.css\" type=\"text/css\" media=\"screen\" charset=\"utf-8\">\n ```\n\n For a custom one:\n\n ```html\n <script src=\"//cdn.jsdelivr.net/npm/medium-editor@5.23.2/dist/js/medium-editor.min.js\"></script>\n <link rel=\"stylesheet\" href=\"//cdn.jsdelivr.net/npm/medium-editor@5.23.2/dist/css/medium-editor.min.css\" type=\"text/css\" media=\"screen\" charset=\"utf-8\">\n ```\n\n* Using [CDNJS](https://cdnjs.com/libraries/medium-editor).\n\n**Manual installation:**\n\nDownload the [latest release](https://github.com/yabwe/medium-editor/releases) and attach medium editor's stylesheets to your page:\n\nFind the files to below mentioned linking in the dist folder. (./medium-editor/dist/...)\n\n```html\n<link rel=\"stylesheet\" href=\"css/medium-editor.css\"> <!-- Core -->\n<link rel=\"stylesheet\" href=\"css/themes/default.css\"> <!-- or any other theme -->\n```\n\n### Usage\n\nThe next step is to reference the editor's script\n\n```html\n<script src=\"js/medium-editor.js\"></script>\n```\n\nYou can now instantiate a new MediumEditor object:\n```html\n<script>var editor = new MediumEditor('.editable');</script>\n```\n\nThe above code will transform all the elements with the .editable class into HTML5 editable contents and add the medium editor toolbar to them.\n\nYou can also pass a list of HTML elements:\n\n```javascript\nvar elements = document.querySelectorAll('.editable'),\n    editor = new MediumEditor(elements);\n```\n\nMediumEditor also supports textarea. If you provide a textarea element, the script will create a new div with `contentEditable=true`, hide the textarea and link the textarea value to the div HTML content.\n\n##### Integrating with various frameworks\n\nPeople have contributed wrappers around MediumEditor for integrating with different frameworks and tech stacks.  Take a look at the list of existing [Wrappers and Integrations](https://github.com/yabwe/medium-editor/wiki/Wrappers-and-Integration) that have already been written for MediumEditor!\n\n## MediumEditor Options\n\nView the [MediumEditor Options documentation](OPTIONS.md) on all the various options for MediumEditor.\n\nOptions to customize medium-editor are passed as the second argument to the [MediumEditor constructor](API.md#mediumeditorelements-options).  Example:\n\n```js\nvar editor = new MediumEditor('.editor', {\n    // options go here\n});\n```\n\n### Core options\n* __activeButtonClass__: CSS class added to active buttons in the toolbar. Default: `'medium-editor-button-active'`\n* __buttonLabels__: type of labels on the buttons. Values: `false` | 'fontawesome'.  Default: `false`\n\n#### NOTE:\nUsing `'fontawesome'` as the buttonLabels requires version 4.1.0 of the fontawesome css to be on the page to ensure all icons will be displayed correctly\n\n* __delay__: time in milliseconds to show the toolbar or anchor tag preview. Default: `0`\n* __disableReturn__:  enables/disables the use of the return-key. You can also set specific element behavior by using setting a data-disable-return attribute. Default: `false`\n* __disableDoubleReturn__:  allows/disallows two (or more) empty new lines. You can also set specific element behavior by using setting a data-disable-double-return attribute. Default: `false`\n* __disableExtraSpaces__:  when set to true, it disallows spaces at the beginning and end of the element. Also it disallows entering 2 consecutive spaces between 2 words. Default: `false`\n* __disableEditing__: enables/disables adding the contenteditable behavior. Useful for using the toolbar with customized buttons/actions. You can also set specific element behavior by using setting a data-disable-editing attribute. Default: `false`\n* __elementsContainer__: specifies a DOM node to contain MediumEditor's toolbar and anchor preview elements. Default: `document.body`\n* __extensions__: extension to use (see [Custom Buttons and Extensions](src/js/extensions)) for more. Default: `{}`\n* __spellcheck__: Enable/disable native contentEditable automatic spellcheck. Default: `true`\n* __targetBlank__: enables/disables target=\"\\_blank\" for anchor tags. Default: `false`\n\n### Toolbar options\n\nThe toolbar for MediumEditor is implemented as a built-in extension which automatically displays whenever the user selects some text.  The toolbar can hold any set of defined built-in buttons, but can also hold any custom buttons passed in as extensions.\n\nOptions for the toolbar are passed as an object that is a member of the outer options object. Example:\n```javascript\nvar editor = new MediumEditor('.editable', {\n    toolbar: {\n        /* These are the default options for the toolbar,\n           if nothing is passed this is what is used */\n        allowMultiParagraphSelection: true,\n        buttons: ['bold', 'italic', 'underline', 'anchor', 'h2', 'h3', 'quote'],\n        diffLeft: 0,\n        diffTop: -10,\n        firstButtonClass: 'medium-editor-button-first',\n        lastButtonClass: 'medium-editor-button-last',\n        relativeContainer: null,\n        standardizeSelectionStart: false,\n        static: false,\n        /* options which only apply when static is true */\n        align: 'center',\n        sticky: false,\n        updateOnEmptySelection: false\n    }\n});\n```\n\n* __allowMultiParagraphSelection__: enables/disables whether the toolbar should be displayed when selecting multiple paragraphs/block elements. Default: `true`\n* __buttons__: the set of buttons to display on the toolbar. Default: `['bold', 'italic', 'underline', 'anchor', 'h2', 'h3', 'quote']`\n  * See [Button Options](#button-options) for details on more button options\n* __diffLeft__: value in pixels to be added to the X axis positioning of the toolbar. Default: `0`\n* __diffTop__: value in pixels to be added to the Y axis positioning of the toolbar. Default: `-10`\n* __firstButtonClass__: CSS class added to the first button in the toolbar. Default: `'medium-editor-button-first'`\n* __lastButtonClass__: CSS class added to the last button in the toolbar. Default: `'medium-editor-button-last'`\n* __relativeContainer__: DOMElement to append the toolbar to instead of the body.  When passed, the toolbar will also be positioned `relative` instead of `absolute`. Default: `null`\n* __standardizeSelectionStart__: enables/disables standardizing how the beginning of a range is decided between browsers whenever the selected text is analyzed for updating toolbar buttons status. Default: `false`\n* __static__: enable/disable the toolbar always displaying in the same location relative to the medium-editor element. Default: `false`\n\n##### Options which only apply when the `static` option is being used:\n* __align__: `left`|`center`|`right` - When the __static__ option is `true`, this aligns the static toolbar relative to the medium-editor element. Default: `center`\n* __sticky__: 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. Default: `false`\n* __updateOnEmptySelection__: 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). Default: `false`\n\nTo disable the toolbar (which also disables the anchor-preview extension), set the value of the `toolbar` option to `false`:\n```javascript\nvar editor = new MediumEditor('.editable', {\n    toolbar: false\n});\n```\n\n#### Button Options\n\nButton behavior can be modified by passing an object into the buttons array instead of a string. This allow for overriding some of the default behavior of buttons. The following options are some of the basic parts of buttons that you may override, but any part of the `MediumEditor.Extension.prototype` can be overridden via these button options. (Check out the [source code for buttons](src/js/extensions/button.js) to see what all can be overridden).\n\n* __name__: name of the button being overridden\n* __action__: argument to pass to `MediumEditor.execAction()` when the button is clicked.\n* __aria__: 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.\n* __tagNames__: 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.\n  * _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']`\n  * __NOTE__: This is not used if `useQueryState` is set to `true`.\n* __style__: 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.\n  * _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' }`\n  * __NOTE__: This is not used if `useQueryState` is set to `true`.\n  * Properties of the __style__ object:\n    * __prop__: name of the css property\n    * __value__: value(s) of the css property (multiple values can be separated by a `'|'`)\n* __useQueryState__: 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\n  * _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\n* __contentDefault__: Default `innerHTML` to put inside the button\n* __contentFA__: The `innerHTML` to use for the content of the button if the __buttonLabels__ option for MediumEditor is set to `'fontawesome'`\n* __classList__: An array of classNames (strings) to be added to the button\n* __attrs__: A set of key-value pairs to add to the button as custom attributes to the button element.\n\nExample of overriding buttons (here, the goal is to mimic medium by having <kbd>H1</kbd> and <kbd>H2</kbd> buttons which actually produce `<h2>` and `<h3>` tags respectively):\n```javascript\nvar editor = new MediumEditor('.editable', {\n    toolbar: {\n        buttons: [\n            'bold',\n            'italic',\n            {\n                name: 'h1',\n                action: 'append-h2',\n                aria: 'header type 1',\n                tagNames: ['h2'],\n                contentDefault: '<b>H1</b>',\n                classList: ['custom-class-h1'],\n                attrs: {\n                    'data-custom-attr': 'attr-value-h1'\n                }\n            },\n            {\n                name: 'h2',\n                action: 'append-h3',\n                aria: 'header type 2',\n                tagNames: ['h3'],\n                contentDefault: '<b>H2</b>',\n                classList: ['custom-class-h2'],\n                attrs: {\n                    'data-custom-attr': 'attr-value-h2'\n                }\n            },\n            'justifyCenter',\n            'quote',\n            'anchor'\n        ]\n    }\n});\n```\n\n### Anchor Preview options\n\nThe anchor preview is a built-in extension which automatically displays a 'tooltip' when the user is hovering over a link in the editor.  The tooltip will display the `href` of the link, and when clicked, will open the anchor editing form in the toolbar.\n\nOptions for the anchor preview 'tooltip' are passed as an object that is a member of the outer options object. Example:\n```javascript\nvar editor = new MediumEditor('.editable', {\n    anchorPreview: {\n        /* These are the default options for anchor preview,\n           if nothing is passed this is what it used */\n        hideDelay: 500,\n        previewValueSelector: 'a'\n    }\n}\n});\n```\n\n* __hideDelay__: time in milliseconds to show the anchor tag preview after the mouse has left the anchor tag. Default: `500`\n* __previewValueSelector__: the default selector to locate where to put the activeAnchor value in the preview. You should only need to override this if you've modified the way in which the anchor-preview extension renders. Default: `'a'`\n* __showWhenToolbarIsVisible__: determines whether the anchor tag preview shows up when the toolbar is visible. You should set this value to true if the static option for the toolbar is true and you want the preview to show at the same time. Default: `false`\n* __showOnEmptyLinks__: determines whether the anchor tag preview shows up on link with href as '' or '#something'. You should set this value to false if you do not want the preview to show up in such use cases. Default: `true`\n\nTo disable the anchor preview, set the value of the `anchorPreview` option to `false`:\n```javascript\nvar editor = new MediumEditor('.editable', {\n    anchorPreview: false\n});\n```\n##### NOTE:\n* If the toolbar is disabled (via `toolbar: false` option or `data-disable-toolbar` attribute) the anchor-preview is automatically disabled.\n* If the anchor editing form is not enabled, clicking on the anchor-preview will not allow the href of the link to be edited\n\n### Placeholder Options\n\nThe placeholder handler is a built-in extension which displays placeholder text when the editor is empty.\n\nOptions for placeholder are passed as an object that is a member of the outer options object. Example:\n```javascript\nvar editor = new MediumEditor('.editable', {\n    placeholder: {\n        /* This example includes the default options for placeholder,\n           if nothing is passed this is what it used */\n        text: 'Type your text',\n        hideOnClick: true\n    }\n});\n```\n\n* __text__: Defines the default placeholder for empty contenteditables when __placeholder__ is not set to false. You can overwrite it by setting a `data-placeholder` attribute on the editor elements. Default: `'Type your text'`\n\n* __hideOnClick__: Causes the placeholder to disappear as soon as the field gains focus. Default: `true`.\nTo hide the placeholder only after starting to type, and to show it again as soon as field is empty, set this option to `false`.\n\n\nTo disable the placeholder, set the value of the `placeholder` option to `false`:\n```javascript\nvar editor = new MediumEditor('.editable', {\n    placeholder: false\n});\n```\n\n### Anchor Form options\n\nThe anchor form is a built-in button extension which allows the user to add/edit/remove links from within the editor.  When 'anchor' is passed in as a button in the list of buttons, this extension will be enabled and can be triggered by clicking the corresponding button in the toolbar.\n\nOptions for the anchor form are passed as an object that is a member of the outer options object. Example:\n```javascript\nvar editor = new MediumEditor('.editable', {\n    toolbar: {\n        buttons: ['bold', 'italic', 'underline', 'anchor']\n    },\n    anchor: {\n        /* These are the default options for anchor form,\n           if nothing is passed this is what it used */\n        customClassOption: null,\n        customClassOptionText: 'Button',\n        linkValidation: false,\n        placeholderText: 'Paste or type a link',\n        targetCheckbox: false,\n        targetCheckboxText: 'Open in new window'\n    }\n}\n});\n```\n\n* __customClassOption__: 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. Default: `null`\n* __customClassOptionText__: text to be shown in the checkbox when the __customClassOption__ is being used. Default: `'Button'`\n* __linkValidation__: enables/disables check for common URL protocols on anchor links. Converts invalid url characters (ie spaces) to valid characters using `encodeURI`. Default: `false`\n* __placeholderText__: text to be shown as placeholder of the anchor input. Default: `'Paste or type a link'`\n* __targetCheckbox__: enables/disables displaying a \"Open in new window\" checkbox, which when checked changes the `target` attribute of the created link. Default: `false`\n* __targetCheckboxText__: text to be shown in the checkbox enabled via the __targetCheckbox__ option. Default: `'Open in new window'`\n\n### Paste Options\n\nThe paste handler is a built-in extension which attempts to filter the content when the user pastes.  How the paste handler filters is configurable via specific options.\n\nOptions for paste handling are passed as an object that is a member of the outer options object. Example:\n```javascript\nvar editor = new MediumEditor('.editable', {\n    paste: {\n        /* This example includes the default options for paste,\n           if nothing is passed this is what it used */\n        forcePlainText: true,\n        cleanPastedHTML: false,\n        cleanReplacements: [],\n        cleanAttrs: ['class', 'style', 'dir'],\n        cleanTags: ['meta'],\n        unwrapTags: []\n    }\n});\n```\n\n* __forcePlainText__: Forces pasting as plain text. Default: `true`\n* __cleanPastedHTML__: cleans pasted content from different sources, like google docs etc. Default: `false`\n* __preCleanReplacements__: 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 _before_ builtin replacements.  Default: `[]`\n* __cleanReplacements__: 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_ builtin replacements.  Default: `[]`\n* __cleanAttrs__: list of element attributes to remove during paste when __cleanPastedHTML__ is `true` or when calling `cleanPaste(text)` or `pasteHTML(html,options)` helper methods. Default: `['class', 'style', 'dir']`\n* __cleanTags__: list of element tag names to remove during paste when __cleanPastedHTML__ is `true` or when calling `cleanPaste(text)` or `pasteHTML(html,options)` helper methods. Default: `['meta']`\n* __unwrapTags__: 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. Default: `[]`\n\n### KeyboardCommands Options\n\nThe keyboard commands handler is a built-in extension for mapping key-combinations to actions to execute in the editor.\n\nOptions for KeyboardCommands are passed as an object that is a member of the outer options object. Example:\n```javascript\nvar editor = new MediumEditor('.editable', {\n    keyboardCommands: {\n        /* This example includes the default options for keyboardCommands,\n           if nothing is passed this is what it used */\n        commands: [\n            {\n                command: 'bold',\n                key: 'B',\n                meta: true,\n                shift: false,\n                alt: false\n            },\n            {\n                command: 'italic',\n                key: 'I',\n                meta: true,\n                shift: false,\n                alt: false\n            },\n            {\n                command: 'underline',\n                key: 'U',\n                meta: true,\n                shift: false,\n                alt: false\n            }\n        ],\n    }\n});\n```\n\n* __commands__: Array of objects describing each command and the combination of keys that will trigger it.  Required for each object:\n  * _command_: argument passed to `editor.execAction()` when key-combination is used\n    * if defined as `false`, the shortcut will be disabled\n  * _key_: keyboard character that triggers this command\n  * _meta_: whether the ctrl/meta key has to be active or inactive\n  * _shift_: whether the shift key has to be active or inactive\n  * _alt_: whether the alt key has to be active or inactive\n\nTo disable the keyboard commands, set the value of the `keyboardCommands` option to `false`:\n```javascript\nvar editor = new MediumEditor('.editable', {\n    keyboardCommands: false\n});\n```\n\n### Auto Link Options\n\nThe auto-link handler is a built-in extension which automatically turns URLs entered into the text field into HTML anchor tags (similar to the functionality of Markdown).  This feature is OFF by default.\n\nTo enable built-in auto-link support, set the value of the `autoLink` option to `true`:\n\n```javascript\nvar editor = new MediumEditor('.editable', {\n    autoLink: true\n});\n```\n\n### Image Dragging Options\n\nThe image dragging handler is a built-in extension for handling dragging & dropping images into the contenteditable.  This feature is ON by default.\n\nTo disable built-in image dragging, set the value of the `imageDragging` option to `false`:\n```javascript\nvar editor = new MediumEditor('.editable', {\n    imageDragging: false\n});\n```\n\n#### Disable File Dragging\nTo stop preventing drag & drop events and disable file dragging in general, provide a dummy ImageDragging extension.\n```javascript\nvar editor = new MediumEditor('.editor', {\n    extensions: {\n        'imageDragging': {}\n    }\n});\n```\nDue to the [state of code](https://github.com/yabwe/medium-editor/issues/966) in 5.0.0, the editor *ALWAYS* prevented any drag and drop actions.\nWe will have a better way to disable file dragging in 6.*\n\n### Options Example:\n\n```javascript\nvar editor = new MediumEditor('.editable', {\n    delay: 1000,\n    targetBlank: true,\n    toolbar: {\n        buttons: ['bold', 'italic', 'quote'],\n        diffLeft: 25,\n        diffTop: 10,\n    },\n    anchor: {\n        placeholderText: 'Type a link',\n        customClassOption: 'btn',\n        customClassOptionText: 'Create Button'\n    },\n    paste: {\n        cleanPastedHTML: true,\n        cleanAttrs: ['style', 'dir'],\n        cleanTags: ['label', 'meta'],\n        unwrapTags: ['sub', 'sup']\n    },\n    anchorPreview: {\n        hideDelay: 300\n    },\n    placeholder: {\n        text: 'Click to edit'\n    }\n});\n```\n\n## Buttons\n\nBy default, MediumEditor supports buttons for most of the commands for `document.execCommand()` that are well-supported across all its supported browsers.\n\n### Default buttons.\n\nMediumEditor, by default, will show only the buttons listed here to avoid a huge toolbar:\n\n* __bold__\n* __italic__\n* __underline__\n* __anchor__ _(built-in support for collecting a URL via the anchor extension)_\n* __h2__\n* __h3__\n* __quote__\n\n### All buttons.\n\nThese are all the built-in buttons supported by MediumEditor.\n\n* __bold__\n* __italic__\n* __underline__\n* __strikethrough__\n* __subscript__\n* __superscript__\n* __anchor__\n* __image__ (this simply converts selected text to an image tag)\n* __quote__\n* __pre__\n* __orderedlist__\n* __unorderedlist__\n* __indent__ (moves the selected text up one level)\n* __outdent__ (moves the selected text down one level)\n* __justifyLeft__\n* __justifyCenter__\n* __justifyRight__\n* __justifyFull__\n* __h1__\n* __h2__\n* __h3__\n* __h4__\n* __h5__\n* __h6__\n* __removeFormat__ (clears inline style formatting, preserves blocks)\n* __html__ (parses selected html and converts into actual html elements)\n\n## Themes\n\nCheck out the Wiki page for a list of available themes: [https://github.com/yabwe/medium-editor/wiki/Themes](https://github.com/yabwe/medium-editor/wiki/Themes)\n\n## API\n\nView the [MediumEditor Object API documentation](API.md) on the Wiki for details on all the methods supported on the MediumEditor object.\n\n### Initialization methods\n* __MediumEditor(elements, options)__:  Creates an instance of MediumEditor\n* __.destroy()__: tears down the editor if already setup, removing all DOM elements and event handlers\n* __.setup()__: rebuilds the editor if it has already been destroyed, recreating DOM elements and attaching event handlers\n* __.addElements()__: add elements to an already initialized instance of MediumEditor\n* __.removeElements()__: remove elements from an already initialized instance of MediumEditor\n\n### Event Methods\n* __.on(target, event, listener, useCapture)__: attach a listener to a DOM event which will be detached when MediumEditor is deactivated\n* __.off(target, event, listener, useCapture)__: detach a listener to a DOM event that was attached via `on()`\n* __.subscribe(event, listener)__: attaches a listener to a custom medium-editor event\n* __.unsubscribe(event, listener)__: detaches a listener from a custom medium-editor event\n* __.trigger(name, data, editable)__: manually triggers a custom medium-editor event\n\n### Selection Methods\n* __.checkSelection()__: manually trigger an update of the toolbar and extensions based on the current selection\n* __.exportSelection()__: return a data representation of the selected text, which can be applied via `importSelection()`\n* __.importSelection(selectionState)__: restore the selection using a data representation of previously selected text (ie value returned by `exportSelection()`)\n* __.getFocusedElement()__: returns an element if any contenteditable element monitored by MediumEditor currently has focused\n* __.getSelectedParentElement(range)__: get the parent contenteditable element that contains the current selection\n* __.restoreSelection()__: restore the selection to what was selected when `saveSelection()` was called\n* __.saveSelection()__: internally store the set of selected text\n* __.selectAllContents()__: expands the selection to contain all text within the focused contenteditable\n* __.selectElement(element)__: change selection to be a specific element and update the toolbar to reflect the selection\n* __.stopSelectionUpdates()__: stop the toolbar from updating to reflect the state of the selected text\n* __.startSelectionUpdates()__: put the toolbar back into its normal updating state\n\n### Editor Action Methods\n* __.cleanPaste(text)__: convert text to plaintext and replace current selection with result\n* __.createLink(opts)__: creates a link via the native `document.execCommand('createLink')` command\n* __.execAction(action, opts)__: executes an built-in action via `document.execCommand`\n* __.pasteHTML(html, options)__: replace the current selection with html\n* __.queryCommandState(action)__: wrapper around the browser's built in `document.queryCommandState(action)` for checking whether a specific action has already been applied to the selection.\n\n### Helper Methods\n* __.delay(fn)__: delay any function from being executed by the amount of time passed as the `delay` option\n* __.getContent(index)__: gets the trimmed `innerHTML` of the element at `index`\n* __.getExtensionByName(name)__: get a reference to an extension with the specified name\n* __.resetContent(element)__: reset the content of all elements or a specific element to its value when added to the editor initially\n* __.serialize()__: returns a JSON object with elements contents\n* __.setContent(html, index)__: sets the `innerHTML` to `html` of the element at `index`\n\n### Static Methods/Properties\n* __.getEditorFromElement(element)__: retrieve the instance of MediumEditor that is monitoring the provided editor element\n* __.version__: the version information for the MediumEditor library\n\n## Dynamically add/remove elements to your instance\n\nIt is possible to dynamically add new elements to your existing MediumEditor instance:\n\n```javascript\nvar editor = new MediumEditor('.editable');\neditor.subscribe('editableInput', this._handleEditableInput.bind(this));\n\n// imagine an ajax fetch/any other dynamic functionality which will add new '.editable' elements to the DOM\n\neditor.addElements('.editable');\n// OR editor.addElements(document.getElementsByClassName('editable'));\n// OR editor.addElements(document.querySelectorAll('.editable'));\n```\n\nPassing an elements or array of elements to `addElements(elements)` will:\n* Add the given element or array of elements to the internal `this.elements` array.\n* Ensure the element(s) are initialized with the proper attributes and event handlers as if the element had been passed during instantiation of the editor.\n* For any `<textarea>` elements:\n  * Hide the `<textarea>`\n  * Create a new `<div contenteditable=true>` element and add it to the elements array.\n  * Ensure the 2 elements remain sync'd.\n* Be intelligent enough to run the necessary code only once per element, no matter how often you will call it.\n\n### Removing elements dynamically\n\nStraight forward, just call `removeElements` with the element or array of elements you to want to tear down. Each element itself will remain a contenteditable - it will just remove all event handlers and all references to it so you can safely remove it from DOM.\n\n```javascript\neditor.removeElements(document.querySelector('#myElement'));\n// OR editor.removeElements(document.getElementById('myElement'));\n// OR editor.removeElements('#myElement');\n\n// in case you have jQuery and don't exactly know when an element was removed, for example after routing state change\nvar removedElements = [];\neditor.elements.forEach(function (element) {\n    // check if the element is still available in current DOM\n    if (!$(element).parents('body').length) {\n        removedElements.push(element);\n    }\n});\n\neditor.removeElements(removedElements);\n```\n\n## Capturing DOM changes\n\nFor observing any changes on contentEditable, use the custom `'editableInput'` event exposed via the `subscribe()` method:\n\n```js\nvar editor = new MediumEditor('.editable');\neditor.subscribe('editableInput', function (event, editable) {\n    // Do some work\n});\n```\n\nThis event is supported in all browsers supported by MediumEditor (including IE9+ and Edge)!  To help with cases when one instance of MediumEditor is monitoring multiple elements, the 2nd argument passed to the event handler (`editable` in the example above) will be a reference to the contenteditable element that has actually changed.\n\nThis is handy when you need to capture any modifications to the contenteditable element including:\n* Typing\n* Cutting/Pasting\n* Changes from clicking on buttons in the toolbar\n* Undo/Redo\n\nWhy is this interesting and why should you use this event instead of just attaching to the `input` event on the contenteditable element?\n\nSo for most modern browsers (Chrome, Firefox, Safari, etc.), the `input` event works just fine. In fact, `editableInput` is just a proxy for the `input` event in those browsers. However, the `input` event [is not supported for contenteditable elements in IE 9-11](https://connect.microsoft.com/IE/feedback/details/794285/ie10-11-input-event-does-not-fire-on-div-with-contenteditable-set) and is _mostly_ supported in Microsoft Edge, but not fully.\n\nSo, to properly support the `editableInput` event in Internet Explorer and Microsoft Edge, MediumEditor uses a combination of the `selectionchange` and `keypress` events, as well as monitoring calls to `document.execCommand`.\n\n## Extensions & Plugins\n\nCheck the [documentation](src/js/extensions) in order to learn how to develop extensions for MediumEditor.\n\nA list of existing extensions and plugins, such as [Images and Media embeds](http://orthes.github.io/medium-editor-insert-plugin/), [Tables](https://github.com/yabwe/medium-editor-tables) and [Markdown](https://github.com/IonicaBizau/medium-editor-markdown) can be found [here](https://github.com/yabwe/medium-editor/wiki/Extensions-Plugins).\n\n## Development\n\nTo run the demo locally:\n\n1. Clone this repo locally\n2. Run `npm install` from your console at the root\n3. Run `node index.js` from the root\n4. Navigate to `http://localhost:8088/demo/index.html` to view the demo\n\nMediumEditor development tasks are managed by Grunt. To install all the necessary packages, just invoke:\n\n```bash\nnpm install\n```\n\nTo run all the test and build the dist files for testing on demo pages, just invoke:\n```bash\ngrunt\n```\n\nThese are the other available grunt tasks:\n\n* __js__: runs jslint and jasmine tests and creates minified and concatenated versions of the script;\n* __css__: runs autoprefixer and csslint\n* __test__: runs jasmine tests, jslint and csslint\n* __watch__: watch for modifications on script/scss files\n* __spec__: runs a task against a specified file\n\nThe source files are located inside the __src__ directory.  Be sure to make changes to these files and not files in the dist directory.\n\n## Contributing\n\n[Kill some bugs :)](https://github.com/yabwe/medium-editor/issues?q=is%3Aopen+is%3Aissue+label%3Abug)\n\n1. Fork it\n2. Create your feature branch (`git checkout -b my-new-feature`)\n3. Test your changes to the best of your ability.\n4. Update the documentation to reflect your changes if they add or changes current functionality.\n5. Commit your changes (`git commit -am 'Added some feature'`) **without files from the _dist_ directory**.\n6. Push to the branch (`git push origin my-new-feature`)\n7. Create a new Pull Request\n\n### Code Consistency\n\nTo help create consistent looking code throughout the project, we use a few tools to help us. They have plugins for most popular editors/IDEs to make coding for our project, but you should use them in your project as well!\n\n#### JSHint\n\nWe use [JSHint](http://jshint.com/) on each build to find easy-to-catch errors and potential problems in our js.  You can find our JSHint settings in the `.jshintrc` file in the root of the project.\n\n#### jscs\n\nWe use [jscs](http://jscs.info/) on each build to enforce some code style rules we have for our project.  You can find our jscs settings in the `.jscsrc` file in the root of the project.\n\n#### EditorConfig\n\nWe use [EditorConfig](http://EditorConfig.org) to maintain consistent coding styles between various editors and IDEs.  You can find our settings in the `.editorconfig` file in the root of the project.\n\n### Easy First Bugs\n\nLooking for something simple for a first contribution? Try fixing an [easy first bug](https://github.com/yabwe/medium-editor/issues?q=is%3Aopen+is%3Aissue+label%3A%22easy+first+bug%22)!\n\n## Contributors (100+ and counting!)\n\n[https://github.com/yabwe/medium-editor/graphs/contributors](https://github.com/yabwe/medium-editor/graphs/contributors)\n\n## Is Your Org Using MediumEditor?\n\nAdd your org [here](https://github.com/yabwe/medium-editor/issues/828) and we can add you to our [landing page](https://yabwe.github.io/medium-editor/#who-is-using-it)!\n\n## License\n\nMIT: https://github.com/yabwe/medium-editor/blob/master/LICENSE\n"
  },
  {
    "path": "UPGRADE-5.md",
    "content": "# Upgrading to v5.0.0\n\nVersion 5.0.0 of MediumEditor introduces a significantly simpler system for building custom extensions as well as extending existing buttons and extensions. As part of moving towards this extendable model, there were significant changes to the way options are passed to MediumEditor, as well as the extensions and buttons themselves.\n\nIn addition to extension related changes, there were several other potential breaking changes related to API methods, as well as utility helper methods.\n\n## MediumEditor Options\n\nFor details on all the currently supported MediumEditor options, refer to the [Medium Editor Options Wiki Page](https://github.com/yabwe/medium-editor/wiki/Options).\n\n### Toolbar Options\n* Options controlling the toolbar are now passed as a `'toolbar'` object inside the outer options object.\n  * `buttons` -> `toolbar.buttons`\n  * `toolbarAlign` -> `toolbar.align`\n  * `diffTop` -> `toolbar.diffTop`\n  * `diffLeft` -> `toolbar.diffLeft`\n  * `staticToolbar` -> `toolbar.static`\n  * `stickyToolbar` -> `toolbar.sticky`\n  * `firstButtonClass` -> `toolbar.firstButtonClass`\n  * `lastButtonClass` -> `toolbar.lastButtonClass`\n  * `updateOnEmptySelection` -> `toolbar.updateOnEmptySelection`\n  * `standardizeSelectionStart` -> `toolbar.standardizeSelectionStart`\n\n### Anchor Options\n* Options controlling the anchor input extension are now passed as a `'anchor'` object inside the outer options object.\n  * `anchorInputPlaceholder` -> `anchor.placeholderText`\n  * `checkLinkFormat` -> `anchor.linkValidation`\n  * `anchorButton` & `anchorButtonClass` -> `anchor.customClassOption`\n  * `anchorTarget` -> `anchor.targetCheckbox`\n  * `anchorInputCheckboxLabel` -> `anchor.targetCheckboxText`\n\n### Anchor Preview Options\n* Options controlling the anchor preview extension are now passed as a `'anchorPreview'` object inside the outer options object.\n  * `anchorPreviewHideDelay` -> `anchorPreview.hideDelay`\n\n### Paste Options\n* Options controlling paste are now passed as a `'paste'` object inside the outer options object.\n  * `forcePlainText` -> `paste.forcePlainText`\n  * `cleanPastedHTML` -> `paste.cleanPastedHTML`\n\n### Placeholder Options\n* Options controlling the placeholder extension are now passed as a `'placeholder'` object inside the outer options object.\n  * `placeholder` -> `placeholder.text`\n\n### Other Options\n\n#### `disableToolbar`\n* Disabling the toolbar extension is now done by setting the `toolbar` option to `false`\n\n#### `disableAnchorPreview`\n* Disabling the anchor preview extension is now done by setting the `anchorPreview` option to `false`\n\n#### `disablePlaceholders`\n* Disabling the placeholder extension is now done by setting the `placeholder` option to `false`\n\n#### `onShowToolbar` & `onHideToolbar`\n* The `onShowToolbar` and `onHideToolbar` options are no longer supported. Instead, attach to the `'showToolbar'` and `'hideToolbar'` [custom events](https://github.com/yabwe/medium-editor/wiki/Custom-Events) via `MediumEditor.subscribe()`\n\n#### `firstHeader` & `secondHeader`\n* The `firstHeader` & `secondHeader` options have been removed.  Instead, any number of the 6 header types can be passed as button names into the `toolbar.buttons` option array.\n  * Example: Where before the code may have sent `firstHeader: 'h2'` and `secondHeader: 'h3'`, it should now pass `['bold', 'italic', 'quote', 'h2', 'h3']` via the `toolbar.buttons` property of the MediumEditor options object.\n\n#### `buttonLabels`\n* The `buttonLabels` option no longer supports taking an object in that specifies custom labels for all buttons. Instead, pass an object into the `toolbar.buttons` option array that contains a `.name` property for the name of the button, a either a `contentDefault` or a `contentFA` property that should be in the innerHTML of the button (for default of `fontawesome` buttonLabels respectively)\n\n\n## MediumEditor Extensions\n#### `.parent`\n* `Extension.parent` is no longer supported.  All extensions will have a reference to the MediumEditor instance via their `.base` property, unless the property already exists.\n\n#### `.init()`\n* `Extension.init()` will no longer be passed any arguments. Previously, `.init(instance)` received an instance of MediumEditor as an argument, but this is not needed now that `.base` will be populated before `.init()` is called.\n\n#### `.deactivate()`\n* `Extension.deactivate()` will no longer be called by MediumEditor. `.destroy()` will be called instead when MediumEditor is destroyed.\n\n#### `.options`\n* The `.options` property of any built-in extensions or buttons has been removed.  All of the properties should be retrieved and set from the prototype of the object itself.\n  * Example: Instead of buttons using `this.options.action`, they should now use `this.action`.\n  * Not all extensions had options before, or saved them via the `.options` property.\n\n## MediumEditor API\n\n#### `.id`\n* The unique identifier used for MediumEditor elements will now remain unique and remain regardless of how many instances are created. After calling `.destroy()` and `.setup()`, the id will remain the same. This id was used to generate unique element ids for things like the id attribute of the toolbar element (`'medium-editor-toolbar-[ID]'`)\n\n#### `.toolbar`\n* The MediumEditor toolbar is now an extension, so `MediumEditor.toolbar` is no longer a valid reference.  Use `MediumEditor.getExtensionByName('toolbar')` instead.\n\n#### `.statics`\n* All of the `.statics` references have been removed as the new style of extensions and buttons has been introduced. The objects exposed via `.statics` have also been changed, so code which uses them may require additonal changes.\n  * `MediumEditor.statics.ButtonsData` -> `MediumEditor.extensions.button.prototype.defaults` (ideally this reference should no longer be needed)\n  * `MediumEditor.statics.DefaultButton` -> `MediumEditor.extensions.button`\n  * `MediumEditor.statics.AnchorExtension` -> `MediumEditor.extensions.anchor`\n  * `MediumEditor.statics.FontSizeExtension` -> `MediumEditor.extensions.fontSize`\n  * `MediumEditor.statics.Toolbar` -> `MediumEditor.extensions.toolbar`\n  * `MediumEditor.statics.AnchorPreview` -> `MediumEditor.extensions.anchorPreview`\n\n#### `.activate()`\n* `MediumEditor.activate()` has been replaced with `MediumEditor.setup()`\n\n#### `.deactivate()`\n* `MediumEditor.deactivate()` has been replaced with `MediumEditor.destroy()`\n\n#### `.createEvent()`\n* `MediumEditor.createEvent()` is no longer needed in order to fire custom events. It has been removed.\n\n#### `.hideToolbarDefaultActions()`\n* `MediumEditor.hideToolbarDefaultActions()` has been removed.  Use the `hideToolbarDefaultActions()` method of the toolbar extension instead.\n\n#### `.setToolbarPosition()`\n* `MediumEditor.setToolbarPosition()` has been removed.  Use the `setToolbarPosition()` method of the toolbar extension instead.\n\n#### `.callExtensions()`\n* `MediumEditor.callExtensions()` has been removed and is no longer supported.\n\n\n## MediumEditor Utility Methods\n\n### MediumEditor.util\n* `MediumEditor.util.getSelectionRange()` has been moved to `MediumEditor.selection.getSelectionRange()`\n* `MediumEditor.util.getSelectionStart()` has been moved to `MediumEditor.selection.getSelectionStart()`\n* `MediumEditor.util.unwrapElement()` has been removed. Use `MediumEditor.util.unwrap()` instead\n* `MediumEditor.util.getSelectionData()` has been removed\n* `MediumEditor.util.setObject()` has been removed\n* `MediumEditor.util.getObject()` has been removed\n* `MediumEditor.util.derives()` has been removed. Objects that can be drived from (like extensions and buttons) will have a `.extend()` method for extending.\n* `MediumEditor.util.now()` has been removed.  Use `Date.now()` instead.\n* `MediumEditor.util.parentElements` has been renamed `MediumEditor.util.blockContainerElementNames`\n\n### MediumEditor.selection\n* `MediumEditor.selection.getSelectionData()` has been removed\n\n\n## MediumEditor CSS & Markup\n* The `.clearfix` class has been removed, and `.clearfix` class is no longer added to the toolbar element.\n* All references to `'medium'` in CSS classes has been replaced with `'medium-editor'`\n  * Example: The image element added by the image dragging extension will now have a `medium-editor-image-loading` class on instead of `medium-image-loading`\n* The `data-medium-element` attribute on all MediumEditor elements has been renamed to `data-medium-editor-element`\n* Toolbar classes `sticky-toolbar` and `static-toolbar` have been renamed `medium-editor-sticky-toolbar` and `medium-editor-static-toolbar` respectively\n\n\n## Other Changes\n\n* The `getFocusedElement()` method of the toolbar extension has been removed. Use `MediumEditor.getFocusedElement()` instead.\n* Keyboard Shortcuts are now controlled via the Keyboard Commands extension.  The `.key` option on buttons is not longer supported for mapping keyboard shortcuts.\n"
  },
  {
    "path": "bower.json",
    "content": "{\n  \"name\": \"medium-editor\",\n  \"homepage\": \"http://yabwe.github.io/medium-editor/\",\n  \"authors\": [\n    \"Davi Ferreira <hi@daviferreira.com>\",\n    \"Nate Mielnik <nathan@outlook.com>\",\n    \"Noah Chase <nchase@gmail.com>\",\n    \"Jeremy Benoist <jeremy.benoist@gmail.com>\"\n  ],\n  \"description\": \"Medium.com WYSIWYG editor clone written in pure JavaScript.\",\n  \"main\": [\"dist/js/medium-editor.js\",\n           \"dist/css/medium-editor.css\",\n           \"dist/css/themes/default.css\"],\n  \"keywords\": [\n    \"contenteditable\",\n    \"wysiwyg\",\n    \"medium\",\n    \"rich-text\",\n    \"editor\"\n  ],\n  \"license\": \"MIT\",\n  \"ignore\": [\n    \"**/.*\",\n    \"node_modules\",\n    \"bower_components\",\n    \"spec\",\n    \"coverage\",\n    \"reports\",\n    \"_SpecRunner.html\",\n    \"Gruntfile.js\",\n    \"demo\",\n    \"package.json\",\n    \"CHANGES.md\",\n    \"MAINTAINERS.md\",\n    \"CODE_OF_CONDUCT.md\",\n    \"CONTRIBUTING.md\",\n    \"UPGRADE-5.md\"\n  ]\n}\n"
  },
  {
    "path": "demo/absolute-container.html",
    "content": "<!DOCTYPE html>\n<html>\n<head>\n    <meta charset=\"UTF-8\">\n    <title>medium editor | demo</title>\n    <link rel=\"stylesheet\" href=\"css/demo.css\">\n    <link rel=\"stylesheet\" href=\"http://netdna.bootstrapcdn.com/font-awesome/4.1.0/css/font-awesome.css\">\n    <link rel=\"stylesheet\" href=\"../dist/css/medium-editor.css\">\n    <link rel=\"stylesheet\" href=\"../dist/css/themes/default.css\" id=\"medium-editor-theme\">\n\n    <style>\n        body {\n            height: 100%;\n        }\n\n        #container {\n            position: absolute;\n            left: 50%;\n            margin-left: -480px;\n            height: 100%;\n            overflow: auto;\n        }\n    </style>\n</head>\n<body>\n    <a href=\"https://github.com/yabwe/medium-editor\" class=\"github-link\"><img style=\"z-index: 100;position: absolute; top: 0; right: 0; border: 0;\" src=\"https://s3.amazonaws.com/github/ribbons/forkme_right_gray_6d6d6d.png\" alt=\"Fork me on GitHub\"></a>\n    <div class=\"top-bar\">\n        Theme:\n        <select id=\"sel-themes\">\n            <option value=\"themes/default\" selected>default</option>\n            <option value=\"themes/roman\">roman</option>\n            <option value=\"themes/mani\">mani</option>\n            <option value=\"themes/flat\">flat</option>\n            <option value=\"themes/bootstrap\">bootstrap</option>\n            <option value=\"themes/tim\">tim</option>\n            <option value=\"themes/beagle\">beagle</option>\n        </select>\n    </div>\n    <div id=\"container\">\n        <h1>Medium Editor</h1>\n        <div class=\"editable\">\n            <p>My father’s family name being <a href=\"https://en.wikipedia.org/wiki/Pip_(Great_Expectations)\">Pirrip</a>, and my Christian name Philip, my infant tongue could make of both names nothing longer or more explicit than Pip. So, I called myself Pip, and came to be called Pip.</p>\n            <p>I give Pirrip as my father’s family name, on the authority of his tombstone and my sister,—Mrs. Joe Gargery, who married the blacksmith. As I never saw my father or my mother, and never saw any likeness of either of them (for their days were long before the days of photographs), my first fancies regarding what they were like were unreasonably derived from their tombstones. The shape of the letters on my father’s, gave me an odd idea that he was a square, stout, dark man, with curly black hair. From the character and turn of the inscription, “Also Georgiana Wife of the Above,” I drew a childish conclusion that my mother was freckled and sickly. To five little stone lozenges, each about a foot and a half long, which were arranged in a neat row beside their grave, and were sacred to the memory of five little brothers of mine,—who gave up trying to get a living, exceedingly early in that universal struggle,—I am indebted for a belief I religiously entertained that they had all been born on their backs with their hands in their trousers-pockets, and had never taken them out in this state of existence.</p>\n            <p>Ours was the marsh country, down by the river, within, as the river wound, twenty miles of the sea. My first most vivid and broad impression of the identity of things seems to me to have been gained on a memorable raw afternoon towards evening. At such a time I found out for certain that this bleak place overgrown with nettles was the churchyard; and that Philip Pirrip, late of this parish, and also Georgiana wife of the above, were dead and buried; and that Alexander, Bartholomew, Abraham, Tobias, and Roger, infant children of the aforesaid, were also dead and buried; and that the dark flat wilderness beyond the churchyard, intersected with dikes and mounds and gates, with scattered cattle feeding on it, was the marshes; and that the low leaden line beyond was the river; and that the distant savage lair from which the wind was rushing was the sea; and that the small bundle of shivers growing afraid of it all and beginning to cry, was Pip.</p>\n        </div>\n    </div>\n    <p style=\"text-align: center;\"><small><a style=\"color: #333;\" target=\"_blank\" href=\"http://www.goodreads.com/reader/475-great-expectations\">Source</a></small></p>\n    <script src=\"../dist/js/medium-editor.js\"></script>\n    <script>\n        var editor = new MediumEditor('.editable', {\n            buttonLabels: 'fontawesome',\n            elementsContainer: document.getElementById('container')\n        }),\n        cssLink = document.getElementById('medium-editor-theme');\n\n        document.getElementById('sel-themes').addEventListener('change', function () {\n            cssLink.href = '../dist/css/' + this.value + '.css';\n        });\n    </script>\n</body>\n</html>\n"
  },
  {
    "path": "demo/auto-link.html",
    "content": "<!DOCTYPE html>\n<html>\n<head>\n    <meta charset=\"UTF-8\">\n    <title>medium editor | demo</title>\n    <link rel=\"stylesheet\" href=\"css/demo.css\">\n    <link rel=\"stylesheet\" href=\"http://netdna.bootstrapcdn.com/font-awesome/4.1.0/css/font-awesome.css\">\n    <link rel=\"stylesheet\" href=\"../dist/css/medium-editor.css\">\n    <link rel=\"stylesheet\" href=\"../dist/css/themes/default.css\" id=\"medium-editor-theme\">\n</head>\n<body>\n    <a href=\"https://github.com/yabwe/medium-editor\" class=\"github-link\"><img style=\"z-index: 100;position: absolute; top: 0; right: 0; border: 0;\" src=\"https://s3.amazonaws.com/github/ribbons/forkme_right_gray_6d6d6d.png\" alt=\"Fork me on GitHub\"></a>\n    <div class=\"top-bar\">\n        Theme:\n        <select id=\"sel-themes\">\n            <option value=\"themes/default\" selected>default</option>\n            <option value=\"themes/roman\">roman</option>\n            <option value=\"themes/mani\">mani</option>\n            <option value=\"themes/flat\">flat</option>\n            <option value=\"themes/bootstrap\">bootstrap</option>\n        </select>\n    </div>\n    <div id=\"container\">\n        <h1>Medium Editor</h1>\n        <div class=\"editable\">\n            <p>My father’s family name being <a href=\"https://en.wikipedia.org/wiki/Pip_(Great_Expectations)\">Pirrip</a>, and my Christian name Philip, my infant tongue could make of both names nothing longer or more explicit than Pip. So, I called myself Pip, and came to be called Pip.</p>\n            <p>I give Pirrip as my father’s family name, on the authority of his tombstone and my sister,—Mrs. Joe Gargery, who married the blacksmith. As I never saw my father or my mother, and never saw any likeness of either of them (for their days were long before the days of photographs), my first fancies regarding what they were like were unreasonably derived from their tombstones. The shape of the letters on my father’s, gave me an odd idea that he was a square, stout, dark man, with curly black hair. From the character and turn of the inscription, “Also Georgiana Wife of the Above,” I drew a childish conclusion that my mother was freckled and sickly. To five little stone lozenges, each about a foot and a half long, which were arranged in a neat row beside their grave, and were sacred to the memory of five little brothers of mine,—who gave up trying to get a living, exceedingly early in that universal struggle,—I am indebted for a belief I religiously entertained that they had all been born on their backs with their hands in their trousers-pockets, and had never taken them out in this state of existence.</p>\n            <p>Ours was the marsh country, down by the river, within, as the river wound, twenty miles of the sea. My first most vivid and broad impression of the identity of things seems to me to have been gained on a memorable raw afternoon towards evening. At such a time I found out for certain that this bleak place overgrown with nettles was the churchyard; and that Philip Pirrip, late of this parish, and also Georgiana wife of the above, were dead and buried; and that Alexander, Bartholomew, Abraham, Tobias, and Roger, infant children of the aforesaid, were also dead and buried; and that the dark flat wilderness beyond the churchyard, intersected with dikes and mounds and gates, with scattered cattle feeding on it, was the marshes; and that the low leaden line beyond was the river; and that the distant savage lair from which the wind was rushing was the sea; and that the small bundle of shivers growing afraid of it all and beginning to cry, was Pip.</p>\n            <p><a href=\"http://google.com\"><img src=\"https://placeholdit.imgix.net/~text?txtsize=33&txt=350%C3%97150&w=350&h=150\"></a></p>\n        </div>\n    </div>\n    <p style=\"text-align: center;\"><small><a style=\"color: #333;\" target=\"_blank\" href=\"http://www.goodreads.com/reader/475-great-expectations\">Source</a></small></p>\n    <script src=\"../dist/js/medium-editor.js\"></script>\n    <script>\n        var editor = new MediumEditor('.editable', {\n            buttonLabels: 'fontawesome',\n            autoLink: true,\n            toolbar: {\n                buttons: ['bold', 'italic', 'unorderedlist', 'orderedlist', 'anchor']\n            }\n        }),\n        cssLink = document.getElementById('medium-editor-theme');\n\n        document.getElementById('sel-themes').addEventListener('change', function () {\n            cssLink.href = '../dist/css/' + this.value + '.css';\n        });\n    </script>\n</body>\n</html>\n"
  },
  {
    "path": "demo/button-example.html",
    "content": "<!DOCTYPE html>\n<html>\n<head>\n    <meta charset=\"UTF-8\">\n    <title>MediumEditor - Button Example</title>\n    <link rel=\"stylesheet\" href=\"css/demo.css\">\n    <link href=\"https://netdna.bootstrapcdn.com/font-awesome/4.2.0/css/font-awesome.css\" rel=\"stylesheet\">\n    <link rel=\"stylesheet\" href=\"../dist/css/medium-editor.css\">\n    <link rel=\"stylesheet\" href=\"../dist/css/themes/bootstrap.css\">\n</head>\n<body>\n    <div id=\"container\">\n        <h1>Medium Editor</h1>\n        <div class=\"editable\">\n            <h2>Font Awesome</h2>\n            <p>My father’s family name being <a href=\"https://en.wikipedia.org/wiki/Pip_(Great_Expectations)\">Pirrip</a>, and my Christian name Philip, my infant tongue could make of both names nothing longer or more explicit than Pip. So, I called myself Pip, and came to be called Pip.</p>\n            <p>I give Pirrip as my father’s family name, on the authority of his tombstone and my sister,—Mrs. Joe Gargery, who married the blacksmith. As I never saw my father or my mother, and never saw any likeness of either of them (for their days were long before the days of photographs), my first fancies regarding what they were like were unreasonably derived from their tombstones...</p>\n        </div>\n    </div>\n    <p style=\"text-align: center;\"><small><a style=\"color: #333;\" target=\"_blank\" href=\"http://www.goodreads.com/reader/475-great-expectations\">Source</a></small></p>\n    <script src=\"../dist/js/medium-editor.js\"></script>\n    <script src=\"https://cdnjs.cloudflare.com/ajax/libs/rangy/1.3.0/rangy-core.js\"></script>\n    <script src=\"https://cdnjs.cloudflare.com/ajax/libs/rangy/1.3.0/rangy-classapplier.min.js\"></script>\n\n    <script>\n        rangy.init();\n\n        var HighlighterButton = MediumEditor.extensions.button.extend({\n            name: 'highlighter',\n            tagNames: ['mark'],\n            contentDefault: '<b>H</b>',\n            contentFA: '<i class=\"fa fa-paint-brush\"></i>',\n            aria: 'Highlight',\n            action: 'highlight',\n\n            init: function () {\n                MediumEditor.extensions.button.prototype.init.call(this);\n\n                this.classApplier = rangy.createClassApplier('highlight', {\n                    elementTagName: 'mark',\n                    normalize: true\n                });\n            },\n\n            handleClick: function (event) {\n                this.classApplier.toggleSelection();\n\n                // Ensure the editor knows about an html change so watchers are notified\n                // ie: <textarea> elements depend on the editableInput event to stay synchronized\n                this.base.checkContentChanged();\n            }\n        });\n        var editor = new MediumEditor('.editable', {\n                toolbar: {\n                    buttons: ['bold', 'italic', 'underline', 'highlighter']\n                },\n                buttonLabels: 'fontawesome',\n                extensions: {\n                    'highlighter': new HighlighterButton()\n                }\n            });\n    </script>\n</body>\n</html>\n"
  },
  {
    "path": "demo/clean-paste.html",
    "content": "<!DOCTYPE html>\n<html>\n<head>\n    <meta charset=\"UTF-8\">\n    <title>medium editor | demo</title>\n    <link rel=\"stylesheet\" href=\"css/demo.css\">\n    <link rel=\"stylesheet\" href=\"http://netdna.bootstrapcdn.com/font-awesome/4.1.0/css/font-awesome.css\">\n    <link rel=\"stylesheet\" href=\"../dist/css/medium-editor.css\">\n    <link rel=\"stylesheet\" href=\"../dist/css/themes/default.css\">\n</head>\n<body>\n    <a href=\"https://github.com/yabwe/medium-editor\"><img style=\"position: absolute; top: 0; right: 0; border: 0;\" src=\"https://s3.amazonaws.com/github/ribbons/forkme_right_gray_6d6d6d.png\" alt=\"Fork me on GitHub\"></a>\n    <div id=\"container\">\n        <h1>Medium Editor</h1>\n        <div class=\"editable\">\n            <p>My father’s family name being <a href=\"https://en.wikipedia.org/wiki/Pip_(Great_Expectations)\">Pirrip</a>, and my Christian name Philip, my infant tongue could make of both names nothing longer or more explicit than Pip. So, I called myself Pip, and came to be called Pip.</p>\n            <p>I give Pirrip as my father’s family name, on the authority of his tombstone and my sister,—Mrs. Joe Gargery, who married the blacksmith. As I never saw my father or my mother, and never saw any likeness of either of them (for their days were long before the days of photographs), my first fancies regarding what they were like were unreasonably derived from their tombstones. The shape of the letters on my father’s, gave me an odd idea that he was a square, stout, dark man, with curly black hair. From the character and turn of the inscription, “Also Georgiana Wife of the Above,” I drew a childish conclusion that my mother was freckled and sickly. To five little stone lozenges, each about a foot and a half long, which were arranged in a neat row beside their grave, and were sacred to the memory of five little brothers of mine,—who gave up trying to get a living, exceedingly early in that universal struggle,—I am indebted for a belief I religiously entertained that they had all been born on their backs with their hands in their trousers-pockets, and had never taken them out in this state of existence.</p>\n            <p>Ours was the marsh country, down by the river, within, as the river wound, twenty miles of the sea. My first most vivid and broad impression of the identity of things seems to me to have been gained on a memorable raw afternoon towards evening. At such a time I found out for certain that this bleak place overgrown with nettles was the churchyard; and that Philip Pirrip, late of this parish, and also Georgiana wife of the above, were dead and buried; and that Alexander, Bartholomew, Abraham, Tobias, and Roger, infant children of the aforesaid, were also dead and buried; and that the dark flat wilderness beyond the churchyard, intersected with dikes and mounds and gates, with scattered cattle feeding on it, was the marshes; and that the low leaden line beyond was the river; and that the distant savage lair from which the wind was rushing was the sea; and that the small bundle of shivers growing afraid of it all and beginning to cry, was Pip.</p>\n        </div>\n    </div>\n    <p style=\"text-align: center;\"><small><a style=\"color: #333;\" target=\"_blank\" href=\"http://www.goodreads.com/reader/475-great-expectations\">Source</a></small></p>\n    <script src=\"../dist/js/medium-editor.js\"></script>\n    <script>\n        var editor = new MediumEditor('.editable', {\n            buttonLabels: 'fontawesome',\n            paste: {\n                cleanPastedHTML: true,\n                forcePlainText: false\n            }\n        });\n    </script>\n</body>\n</html>\n"
  },
  {
    "path": "demo/css/demo.css",
    "content": "*:focus {\n    outline: none;\n}\n\nbody {\n    font-family: Helvetica, Arial, sans-serif;\n    font-size: 22px;\n    line-height: 30px;\n}\n\n.top-bar {\n    position: fixed;\n    top: 0;\n    left: 0;\n    width: auto;\n    z-index: 10;\n    padding: 10px;\n    background-color: #000;\n    background-color: rgba(0, 0, 0, .8);\n    box-shadow: 0 0 4px #000;\n    box-sizing: border-box;\n    color: #ccc;\n    font-size: 12px;\n    font-weight: bold;\n    text-align: center;\n    text-transform: uppercase;\n}\n\nh1 {\n    font-size: 60px;\n    font-weight: bold;\n    text-align: center;\n    margin-bottom: 40px;\n    padding-bottom: 40px;\n    letter-spacing: -2px;\n    border-bottom: 1px solid #dbdbdb;\n}\n\nh2 {\n    font-size: 32px;\n    line-height: 42px;\n}\n\nh3 {\n    font-size: 26px;\n    line-height: 32px;\n}\n\nh4 {\n    font-size: 24px;\n    line-height: 28px;\n}\n\np {\n    margin-bottom: 40px;\n}\n\na {\n    color:black;\n}\n\na:hover {\n    color:green;\n}\n\npre {\n    font-family: 'Menlo', monospace;\n    font-size: 15px;\n    background-color: #f0f0f0;\n    padding: 15px;\n    border: 1px solid #ccc;\n    border-radius: 5px;\n    color: #666;\n}\n\n\nblockquote {\n    display: block;\n    padding-left: 20px;\n    border-left: 6px solid #df0d32;\n    margin-left: -15px;\n    padding-left: 15px;\n    font-style: italic;\n    color: #555;\n}\n\n#container {\n    width: 960px;\n    margin: 30px auto;\n}\n\n#all-demos {\n    text-align: center;\n    border-bottom: 1px solid #dbdbdb;\n    padding-bottom: 40px;\n}\n\n.editable,\n.secondEditable\n {\n    outline: none;\n    margin: 0 0 20px 0;\n    padding: 0 0 20px 0;\n    border-bottom: 1px solid #dbdbdb;\n}\n\n#columns {\n    width: 90%;\n    margin: 30px auto;\n}\n\n.column-container {\n\n}\n\n.column {\n    vertical-align: top;\n    display: inline-block;\n    width: 30%;\n    margin: 10px 1%;\n}\n\n"
  },
  {
    "path": "demo/custom-toolbar.html",
    "content": "<!DOCTYPE html>\n<html>\n<head>\n    <meta charset=\"UTF-8\">\n    <title>medium editor | demo</title>\n    <link rel=\"stylesheet\" href=\"css/demo.css\">\n    <link href=\"http://netdna.bootstrapcdn.com/font-awesome/4.1.0/css/font-awesome.css\" rel=\"stylesheet\">\n    <link rel=\"stylesheet\" href=\"../dist/css/medium-editor.css\">\n    <link rel=\"stylesheet\" href=\"../dist/css/themes/flat.css\">\n</head>\n<body>\n    <a href=\"https://github.com/yabwe/medium-editor\"><img style=\"position: absolute; top: 0; right: 0; border: 0;\" src=\"https://s3.amazonaws.com/github/ribbons/forkme_right_gray_6d6d6d.png\" alt=\"Fork me on GitHub\"></a>\n    <div id=\"container\">\n        <h1>Medium Editor</h1>\n        <div class=\"editable\">\n            <h2>Font Awesome</h2>\n            <p>My father’s family name being <a href=\"https://en.wikipedia.org/wiki/Pip_(Great_Expectations)\">Pirrip</a>, and my Christian name Philip, my infant tongue could make of both names nothing longer or more explicit than Pip. So, I called myself Pip, and came to be called Pip.</p>\n            <p>I give Pirrip as my father’s family name, on the authority of his tombstone and my sister,—Mrs. Joe Gargery, who married the blacksmith. As I never saw my father or my mother, and never saw any likeness of either of them (for their days were long before the days of photographs), my first fancies regarding what they were like were unreasonably derived from their tombstones...</p>\n        </div>\n        <div class=\"secondEditable\" id=\"c\">\n            <h2>Custom Labels </h2>\n            <p>... Ours was the marsh country, down by the river, within, as the river wound, twenty miles of the sea. My first most vivid and broad impression of the identity of things seems to me to have been gained on a memorable raw afternoon towards evening. At such a time I found out for certain that this bleak place overgrown with nettles was the churchyard; and that Philip Pirrip, late of this parish, and also Georgiana wife of the above, were dead and buried; and that Alexander, Bartholomew, Abraham, Tobias, and Roger, infant children of the aforesaid, were also dead and buried; and that the dark flat wilderness beyond the churchyard, intersected with dikes and mounds and gates, with scattered cattle feeding on it, was the marshes; and that the low leaden line beyond was the river; and that the distant savage lair from which the wind was rushing was the sea; and that the small bundle of shivers growing afraid of it all and beginning to cry, was Pip.</p>\n        </div>\n    </div>\n    <p style=\"text-align: center;\"><small><a style=\"color: #333;\" target=\"_blank\" href=\"http://www.goodreads.com/reader/475-great-expectations\">Source</a></small></p>\n    <script src=\"../dist/js/medium-editor.js\"></script>\n    <script>\n        var editor = new MediumEditor('.editable', {\n                toolbar: {\n                    buttons: ['bold', 'italic', 'underline', 'strikethrough', 'quote', 'anchor', 'image', 'justifyLeft', 'justifyCenter', 'justifyRight', 'justifyFull', 'superscript', 'subscript', 'orderedlist', 'unorderedlist', 'pre', 'removeFormat', 'outdent', 'indent', 'h2', 'h3', 'html'],\n                },\n                buttonLabels: 'fontawesome',\n                anchor: {\n                    targetCheckbox: true\n                }\n            });\n        var editor2 = new MediumEditor('.secondEditable', {\n                toolbar: {\n                    buttons: [{\n                            name: 'bold',\n                            contentDefault: 'bold'\n                        },\n                        {\n                            name: 'italic',\n                            contentDefault: '<i>italic</i>'\n                        },\n                        {\n                            name: 'underline',\n                            contentDefault: '<u>underline</u>'\n                        },\n                        {\n                            name: 'anchor',\n                            contentDefault: 'link'\n                        }\n                    ]\n                }\n            });\n    </script>\n</body>\n</html>\n"
  },
  {
    "path": "demo/extension-example.html",
    "content": "<!DOCTYPE html>\n<html>\n<head>\n    <meta charset=\"UTF-8\">\n    <title>MediumEditor - Extension Example</title>\n    <link rel=\"stylesheet\" href=\"css/demo.css\">\n    <link rel=\"stylesheet\" href=\"../dist/css/medium-editor.css\">\n    <link rel=\"stylesheet\" href=\"../dist/css/themes/default.css\">\n</head>\n<body>\n    <div id=\"container\">\n        <h1>Medium Editor</h1>\n        <div class=\"editable\">\n            <h2>Font Awesome</h2>\n            <p>My father’s family name being <a href=\"https://en.wikipedia.org/wiki/Pip_(Great_Expectations)\">Pirrip</a>, and my Christian name Philip, my infant tongue could make of both names nothing longer or more explicit than Pip. So, I called myself Pip, and came to be called Pip.</p>\n            <p>I give Pirrip as my father’s family name, on the authority of his tombstone and my sister,—Mrs. Joe Gargery, who married the blacksmith. As I never saw my father or my mother, and never saw any likeness of either of them (for their days were long before the days of photographs), my first fancies regarding what they were like were unreasonably derived from their tombstones...</p>\n        </div>\n    </div>\n    <p style=\"text-align: center;\"><small><a style=\"color: #333;\" target=\"_blank\" href=\"http://www.goodreads.com/reader/475-great-expectations\">Source</a></small></p>\n    <script src=\"../dist/js/medium-editor.js\"></script>\n    <script>\n        var DisableContextMenuExtension = MediumEditor.Extension.extend({\n            name: 'disable-context-menu',\n\n            init: function () {\n                this.getEditorElements().forEach(function (element) {\n                    this.on(element, 'contextmenu', this.handleContextmenu.bind(this));\n                }, this);\n                this.subscribe('editableKeydown', this.handleKeydown.bind(this));\n            },\n\n            handleContextmenu: function (event) {\n                if (!event.currentTarget.getAttribute('data-allow-context-menu')) {\n                    event.preventDefault();\n                }\n            },\n\n            handleKeydown: function (event, editable) {\n                // If the user hits escape, toggle the data-allow-context-menu attribute\n                if (MediumEditor.util.isKey(event, MediumEditor.util.keyCode.ESCAPE)) {\n                    if (editable.hasAttribute('data-allow-context-menu')) {\n                        editable.removeAttribute('data-allow-context-menu');\n                    } else {\n                        editable.setAttribute('data-allow-context-menu', true);\n                    }\n                }\n                \n            }\n        });\n        var editor = new MediumEditor('.editable', {\n                extensions: {\n                    'disable-context-menu': new DisableContextMenuExtension()\n                }\n            });\n    </script>\n</body>\n</html>\n"
  },
  {
    "path": "demo/index.html",
    "content": "<!DOCTYPE html>\n<html>\n<head>\n    <meta charset=\"UTF-8\">\n    <title>medium editor | demo</title>\n    <link rel=\"stylesheet\" href=\"css/demo.css\">\n    <link rel=\"stylesheet\" href=\"http://netdna.bootstrapcdn.com/font-awesome/4.1.0/css/font-awesome.css\">\n    <link rel=\"stylesheet\" href=\"../dist/css/medium-editor.css\">\n    <link rel=\"stylesheet\" href=\"../dist/css/themes/default.css\" id=\"medium-editor-theme\">\n</head>\n<body>\n    <a href=\"https://github.com/yabwe/medium-editor\" class=\"github-link\"><img style=\"z-index: 100;position: absolute; top: 0; right: 0; border: 0;\" src=\"https://s3.amazonaws.com/github/ribbons/forkme_right_gray_6d6d6d.png\" alt=\"Fork me on GitHub\"></a>\n    <div class=\"top-bar\">\n        Theme:\n        <select id=\"sel-themes\">\n            <option value=\"themes/default\" selected>default</option>\n            <option value=\"themes/roman\">roman</option>\n            <option value=\"themes/mani\">mani</option>\n            <option value=\"themes/flat\">flat</option>\n            <option value=\"themes/bootstrap\">bootstrap</option>\n            <option value=\"themes/tim\">tim</option>\n            <option value=\"themes/beagle\">beagle</option>\n        </select>\n    </div>\n    <div id=\"container\">\n        <h1>Medium Editor</h1>\n        <div id=\"all-demos\">\n            <a href=\"./absolute-container.html\">Absolute Container</a> &#8226;\n            <a href=\"./auto-link.html\">Auto Link</a> &#8226;\n            <a href=\"./button-example.html\">Button Example</a> &#8226;\n            <a href=\"./clean-paste.html\">Clean Paste</a> &#8226;\n            <a href=\"./custom-toolbar.html\">Custom Toolbar</a> &#8226;\n            <a href=\"./extension-example.html\">Extension Example</a> &#8226;\n            <a href=\"./multi-editor.html\">Multi Editor</a> &#8226;\n            <a href=\"./multi-one-instance.html\">Multi One Instance</a> &#8226;\n            <a href=\"./multi-paragraph.html\">Multi Paragraph</a> &#8226;\n            <a href=\"./nested-editable.html\">Nested Editable</a> &#8226;\n            <a href=\"./pass-instance.html\">Pass Instance</a> &#8226;\n            <a href=\"./relative-toolbar.html\">Relative Toolbar</a> &#8226;\n            <a href=\"./static-toolbar.html\">Static Toolbar</a> &#8226;\n            <a href=\"./table-extension.html\">Table Extension</a> &#8226;\n            <a href=\"./textarea.html\">Textarea</a>\n        </div>\n        <div class=\"editable\">\n            <p>My father’s family name being <a href=\"https://en.wikipedia.org/wiki/Pip_(Great_Expectations)\">Pirrip</a>, and my Christian name Philip, my infant tongue could make of both names nothing longer or more explicit than Pip. So, I called myself Pip, and came to be called Pip.</p>\n            <p>I give Pirrip as my father’s family name, on the authority of his tombstone and my sister,—Mrs. Joe Gargery, who married the blacksmith. As I never saw my father or my mother, and never saw any likeness of either of them (for their days were long before the days of photographs), my first fancies regarding what they were like were unreasonably derived from their tombstones. The shape of the letters on my father’s, gave me an odd idea that he was a square, stout, dark man, with curly black hair. From the character and turn of the inscription, “Also Georgiana Wife of the Above,” I drew a childish conclusion that my mother was freckled and sickly. To five little stone lozenges, each about a foot and a half long, which were arranged in a neat row beside their grave, and were sacred to the memory of five little brothers of mine,—who gave up trying to get a living, exceedingly early in that universal struggle,—I am indebted for a belief I religiously entertained that they had all been born on their backs with their hands in their trousers-pockets, and had never taken them out in this state of existence.</p>\n            <p>Ours was the marsh country, down by the river, within, as the river wound, twenty miles of the sea. My first most vivid and broad impression of the identity of things seems to me to have been gained on a memorable raw afternoon towards evening. At such a time I found out for certain that this bleak place overgrown with nettles was the churchyard; and that Philip Pirrip, late of this parish, and also Georgiana wife of the above, were dead and buried; and that Alexander, Bartholomew, Abraham, Tobias, and Roger, infant children of the aforesaid, were also dead and buried; and that the dark flat wilderness beyond the churchyard, intersected with dikes and mounds and gates, with scattered cattle feeding on it, was the marshes; and that the low leaden line beyond was the river; and that the distant savage lair from which the wind was rushing was the sea; and that the small bundle of shivers growing afraid of it all and beginning to cry, was Pip.</p>\n        </div>\n    </div>\n    <p style=\"text-align: center;\"><small><a style=\"color: #333;\" target=\"_blank\" href=\"http://www.goodreads.com/reader/475-great-expectations\">Source</a></small></p>\n    <script src=\"../dist/js/medium-editor.js\"></script>\n    <script>\n        var editor = new MediumEditor('.editable', {\n            buttonLabels: 'fontawesome'\n        }),\n        cssLink = document.getElementById('medium-editor-theme');\n\n        document.getElementById('sel-themes').addEventListener('change', function () {\n            cssLink.href = '../dist/css/' + this.value + '.css';\n        });\n    </script>\n</body>\n</html>\n"
  },
  {
    "path": "demo/js/extension-table.js",
    "content": "var TableExtension = MediumEditor.extensions.anchor.extend({\n  name: 'table',\n  action: 'createTable',\n  aria: 'table',\n  tagNames: ['table'],\n  contentDefault: '<b>T</b>',\n  contentFA: '<i class=\"fa fa-table\"></i>',\n\n  doFormSave: function () {\n    var columnCount = this.getColumnsInput().value,\n    rowCount = this.getRowsInput().value,\n    table = this.createTable(columnCount, rowCount);\n\n    // Restore Medium Editor's selection before pasting HTML\n    this.base.restoreSelection();\n\n    // Paste newly created table.\n    this.base.pasteHTML(table.innerHTML);\n\n    // Update toolbar -> hide this form\n    this.base.checkSelection();\n  },\n\n  createTable: function (cols, rows) {\n    var doc = this.base.options.ownerDocument,\n    table = doc.createElement('table'),\n    header = doc.createElement('thead'),\n    headerRow = doc.createElement('tr'),\n    body = doc.createElement('tbody'),\n    wrap = doc.createElement('div'),\n    h, r, c, headerCol, bodyRow, bodyCol;\n\n    for (h = 1; h <= cols; h++) {\n      headerCol = doc.createElement('th');\n      headerCol.innerHTML = '...';\n      headerRow.appendChild(headerCol);\n    }\n\n    header.appendChild(headerRow);\n\n    for (r = 1; r <= rows; r++) {\n      bodyRow = doc.createElement('tr');\n      for (c = 1; c <= cols; c++) {\n        bodyCol = doc.createElement('td');\n        bodyCol.innerHTML = '...';\n        bodyRow.appendChild(bodyCol);\n      }\n      body.appendChild(bodyRow);\n    }\n\n    table.appendChild(header);\n    table.appendChild(body);\n    wrap.appendChild(table);\n\n    return wrap;\n  },\n\n  // Called when the button the toolbar is clicked\n  // Overrides DefaultButton.handleClick\n  handleClick: function (evt) {\n    evt.preventDefault();\n    evt.stopPropagation();\n\n    if (!this.isDisplayed()) {\n      this.showForm();\n    }\n\n    return false;\n  },\n\n  hideForm: function () {\n    this.getColumnsInput().value = '';\n    this.getRowsInput().value = '';\n    this.getForm().style.display = 'none';\n  },\n\n  showForm: function () {\n    var colsInput = this.getColumnsInput(),\n    rowsInput = this.getRowsInput();\n\n    this.base.saveSelection();\n    this.hideToolbarDefaultActions();\n    this.getForm().style.display = 'block';\n    this.setToolbarPosition();\n\n    colsInput.focus();\n  },\n\n  createForm: function () {\n    var doc = this.base.options.ownerDocument,\n    form = doc.createElement('div'),\n    close = doc.createElement('a'),\n    save = doc.createElement('a'),\n    columnInput = doc.createElement('input'),\n    rowInput = doc.createElement('input');\n\n    form.className = 'medium-editor-toolbar-form';\n    form.id = 'medium-editor-toolbar-form-table-' + this.base.id;\n\n    // Handle clicks on the form itself\n    this.base.on(form, 'click', this.handleFormClick.bind(this));\n\n    // Add columns textbox\n    columnInput.setAttribute('type', 'text');\n    columnInput.className = 'medium-editor-toolbar-input medium-editor-toolbar-input-columns';\n    columnInput.setAttribute('placeholder', 'Column Count');\n    form.appendChild(columnInput);\n\n    // Add rows textbox\n    rowInput.setAttribute('type', 'text');\n    rowInput.className = 'medium-editor-toolbar-input medium-editor-toolbar-input-rows';\n    rowInput.setAttribute('placeholder', 'Row Count');\n    form.appendChild(rowInput);\n\n    // Handle typing in the textboxes\n    this.base.on(columnInput, 'keyup', this.handleTextboxKeyup.bind(this));\n    this.base.on(rowInput, 'keyup', this.handleTextboxKeyup.bind(this));\n\n    // Add save buton\n    save.setAttribute('href', '#');\n    save.className = 'medium-editor-toolbar-save';\n    save.innerHTML = this.base.options.buttonLabels === 'fontawesome' ?\n    '<i class=\"fa fa-check\"></i>' :\n    '&#10003;';\n    form.appendChild(save);\n\n    // Handle save button clicks (capture)\n    this.base.on(save, 'click', this.handleSaveClick.bind(this), true);\n\n    // Add close button\n    close.setAttribute('href', '#');\n    close.className = 'medium-editor-toolbar-close';\n    close.innerHTML = this.base.options.buttonLabels === 'fontawesome' ?\n    '<i class=\"fa fa-times\"></i>' :\n    '&times;';\n    form.appendChild(close);\n\n    // Handle close button clicks\n    this.base.on(close, 'click', this.handleCloseClick.bind(this));\n\n    return form;\n  },\n\n  getColumnsInput: function () {\n    return this.getForm().querySelector('input.medium-editor-toolbar-input-columns');\n  },\n\n  getRowsInput: function () {\n    return this.getForm().querySelector('input.medium-editor-toolbar-input-rows');\n  }\n})\n"
  },
  {
    "path": "demo/multi-editor.html",
    "content": "<!DOCTYPE html>\n<html>\n<head>\n    <meta charset=\"UTF-8\">\n    <title>medium editor | demo</title>\n    <link rel=\"stylesheet\" href=\"css/demo.css\">\n    <link rel=\"stylesheet\" href=\"../dist/css/medium-editor.css\">\n    <link rel=\"stylesheet\" href=\"../dist/css/themes/default.css\">\n</head>\n<body>\n    <a href=\"https://github.com/yabwe/medium-editor\"><img style=\"position: absolute; top: 0; right: 0; border: 0;\" src=\"https://s3.amazonaws.com/github/ribbons/forkme_right_gray_6d6d6d.png\" alt=\"Fork me on GitHub\"></a>\n    <div id=\"container\">\n        <h1>Medium Multi Editor</h1>\n        <div class=\"editable\" id=\"a\">\n            <h2>First Editor </h2>\n            <p>This text is a <b>paragraph</b> with some <a href=\"http://www.w3schools.com/jsref/dom_obj_all.asp\"><i>tags elements</i></a></p>\n        </div>\n        <div class=\"editable\" id=\"b\">\n            <h2>Second Editor </h2>\n            <p>This text is another paragraph in the same instance.</p>\n        </div>\n        <div class=\"secondEditable\" id=\"c\">\n            <h2>Third Editor in another instance </h2>\n            <p>This text is another paragraph in another editor instance.<br>\n            </p>\n        </div>\n        <div class=\"editable\" id=\"d\" data-disable-toolbar=\"true\">\n            <h2>Disabled Toolbar Editor</h2>\n            <p>This text is a paragraph in a<br> <i> data-disable-toolbar=\"true\"</i></p>\n        </div>\n        <div id=\"d\">\n            <h2>Non editable Div</h2>\n            <p>This text is another paragraph is a normal div.</p>\n        </div>\n\n    </div>\n\n    <script src=\"../dist/js/medium-editor.js\"></script>\n    <script>\n        var editor = new MediumEditor('.editable'),\n            editor2 = new MediumEditor('.secondEditable', {\n                toolbar: {\n                    buttons: ['bold', 'italic', 'quote', 'pre']\n                }\n            });\n    </script>\n\n</body>\n</html>\n"
  },
  {
    "path": "demo/multi-one-instance.html",
    "content": "<!DOCTYPE html>\n<html>\n<head>\n    <meta charset=\"UTF-8\">\n    <title>medium editor | demo</title>\n    <link rel=\"stylesheet\" href=\"css/demo.css\">\n    <link rel=\"stylesheet\" href=\"http://netdna.bootstrapcdn.com/font-awesome/4.1.0/css/font-awesome.css\">\n    <link rel=\"stylesheet\" href=\"../dist/css/medium-editor.css\">\n    <link rel=\"stylesheet\" href=\"../dist/css/themes/default.css\" id=\"medium-editor-theme\">\n</head>\n<body>\n    <a href=\"https://github.com/yabwe/medium-editor\" class=\"github-link\"><img style=\"z-index: 100;position: absolute; top: 0; right: 0; border: 0;\" src=\"https://s3.amazonaws.com/github/ribbons/forkme_right_gray_6d6d6d.png\" alt=\"Fork me on GitHub\"></a>\n    <div class=\"top-bar\">\n        Theme:\n        <select id=\"sel-themes\">\n            <option value=\"themes/default\" selected>default</option>\n            <option value=\"themes/roman\">roman</option>\n            <option value=\"themes/mani\">mani</option>\n            <option value=\"themes/flat\">flat</option>\n            <option value=\"themes/bootstrap\">bootstrap</option>\n            <option value=\"themes/tim\">tim</option>\n            <option value=\"themes/beagle\">beagle</option>\n        </select>\n    </div>\n    <div id=\"container\">\n        <h1>Medium Editor</h1>\n        <div class=\"editable\">\n            <p>My father’s family name being <a href=\"https://en.wikipedia.org/wiki/Pip_(Great_Expectations)\">Pirrip</a>, and my Christian name Philip, my infant tongue could make of both names nothing longer or more explicit than Pip. So, I called myself Pip, and came to be called Pip.</p>\n            <p>I give Pirrip as my father’s family name, on the authority of his tombstone and my sister,—Mrs. Joe Gargery, who married the blacksmith. As I never saw my father or my mother, and never saw any likeness of either of them (for their days were long before the days of photographs), my first fancies regarding what they were like were unreasonably derived from their tombstones. The shape of the letters on my father’s, gave me an odd idea that he was a square, stout, dark man, with curly black hair. From the character and turn of the inscription, “Also Georgiana Wife of the Above,” I drew a childish conclusion that my mother was freckled and sickly. To five little stone lozenges, each about a foot and a half long, which were arranged in a neat row beside their grave, and were sacred to the memory of five little brothers of mine,—who gave up trying to get a living, exceedingly early in that universal struggle,—I am indebted for a belief I religiously entertained that they had all been born on their backs with their hands in their trousers-pockets, and had never taken them out in this state of existence.</p>\n            <p>Ours was the marsh country, down by the river, within, as the river wound, twenty miles of the sea. My first most vivid and broad impression of the identity of things seems to me to have been gained on a memorable raw afternoon towards evening. At such a time I found out for certain that this bleak place overgrown with nettles was the churchyard; and that Philip Pirrip, late of this parish, and also Georgiana wife of the above, were dead and buried; and that Alexander, Bartholomew, Abraham, Tobias, and Roger, infant children of the aforesaid, were also dead and buried; and that the dark flat wilderness beyond the churchyard, intersected with dikes and mounds and gates, with scattered cattle feeding on it, was the marshes; and that the low leaden line beyond was the river; and that the distant savage lair from which the wind was rushing was the sea; and that the small bundle of shivers growing afraid of it all and beginning to cry, was Pip.</p>\n        </div>\n        <div class=\"editable2\">\n            <p>My father’s family name being <a href=\"https://en.wikipedia.org/wiki/Pip_(Great_Expectations)\">Pirrip</a>, and my Christian name Philip, my infant tongue could make of both names nothing longer or more explicit than Pip. So, I called myself Pip, and came to be called Pip.</p>\n            <p>I give Pirrip as my father’s family name, on the authority of his tombstone and my sister,—Mrs. Joe Gargery, who married the blacksmith. As I never saw my father or my mother, and never saw any likeness of either of them (for their days were long before the days of photographs), my first fancies regarding what they were like were unreasonably derived from their tombstones. The shape of the letters on my father’s, gave me an odd idea that he was a square, stout, dark man, with curly black hair. From the character and turn of the inscription, “Also Georgiana Wife of the Above,” I drew a childish conclusion that my mother was freckled and sickly. To five little stone lozenges, each about a foot and a half long, which were arranged in a neat row beside their grave, and were sacred to the memory of five little brothers of mine,—who gave up trying to get a living, exceedingly early in that universal struggle,—I am indebted for a belief I religiously entertained that they had all been born on their backs with their hands in their trousers-pockets, and had never taken them out in this state of existence.</p>\n            <p>Ours was the marsh country, down by the river, within, as the river wound, twenty miles of the sea. My first most vivid and broad impression of the identity of things seems to me to have been gained on a memorable raw afternoon towards evening. At such a time I found out for certain that this bleak place overgrown with nettles was the churchyard; and that Philip Pirrip, late of this parish, and also Georgiana wife of the above, were dead and buried; and that Alexander, Bartholomew, Abraham, Tobias, and Roger, infant children of the aforesaid, were also dead and buried; and that the dark flat wilderness beyond the churchyard, intersected with dikes and mounds and gates, with scattered cattle feeding on it, was the marshes; and that the low leaden line beyond was the river; and that the distant savage lair from which the wind was rushing was the sea; and that the small bundle of shivers growing afraid of it all and beginning to cry, was Pip.</p>\n        </div>\n    </div>\n    <p style=\"text-align: center;\"><small><a style=\"color: #333;\" target=\"_blank\" href=\"http://www.goodreads.com/reader/475-great-expectations\">Source</a></small></p>\n    <script src=\"../dist/js/medium-editor.js\"></script>\n    <script>\n        var editor = new MediumEditor('.editable', {\n            buttonLabels: 'fontawesome'\n        }),\n        cssLink = document.getElementById('medium-editor-theme');\n\n        editor.subscribe('editableInput', function(event, editable) {\n            console.info('editableInput fired!', editable);\n        });\n\n        document.getElementById('sel-themes').addEventListener('change', function () {\n            cssLink.href = '../dist/css/' + this.value + '.css';\n        });\n\n        editor.addElements(document.querySelector('.editable2'));\n    </script>\n</body>\n</html>\n"
  },
  {
    "path": "demo/multi-paragraph.html",
    "content": "<!DOCTYPE html>\n<html>\n<head>\n    <meta charset=\"UTF-8\">\n    <title>medium editor | demo multi paragraph</title>\n    <link rel=\"stylesheet\" href=\"css/demo.css\">\n    <link rel=\"stylesheet\" href=\"../dist/css/medium-editor.css\">\n    <link rel=\"stylesheet\" href=\"../dist/css/themes/default.css\">\n</head>\n<body>\n    <a href=\"https://github.com/yabwe/medium-editor\"><img style=\"position: absolute; top: 0; right: 0; border: 0;\" src=\"https://s3.amazonaws.com/github/ribbons/forkme_right_gray_6d6d6d.png\" alt=\"Fork me on GitHub\"></a>\n    <div id=\"container\">\n        <h1>Medium Editor Demo</h1>\n        <div class=\"editable\" data-placeholder=\"Type some text\" spellcheck=\"false\">\n        <p><b>In this demo the toolbar will not appear if you select several paragraphs or block quotes.</b></p>\n        <p><h2>First Paragraph</h2> Lorem ipsum dolor sit amet, consectetur adipiscing elit. Nullam ultrices ullamcorper nibh, ut imperdiet arcu rutrum et. Vestibulum vitae orci metus. Praesent dapibus interdum purus, vitae mattis urna pharetra a. Praesent sodales volutpat mi et rhoncus. Phasellus quis tortor nulla. Pellentesque dapibus lorem et eros lobortis, et iaculis est accumsan. Nunc et ligula laoreet, egestas est id, placerat dolor. Praesent sed gravida tortor, non elementum enim. Integer et nulla sit amet orci suscipit sagittis pellentesque a est. Maecenas vitae purus odio. Aenean tincidunt varius arcu a vehicula. Quisque vestibulum venenatis vestibulum. Nullam pellentesque purus non dui adipiscing tempus.</p>\n\n        <p><h2>Second Paragraph </h2>Pellentesque sit amet turpis a felis ornare euismod id vitae ante. Vivamus nec orci interdum, blandit dui id, gravida orci. Aliquam malesuada tristique imperdiet. Mauris lobortis, mi vel dictum feugiat, sem nibh hendrerit mi, eu pellentesque mauris quam et lorem. Integer ante ligula, placerat id pharetra ut, lacinia sit amet ante. Donec luctus, orci eu vestibulum suscipit, nisl lorem dignissim mauris, et auctor dolor odio nec mauris. Maecenas velit justo, lobortis a accumsan et, adipiscing sit amet leo. Vestibulum pharetra nisi erat, vitae dictum eros vestibulum eget. Ut aliquet lorem eu dui auctor aliquam. Nunc mollis elementum justo non ultricies. Nam dictum egestas augue sit amet ullamcorper. Praesent laoreet lectus ut velit porta varius. Vestibulum eget gravida sapien. Mauris viverra, metus vel varius posuere, augue metus aliquam sem, in pellentesque ipsum mauris non enim.</p>\n\n        <blockquote>This is a block quote. Suspendisse potenti. Vestibulum semper felis vitae sapien ultricies venenatis. Aliquam mollis dui dolor, in auctor urna iaculis dapibus. Nam condimentum mollis sapien, non bibendum lacus feugiat eu. Phasellus feugiat erat ut varius tincidunt. Quisque suscipit ornare lacus, nec dapibus lacus vulputate at. Morbi at ipsum sollicitudin, suscipit elit sed, ultrices ligula. Suspendisse tincidunt libero iaculis velit iaculis, ac congue enim euismod. Nam molestie ligula at mattis tincidunt. Praesent porttitor nisi lectus, nec suscipit lectus porttitor nec.</blockquote>\n\n        <p>Another paragrph. Suspendisse potenti. Vestibulum semper felis vitae sapien ultricies venenatis. Aliquam mollis dui dolor, in auctor urna iaculis dapibus. Nam condimentum mollis sapien, non bibendum lacus feugiat eu. Phasellus feugiat erat ut varius tincidunt. Quisque suscipit ornare lacus, nec dapibus lacus vulputate at. Morbi at ipsum sollicitudin, suscipit elit sed, ultrices ligula. Suspendisse tincidunt libero iaculis velit iaculis, ac congue enim euismod. Nam molestie ligula at mattis tincidunt. Praesent porttitor nisi lectus, nec suscipit lectus porttitor nec.</p>\n        </div>\n\n\n    </div>\n    <script src=\"../dist/js/medium-editor.js\"></script>\n    <script>\n        var editor = new MediumEditor('.editable', {\n            delay: 0,\n            toolbar: {\n                diffTop: -12,\n                allowMultiParagraphSelection: false\n            },\n            anchor: {\n                placeholderText: 'Type a link'\n            }\n        });\n\n    </script>\n</body>\n</html>\n"
  },
  {
    "path": "demo/nested-editable.html",
    "content": "<!DOCTYPE html>\n<html>\n<head>\n    <meta charset=\"UTF-8\">\n    <title>medium editor | demo</title>\n    <link rel=\"stylesheet\" href=\"css/demo.css\">\n    <link rel=\"stylesheet\" href=\"http://netdna.bootstrapcdn.com/font-awesome/4.1.0/css/font-awesome.css\">\n    <link rel=\"stylesheet\" href=\"../dist/css/medium-editor.css\">\n    <link rel=\"stylesheet\" href=\"../dist/css/themes/default.css\" id=\"medium-editor-theme\">\n</head>\n<body>\n    <a href=\"https://github.com/yabwe/medium-editor\" class=\"github-link\"><img style=\"z-index: 100;position: absolute; top: 0; right: 0; border: 0;\" src=\"https://s3.amazonaws.com/github/ribbons/forkme_right_gray_6d6d6d.png\" alt=\"Fork me on GitHub\"></a>\n    <div class=\"top-bar\">\n        Theme:\n        <select id=\"sel-themes\">\n            <option value=\"themes/default\" selected>default</option>\n            <option value=\"themes/roman\">roman</option>\n            <option value=\"themes/mani\">mani</option>\n            <option value=\"themes/flat\">flat</option>\n            <option value=\"themes/bootstrap\">bootstrap</option>\n        </select>\n    </div>\n    <div id=\"container\">\n        <h1>Medium Editor</h1>\n        <div class=\"editable\">\n            <p>My father’s family name being <a href=\"https://en.wikipedia.org/wiki/Pip_(Great_Expectations)\">Pirrip</a>, and my Christian name Philip, my infant tongue could make of both names nothing longer or more explicit than Pip. So, I called myself Pip, and came to be called Pip.</p>\n\n            <div>\n            <div contenteditable=\"false\">\n                <div>\n                    <h4 selectable=\"false\">this portion is not editable</h4>\n                    <div style=\"border:1px solid #ededed; padding:40px\" contenteditable=\"true\">\n                        <p>this is editable</p>\n                        <p>that seems really neat</p>\n                    </div>\n                </div>\n            </div>\n            </div>\n\n            <p>I give Pirrip as my father’s family name, on the authority of his tombstone and my sister,—Mrs. Joe Gargery, who married the blacksmith. As I never saw my father or my mother, and never saw any likeness of either of them (for their days were long before the days of photographs), my first fancies regarding what they were like were unreasonably </p><p>derived from their tombstones. The shape of the letters on my father’s, gave me an odd idea that he was a square, stout, dark man, with curly black hair. From the character and turn of the inscription, “Also Georgiana Wife of the Above,” I drew a childish conclusion that my mother was freckled and sickly. To five little stone lozenges, each about a foot and a half long, which were arranged in a neat row beside their grave, and were sacred to the memory of five little brothers of mine,—who gave up trying to get a living, exceedingly early in that universal struggle,—I am indebted for a belief I religiously entertained that they had all been born on their backs with their hands in their trousers-pockets, and had never taken them out in this state of existence.</p>\n        </div>\n    </div>\n    <p style=\"text-align: center;\"><small><a style=\"color: #333;\" target=\"_blank\" href=\"http://www.goodreads.com/reader/475-great-expectations\">Source</a></small></p>\n    <script src=\"../dist/js/medium-editor.js\"></script>\n    <script>\n        var editor = new MediumEditor('.editable', {\n            buttonLabels: 'fontawesome'\n        }),\n        cssLink = document.getElementById('medium-editor-theme');\n\n        document.getElementById('sel-themes').addEventListener('change', function () {\n            cssLink.href = '../dist/css/' + this.value + '.css';\n        });\n    </script>\n</body>\n</html>\n"
  },
  {
    "path": "demo/pass-instance.html",
    "content": "<!DOCTYPE html>\n<html>\n<head>\n    <meta charset=\"UTF-8\">\n    <title>medium editor | demo</title>\n    <link rel=\"stylesheet\" href=\"css/demo.css\">\n    <link rel=\"stylesheet\" href=\"http://netdna.bootstrapcdn.com/font-awesome/4.1.0/css/font-awesome.css\">\n    <link rel=\"stylesheet\" href=\"../dist/css/medium-editor.css\">\n    <link rel=\"stylesheet\" href=\"../dist/css/themes/default.css\">\n</head>\n<body>\n    <a href=\"https://github.com/yabwe/medium-editor\"><img style=\"position: absolute; top: 0; right: 0; border: 0;\" src=\"https://s3.amazonaws.com/github/ribbons/forkme_right_gray_6d6d6d.png\" alt=\"Fork me on GitHub\"></a>\n    <div id=\"container\">\n        <h1>Medium Editor</h1>\n        <div class=\"one\">\n            <p>My father’s family name being <a href=\"https://en.wikipedia.org/wiki/Pip_(Great_Expectations)\">Pirrip</a>, and my Christian name Philip, my infant tongue could make of both names nothing longer or more explicit than Pip. So, I called myself Pip, and came to be called Pip.</p>\n            <p>I give Pirrip as my father’s family name, on the authority of his tombstone and my sister,—Mrs. Joe Gargery, who married the blacksmith. As I never saw my father or my mother, and never saw any likeness of either of them (for their days were long before the days of photographs), my first fancies regarding what they were like were unreasonably derived from their tombstones. The shape of the letters on my father’s, gave me an odd idea that he was a square, stout, dark man, with curly black hair. From the character and turn of the inscription, “Also Georgiana Wife of the Above,” I drew a childish conclusion that my mother was freckled and sickly. To five little stone lozenges, each about a foot and a half long, which were arranged in a neat row beside their grave, and were sacred to the memory of five little brothers of mine,—who gave up trying to get a living, exceedingly early in that universal struggle,—I am indebted for a belief I religiously entertained that they had all been born on their backs with their hands in their trousers-pockets, and had never taken them out in this state of existence.</p>\n        </div>\n        <div class=\"two\">\n            <p>Ours was the marsh country, down by the river, within, as the river wound, twenty miles of the sea. My first most vivid and broad impression of the identity of things seems to me to have been gained on a memorable raw afternoon towards evening. At such a time I found out for certain that this bleak place overgrown with nettles was the churchyard; and that Philip Pirrip, late of this parish, and also Georgiana wife of the above, were dead and buried; and that Alexander, Bartholomew, Abraham, Tobias, and Roger, infant children of the aforesaid, were also dead and buried; and that the dark flat wilderness beyond the churchyard, intersected with dikes and mounds and gates, with scattered cattle feeding on it, was the marshes; and that the low leaden line beyond was the river; and that the distant savage lair from which the wind was rushing was the sea; and that the small bundle of shivers growing afraid of it all and beginning to cry, was Pip.</p>\n        </div>\n    </div>\n    <p style=\"text-align: center;\"><small><a style=\"color: #333;\" target=\"_blank\" href=\"http://www.goodreads.com/reader/475-great-expectations\">Source</a></small></p>\n    <script src=\"../dist/js/medium-editor.js\"></script>\n    <script>\n        function Extension() {\n          this.button = document.createElement('button');\n          this.button.className = 'medium-editor-action';\n          this.button.innerText = 'X';\n          this.button.onclick = this.onClick.bind(this);\n        }\n\n        Extension.prototype.getButton = function() {\n          return this.button;\n        };\n\n        Extension.prototype.onClick = function() {\n          alert('This is editor: #' + this.base.id);\n        };\n\n        var one = new MediumEditor('.one', {\n            toolbar: {\n                buttons: ['extension'],\n            },\n            extensions: {\n              extension: new Extension()\n            }\n        });\n\n        var two = new MediumEditor('.two', {\n            toolbar: {\n                buttons: ['extension'],\n            },\n            extensions: {\n              extension: new Extension()\n            }\n        });\n    </script>\n</body>\n</html>\n"
  },
  {
    "path": "demo/relative-toolbar.html",
    "content": "<!DOCTYPE html>\n<html>\n<head>\n    <meta charset=\"UTF-8\">\n    <title>medium editor | demo</title>\n    <link rel=\"stylesheet\" href=\"css/demo.css\">\n    <link href=\"http://netdna.bootstrapcdn.com/font-awesome/4.1.0/css/font-awesome.css\" rel=\"stylesheet\">\n    <link rel=\"stylesheet\" href=\"../dist/css/medium-editor.css\">\n    <link rel=\"stylesheet\" href=\"../dist/css/themes/default.css\">\n</head>\n<body>\n    <a href=\"https://github.com/yabwe/medium-editor\" class=\"github-link\"><img style=\"z-index: 100;position: absolute; top: 0; right: 0; border: 0;\" src=\"https://s3.amazonaws.com/github/ribbons/forkme_right_gray_6d6d6d.png\" alt=\"Fork me on GitHub\"></a>\n    <div id=\"container\">\n        <h1>Medium Editor</h1>\n        <div class=\"editable\">\n            <h3>Relative Toolbar Container</h3>\n            <p>My father’s family name being <a href=\"https://en.wikipedia.org/wiki/Pip_(Great_Expectations)\">Pirrip</a>, and my Christian name Philip, my infant tongue could make of both names nothing longer or more explicit than Pip. So, I called myself Pip, and came to be called Pip.</p>\n\n            <p>I give Pirrip as my father’s family name, on the authority of his tombstone and my sister,—Mrs. Joe Gargery, who married the blacksmith. As I never saw my father or my mother, and never saw any likeness of either of them (for their days were long before the days of photographs), my first fancies regarding what they were like were unreasonably derived from their tombstones...</p>\n        </div>\n    </div>\n    <hr>\n    <div id=\"someRelativeDiv\" style=\"height: 80px; border: 1px solid red;\"></div>\n    <script src=\"../dist/js/medium-editor.js\"></script>\n    <script>\n        var editor = new MediumEditor('.editable', {\n                toolbar: {\n                    buttons: ['bold', 'italic', 'underline', 'strikethrough', 'quote', 'anchor', 'image', 'justifyLeft', 'justifyCenter', 'justifyRight', 'justifyFull', 'superscript', 'subscript', 'orderedlist', 'unorderedlist', 'pre', 'outdent', 'indent', 'h2', 'h3'],\n                    relativeContainer: document.getElementById('someRelativeDiv')\n                }\n            });\n    </script>\n</body>\n</html>\n"
  },
  {
    "path": "demo/static-toolbar.html",
    "content": "<!DOCTYPE html>\n<html>\n<head>\n    <meta charset=\"UTF-8\">\n    <title>medium editor | demo</title>\n    <link rel=\"stylesheet\" href=\"css/demo.css\">\n    <link href=\"http://netdna.bootstrapcdn.com/font-awesome/4.1.0/css/font-awesome.css\" rel=\"stylesheet\">\n    <link rel=\"stylesheet\" href=\"../dist/css/medium-editor.css\">\n    <link rel=\"stylesheet\" href=\"../dist/css/themes/default.css\">\n</head>\n<body>\n    <a href=\"https://github.com/yabwe/medium-editor\" class=\"github-link\"><img style=\"z-index: 100;position: absolute; top: 0; right: 0; border: 0;\" src=\"https://s3.amazonaws.com/github/ribbons/forkme_right_gray_6d6d6d.png\" alt=\"Fork me on GitHub\"></a>\n    <div id=\"container\">\n        <h1>Medium Editor</h1>\n        <div class=\"editable\">\n            <h3>Font Awesome</h3>\n            <p>My father’s family name being <a href=\"https://en.wikipedia.org/wiki/Pip_(Great_Expectations)\">Pirrip</a>, and my Christian name Philip, my infant tongue could make of both names nothing longer or more explicit than Pip. So, I called myself Pip, and came to be called Pip.</p>\n\n            <ul>\n                <li>List Item One</li>\n                <li>List Item Two</li>\n                <li>List Item Three</li>\n            </ul>\n            <p>I give Pirrip as my father’s family name, on the authority of his tombstone and my sister,—Mrs. Joe Gargery, who married the blacksmith. As I never saw my father or my mother, and never saw any likeness of either of them (for their days were long before the days of photographs), my first fancies regarding what they were like were unreasonably derived from their tombstones...</p>\n        </div>\n    </div>\n    <div id=\"columns\">\n        <div class=\"column-container\">\n            <div class=\"column\" id=\"column-one\">\n                <h3>Left Align</h3>\n                <p>My father’s family name being <a href=\"https://en.wikipedia.org/wiki/Pip_(Great_Expectations)\">Pirrip</a>, and my Christian name Philip, my infant tongue could make of both names nothing longer or more explicit than Pip. So, I called myself Pip, and came to be called Pip.</p>\n                <p>I give Pirrip as my father’s family name, on the authority of his tombstone and my sister,—Mrs. Joe Gargery, who married the blacksmith. As I never saw my father or my mother, and never saw any likeness of either of them (for their days were long before the days of photographs), my first fancies regarding what they were like were unreasonably derived from their tombstones...</p>\n            </div>\n            <div class=\"column\" id=\"column-two\">\n                <h3>Center Align</h3>\n                <p>... Ours was the marsh country, down by the river, within, as the river wound, twenty miles of the sea. My first most vivid and broad impression of the identity of things seems to me to have been gained on a memorable raw afternoon towards evening. At such a time I found out for certain that this bleak place overgrown with nettles was the churchyard; and that Philip Pirrip, late of this parish, and also Georgiana wife of the above, were dead and buried; and that Alexander, Bartholomew, Abraham, Tobias, and Roger, infant children of the aforesaid, were also dead and buried; and that the dark flat wilderness beyond the churchyard, intersected with dikes and mounds and gates, with scattered cattle feeding on it, was the marshes; and that the low leaden line beyond was the river; and that the distant savage lair from which the wind was rushing was the sea; and that the small bundle of shivers growing afraid of it all and beginning to cry, was Pip.</p>\n            </div>\n            <div class=\"column\" id=\"column-three\">\n                <h3>Right Align</h3>\n                <p>\"Hold your noise!\" cried a terrible voice, as a man started up from among the graves at the side of the church porch.  \"Keep still, you little devil, or I’ll cut your throat!”</p>\n                <p>A fearful man, all in coarse gray, with a great iron on his leg. A man with no hat, and with broken shoes, and with an old rag tied round his head. A man who had been soaked in water, and smothered in mud, and lamed by stones, and cut by flints, and stung by nettles, and torn by briars; who limped, and shivered, and glared, and growled; and whose teeth chattered in his head as he seized me by the chin.</p>\n            </div>\n        </div>\n    </div>\n    <p style=\"text-align: center;\"><small><a style=\"color: #333;\" target=\"_blank\" href=\"http://www.goodreads.com/reader/475-great-expectations\">Source</a></small></p>\n    <script src=\"../dist/js/medium-editor.js\"></script>\n    <script>\n        var editor = new MediumEditor('.editable', {\n                toolbar: {\n                    buttons: ['bold', 'italic', 'underline', 'strikethrough', 'quote', 'anchor', 'image', 'justifyLeft', 'justifyCenter', 'justifyRight', 'justifyFull', 'superscript', 'subscript', 'orderedlist', 'unorderedlist', 'pre', 'outdent', 'indent', 'h2', 'h3'],\n                    static: true,\n                    sticky: true\n                }\n            });\n\n        var editorColOne = new MediumEditor('#column-one', {\n                toolbar: {\n                    buttons: ['bold', 'italic', 'underline', 'strikethrough', 'quote', 'anchor', 'image', 'justifyLeft', 'justifyCenter', 'justifyRight', 'justifyFull', 'superscript', 'subscript', 'orderedlist', 'unorderedlist', 'pre', 'outdent', 'indent', 'h2', 'h3'],\n                    sticky: true,\n                    static: true,\n                    align: 'left',\n                    updateOnEmptySelection: true\n                },\n                buttonLabels: 'fontawesome'\n            });\n\n        var editorColTwo = new MediumEditor('#column-two', {\n                toolbar: {\n                    buttons: ['bold', 'italic', 'underline', 'strikethrough', 'quote', 'anchor', 'image', 'justifyLeft', 'justifyCenter', 'justifyRight', 'justifyFull', 'superscript', 'subscript', 'orderedlist', 'unorderedlist', 'pre', 'outdent', 'indent', 'h2', 'h3'],\n                    sticky: true,\n                    static: true,\n                    align: 'center',\n                    updateOnEmptySelection: true\n                },\n                buttonLabels: 'fontawesome'\n            });\n\n        var editorColThree = new MediumEditor('#column-three', {\n                toolbar: {\n                    buttons: ['bold', 'italic', 'underline', 'strikethrough', 'quote', 'anchor', 'image', 'justifyLeft', 'justifyCenter', 'justifyRight', 'justifyFull', 'superscript', 'subscript', 'orderedlist', 'unorderedlist', 'pre', 'outdent', 'indent', 'h2', 'h3'],\n                    sticky: true,\n                    static: true,\n                    align: 'right',\n                    updateOnEmptySelection: true\n                },\n                buttonLabels: 'fontawesome'\n            });\n    </script>\n</body>\n</html>\n"
  },
  {
    "path": "demo/table-extension.html",
    "content": "<!DOCTYPE html>\n<html>\n<head>\n    <meta charset=\"UTF-8\">\n    <title>medium editor | demo</title>\n    <link rel=\"stylesheet\" href=\"css/demo.css\">\n    <link href=\"http://netdna.bootstrapcdn.com/font-awesome/4.1.0/css/font-awesome.css\" rel=\"stylesheet\">\n    <link rel=\"stylesheet\" href=\"../dist/css/medium-editor.css\">\n    <link rel=\"stylesheet\" href=\"../dist/css/themes/default.css\">\n</head>\n<body>\n    <a href=\"https://github.com/yabwe/medium-editor\"><img style=\"position: absolute; top: 0; right: 0; border: 0;\" src=\"https://s3.amazonaws.com/github/ribbons/forkme_right_gray_6d6d6d.png\" alt=\"Fork me on GitHub\"></a>\n    <div id=\"container\">\n        <h1>Medium Editor Extension Form</h1>\n        <p>Medium Editor allows you to add custom forms for your extensions. This example demonstrates a form for creating a table.</p>\n        <div class=\"editable\">\n\n        </div>\n    </div>\n    <script src=\"../dist/js/medium-editor.js\"></script>\n    <!-- Medium Table Extension -->\n    <script src=\"js/extension-table.js\"></script>\n    <script>\n        var editor = new MediumEditor('.editable', {\n                toolbar: {\n                    buttons: ['bold', 'italic', 'underline', 'anchor', 'table'],\n                    static: true,\n                },\n                buttonLabels: 'fontawesome',\n                extensions: {\n                    'table': new TableExtension(),\n                }\n            });\n    </script>\n</body>\n</html>"
  },
  {
    "path": "demo/textarea.html",
    "content": "<!DOCTYPE html>\n<html>\n<head>\n    <meta charset=\"UTF-8\">\n    <title>medium editor | demo</title>\n    <link rel=\"stylesheet\" href=\"css/demo.css\">\n    <link rel=\"stylesheet\" href=\"http://netdna.bootstrapcdn.com/font-awesome/4.1.0/css/font-awesome.css\">\n    <link rel=\"stylesheet\" href=\"../dist/css/medium-editor.css\">\n    <link rel=\"stylesheet\" href=\"../dist/css/themes/default.css\" id=\"medium-editor-theme\">\n</head>\n<body>\n    <a href=\"https://github.com/yabwe/medium-editor\" class=\"github-link\"><img style=\"z-index: 100;position: absolute; top: 0; right: 0; border: 0;\" src=\"https://s3.amazonaws.com/github/ribbons/forkme_right_gray_6d6d6d.png\" alt=\"Fork me on GitHub\"></a>\n    <div id=\"container\">\n        <h1>Medium Editor</h1>\n        <textarea class=\"editable medium-editor-textarea\">&lt;p&gt;Textarea is now supported&lt;/p&gt;</textarea>\n    </div>\n    <script src=\"../dist/js/medium-editor.js\"></script>\n    <script>\n        var editor = new MediumEditor('.editable', {\n            buttonLabels: 'fontawesome'\n        });\n    </script>\n</body>\n</html>\n"
  },
  {
    "path": "dist/css/medium-editor.css",
    "content": "@-webkit-keyframes medium-editor-image-loading {\n  0% {\n    -webkit-transform: scale(0);\n            transform: scale(0); }\n  100% {\n    -webkit-transform: scale(1);\n            transform: scale(1); } }\n\n@keyframes medium-editor-image-loading {\n  0% {\n    -webkit-transform: scale(0);\n            transform: scale(0); }\n  100% {\n    -webkit-transform: scale(1);\n            transform: scale(1); } }\n\n@-webkit-keyframes medium-editor-pop-upwards {\n  0% {\n    opacity: 0;\n    -webkit-transform: matrix(0.97, 0, 0, 1, 0, 12);\n            transform: matrix(0.97, 0, 0, 1, 0, 12); }\n  20% {\n    opacity: .7;\n    -webkit-transform: matrix(0.99, 0, 0, 1, 0, 2);\n            transform: matrix(0.99, 0, 0, 1, 0, 2); }\n  40% {\n    opacity: 1;\n    -webkit-transform: matrix(1, 0, 0, 1, 0, -1);\n            transform: matrix(1, 0, 0, 1, 0, -1); }\n  100% {\n    -webkit-transform: matrix(1, 0, 0, 1, 0, 0);\n            transform: matrix(1, 0, 0, 1, 0, 0); } }\n\n@keyframes medium-editor-pop-upwards {\n  0% {\n    opacity: 0;\n    -webkit-transform: matrix(0.97, 0, 0, 1, 0, 12);\n            transform: matrix(0.97, 0, 0, 1, 0, 12); }\n  20% {\n    opacity: .7;\n    -webkit-transform: matrix(0.99, 0, 0, 1, 0, 2);\n            transform: matrix(0.99, 0, 0, 1, 0, 2); }\n  40% {\n    opacity: 1;\n    -webkit-transform: matrix(1, 0, 0, 1, 0, -1);\n            transform: matrix(1, 0, 0, 1, 0, -1); }\n  100% {\n    -webkit-transform: matrix(1, 0, 0, 1, 0, 0);\n            transform: matrix(1, 0, 0, 1, 0, 0); } }\n\n.medium-editor-anchor-preview {\n  font-family: \"Helvetica Neue\", Helvetica, Arial, sans-serif;\n  font-size: 16px;\n  left: 0;\n  line-height: 1.4;\n  max-width: 280px;\n  position: absolute;\n  text-align: center;\n  top: 0;\n  word-break: break-all;\n  word-wrap: break-word;\n  visibility: hidden;\n  z-index: 2000; }\n  .medium-editor-anchor-preview a {\n    color: #fff;\n    display: inline-block;\n    margin: 5px 5px 10px; }\n\n.medium-editor-anchor-preview-active {\n  visibility: visible; }\n\n.medium-editor-dragover {\n  background: #ddd; }\n\n.medium-editor-image-loading {\n  -webkit-animation: medium-editor-image-loading 1s infinite ease-in-out;\n          animation: medium-editor-image-loading 1s infinite ease-in-out;\n  background-color: #333;\n  border-radius: 100%;\n  display: inline-block;\n  height: 40px;\n  width: 40px; }\n\n.medium-editor-placeholder {\n  position: relative; }\n  .medium-editor-placeholder:after {\n    content: attr(data-placeholder) !important;\n    font-style: italic;\n    position: absolute;\n    left: 0;\n    top: 0;\n    white-space: pre;\n    padding: inherit;\n    margin: inherit; }\n\n.medium-editor-placeholder-relative {\n  position: relative; }\n  .medium-editor-placeholder-relative:after {\n    content: attr(data-placeholder) !important;\n    font-style: italic;\n    position: relative;\n    white-space: pre;\n    padding: inherit;\n    margin: inherit; }\n\n.medium-toolbar-arrow-under:after, .medium-toolbar-arrow-over:before {\n  border-style: solid;\n  content: '';\n  display: block;\n  height: 0;\n  left: 50%;\n  margin-left: -8px;\n  position: absolute;\n  width: 0; }\n\n.medium-toolbar-arrow-under:after {\n  border-width: 8px 8px 0 8px; }\n\n.medium-toolbar-arrow-over:before {\n  border-width: 0 8px 8px 8px;\n  top: -8px; }\n\n.medium-editor-toolbar {\n  font-family: \"Helvetica Neue\", Helvetica, Arial, sans-serif;\n  font-size: 16px;\n  left: 0;\n  position: absolute;\n  top: 0;\n  visibility: hidden;\n  z-index: 2000; }\n  .medium-editor-toolbar ul {\n    margin: 0;\n    padding: 0; }\n  .medium-editor-toolbar li {\n    float: left;\n    list-style: none;\n    margin: 0;\n    padding: 0; }\n    .medium-editor-toolbar li button {\n      box-sizing: border-box;\n      cursor: pointer;\n      display: block;\n      font-size: 14px;\n      line-height: 1.33;\n      margin: 0;\n      padding: 15px;\n      text-decoration: none; }\n      .medium-editor-toolbar li button:focus {\n        outline: none; }\n    .medium-editor-toolbar li .medium-editor-action-underline {\n      text-decoration: underline; }\n    .medium-editor-toolbar li .medium-editor-action-pre {\n      font-family: Consolas, \"Liberation Mono\", Menlo, Courier, monospace;\n      font-size: 12px;\n      font-weight: 100;\n      padding: 15px 0; }\n\n.medium-editor-toolbar-active {\n  visibility: visible; }\n\n.medium-editor-sticky-toolbar {\n  position: fixed;\n  top: 1px; }\n\n.medium-editor-relative-toolbar {\n  position: relative; }\n\n.medium-editor-toolbar-active.medium-editor-stalker-toolbar {\n  -webkit-animation: medium-editor-pop-upwards 160ms forwards linear;\n          animation: medium-editor-pop-upwards 160ms forwards linear; }\n\n.medium-editor-action-bold {\n  font-weight: bolder; }\n\n.medium-editor-action-italic {\n  font-style: italic; }\n\n.medium-editor-toolbar-form {\n  display: none; }\n  .medium-editor-toolbar-form input,\n  .medium-editor-toolbar-form a {\n    font-family: \"Helvetica Neue\", Helvetica, Arial, sans-serif; }\n  .medium-editor-toolbar-form .medium-editor-toolbar-form-row {\n    line-height: 14px;\n    margin-left: 5px;\n    padding-bottom: 5px; }\n  .medium-editor-toolbar-form .medium-editor-toolbar-input,\n  .medium-editor-toolbar-form label {\n    border: none;\n    box-sizing: border-box;\n    font-size: 14px;\n    margin: 0;\n    padding: 6px;\n    width: 316px;\n    display: inline-block; }\n    .medium-editor-toolbar-form .medium-editor-toolbar-input:focus,\n    .medium-editor-toolbar-form label:focus {\n      -webkit-appearance: none;\n         -moz-appearance: none;\n              appearance: none;\n      border: none;\n      box-shadow: none;\n      outline: 0; }\n  .medium-editor-toolbar-form a {\n    display: inline-block;\n    font-size: 24px;\n    font-weight: bolder;\n    margin: 0 10px;\n    text-decoration: none; }\n\n.medium-editor-toolbar-form-active {\n  display: block; }\n\n.medium-editor-toolbar-actions:after {\n  clear: both;\n  content: \"\";\n  display: table; }\n\n.medium-editor-element {\n  word-wrap: break-word;\n  min-height: 30px; }\n  .medium-editor-element img {\n    max-width: 100%; }\n  .medium-editor-element sub {\n    vertical-align: sub; }\n  .medium-editor-element sup {\n    vertical-align: super; }\n\n.medium-editor-hidden {\n  display: none; }\n"
  },
  {
    "path": "dist/css/themes/beagle.css",
    "content": ".medium-toolbar-arrow-under:after {\n  border-color: #000 transparent transparent transparent;\n  top: 40px; }\n\n.medium-toolbar-arrow-over:before {\n  border-color: transparent transparent #000 transparent; }\n\n.medium-editor-toolbar {\n  background-color: #000;\n  border: none;\n  border-radius: 50px; }\n  .medium-editor-toolbar li button {\n    background-color: transparent;\n    border: none;\n    box-sizing: border-box;\n    color: #ccc;\n    height: 40px;\n    min-width: 40px;\n    padding: 5px 12px;\n    -webkit-transition: background-color .2s ease-in, color .2s ease-in;\n            transition: background-color .2s ease-in, color .2s ease-in; }\n    .medium-editor-toolbar li button:hover {\n      background-color: #000;\n      color: #a2d7c7; }\n  .medium-editor-toolbar li .medium-editor-button-first {\n    border-bottom-left-radius: 50px;\n    border-top-left-radius: 50px;\n    padding-left: 24px; }\n  .medium-editor-toolbar li .medium-editor-button-last {\n    border-bottom-right-radius: 50px;\n    border-right: none;\n    border-top-right-radius: 50px;\n    padding-right: 24px; }\n  .medium-editor-toolbar li .medium-editor-button-active {\n    background-color: #000;\n    color: #a2d7c7; }\n\n.medium-editor-toolbar-form {\n  background: #000;\n  border-radius: 50px;\n  color: #ccc;\n  overflow: hidden; }\n  .medium-editor-toolbar-form .medium-editor-toolbar-input {\n    background: #000;\n    box-sizing: border-box;\n    color: #ccc;\n    height: 40px;\n    padding-left: 16px;\n    width: 220px; }\n    .medium-editor-toolbar-form .medium-editor-toolbar-input::-webkit-input-placeholder {\n      color: #f8f5f3;\n      color: rgba(248, 245, 243, 0.8); }\n    .medium-editor-toolbar-form .medium-editor-toolbar-input:-moz-placeholder {\n      /* Firefox 18- */\n      color: #f8f5f3;\n      color: rgba(248, 245, 243, 0.8); }\n    .medium-editor-toolbar-form .medium-editor-toolbar-input::-moz-placeholder {\n      /* Firefox 19+ */\n      color: #f8f5f3;\n      color: rgba(248, 245, 243, 0.8); }\n    .medium-editor-toolbar-form .medium-editor-toolbar-input:-ms-input-placeholder {\n      color: #f8f5f3;\n      color: rgba(248, 245, 243, 0.8); }\n  .medium-editor-toolbar-form a {\n    color: #ccc;\n    -webkit-transform: translateY(2px);\n            transform: translateY(2px); }\n  .medium-editor-toolbar-form .medium-editor-toolbar-close {\n    margin-right: 16px; }\n\n.medium-editor-toolbar-anchor-preview {\n  background: #000;\n  border-radius: 50px;\n  padding: 5px 12px; }\n\n.medium-editor-anchor-preview a {\n  color: #ccc;\n  text-decoration: none; }\n\n.medium-editor-toolbar-actions li, .medium-editor-toolbar-actions button {\n  border-radius: 50px; }\n"
  },
  {
    "path": "dist/css/themes/bootstrap.css",
    "content": ".medium-toolbar-arrow-under:after {\n  border-color: #428bca transparent transparent transparent;\n  top: 60px; }\n\n.medium-toolbar-arrow-over:before {\n  border-color: transparent transparent #428bca transparent; }\n\n.medium-editor-toolbar {\n  background-color: #428bca;\n  border: 1px solid #357ebd;\n  border-radius: 4px; }\n  .medium-editor-toolbar li button {\n    background-color: transparent;\n    border: none;\n    border-right: 1px solid #357ebd;\n    box-sizing: border-box;\n    color: #fff;\n    height: 60px;\n    min-width: 60px;\n    -webkit-transition: background-color .2s ease-in, color .2s ease-in;\n            transition: background-color .2s ease-in, color .2s ease-in; }\n    .medium-editor-toolbar li button:hover {\n      background-color: #3276b1;\n      color: #fff; }\n  .medium-editor-toolbar li .medium-editor-button-first {\n    border-bottom-left-radius: 4px;\n    border-top-left-radius: 4px; }\n  .medium-editor-toolbar li .medium-editor-button-last {\n    border-bottom-right-radius: 4px;\n    border-right: none;\n    border-top-right-radius: 4px; }\n  .medium-editor-toolbar li .medium-editor-button-active {\n    background-color: #3276b1;\n    color: #fff; }\n\n.medium-editor-toolbar-form {\n  background: #428bca;\n  border-radius: 4px;\n  color: #fff; }\n  .medium-editor-toolbar-form .medium-editor-toolbar-input {\n    background: #428bca;\n    color: #fff;\n    height: 60px; }\n    .medium-editor-toolbar-form .medium-editor-toolbar-input::-webkit-input-placeholder {\n      color: #fff;\n      color: rgba(255, 255, 255, 0.8); }\n    .medium-editor-toolbar-form .medium-editor-toolbar-input:-moz-placeholder {\n      /* Firefox 18- */\n      color: #fff;\n      color: rgba(255, 255, 255, 0.8); }\n    .medium-editor-toolbar-form .medium-editor-toolbar-input::-moz-placeholder {\n      /* Firefox 19+ */\n      color: #fff;\n      color: rgba(255, 255, 255, 0.8); }\n    .medium-editor-toolbar-form .medium-editor-toolbar-input:-ms-input-placeholder {\n      color: #fff;\n      color: rgba(255, 255, 255, 0.8); }\n  .medium-editor-toolbar-form a {\n    color: #fff; }\n\n.medium-editor-toolbar-anchor-preview {\n  background: #428bca;\n  border-radius: 4px;\n  color: #fff; }\n\n.medium-editor-placeholder:after {\n  color: #357ebd; }\n"
  },
  {
    "path": "dist/css/themes/default.css",
    "content": ".medium-toolbar-arrow-under:after {\n  border-color: #242424 transparent transparent transparent;\n  top: 50px; }\n\n.medium-toolbar-arrow-over:before {\n  border-color: transparent transparent #242424 transparent;\n  top: -8px; }\n\n.medium-editor-toolbar {\n  background-color: #242424;\n  background: -webkit-linear-gradient(top, #242424, rgba(36, 36, 36, 0.75));\n  background: linear-gradient(to bottom, #242424, rgba(36, 36, 36, 0.75));\n  border: 1px solid #000;\n  border-radius: 5px;\n  box-shadow: 0 0 3px #000; }\n  .medium-editor-toolbar li button {\n    background-color: #242424;\n    background: -webkit-linear-gradient(top, #242424, rgba(36, 36, 36, 0.89));\n    background: linear-gradient(to bottom, #242424, rgba(36, 36, 36, 0.89));\n    border: 0;\n    border-right: 1px solid #000;\n    border-left: 1px solid #333;\n    border-left: 1px solid rgba(255, 255, 255, 0.1);\n    box-shadow: 0 2px 2px rgba(0, 0, 0, 0.3);\n    color: #fff;\n    height: 50px;\n    min-width: 50px;\n    -webkit-transition: background-color .2s ease-in;\n            transition: background-color .2s ease-in; }\n    .medium-editor-toolbar li button:hover {\n      background-color: #000;\n      color: yellow; }\n  .medium-editor-toolbar li .medium-editor-button-first {\n    border-bottom-left-radius: 5px;\n    border-top-left-radius: 5px; }\n  .medium-editor-toolbar li .medium-editor-button-last {\n    border-bottom-right-radius: 5px;\n    border-top-right-radius: 5px; }\n  .medium-editor-toolbar li .medium-editor-button-active {\n    background-color: #000;\n    background: -webkit-linear-gradient(top, #242424, rgba(0, 0, 0, 0.89));\n    background: linear-gradient(to bottom, #242424, rgba(0, 0, 0, 0.89));\n    color: #fff; }\n\n.medium-editor-toolbar-form {\n  background: #242424;\n  border-radius: 5px;\n  color: #999; }\n  .medium-editor-toolbar-form .medium-editor-toolbar-input {\n    background: #242424;\n    box-sizing: border-box;\n    color: #ccc;\n    height: 50px; }\n  .medium-editor-toolbar-form a {\n    color: #fff; }\n\n.medium-editor-toolbar-anchor-preview {\n  background: #242424;\n  border-radius: 5px;\n  color: #fff; }\n\n.medium-editor-placeholder:after {\n  color: #b3b3b1; }\n"
  },
  {
    "path": "dist/css/themes/flat.css",
    "content": ".medium-toolbar-arrow-under:after {\n  top: 60px;\n  border-color: #57ad68 transparent transparent transparent; }\n\n.medium-toolbar-arrow-over:before {\n  top: -8px;\n  border-color: transparent transparent #57ad68 transparent; }\n\n.medium-editor-toolbar {\n  background-color: #57ad68; }\n  .medium-editor-toolbar li {\n    padding: 0; }\n    .medium-editor-toolbar li button {\n      min-width: 60px;\n      height: 60px;\n      border: none;\n      border-right: 1px solid #9ccea6;\n      background-color: transparent;\n      color: #fff;\n      -webkit-transition: background-color .2s ease-in, color .2s ease-in;\n              transition: background-color .2s ease-in, color .2s ease-in; }\n      .medium-editor-toolbar li button:hover {\n        background-color: #346a3f;\n        color: #fff; }\n    .medium-editor-toolbar li .medium-editor-button-active {\n      background-color: #23482a;\n      color: #fff; }\n    .medium-editor-toolbar li .medium-editor-button-last {\n      border-right: none; }\n\n.medium-editor-toolbar-form .medium-editor-toolbar-input {\n  height: 60px;\n  background: #57ad68;\n  color: #fff; }\n  .medium-editor-toolbar-form .medium-editor-toolbar-input::-webkit-input-placeholder {\n    color: #fff;\n    color: rgba(255, 255, 255, 0.8); }\n  .medium-editor-toolbar-form .medium-editor-toolbar-input:-moz-placeholder {\n    /* Firefox 18- */\n    color: #fff;\n    color: rgba(255, 255, 255, 0.8); }\n  .medium-editor-toolbar-form .medium-editor-toolbar-input::-moz-placeholder {\n    /* Firefox 19+ */\n    color: #fff;\n    color: rgba(255, 255, 255, 0.8); }\n  .medium-editor-toolbar-form .medium-editor-toolbar-input:-ms-input-placeholder {\n    color: #fff;\n    color: rgba(255, 255, 255, 0.8); }\n\n.medium-editor-toolbar-form a {\n  color: #fff; }\n\n.medium-editor-toolbar-anchor-preview {\n  background: #57ad68;\n  color: #fff; }\n\n.medium-editor-placeholder:after {\n  color: #9ccea6; }\n"
  },
  {
    "path": "dist/css/themes/mani.css",
    "content": ".medium-toolbar-arrow-under:after,\n.medium-toolbar-arrow-over:before {\n  display: none; }\n\n.medium-editor-toolbar {\n  border: 1px solid #cdd6e0;\n  background-color: #dee7f0;\n  background-color: rgba(222, 231, 240, 0.95);\n  background: -webkit-linear-gradient(bottom, #dee7f0, white);\n  background: linear-gradient(to top, #dee7f0, white);\n  border-radius: 2px;\n  box-shadow: 0 2px 6px rgba(0, 0, 0, 0.45); }\n  .medium-editor-toolbar li button {\n    min-width: 50px;\n    height: 50px;\n    border: none;\n    border-right: 1px solid #cdd6e0;\n    background-color: transparent;\n    color: #40648a;\n    -webkit-transition: background-color .2s ease-in, color .2s ease-in;\n            transition: background-color .2s ease-in, color .2s ease-in; }\n    .medium-editor-toolbar li button:hover {\n      background-color: #5c90c7;\n      background-color: rgba(92, 144, 199, 0.45);\n      color: #fff; }\n  .medium-editor-toolbar li .medium-editor-button-first {\n    border-top-left-radius: 2px;\n    border-bottom-left-radius: 2px; }\n  .medium-editor-toolbar li .medium-editor-button-last {\n    border-top-right-radius: 2px;\n    border-bottom-right-radius: 2px; }\n  .medium-editor-toolbar li .medium-editor-button-active {\n    background-color: #5c90c7;\n    background-color: rgba(92, 144, 199, 0.45);\n    color: #000;\n    background: -webkit-linear-gradient(top, #dee7f0, rgba(0, 0, 0, 0.1));\n    background: linear-gradient(to bottom, #dee7f0, rgba(0, 0, 0, 0.1)); }\n\n.medium-editor-toolbar-form {\n  background: #dee7f0;\n  color: #999;\n  border-radius: 2px; }\n  .medium-editor-toolbar-form .medium-editor-toolbar-input {\n    height: 50px;\n    background: #dee7f0;\n    color: #40648a;\n    box-sizing: border-box; }\n  .medium-editor-toolbar-form a {\n    color: #40648a; }\n\n.medium-editor-toolbar-anchor-preview {\n  background: #dee7f0;\n  color: #40648a;\n  border-radius: 2px; }\n\n.medium-editor-placeholder:after {\n  color: #cdd6e0; }\n"
  },
  {
    "path": "dist/css/themes/roman.css",
    "content": ".medium-toolbar-arrow-under:after,\n.medium-toolbar-arrow-over:before {\n  display: none; }\n\n.medium-editor-toolbar {\n  background-color: #fff;\n  background-color: rgba(255, 255, 255, 0.95);\n  border-radius: 5px;\n  box-shadow: 0 2px 6px rgba(0, 0, 0, 0.45); }\n  .medium-editor-toolbar li button {\n    min-width: 50px;\n    height: 50px;\n    border: none;\n    border-right: 1px solid #a8a8a8;\n    background-color: transparent;\n    color: #889aac;\n    box-shadow: inset 0 0 3px #f8f8e6;\n    background: -webkit-linear-gradient(top, #fff, rgba(0, 0, 0, 0.2));\n    background: linear-gradient(to bottom, #fff, rgba(0, 0, 0, 0.2));\n    text-shadow: 1px 4px 6px #def, 0 0 0 #000, 1px 4px 6px #def;\n    -webkit-transition: background-color .2s ease-in;\n            transition: background-color .2s ease-in; }\n    .medium-editor-toolbar li button:hover {\n      background-color: #fff;\n      color: #fff;\n      color: rgba(0, 0, 0, 0.8); }\n  .medium-editor-toolbar li .medium-editor-button-first {\n    border-top-left-radius: 5px;\n    border-bottom-left-radius: 5px; }\n  .medium-editor-toolbar li .medium-editor-button-last {\n    border-top-right-radius: 5px;\n    border-bottom-right-radius: 5px; }\n  .medium-editor-toolbar li .medium-editor-button-active {\n    background-color: #ccc;\n    color: #000;\n    color: rgba(0, 0, 0, 0.8);\n    background: -webkit-linear-gradient(bottom, #fff, rgba(0, 0, 0, 0.1));\n    background: linear-gradient(to top, #fff, rgba(0, 0, 0, 0.1)); }\n\n.medium-editor-toolbar-form {\n  background: #fff;\n  color: #999;\n  border-radius: 5px; }\n  .medium-editor-toolbar-form .medium-editor-toolbar-input {\n    margin: 0;\n    height: 50px;\n    background: #fff;\n    color: #a8a8a8; }\n  .medium-editor-toolbar-form a {\n    color: #889aac; }\n\n.medium-editor-toolbar-anchor-preview {\n  background: #fff;\n  color: #889aac;\n  border-radius: 5px; }\n\n.medium-editor-placeholder:after {\n  color: #a8a8a8; }\n"
  },
  {
    "path": "dist/css/themes/tim.css",
    "content": ".medium-toolbar-arrow-under:after {\n  border-color: #2f1e07 transparent transparent transparent;\n  top: 60px; }\n\n.medium-toolbar-arrow-over:before {\n  border-color: transparent transparent #2f1e07 transparent; }\n\n.medium-editor-toolbar {\n  background-color: #2f1e07;\n  border: 1px solid #5b3a0e;\n  border-radius: 6px; }\n  .medium-editor-toolbar li button {\n    background-color: transparent;\n    border: none;\n    border-right: 1px solid #5b3a0e;\n    box-sizing: border-box;\n    color: #ffedd5;\n    height: 60px;\n    min-width: 60px;\n    -webkit-transition: background-color .2s ease-in, color .2s ease-in;\n            transition: background-color .2s ease-in, color .2s ease-in; }\n    .medium-editor-toolbar li button:hover {\n      background-color: #030200;\n      color: #ffedd5; }\n  .medium-editor-toolbar li .medium-editor-button-first {\n    border-bottom-left-radius: 6px;\n    border-top-left-radius: 6px; }\n  .medium-editor-toolbar li .medium-editor-button-last {\n    border-bottom-right-radius: 6px;\n    border-right: none;\n    border-top-right-radius: 6px; }\n  .medium-editor-toolbar li .medium-editor-button-active {\n    background-color: #030200;\n    color: #ffedd5; }\n\n.medium-editor-toolbar-form {\n  background: #2f1e07;\n  border-radius: 6px;\n  color: #ffedd5; }\n  .medium-editor-toolbar-form .medium-editor-toolbar-input {\n    background: #2f1e07;\n    color: #ffedd5;\n    height: 60px; }\n    .medium-editor-toolbar-form .medium-editor-toolbar-input::-webkit-input-placeholder {\n      color: #ffedd5;\n      color: rgba(255, 237, 213, 0.8); }\n    .medium-editor-toolbar-form .medium-editor-toolbar-input:-moz-placeholder {\n      /* Firefox 18- */\n      color: #ffedd5;\n      color: rgba(255, 237, 213, 0.8); }\n    .medium-editor-toolbar-form .medium-editor-toolbar-input::-moz-placeholder {\n      /* Firefox 19+ */\n      color: #ffedd5;\n      color: rgba(255, 237, 213, 0.8); }\n    .medium-editor-toolbar-form .medium-editor-toolbar-input:-ms-input-placeholder {\n      color: #ffedd5;\n      color: rgba(255, 237, 213, 0.8); }\n  .medium-editor-toolbar-form a {\n    color: #ffedd5; }\n\n.medium-editor-toolbar-anchor-preview {\n  background: #2f1e07;\n  border-radius: 6px;\n  color: #ffedd5; }\n\n.medium-editor-placeholder:after {\n  color: #5b3a0e; }\n"
  },
  {
    "path": "dist/js/medium-editor.js",
    "content": "/*global self, document, DOMException */\n\n/*! @source http://purl.eligrey.com/github/classList.js/blob/master/classList.js */\n\n// Full polyfill for browsers with no classList support\nif (!(\"classList\" in document.createElement(\"_\"))) {\n  (function (view) {\n\n  \"use strict\";\n\n  if (!('Element' in view)) return;\n\n  var\n      classListProp = \"classList\"\n    , protoProp = \"prototype\"\n    , elemCtrProto = view.Element[protoProp]\n    , objCtr = Object\n    , strTrim = String[protoProp].trim || function () {\n      return this.replace(/^\\s+|\\s+$/g, \"\");\n    }\n    , arrIndexOf = Array[protoProp].indexOf || function (item) {\n      var\n          i = 0\n        , len = this.length\n      ;\n      for (; i < len; i++) {\n        if (i in this && this[i] === item) {\n          return i;\n        }\n      }\n      return -1;\n    }\n    // Vendors: please allow content code to instantiate DOMExceptions\n    , DOMEx = function (type, message) {\n      this.name = type;\n      this.code = DOMException[type];\n      this.message = message;\n    }\n    , checkTokenAndGetIndex = function (classList, token) {\n      if (token === \"\") {\n        throw new DOMEx(\n            \"SYNTAX_ERR\"\n          , \"An invalid or illegal string was specified\"\n        );\n      }\n      if (/\\s/.test(token)) {\n        throw new DOMEx(\n            \"INVALID_CHARACTER_ERR\"\n          , \"String contains an invalid character\"\n        );\n      }\n      return arrIndexOf.call(classList, token);\n    }\n    , ClassList = function (elem) {\n      var\n          trimmedClasses = strTrim.call(elem.getAttribute(\"class\") || \"\")\n        , classes = trimmedClasses ? trimmedClasses.split(/\\s+/) : []\n        , i = 0\n        , len = classes.length\n      ;\n      for (; i < len; i++) {\n        this.push(classes[i]);\n      }\n      this._updateClassName = function () {\n        elem.setAttribute(\"class\", this.toString());\n      };\n    }\n    , classListProto = ClassList[protoProp] = []\n    , classListGetter = function () {\n      return new ClassList(this);\n    }\n  ;\n  // Most DOMException implementations don't allow calling DOMException's toString()\n  // on non-DOMExceptions. Error's toString() is sufficient here.\n  DOMEx[protoProp] = Error[protoProp];\n  classListProto.item = function (i) {\n    return this[i] || null;\n  };\n  classListProto.contains = function (token) {\n    token += \"\";\n    return checkTokenAndGetIndex(this, token) !== -1;\n  };\n  classListProto.add = function () {\n    var\n        tokens = arguments\n      , i = 0\n      , l = tokens.length\n      , token\n      , updated = false\n    ;\n    do {\n      token = tokens[i] + \"\";\n      if (checkTokenAndGetIndex(this, token) === -1) {\n        this.push(token);\n        updated = true;\n      }\n    }\n    while (++i < l);\n\n    if (updated) {\n      this._updateClassName();\n    }\n  };\n  classListProto.remove = function () {\n    var\n        tokens = arguments\n      , i = 0\n      , l = tokens.length\n      , token\n      , updated = false\n      , index\n    ;\n    do {\n      token = tokens[i] + \"\";\n      index = checkTokenAndGetIndex(this, token);\n      while (index !== -1) {\n        this.splice(index, 1);\n        updated = true;\n        index = checkTokenAndGetIndex(this, token);\n      }\n    }\n    while (++i < l);\n\n    if (updated) {\n      this._updateClassName();\n    }\n  };\n  classListProto.toggle = function (token, force) {\n    token += \"\";\n\n    var\n        result = this.contains(token)\n      , method = result ?\n        force !== true && \"remove\"\n      :\n        force !== false && \"add\"\n    ;\n\n    if (method) {\n      this[method](token);\n    }\n\n    if (force === true || force === false) {\n      return force;\n    } else {\n      return !result;\n    }\n  };\n  classListProto.toString = function () {\n    return this.join(\" \");\n  };\n\n  if (objCtr.defineProperty) {\n    var classListPropDesc = {\n        get: classListGetter\n      , enumerable: true\n      , configurable: true\n    };\n    try {\n      objCtr.defineProperty(elemCtrProto, classListProp, classListPropDesc);\n    } catch (ex) { // IE 8 doesn't support enumerable:true\n      if (ex.number === -0x7FF5EC54) {\n        classListPropDesc.enumerable = false;\n        objCtr.defineProperty(elemCtrProto, classListProp, classListPropDesc);\n      }\n    }\n  } else if (objCtr[protoProp].__defineGetter__) {\n    elemCtrProto.__defineGetter__(classListProp, classListGetter);\n  }\n\n  }(self));\n}\n\n/* Blob.js\n * A Blob implementation.\n * 2014-07-24\n *\n * By Eli Grey, http://eligrey.com\n * By Devin Samarin, https://github.com/dsamarin\n * License: X11/MIT\n *   See https://github.com/eligrey/Blob.js/blob/master/LICENSE.md\n */\n\n/*global self, unescape */\n/*jslint bitwise: true, regexp: true, confusion: true, es5: true, vars: true, white: true,\n  plusplus: true */\n\n/*! @source http://purl.eligrey.com/github/Blob.js/blob/master/Blob.js */\n\n(function (view) {\n  \"use strict\";\n\n  view.URL = view.URL || view.webkitURL;\n\n  if (view.Blob && view.URL) {\n    try {\n      new Blob;\n      return;\n    } catch (e) {}\n  }\n\n  // Internally we use a BlobBuilder implementation to base Blob off of\n  // in order to support older browsers that only have BlobBuilder\n  var BlobBuilder = view.BlobBuilder || view.WebKitBlobBuilder || view.MozBlobBuilder || (function(view) {\n    var\n        get_class = function(object) {\n        return Object.prototype.toString.call(object).match(/^\\[object\\s(.*)\\]$/)[1];\n      }\n      , FakeBlobBuilder = function BlobBuilder() {\n        this.data = [];\n      }\n      , FakeBlob = function Blob(data, type, encoding) {\n        this.data = data;\n        this.size = data.length;\n        this.type = type;\n        this.encoding = encoding;\n      }\n      , FBB_proto = FakeBlobBuilder.prototype\n      , FB_proto = FakeBlob.prototype\n      , FileReaderSync = view.FileReaderSync\n      , FileException = function(type) {\n        this.code = this[this.name = type];\n      }\n      , file_ex_codes = (\n          \"NOT_FOUND_ERR SECURITY_ERR ABORT_ERR NOT_READABLE_ERR ENCODING_ERR \"\n        + \"NO_MODIFICATION_ALLOWED_ERR INVALID_STATE_ERR SYNTAX_ERR\"\n      ).split(\" \")\n      , file_ex_code = file_ex_codes.length\n      , real_URL = view.URL || view.webkitURL || view\n      , real_create_object_URL = real_URL.createObjectURL\n      , real_revoke_object_URL = real_URL.revokeObjectURL\n      , URL = real_URL\n      , btoa = view.btoa\n      , atob = view.atob\n\n      , ArrayBuffer = view.ArrayBuffer\n      , Uint8Array = view.Uint8Array\n\n      , origin = /^[\\w-]+:\\/*\\[?[\\w\\.:-]+\\]?(?::[0-9]+)?/\n    ;\n    FakeBlob.fake = FB_proto.fake = true;\n    while (file_ex_code--) {\n      FileException.prototype[file_ex_codes[file_ex_code]] = file_ex_code + 1;\n    }\n    // Polyfill URL\n    if (!real_URL.createObjectURL) {\n      URL = view.URL = function(uri) {\n        var\n            uri_info = document.createElementNS(\"http://www.w3.org/1999/xhtml\", \"a\")\n          , uri_origin\n        ;\n        uri_info.href = uri;\n        if (!(\"origin\" in uri_info)) {\n          if (uri_info.protocol.toLowerCase() === \"data:\") {\n            uri_info.origin = null;\n          } else {\n            uri_origin = uri.match(origin);\n            uri_info.origin = uri_origin && uri_origin[1];\n          }\n        }\n        return uri_info;\n      };\n    }\n    URL.createObjectURL = function(blob) {\n      var\n          type = blob.type\n        , data_URI_header\n      ;\n      if (type === null) {\n        type = \"application/octet-stream\";\n      }\n      if (blob instanceof FakeBlob) {\n        data_URI_header = \"data:\" + type;\n        if (blob.encoding === \"base64\") {\n          return data_URI_header + \";base64,\" + blob.data;\n        } else if (blob.encoding === \"URI\") {\n          return data_URI_header + \",\" + decodeURIComponent(blob.data);\n        } if (btoa) {\n          return data_URI_header + \";base64,\" + btoa(blob.data);\n        } else {\n          return data_URI_header + \",\" + encodeURIComponent(blob.data);\n        }\n      } else if (real_create_object_URL) {\n        return real_create_object_URL.call(real_URL, blob);\n      }\n    };\n    URL.revokeObjectURL = function(object_URL) {\n      if (object_URL.substring(0, 5) !== \"data:\" && real_revoke_object_URL) {\n        real_revoke_object_URL.call(real_URL, object_URL);\n      }\n    };\n    FBB_proto.append = function(data/*, endings*/) {\n      var bb = this.data;\n      // decode data to a binary string\n      if (Uint8Array && (data instanceof ArrayBuffer || data instanceof Uint8Array)) {\n        var\n            str = \"\"\n          , buf = new Uint8Array(data)\n          , i = 0\n          , buf_len = buf.length\n        ;\n        for (; i < buf_len; i++) {\n          str += String.fromCharCode(buf[i]);\n        }\n        bb.push(str);\n      } else if (get_class(data) === \"Blob\" || get_class(data) === \"File\") {\n        if (FileReaderSync) {\n          var fr = new FileReaderSync;\n          bb.push(fr.readAsBinaryString(data));\n        } else {\n          // async FileReader won't work as BlobBuilder is sync\n          throw new FileException(\"NOT_READABLE_ERR\");\n        }\n      } else if (data instanceof FakeBlob) {\n        if (data.encoding === \"base64\" && atob) {\n          bb.push(atob(data.data));\n        } else if (data.encoding === \"URI\") {\n          bb.push(decodeURIComponent(data.data));\n        } else if (data.encoding === \"raw\") {\n          bb.push(data.data);\n        }\n      } else {\n        if (typeof data !== \"string\") {\n          data += \"\"; // convert unsupported types to strings\n        }\n        // decode UTF-16 to binary string\n        bb.push(unescape(encodeURIComponent(data)));\n      }\n    };\n    FBB_proto.getBlob = function(type) {\n      if (!arguments.length) {\n        type = null;\n      }\n      return new FakeBlob(this.data.join(\"\"), type, \"raw\");\n    };\n    FBB_proto.toString = function() {\n      return \"[object BlobBuilder]\";\n    };\n    FB_proto.slice = function(start, end, type) {\n      var args = arguments.length;\n      if (args < 3) {\n        type = null;\n      }\n      return new FakeBlob(\n          this.data.slice(start, args > 1 ? end : this.data.length)\n        , type\n        , this.encoding\n      );\n    };\n    FB_proto.toString = function() {\n      return \"[object Blob]\";\n    };\n    FB_proto.close = function() {\n      this.size = 0;\n      delete this.data;\n    };\n    return FakeBlobBuilder;\n  }(view));\n\n  view.Blob = function(blobParts, options) {\n    var type = options ? (options.type || \"\") : \"\";\n    var builder = new BlobBuilder();\n    if (blobParts) {\n      for (var i = 0, len = blobParts.length; i < len; i++) {\n        if (Uint8Array && blobParts[i] instanceof Uint8Array) {\n          builder.append(blobParts[i].buffer);\n        }\n        else {\n          builder.append(blobParts[i]);\n        }\n      }\n    }\n    var blob = builder.getBlob(type);\n    if (!blob.slice && blob.webkitSlice) {\n      blob.slice = blob.webkitSlice;\n    }\n    return blob;\n  };\n\n  var getPrototypeOf = Object.getPrototypeOf || function(object) {\n    return object.__proto__;\n  };\n  view.Blob.prototype = getPrototypeOf(new view.Blob());\n}(typeof self !== \"undefined\" && self || typeof window !== \"undefined\" && window || this.content || this));\n\n(function (root, factory) {\n    'use strict';\n    var isElectron = typeof module === 'object' && typeof process !== 'undefined' && process && process.versions && process.versions.electron;\n    if (!isElectron && typeof module === 'object') {\n        module.exports = factory;\n    } else if (typeof define === 'function' && define.amd) {\n        define(function () {\n            return factory;\n        });\n    } else {\n        root.MediumEditor = factory;\n    }\n}(this, function () {\n\n    'use strict';\n\nfunction MediumEditor(elements, options) {\n    'use strict';\n    return this.init(elements, options);\n}\n\nMediumEditor.extensions = {};\n/*jshint unused: true */\n(function (window) {\n    'use strict';\n\n    function copyInto(overwrite, dest) {\n        var prop,\n            sources = Array.prototype.slice.call(arguments, 2);\n        dest = dest || {};\n        for (var i = 0; i < sources.length; i++) {\n            var source = sources[i];\n            if (source) {\n                for (prop in source) {\n                    if (source.hasOwnProperty(prop) &&\n                        typeof source[prop] !== 'undefined' &&\n                        (overwrite || dest.hasOwnProperty(prop) === false)) {\n                        dest[prop] = source[prop];\n                    }\n                }\n            }\n        }\n        return dest;\n    }\n\n    // https://developer.mozilla.org/en-US/docs/Web/API/Node/contains\n    // Some browsers (including phantom) don't return true for Node.contains(child)\n    // if child is a text node.  Detect these cases here and use a fallback\n    // for calls to Util.isDescendant()\n    var nodeContainsWorksWithTextNodes = false;\n    try {\n        var testParent = document.createElement('div'),\n            testText = document.createTextNode(' ');\n        testParent.appendChild(testText);\n        nodeContainsWorksWithTextNodes = testParent.contains(testText);\n    } catch (exc) {}\n\n    var Util = {\n\n        // http://stackoverflow.com/questions/17907445/how-to-detect-ie11#comment30165888_17907562\n        // by rg89\n        isIE: ((navigator.appName === 'Microsoft Internet Explorer') || ((navigator.appName === 'Netscape') && (new RegExp('Trident/.*rv:([0-9]{1,}[.0-9]{0,})').exec(navigator.userAgent) !== null))),\n\n        isEdge: (/Edge\\/\\d+/).exec(navigator.userAgent) !== null,\n\n        // if firefox\n        isFF: (navigator.userAgent.toLowerCase().indexOf('firefox') > -1),\n\n        // http://stackoverflow.com/a/11752084/569101\n        isMac: (window.navigator.platform.toUpperCase().indexOf('MAC') >= 0),\n\n        // https://github.com/jashkenas/underscore\n        // Lonely letter MUST USE the uppercase code\n        keyCode: {\n            BACKSPACE: 8,\n            TAB: 9,\n            ENTER: 13,\n            ESCAPE: 27,\n            SPACE: 32,\n            DELETE: 46,\n            K: 75, // K keycode, and not k\n            M: 77,\n            V: 86\n        },\n\n        /**\n         * Returns true if it's metaKey on Mac, or ctrlKey on non-Mac.\n         * See #591\n         */\n        isMetaCtrlKey: function (event) {\n            if ((Util.isMac && event.metaKey) || (!Util.isMac && event.ctrlKey)) {\n                return true;\n            }\n\n            return false;\n        },\n\n        /**\n         * Returns true if the key associated to the event is inside keys array\n         *\n         * @see : https://github.com/jquery/jquery/blob/0705be475092aede1eddae01319ec931fb9c65fc/src/event.js#L473-L484\n         * @see : http://stackoverflow.com/q/4471582/569101\n         */\n        isKey: function (event, keys) {\n            var keyCode = Util.getKeyCode(event);\n\n            // it's not an array let's just compare strings!\n            if (false === Array.isArray(keys)) {\n                return keyCode === keys;\n            }\n\n            if (-1 === keys.indexOf(keyCode)) {\n                return false;\n            }\n\n            return true;\n        },\n\n        getKeyCode: function (event) {\n            var keyCode = event.which;\n\n            // getting the key code from event\n            if (null === keyCode) {\n                keyCode = event.charCode !== null ? event.charCode : event.keyCode;\n            }\n\n            return keyCode;\n        },\n\n        blockContainerElementNames: [\n            // elements our editor generates\n            'p', 'h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'blockquote', 'pre', 'ul', 'li', 'ol',\n            // all other known block elements\n            'address', 'article', 'aside', 'audio', 'canvas', 'dd', 'dl', 'dt', 'fieldset',\n            'figcaption', 'figure', 'footer', 'form', 'header', 'hgroup', 'main', 'nav',\n            'noscript', 'output', 'section', 'video',\n            'table', 'thead', 'tbody', 'tfoot', 'tr', 'th', 'td'\n        ],\n\n        emptyElementNames: ['br', 'col', 'colgroup', 'hr', 'img', 'input', 'source', 'wbr'],\n\n        extend: function extend(/* dest, source1, source2, ...*/) {\n            var args = [true].concat(Array.prototype.slice.call(arguments));\n            return copyInto.apply(this, args);\n        },\n\n        defaults: function defaults(/*dest, source1, source2, ...*/) {\n            var args = [false].concat(Array.prototype.slice.call(arguments));\n            return copyInto.apply(this, args);\n        },\n\n        /*\n         * Create a link around the provided text nodes which must be adjacent to each other and all be\n         * descendants of the same closest block container. If the preconditions are not met, unexpected\n         * behavior will result.\n         */\n        createLink: function (document, textNodes, href, target) {\n            var anchor = document.createElement('a');\n            Util.moveTextRangeIntoElement(textNodes[0], textNodes[textNodes.length - 1], anchor);\n            anchor.setAttribute('href', href);\n            if (target) {\n                if (target === '_blank') {\n                    anchor.setAttribute('rel', 'noopener noreferrer');\n                }\n                anchor.setAttribute('target', target);\n            }\n            return anchor;\n        },\n\n        /*\n         * Given the provided match in the format {start: 1, end: 2} where start and end are indices into the\n         * textContent of the provided element argument, modify the DOM inside element to ensure that the text\n         * identified by the provided match can be returned as text nodes that contain exactly that text, without\n         * any additional text at the beginning or end of the returned array of adjacent text nodes.\n         *\n         * The only DOM manipulation performed by this function is splitting the text nodes, non-text nodes are\n         * not affected in any way.\n         */\n        findOrCreateMatchingTextNodes: function (document, element, match) {\n            var treeWalker = document.createTreeWalker(element, NodeFilter.SHOW_ALL, null, false),\n                matchedNodes = [],\n                currentTextIndex = 0,\n                startReached = false,\n                currentNode = null,\n                newNode = null;\n\n            while ((currentNode = treeWalker.nextNode()) !== null) {\n                if (currentNode.nodeType > 3) {\n                    continue;\n                } else if (currentNode.nodeType === 3) {\n                    if (!startReached && match.start < (currentTextIndex + currentNode.nodeValue.length)) {\n                        startReached = true;\n                        newNode = Util.splitStartNodeIfNeeded(currentNode, match.start, currentTextIndex);\n                    }\n                    if (startReached) {\n                        Util.splitEndNodeIfNeeded(currentNode, newNode, match.end, currentTextIndex);\n                    }\n                    if (startReached && currentTextIndex === match.end) {\n                        break; // Found the node(s) corresponding to the link. Break out and move on to the next.\n                    } else if (startReached && currentTextIndex > (match.end + 1)) {\n                        throw new Error('PerformLinking overshot the target!'); // should never happen...\n                    }\n\n                    if (startReached) {\n                        matchedNodes.push(newNode || currentNode);\n                    }\n\n                    currentTextIndex += currentNode.nodeValue.length;\n                    if (newNode !== null) {\n                        currentTextIndex += newNode.nodeValue.length;\n                        // Skip the newNode as we'll already have pushed it to the matches\n                        treeWalker.nextNode();\n                    }\n                    newNode = null;\n                } else if (currentNode.tagName.toLowerCase() === 'img') {\n                    if (!startReached && (match.start <= currentTextIndex)) {\n                        startReached = true;\n                    }\n                    if (startReached) {\n                        matchedNodes.push(currentNode);\n                    }\n                }\n            }\n            return matchedNodes;\n        },\n\n        /*\n         * Given the provided text node and text coordinates, split the text node if needed to make it align\n         * precisely with the coordinates.\n         *\n         * This function is intended to be called from Util.findOrCreateMatchingTextNodes.\n         */\n        splitStartNodeIfNeeded: function (currentNode, matchStartIndex, currentTextIndex) {\n            if (matchStartIndex !== currentTextIndex) {\n                return currentNode.splitText(matchStartIndex - currentTextIndex);\n            }\n            return null;\n        },\n\n        /*\n         * Given the provided text node and text coordinates, split the text node if needed to make it align\n         * precisely with the coordinates. The newNode argument should from the result of Util.splitStartNodeIfNeeded,\n         * if that function has been called on the same currentNode.\n         *\n         * This function is intended to be called from Util.findOrCreateMatchingTextNodes.\n         */\n        splitEndNodeIfNeeded: function (currentNode, newNode, matchEndIndex, currentTextIndex) {\n            var textIndexOfEndOfFarthestNode,\n                endSplitPoint;\n            textIndexOfEndOfFarthestNode = currentTextIndex + currentNode.nodeValue.length +\n                    (newNode ? newNode.nodeValue.length : 0) - 1;\n            endSplitPoint = matchEndIndex - currentTextIndex -\n                    (newNode ? currentNode.nodeValue.length : 0);\n            if (textIndexOfEndOfFarthestNode >= matchEndIndex &&\n                    currentTextIndex !== textIndexOfEndOfFarthestNode &&\n                    endSplitPoint !== 0) {\n                (newNode || currentNode).splitText(endSplitPoint);\n            }\n        },\n\n        /*\n        * Take an element, and break up all of its text content into unique pieces such that:\n         * 1) All text content of the elements are in separate blocks. No piece of text content should span\n         *    across multiple blocks. This means no element return by this function should have\n         *    any blocks as children.\n         * 2) The union of the textcontent of all of the elements returned here covers all\n         *    of the text within the element.\n         *\n         *\n         * EXAMPLE:\n         * In the event that we have something like:\n         *\n         * <blockquote>\n         *   <p>Some Text</p>\n         *   <ol>\n         *     <li>List Item 1</li>\n         *     <li>List Item 2</li>\n         *   </ol>\n         * </blockquote>\n         *\n         * This function would return these elements as an array:\n         *   [ <p>Some Text</p>, <li>List Item 1</li>, <li>List Item 2</li> ]\n         *\n         * Since the <blockquote> and <ol> elements contain blocks within them they are not returned.\n         * Since the <p> and <li>'s don't contain block elements and cover all the text content of the\n         * <blockquote> container, they are the elements returned.\n         */\n        splitByBlockElements: function (element) {\n            if (element.nodeType !== 3 && element.nodeType !== 1) {\n                return [];\n            }\n\n            var toRet = [],\n                blockElementQuery = MediumEditor.util.blockContainerElementNames.join(',');\n\n            if (element.nodeType === 3 || element.querySelectorAll(blockElementQuery).length === 0) {\n                return [element];\n            }\n\n            for (var i = 0; i < element.childNodes.length; i++) {\n                var child = element.childNodes[i];\n                if (child.nodeType === 3) {\n                    toRet.push(child);\n                } else if (child.nodeType === 1) {\n                    var blockElements = child.querySelectorAll(blockElementQuery);\n                    if (blockElements.length === 0) {\n                        toRet.push(child);\n                    } else {\n                        toRet = toRet.concat(MediumEditor.util.splitByBlockElements(child));\n                    }\n                }\n            }\n\n            return toRet;\n        },\n\n        // Find the next node in the DOM tree that represents any text that is being\n        // displayed directly next to the targetNode (passed as an argument)\n        // Text that appears directly next to the current node can be:\n        //  - A sibling text node\n        //  - A descendant of a sibling element\n        //  - A sibling text node of an ancestor\n        //  - A descendant of a sibling element of an ancestor\n        findAdjacentTextNodeWithContent: function findAdjacentTextNodeWithContent(rootNode, targetNode, ownerDocument) {\n            var pastTarget = false,\n                nextNode,\n                nodeIterator = ownerDocument.createNodeIterator(rootNode, NodeFilter.SHOW_TEXT, null, false);\n\n            // Use a native NodeIterator to iterate over all the text nodes that are descendants\n            // of the rootNode.  Once past the targetNode, choose the first non-empty text node\n            nextNode = nodeIterator.nextNode();\n            while (nextNode) {\n                if (nextNode === targetNode) {\n                    pastTarget = true;\n                } else if (pastTarget) {\n                    if (nextNode.nodeType === 3 && nextNode.nodeValue && nextNode.nodeValue.trim().length > 0) {\n                        break;\n                    }\n                }\n                nextNode = nodeIterator.nextNode();\n            }\n\n            return nextNode;\n        },\n\n        // Find an element's previous sibling within a medium-editor element\n        // If one doesn't exist, find the closest ancestor's previous sibling\n        findPreviousSibling: function (node) {\n            if (!node || Util.isMediumEditorElement(node)) {\n                return false;\n            }\n\n            var previousSibling = node.previousSibling;\n            while (!previousSibling && !Util.isMediumEditorElement(node.parentNode)) {\n                node = node.parentNode;\n                previousSibling = node.previousSibling;\n            }\n\n            return previousSibling;\n        },\n\n        isDescendant: function isDescendant(parent, child, checkEquality) {\n            if (!parent || !child) {\n                return false;\n            }\n            if (parent === child) {\n                return !!checkEquality;\n            }\n            // If parent is not an element, it can't have any descendants\n            if (parent.nodeType !== 1) {\n                return false;\n            }\n            if (nodeContainsWorksWithTextNodes || child.nodeType !== 3) {\n                return parent.contains(child);\n            }\n            var node = child.parentNode;\n            while (node !== null) {\n                if (node === parent) {\n                    return true;\n                }\n                node = node.parentNode;\n            }\n            return false;\n        },\n\n        // https://github.com/jashkenas/underscore\n        isElement: function isElement(obj) {\n            return !!(obj && obj.nodeType === 1);\n        },\n\n        // https://github.com/jashkenas/underscore\n        throttle: function (func, wait) {\n            var THROTTLE_INTERVAL = 50,\n                context,\n                args,\n                result,\n                timeout = null,\n                previous = 0,\n                later = function () {\n                    previous = Date.now();\n                    timeout = null;\n                    result = func.apply(context, args);\n                    if (!timeout) {\n                        context = args = null;\n                    }\n                };\n\n            if (!wait && wait !== 0) {\n                wait = THROTTLE_INTERVAL;\n            }\n\n            return function () {\n                var now = Date.now(),\n                    remaining = wait - (now - previous);\n\n                context = this;\n                args = arguments;\n                if (remaining <= 0 || remaining > wait) {\n                    if (timeout) {\n                        clearTimeout(timeout);\n                        timeout = null;\n                    }\n                    previous = now;\n                    result = func.apply(context, args);\n                    if (!timeout) {\n                        context = args = null;\n                    }\n                } else if (!timeout) {\n                    timeout = setTimeout(later, remaining);\n                }\n                return result;\n            };\n        },\n\n        traverseUp: function (current, testElementFunction) {\n            if (!current) {\n                return false;\n            }\n\n            do {\n                if (current.nodeType === 1) {\n                    if (testElementFunction(current)) {\n                        return current;\n                    }\n                    // do not traverse upwards past the nearest containing editor\n                    if (Util.isMediumEditorElement(current)) {\n                        return false;\n                    }\n                }\n\n                current = current.parentNode;\n            } while (current);\n\n            return false;\n        },\n\n        htmlEntities: function (str) {\n            // converts special characters (like <) into their escaped/encoded values (like &lt;).\n            // This allows you to show to display the string without the browser reading it as HTML.\n            return String(str).replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;').replace(/\"/g, '&quot;');\n        },\n\n        // http://stackoverflow.com/questions/6690752/insert-html-at-caret-in-a-contenteditable-div\n        insertHTMLCommand: function (doc, html) {\n            var selection, range, el, fragment, node, lastNode, toReplace,\n                res = false,\n                ecArgs = ['insertHTML', false, html];\n\n            /* Edge's implementation of insertHTML is just buggy right now:\n             * - Doesn't allow leading white space at the beginning of an element\n             * - Found a case when a <font size=\"2\"> tag was inserted when calling alignCenter inside a blockquote\n             *\n             * There are likely other bugs, these are just the ones we found so far.\n             * For now, let's just use the same fallback we did for IE\n             */\n            if (!MediumEditor.util.isEdge && doc.queryCommandSupported('insertHTML')) {\n                try {\n                    return doc.execCommand.apply(doc, ecArgs);\n                } catch (ignore) {}\n            }\n\n            selection = doc.getSelection();\n            if (selection.rangeCount) {\n                range = selection.getRangeAt(0);\n                toReplace = range.commonAncestorContainer;\n\n                // https://github.com/yabwe/medium-editor/issues/748\n                // If the selection is an empty editor element, create a temporary text node inside of the editor\n                // and select it so that we don't delete the editor element\n                if (Util.isMediumEditorElement(toReplace) && !toReplace.firstChild) {\n                    range.selectNode(toReplace.appendChild(doc.createTextNode('')));\n                } else if ((toReplace.nodeType === 3 && range.startOffset === 0 && range.endOffset === toReplace.nodeValue.length) ||\n                        (toReplace.nodeType !== 3 && toReplace.innerHTML === range.toString())) {\n                    // Ensure range covers maximum amount of nodes as possible\n                    // By moving up the DOM and selecting ancestors whose only child is the range\n                    while (!Util.isMediumEditorElement(toReplace) &&\n                            toReplace.parentNode &&\n                            toReplace.parentNode.childNodes.length === 1 &&\n                            !Util.isMediumEditorElement(toReplace.parentNode)) {\n                        toReplace = toReplace.parentNode;\n                    }\n                    range.selectNode(toReplace);\n                }\n                range.deleteContents();\n\n                el = doc.createElement('div');\n                el.innerHTML = html;\n                fragment = doc.createDocumentFragment();\n                while (el.firstChild) {\n                    node = el.firstChild;\n                    lastNode = fragment.appendChild(node);\n                }\n                range.insertNode(fragment);\n\n                // Preserve the selection:\n                if (lastNode) {\n                    range = range.cloneRange();\n                    range.setStartAfter(lastNode);\n                    range.collapse(true);\n                    MediumEditor.selection.selectRange(doc, range);\n                }\n                res = true;\n            }\n\n            // https://github.com/yabwe/medium-editor/issues/992\n            // If we're monitoring calls to execCommand, notify listeners as if a real call had happened\n            if (doc.execCommand.callListeners) {\n                doc.execCommand.callListeners(ecArgs, res);\n            }\n            return res;\n        },\n\n        execFormatBlock: function (doc, tagName) {\n            // Get the top level block element that contains the selection\n            var blockContainer = Util.getTopBlockContainer(MediumEditor.selection.getSelectionStart(doc)),\n                childNodes;\n\n            // Special handling for blockquote\n            if (tagName === 'blockquote') {\n                if (blockContainer) {\n                    childNodes = Array.prototype.slice.call(blockContainer.childNodes);\n                    // Check if the blockquote has a block element as a child (nested blocks)\n                    if (childNodes.some(function (childNode) {\n                        return Util.isBlockContainer(childNode);\n                    })) {\n                        // FF handles blockquote differently on formatBlock\n                        // allowing nesting, we need to use outdent\n                        // https://developer.mozilla.org/en-US/docs/Rich-Text_Editing_in_Mozilla\n                        return doc.execCommand('outdent', false, null);\n                    }\n                }\n\n                // When IE blockquote needs to be called as indent\n                // http://stackoverflow.com/questions/1816223/rich-text-editor-with-blockquote-function/1821777#1821777\n                if (Util.isIE) {\n                    return doc.execCommand('indent', false, tagName);\n                }\n            }\n\n            // If the blockContainer is already the element type being passed in\n            // treat it as 'undo' formatting and just convert it to a <p>\n            if (blockContainer && tagName === blockContainer.nodeName.toLowerCase()) {\n                tagName = 'p';\n            }\n\n            // When IE we need to add <> to heading elements\n            // http://stackoverflow.com/questions/10741831/execcommand-formatblock-headings-in-ie\n            if (Util.isIE) {\n                tagName = '<' + tagName + '>';\n            }\n\n            // When FF, IE and Edge, we have to handle blockquote node seperately as 'formatblock' does not work.\n            // https://developer.mozilla.org/en-US/docs/Web/API/Document/execCommand#Commands\n            if (blockContainer && blockContainer.nodeName.toLowerCase() === 'blockquote') {\n                // For IE, just use outdent\n                if (Util.isIE && tagName === '<p>') {\n                    return doc.execCommand('outdent', false, tagName);\n                }\n\n                // For Firefox and Edge, make sure there's a nested block element before calling outdent\n                if ((Util.isFF || Util.isEdge) && tagName === 'p') {\n                    childNodes = Array.prototype.slice.call(blockContainer.childNodes);\n                    // If there are some non-block elements we need to wrap everything in a <p> before we outdent\n                    if (childNodes.some(function (childNode) {\n                        return !Util.isBlockContainer(childNode);\n                    })) {\n                        doc.execCommand('formatBlock', false, tagName);\n                    }\n                    return doc.execCommand('outdent', false, tagName);\n                }\n            }\n\n            return doc.execCommand('formatBlock', false, tagName);\n        },\n\n        /**\n         * Set target to blank on the given el element\n         *\n         * TODO: not sure if this should be here\n         *\n         * When creating a link (using core -> createLink) the selection returned by Firefox will be the parent of the created link\n         * 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\n         * using `anchorUrl` to ensure that we are adding target=\"_blank\" on the good one.\n         * This isn't a bulletproof solution anyway ..\n         */\n        setTargetBlank: function (el, anchorUrl) {\n            var i, url = anchorUrl || false;\n            if (el.nodeName.toLowerCase() === 'a') {\n                el.target = '_blank';\n                el.rel = 'noopener noreferrer';\n            } else {\n                el = el.getElementsByTagName('a');\n\n                for (i = 0; i < el.length; i += 1) {\n                    if (false === url || url === el[i].attributes.href.value) {\n                        el[i].target = '_blank';\n                        el[i].rel = 'noopener noreferrer';\n                    }\n                }\n            }\n        },\n\n        /*\n         * this function is called to explicitly remove the target='_blank' as FF holds on to _blank value even\n         * after unchecking the checkbox on anchor form\n         */\n        removeTargetBlank: function (el, anchorUrl) {\n            var i;\n            if (el.nodeName.toLowerCase() === 'a') {\n                el.removeAttribute('target');\n                el.removeAttribute('rel');\n            } else {\n                el = el.getElementsByTagName('a');\n\n                for (i = 0; i < el.length; i += 1) {\n                    if (anchorUrl === el[i].attributes.href.value) {\n                        el[i].removeAttribute('target');\n                        el[i].removeAttribute('rel');\n                    }\n                }\n            }\n        },\n\n        /*\n         * this function adds one or several classes on an a element.\n         * if el parameter is not an a, it will look for a children of el.\n         * if no a children are found, it will look for the a parent.\n         */\n        addClassToAnchors: function (el, buttonClass) {\n            var classes = buttonClass.split(' '),\n                i,\n                j;\n            if (el.nodeName.toLowerCase() === 'a') {\n                for (j = 0; j < classes.length; j += 1) {\n                    el.classList.add(classes[j]);\n                }\n            } else {\n                var aChildren = el.getElementsByTagName('a');\n                if (aChildren.length === 0) {\n                    var parentAnchor = Util.getClosestTag(el, 'a');\n                    el = parentAnchor ? [parentAnchor] : [];\n                } else {\n                    el = aChildren;\n                }\n                for (i = 0; i < el.length; i += 1) {\n                    for (j = 0; j < classes.length; j += 1) {\n                        el[i].classList.add(classes[j]);\n                    }\n                }\n            }\n        },\n\n        isListItem: function (node) {\n            if (!node) {\n                return false;\n            }\n            if (node.nodeName.toLowerCase() === 'li') {\n                return true;\n            }\n\n            var parentNode = node.parentNode,\n                tagName = parentNode.nodeName.toLowerCase();\n            while (tagName === 'li' || (!Util.isBlockContainer(parentNode) && tagName !== 'div')) {\n                if (tagName === 'li') {\n                    return true;\n                }\n                parentNode = parentNode.parentNode;\n                if (parentNode) {\n                    tagName = parentNode.nodeName.toLowerCase();\n                } else {\n                    return false;\n                }\n            }\n            return false;\n        },\n\n        cleanListDOM: function (ownerDocument, element) {\n            if (element.nodeName.toLowerCase() !== 'li') {\n                return;\n            }\n\n            var list = element.parentElement;\n\n            if (list.parentElement.nodeName.toLowerCase() === 'p') { // yes we need to clean up\n                Util.unwrap(list.parentElement, ownerDocument);\n\n                // move cursor at the end of the text inside the list\n                // for some unknown reason, the cursor is moved to end of the \"visual\" line\n                MediumEditor.selection.moveCursor(ownerDocument, element.firstChild, element.firstChild.textContent.length);\n            }\n        },\n\n        /* splitDOMTree\n         *\n         * Given a root element some descendant element, split the root element\n         * into its own element containing the descendant element and all elements\n         * on the left or right side of the descendant ('right' is default)\n         *\n         * example:\n         *\n         *         <div>\n         *      /    |   \\\n         *  <span> <span> <span>\n         *   / \\    / \\    / \\\n         *  1   2  3   4  5   6\n         *\n         *  If I wanted to split this tree given the <div> as the root and \"4\" as the leaf\n         *  the result would be (the prime ' marks indicates nodes that are created as clones):\n         *\n         *   SPLITTING OFF 'RIGHT' TREE       SPLITTING OFF 'LEFT' TREE\n         *\n         *     <div>            <div>'              <div>'      <div>\n         *      / \\              / \\                 / \\          |\n         * <span> <span>   <span>' <span>       <span> <span>   <span>\n         *   / \\    |        |      / \\           /\\     /\\       /\\\n         *  1   2   3        4     5   6         1  2   3  4     5  6\n         *\n         *  The above example represents splitting off the 'right' or 'left' part of a tree, where\n         *  the <div>' would be returned as an element not appended to the DOM, and the <div>\n         *  would remain in place where it was\n         *\n        */\n        splitOffDOMTree: function (rootNode, leafNode, splitLeft) {\n            var splitOnNode = leafNode,\n                createdNode = null,\n                splitRight = !splitLeft;\n\n            // loop until we hit the root\n            while (splitOnNode !== rootNode) {\n                var currParent = splitOnNode.parentNode,\n                    newParent = currParent.cloneNode(false),\n                    targetNode = (splitRight ? splitOnNode : currParent.firstChild),\n                    appendLast;\n\n                // Create a new parent element which is a clone of the current parent\n                if (createdNode) {\n                    if (splitRight) {\n                        // If we're splitting right, add previous created element before siblings\n                        newParent.appendChild(createdNode);\n                    } else {\n                        // If we're splitting left, add previous created element last\n                        appendLast = createdNode;\n                    }\n                }\n                createdNode = newParent;\n\n                while (targetNode) {\n                    var sibling = targetNode.nextSibling;\n                    // Special handling for the 'splitNode'\n                    if (targetNode === splitOnNode) {\n                        if (!targetNode.hasChildNodes()) {\n                            targetNode.parentNode.removeChild(targetNode);\n                        } else {\n                            // For the node we're splitting on, if it has children, we need to clone it\n                            // and not just move it\n                            targetNode = targetNode.cloneNode(false);\n                        }\n                        // If the resulting split node has content, add it\n                        if (targetNode.textContent) {\n                            createdNode.appendChild(targetNode);\n                        }\n\n                        targetNode = (splitRight ? sibling : null);\n                    } else {\n                        // For general case, just remove the element and only\n                        // add it to the split tree if it contains something\n                        targetNode.parentNode.removeChild(targetNode);\n                        if (targetNode.hasChildNodes() || targetNode.textContent) {\n                            createdNode.appendChild(targetNode);\n                        }\n\n                        targetNode = sibling;\n                    }\n                }\n\n                // If we had an element we wanted to append at the end, do that now\n                if (appendLast) {\n                    createdNode.appendChild(appendLast);\n                }\n\n                splitOnNode = currParent;\n            }\n\n            return createdNode;\n        },\n\n        moveTextRangeIntoElement: function (startNode, endNode, newElement) {\n            if (!startNode || !endNode) {\n                return false;\n            }\n\n            var rootNode = Util.findCommonRoot(startNode, endNode);\n            if (!rootNode) {\n                return false;\n            }\n\n            if (endNode === startNode) {\n                var temp = startNode.parentNode,\n                    sibling = startNode.nextSibling;\n                temp.removeChild(startNode);\n                newElement.appendChild(startNode);\n                if (sibling) {\n                    temp.insertBefore(newElement, sibling);\n                } else {\n                    temp.appendChild(newElement);\n                }\n                return newElement.hasChildNodes();\n            }\n\n            // create rootChildren array which includes all the children\n            // we care about\n            var rootChildren = [],\n                firstChild,\n                lastChild,\n                nextNode;\n            for (var i = 0; i < rootNode.childNodes.length; i++) {\n                nextNode = rootNode.childNodes[i];\n                if (!firstChild) {\n                    if (Util.isDescendant(nextNode, startNode, true)) {\n                        firstChild = nextNode;\n                    }\n                } else {\n                    if (Util.isDescendant(nextNode, endNode, true)) {\n                        lastChild = nextNode;\n                        break;\n                    } else {\n                        rootChildren.push(nextNode);\n                    }\n                }\n            }\n\n            var afterLast = lastChild.nextSibling,\n                fragment = rootNode.ownerDocument.createDocumentFragment();\n\n            // build up fragment on startNode side of tree\n            if (firstChild === startNode) {\n                firstChild.parentNode.removeChild(firstChild);\n                fragment.appendChild(firstChild);\n            } else {\n                fragment.appendChild(Util.splitOffDOMTree(firstChild, startNode));\n            }\n\n            // add any elements between firstChild & lastChild\n            rootChildren.forEach(function (element) {\n                element.parentNode.removeChild(element);\n                fragment.appendChild(element);\n            });\n\n            // build up fragment on endNode side of the tree\n            if (lastChild === endNode) {\n                lastChild.parentNode.removeChild(lastChild);\n                fragment.appendChild(lastChild);\n            } else {\n                fragment.appendChild(Util.splitOffDOMTree(lastChild, endNode, true));\n            }\n\n            // Add fragment into passed in element\n            newElement.appendChild(fragment);\n\n            if (lastChild.parentNode === rootNode) {\n                // If last child is in the root, insert newElement in front of it\n                rootNode.insertBefore(newElement, lastChild);\n            } else if (afterLast) {\n                // If last child was removed, but it had a sibling, insert in front of it\n                rootNode.insertBefore(newElement, afterLast);\n            } else {\n                // lastChild was removed and was the last actual element just append\n                rootNode.appendChild(newElement);\n            }\n\n            return newElement.hasChildNodes();\n        },\n\n        /* based on http://stackoverflow.com/a/6183069 */\n        depthOfNode: function (inNode) {\n            var theDepth = 0,\n                node = inNode;\n            while (node.parentNode !== null) {\n                node = node.parentNode;\n                theDepth++;\n            }\n            return theDepth;\n        },\n\n        findCommonRoot: function (inNode1, inNode2) {\n            var depth1 = Util.depthOfNode(inNode1),\n                depth2 = Util.depthOfNode(inNode2),\n                node1 = inNode1,\n                node2 = inNode2;\n\n            while (depth1 !== depth2) {\n                if (depth1 > depth2) {\n                    node1 = node1.parentNode;\n                    depth1 -= 1;\n                } else {\n                    node2 = node2.parentNode;\n                    depth2 -= 1;\n                }\n            }\n\n            while (node1 !== node2) {\n                node1 = node1.parentNode;\n                node2 = node2.parentNode;\n            }\n\n            return node1;\n        },\n        /* END - based on http://stackoverflow.com/a/6183069 */\n\n        isElementAtBeginningOfBlock: function (node) {\n            var textVal,\n                sibling;\n            while (!Util.isBlockContainer(node) && !Util.isMediumEditorElement(node)) {\n                sibling = node;\n                while (sibling = sibling.previousSibling) {\n                    textVal = sibling.nodeType === 3 ? sibling.nodeValue : sibling.textContent;\n                    if (textVal.length > 0) {\n                        return false;\n                    }\n                }\n                node = node.parentNode;\n            }\n            return true;\n        },\n\n        isMediumEditorElement: function (element) {\n            return element && element.getAttribute && !!element.getAttribute('data-medium-editor-element');\n        },\n\n        getContainerEditorElement: function (element) {\n            return Util.traverseUp(element, function (node) {\n                return Util.isMediumEditorElement(node);\n            });\n        },\n\n        isBlockContainer: function (element) {\n            return element && element.nodeType !== 3 && Util.blockContainerElementNames.indexOf(element.nodeName.toLowerCase()) !== -1;\n        },\n\n        /* Finds the closest ancestor which is a block container element\n         * If element is within editor element but not within any other block element,\n         * the editor element is returned\n         */\n        getClosestBlockContainer: function (node) {\n            return Util.traverseUp(node, function (node) {\n                return Util.isBlockContainer(node) || Util.isMediumEditorElement(node);\n            });\n        },\n\n        /* Finds highest level ancestor element which is a block container element\n         * If element is within editor element but not within any other block element,\n         * the editor element is returned\n         */\n        getTopBlockContainer: function (element) {\n            var topBlock = Util.isBlockContainer(element) ? element : false;\n            Util.traverseUp(element, function (el) {\n                if (Util.isBlockContainer(el)) {\n                    topBlock = el;\n                }\n                if (!topBlock && Util.isMediumEditorElement(el)) {\n                    topBlock = el;\n                    return true;\n                }\n                return false;\n            });\n            return topBlock;\n        },\n\n        getFirstSelectableLeafNode: function (element) {\n            while (element && element.firstChild) {\n                element = element.firstChild;\n            }\n\n            // We don't want to set the selection to an element that can't have children, this messes up Gecko.\n            element = Util.traverseUp(element, function (el) {\n                return Util.emptyElementNames.indexOf(el.nodeName.toLowerCase()) === -1;\n            });\n            // Selecting at the beginning of a table doesn't work in PhantomJS.\n            if (element.nodeName.toLowerCase() === 'table') {\n                var firstCell = element.querySelector('th, td');\n                if (firstCell) {\n                    element = firstCell;\n                }\n            }\n            return element;\n        },\n\n        // TODO: remove getFirstTextNode AND _getFirstTextNode when jumping in 6.0.0 (no code references)\n        getFirstTextNode: function (element) {\n            Util.warn('getFirstTextNode is deprecated and will be removed in version 6.0.0');\n            return Util._getFirstTextNode(element);\n        },\n\n        _getFirstTextNode: function (element) {\n            if (element.nodeType === 3) {\n                return element;\n            }\n\n            for (var i = 0; i < element.childNodes.length; i++) {\n                var textNode = Util._getFirstTextNode(element.childNodes[i]);\n                if (textNode !== null) {\n                    return textNode;\n                }\n            }\n            return null;\n        },\n\n        ensureUrlHasProtocol: function (url) {\n            if (url.indexOf('://') === -1) {\n                return 'http://' + url;\n            }\n            return url;\n        },\n\n        warn: function () {\n            if (window.console !== undefined && typeof window.console.warn === 'function') {\n                window.console.warn.apply(window.console, arguments);\n            }\n        },\n\n        deprecated: function (oldName, newName, version) {\n            // simple deprecation warning mechanism.\n            var m = oldName + ' is deprecated, please use ' + newName + ' instead.';\n            if (version) {\n                m += ' Will be removed in ' + version;\n            }\n            Util.warn(m);\n        },\n\n        deprecatedMethod: function (oldName, newName, args, version) {\n            // run the replacement and warn when someone calls a deprecated method\n            Util.deprecated(oldName, newName, version);\n            if (typeof this[newName] === 'function') {\n                this[newName].apply(this, args);\n            }\n        },\n\n        cleanupAttrs: function (el, attrs) {\n            attrs.forEach(function (attr) {\n                el.removeAttribute(attr);\n            });\n        },\n\n        cleanupTags: function (el, tags) {\n            if (tags.indexOf(el.nodeName.toLowerCase()) !== -1) {\n                el.parentNode.removeChild(el);\n            }\n        },\n\n        unwrapTags: function (el, tags) {\n            if (tags.indexOf(el.nodeName.toLowerCase()) !== -1) {\n                MediumEditor.util.unwrap(el, document);\n            }\n        },\n\n        // get the closest parent\n        getClosestTag: function (el, tag) {\n            return Util.traverseUp(el, function (element) {\n                return element.nodeName.toLowerCase() === tag.toLowerCase();\n            });\n        },\n\n        unwrap: function (el, doc) {\n            var fragment = doc.createDocumentFragment(),\n                nodes = Array.prototype.slice.call(el.childNodes);\n\n            // cast nodeList to array since appending child\n            // to a different node will alter length of el.childNodes\n            for (var i = 0; i < nodes.length; i++) {\n                fragment.appendChild(nodes[i]);\n            }\n\n            if (fragment.childNodes.length) {\n                el.parentNode.replaceChild(fragment, el);\n            } else {\n                el.parentNode.removeChild(el);\n            }\n        },\n\n        guid: function () {\n            function _s4() {\n                return Math\n                    .floor((1 + Math.random()) * 0x10000)\n                    .toString(16)\n                    .substring(1);\n            }\n\n            return _s4() + _s4() + '-' + _s4() + '-' + _s4() + '-' + _s4() + '-' + _s4() + _s4() + _s4();\n        }\n    };\n\n    MediumEditor.util = Util;\n}(window));\n\n(function () {\n    'use strict';\n\n    var Extension = function (options) {\n        MediumEditor.util.extend(this, options);\n    };\n\n    Extension.extend = function (protoProps) {\n        // magic extender thinger. mostly borrowed from backbone/goog.inherits\n        // place this function on some thing you want extend-able.\n        //\n        // example:\n        //\n        //      function Thing(args){\n        //          this.options = args;\n        //      }\n        //\n        //      Thing.prototype = { foo: \"bar\" };\n        //      Thing.extend = extenderify;\n        //\n        //      var ThingTwo = Thing.extend({ foo: \"baz\" });\n        //\n        //      var thingOne = new Thing(); // foo === \"bar\"\n        //      var thingTwo = new ThingTwo(); // foo === \"baz\"\n        //\n        //      which seems like some simply shallow copy nonsense\n        //      at first, but a lot more is going on there.\n        //\n        //      passing a `constructor` to the extend props\n        //      will cause the instance to instantiate through that\n        //      instead of the parent's constructor.\n\n        var parent = this,\n            child;\n\n        // The constructor function for the new subclass is either defined by you\n        // (the \"constructor\" property in your `extend` definition), or defaulted\n        // by us to simply call the parent's constructor.\n\n        if (protoProps && protoProps.hasOwnProperty('constructor')) {\n            child = protoProps.constructor;\n        } else {\n            child = function () {\n                return parent.apply(this, arguments);\n            };\n        }\n\n        // das statics (.extend comes over, so your subclass can have subclasses too)\n        MediumEditor.util.extend(child, parent);\n\n        // Set the prototype chain to inherit from `parent`, without calling\n        // `parent`'s constructor function.\n        var Surrogate = function () {\n            this.constructor = child;\n        };\n        Surrogate.prototype = parent.prototype;\n        child.prototype = new Surrogate();\n\n        if (protoProps) {\n            MediumEditor.util.extend(child.prototype, protoProps);\n        }\n\n        // todo: $super?\n\n        return child;\n    };\n\n    Extension.prototype = {\n        /* init: [function]\n         *\n         * Called by MediumEditor during initialization.\n         * The .base property will already have been set to\n         * current instance of MediumEditor when this is called.\n         * All helper methods will exist as well\n         */\n        init: function () {},\n\n        /* base: [MediumEditor instance]\n         *\n         * If not overriden, this will be set to the current instance\n         * of MediumEditor, before the init method is called\n         */\n        base: undefined,\n\n        /* name: [string]\n         *\n         * 'name' of the extension, used for retrieving the extension.\n         * If not set, MediumEditor will set this to be the key\n         * used when passing the extension into MediumEditor via the\n         * 'extensions' option\n         */\n        name: undefined,\n\n        /* checkState: [function (node)]\n         *\n         * If implemented, this function will be called one or more times\n         * the state of the editor & toolbar are updated.\n         * When the state is updated, the editor does the following:\n         *\n         * 1) Find the parent node containing the current selection\n         * 2) Call checkState on the extension, passing the node as an argument\n         * 3) Get the parent node of the previous node\n         * 4) Repeat steps #2 and #3 until we move outside the parent contenteditable\n         */\n        checkState: undefined,\n\n        /* destroy: [function ()]\n         *\n         * This method should remove any created html, custom event handlers\n         * or any other cleanup tasks that should be performed.\n         * If implemented, this function will be called when MediumEditor's\n         * destroy method has been called.\n         */\n        destroy: undefined,\n\n        /* As alternatives to checkState, these functions provide a more structured\n         * path to updating the state of an extension (usually a button) whenever\n         * the state of the editor & toolbar are updated.\n         */\n\n        /* queryCommandState: [function ()]\n         *\n         * If implemented, this function will be called once on each extension\n         * when the state of the editor/toolbar is being updated.\n         *\n         * If this function returns a non-null value, the extension will\n         * be ignored as the code climbs the dom tree.\n         *\n         * If this function returns true, and the setActive() function is defined\n         * setActive() will be called\n         */\n        queryCommandState: undefined,\n\n        /* isActive: [function ()]\n         *\n         * If implemented, this function will be called when MediumEditor\n         * has determined that this extension is 'active' for the current selection.\n         * This may be called when the editor & toolbar are being updated,\n         * but only if queryCommandState() or isAlreadyApplied() functions\n         * are implemented, and when called, return true.\n         */\n        isActive: undefined,\n\n        /* isAlreadyApplied: [function (node)]\n         *\n         * If implemented, this function is similar to checkState() in\n         * that it will be called repeatedly as MediumEditor moves up\n         * the DOM to update the editor & toolbar after a state change.\n         *\n         * NOTE: This function will NOT be called if checkState() has\n         * been implemented. This function will NOT be called if\n         * queryCommandState() is implemented and returns a non-null\n         * value when called\n         */\n        isAlreadyApplied: undefined,\n\n        /* setActive: [function ()]\n         *\n         * If implemented, this function is called when MediumEditor knows\n         * that this extension is currently enabled.  Currently, this\n         * function is called when updating the editor & toolbar, and\n         * only if queryCommandState() or isAlreadyApplied(node) return\n         * true when called\n         */\n        setActive: undefined,\n\n        /* setInactive: [function ()]\n         *\n         * If implemented, this function is called when MediumEditor knows\n         * that this extension is currently disabled.  Curently, this\n         * is called at the beginning of each state change for\n         * the editor & toolbar. After calling this, MediumEditor\n         * will attempt to update the extension, either via checkState()\n         * or the combination of queryCommandState(), isAlreadyApplied(node),\n         * isActive(), and setActive()\n         */\n        setInactive: undefined,\n\n        /* getInteractionElements: [function ()]\n         *\n         * If the extension renders any elements that the user can interact with,\n         * this method should be implemented and return the root element or an array\n         * containing all of the root elements. MediumEditor will call this function\n         * during interaction to see if the user clicked on something outside of the editor.\n         * The elements are used to check if the target element of a click or\n         * other user event is a descendant of any extension elements.\n         * This way, the editor can also count user interaction within editor elements as\n         * interactions with the editor, and thus not trigger 'blur'\n         */\n        getInteractionElements: undefined,\n\n        /************************ Helpers ************************\n         * The following are helpers that are either set by MediumEditor\n         * during initialization, or are helper methods which either\n         * route calls to the MediumEditor instance or provide common\n         * functionality for all extensions\n         *********************************************************/\n\n        /* window: [Window]\n         *\n         * If not overriden, this will be set to the window object\n         * to be used by MediumEditor and its extensions.  This is\n         * passed via the 'contentWindow' option to MediumEditor\n         * and is the global 'window' object by default\n         */\n        'window': undefined,\n\n        /* document: [Document]\n         *\n         * If not overriden, this will be set to the document object\n         * to be used by MediumEditor and its extensions. This is\n         * passed via the 'ownerDocument' optin to MediumEditor\n         * and is the global 'document' object by default\n         */\n        'document': undefined,\n\n        /* getEditorElements: [function ()]\n         *\n         * Helper function which returns an array containing\n         * all the contenteditable elements for this instance\n         * of MediumEditor\n         */\n        getEditorElements: function () {\n            return this.base.elements;\n        },\n\n        /* getEditorId: [function ()]\n         *\n         * Helper function which returns a unique identifier\n         * for this instance of MediumEditor\n         */\n        getEditorId: function () {\n            return this.base.id;\n        },\n\n        /* getEditorOptions: [function (option)]\n         *\n         * Helper function which returns the value of an option\n         * used to initialize this instance of MediumEditor\n         */\n        getEditorOption: function (option) {\n            return this.base.options[option];\n        }\n    };\n\n    /* List of method names to add to the prototype of Extension\n     * Each of these methods will be defined as helpers that\n     * just call directly into the MediumEditor instance.\n     *\n     * example for 'on' method:\n     * Extension.prototype.on = function () {\n     *     return this.base.on.apply(this.base, arguments);\n     * }\n     */\n    [\n        // general helpers\n        'execAction',\n\n        // event handling\n        'on',\n        'off',\n        'subscribe',\n        'trigger'\n\n    ].forEach(function (helper) {\n        Extension.prototype[helper] = function () {\n            return this.base[helper].apply(this.base, arguments);\n        };\n    });\n\n    MediumEditor.Extension = Extension;\n})();\n\n(function () {\n    'use strict';\n\n    function filterOnlyParentElements(node) {\n        if (MediumEditor.util.isBlockContainer(node)) {\n            return NodeFilter.FILTER_ACCEPT;\n        } else {\n            return NodeFilter.FILTER_SKIP;\n        }\n    }\n\n    var Selection = {\n        findMatchingSelectionParent: function (testElementFunction, contentWindow) {\n            var selection = contentWindow.getSelection(),\n                range,\n                current;\n\n            if (selection.rangeCount === 0) {\n                return false;\n            }\n\n            range = selection.getRangeAt(0);\n            current = range.commonAncestorContainer;\n\n            return MediumEditor.util.traverseUp(current, testElementFunction);\n        },\n\n        getSelectionElement: function (contentWindow) {\n            return this.findMatchingSelectionParent(function (el) {\n                return MediumEditor.util.isMediumEditorElement(el);\n            }, contentWindow);\n        },\n\n        // http://stackoverflow.com/questions/17678843/cant-restore-selection-after-html-modify-even-if-its-the-same-html\n        // Tim Down\n        exportSelection: function (root, doc) {\n            if (!root) {\n                return null;\n            }\n\n            var selectionState = null,\n                selection = doc.getSelection();\n\n            if (selection.rangeCount > 0) {\n                var range = selection.getRangeAt(0),\n                    preSelectionRange = range.cloneRange(),\n                    start;\n\n                preSelectionRange.selectNodeContents(root);\n                preSelectionRange.setEnd(range.startContainer, range.startOffset);\n                start = preSelectionRange.toString().length;\n\n                selectionState = {\n                    start: start,\n                    end: start + range.toString().length\n                };\n\n                // Check to see if the selection starts with any images\n                // if so we need to make sure the the beginning of the selection is\n                // set correctly when importing selection\n                if (this.doesRangeStartWithImages(range, doc)) {\n                    selectionState.startsWithImage = true;\n                }\n\n                // Check to see if the selection has any trailing images\n                // if so, this this means we need to look for them when we import selection\n                var trailingImageCount = this.getTrailingImageCount(root, selectionState, range.endContainer, range.endOffset);\n                if (trailingImageCount) {\n                    selectionState.trailingImageCount = trailingImageCount;\n                }\n\n                // If start = 0 there may still be an empty paragraph before it, but we don't care.\n                if (start !== 0) {\n                    var emptyBlocksIndex = this.getIndexRelativeToAdjacentEmptyBlocks(doc, root, range.startContainer, range.startOffset);\n                    if (emptyBlocksIndex !== -1) {\n                        selectionState.emptyBlocksIndex = emptyBlocksIndex;\n                    }\n                }\n            }\n\n            return selectionState;\n        },\n\n        // http://stackoverflow.com/questions/17678843/cant-restore-selection-after-html-modify-even-if-its-the-same-html\n        // Tim Down\n        //\n        // {object} selectionState - the selection to import\n        // {DOMElement} root - the root element the selection is being restored inside of\n        // {Document} doc - the document to use for managing selection\n        // {boolean} [favorLaterSelectionAnchor] - defaults to false. If true, import the cursor immediately\n        //      subsequent to an anchor tag if it would otherwise be placed right at the trailing edge inside the\n        //      anchor. This cursor positioning, even though visually equivalent to the user, can affect behavior\n        //      in MS IE.\n        importSelection: function (selectionState, root, doc, favorLaterSelectionAnchor) {\n            if (!selectionState || !root) {\n                return;\n            }\n\n            var range = doc.createRange();\n            range.setStart(root, 0);\n            range.collapse(true);\n\n            var node = root,\n                nodeStack = [],\n                charIndex = 0,\n                foundStart = false,\n                foundEnd = false,\n                trailingImageCount = 0,\n                stop = false,\n                nextCharIndex,\n                allowRangeToStartAtEndOfNode = false,\n                lastTextNode = null;\n\n            // When importing selection, the start of the selection may lie at the end of an element\n            // or at the beginning of an element.  Since visually there is no difference between these 2\n            // we will try to move the selection to the beginning of an element since this is generally\n            // what users will expect and it's a more predictable behavior.\n            //\n            // However, there are some specific cases when we don't want to do this:\n            //  1) We're attempting to move the cursor outside of the end of an anchor [favorLaterSelectionAnchor = true]\n            //  2) The selection starts with an image, which is special since an image doesn't have any 'content'\n            //     as far as selection and ranges are concerned\n            //  3) The selection starts after a specified number of empty block elements (selectionState.emptyBlocksIndex)\n            //\n            // For these cases, we want the selection to start at a very specific location, so we should NOT\n            // automatically move the cursor to the beginning of the first actual chunk of text\n            if (favorLaterSelectionAnchor || selectionState.startsWithImage || typeof selectionState.emptyBlocksIndex !== 'undefined') {\n                allowRangeToStartAtEndOfNode = true;\n            }\n\n            while (!stop && node) {\n                // Only iterate over elements and text nodes\n                if (node.nodeType > 3) {\n                    node = nodeStack.pop();\n                    continue;\n                }\n\n                // If we hit a text node, we need to add the amount of characters to the overall count\n                if (node.nodeType === 3 && !foundEnd) {\n                    nextCharIndex = charIndex + node.length;\n                    // Check if we're at or beyond the start of the selection we're importing\n                    if (!foundStart && selectionState.start >= charIndex && selectionState.start <= nextCharIndex) {\n                        // NOTE: We only want to allow a selection to start at the END of an element if\n                        //  allowRangeToStartAtEndOfNode is true\n                        if (allowRangeToStartAtEndOfNode || selectionState.start < nextCharIndex) {\n                            range.setStart(node, selectionState.start - charIndex);\n                            foundStart = true;\n                        }\n                        // We're at the end of a text node where the selection could start but we shouldn't\n                        // make the selection start here because allowRangeToStartAtEndOfNode is false.\n                        // However, we should keep a reference to this node in case there aren't any more\n                        // text nodes after this, so that we have somewhere to import the selection to\n                        else {\n                            lastTextNode = node;\n                        }\n                    }\n                    // We've found the start of the selection, check if we're at or beyond the end of the selection we're importing\n                    if (foundStart && selectionState.end >= charIndex && selectionState.end <= nextCharIndex) {\n                        if (!selectionState.trailingImageCount) {\n                            range.setEnd(node, selectionState.end - charIndex);\n                            stop = true;\n                        } else {\n                            foundEnd = true;\n                        }\n                    }\n                    charIndex = nextCharIndex;\n                } else {\n                    if (selectionState.trailingImageCount && foundEnd) {\n                        if (node.nodeName.toLowerCase() === 'img') {\n                            trailingImageCount++;\n                        }\n                        if (trailingImageCount === selectionState.trailingImageCount) {\n                            // Find which index the image is in its parent's children\n                            var endIndex = 0;\n                            while (node.parentNode.childNodes[endIndex] !== node) {\n                                endIndex++;\n                            }\n                            range.setEnd(node.parentNode, endIndex + 1);\n                            stop = true;\n                        }\n                    }\n\n                    if (!stop && node.nodeType === 1) {\n                        // this is an element\n                        // add all its children to the stack\n                        var i = node.childNodes.length - 1;\n                        while (i >= 0) {\n                            nodeStack.push(node.childNodes[i]);\n                            i -= 1;\n                        }\n                    }\n                }\n\n                if (!stop) {\n                    node = nodeStack.pop();\n                }\n            }\n\n            // If we've gone through the entire text but didn't find the beginning of a text node\n            // to make the selection start at, we should fall back to starting the selection\n            // at the END of the last text node we found\n            if (!foundStart && lastTextNode) {\n                range.setStart(lastTextNode, lastTextNode.length);\n                range.setEnd(lastTextNode, lastTextNode.length);\n            }\n\n            if (typeof selectionState.emptyBlocksIndex !== 'undefined') {\n                range = this.importSelectionMoveCursorPastBlocks(doc, root, selectionState.emptyBlocksIndex, range);\n            }\n\n            // If the selection is right at the ending edge of a link, put it outside the anchor tag instead of inside.\n            if (favorLaterSelectionAnchor) {\n                range = this.importSelectionMoveCursorPastAnchor(selectionState, range);\n            }\n\n            this.selectRange(doc, range);\n        },\n\n        // Utility method called from importSelection only\n        importSelectionMoveCursorPastAnchor: function (selectionState, range) {\n            var nodeInsideAnchorTagFunction = function (node) {\n                return node.nodeName.toLowerCase() === 'a';\n            };\n            if (selectionState.start === selectionState.end &&\n                    range.startContainer.nodeType === 3 &&\n                    range.startOffset === range.startContainer.nodeValue.length &&\n                    MediumEditor.util.traverseUp(range.startContainer, nodeInsideAnchorTagFunction)) {\n                var prevNode = range.startContainer,\n                    currentNode = range.startContainer.parentNode;\n                while (currentNode !== null && currentNode.nodeName.toLowerCase() !== 'a') {\n                    if (currentNode.childNodes[currentNode.childNodes.length - 1] !== prevNode) {\n                        currentNode = null;\n                    } else {\n                        prevNode = currentNode;\n                        currentNode = currentNode.parentNode;\n                    }\n                }\n                if (currentNode !== null && currentNode.nodeName.toLowerCase() === 'a') {\n                    var currentNodeIndex = null;\n                    for (var i = 0; currentNodeIndex === null && i < currentNode.parentNode.childNodes.length; i++) {\n                        if (currentNode.parentNode.childNodes[i] === currentNode) {\n                            currentNodeIndex = i;\n                        }\n                    }\n                    range.setStart(currentNode.parentNode, currentNodeIndex + 1);\n                    range.collapse(true);\n                }\n            }\n            return range;\n        },\n\n        // Uses the emptyBlocksIndex calculated by getIndexRelativeToAdjacentEmptyBlocks\n        // to move the cursor back to the start of the correct paragraph\n        importSelectionMoveCursorPastBlocks: function (doc, root, index, range) {\n            var treeWalker = doc.createTreeWalker(root, NodeFilter.SHOW_ELEMENT, filterOnlyParentElements, false),\n                startContainer = range.startContainer,\n                startBlock,\n                targetNode,\n                currIndex = 0;\n            index = index || 1; // If index is 0, we still want to move to the next block\n\n            // Chrome counts newlines and spaces that separate block elements as actual elements.\n            // If the selection is inside one of these text nodes, and it has a previous sibling\n            // which is a block element, we want the treewalker to start at the previous sibling\n            // and NOT at the parent of the textnode\n            if (startContainer.nodeType === 3 && MediumEditor.util.isBlockContainer(startContainer.previousSibling)) {\n                startBlock = startContainer.previousSibling;\n            } else {\n                startBlock = MediumEditor.util.getClosestBlockContainer(startContainer);\n            }\n\n            // Skip over empty blocks until we hit the block we want the selection to be in\n            while (treeWalker.nextNode()) {\n                if (!targetNode) {\n                    // Loop through all blocks until we hit the starting block element\n                    if (startBlock === treeWalker.currentNode) {\n                        targetNode = treeWalker.currentNode;\n                    }\n                } else {\n                    targetNode = treeWalker.currentNode;\n                    currIndex++;\n                    // We hit the target index, bail\n                    if (currIndex === index) {\n                        break;\n                    }\n                    // If we find a non-empty block, ignore the emptyBlocksIndex and just put selection here\n                    if (targetNode.textContent.length > 0) {\n                        break;\n                    }\n                }\n            }\n\n            if (!targetNode) {\n                targetNode = startBlock;\n            }\n\n            // We're selecting a high-level block node, so make sure the cursor gets moved into the deepest\n            // element at the beginning of the block\n            range.setStart(MediumEditor.util.getFirstSelectableLeafNode(targetNode), 0);\n\n            return range;\n        },\n\n        // Returns -1 unless the cursor is at the beginning of a paragraph/block\n        // If the paragraph/block is preceeded by empty paragraphs/block (with no text)\n        // it will return the number of empty paragraphs before the cursor.\n        // Otherwise, it will return 0, which indicates the cursor is at the beginning\n        // of a paragraph/block, and not at the end of the paragraph/block before it\n        getIndexRelativeToAdjacentEmptyBlocks: function (doc, root, cursorContainer, cursorOffset) {\n            // If there is text in front of the cursor, that means there isn't only empty blocks before it\n            if (cursorContainer.textContent.length > 0 && cursorOffset > 0) {\n                return -1;\n            }\n\n            // Check if the block that contains the cursor has any other text in front of the cursor\n            var node = cursorContainer;\n            if (node.nodeType !== 3) {\n                node = cursorContainer.childNodes[cursorOffset];\n            }\n            if (node) {\n                // The element isn't at the beginning of a block, so it has content before it\n                if (!MediumEditor.util.isElementAtBeginningOfBlock(node)) {\n                    return -1;\n                }\n\n                var previousSibling = MediumEditor.util.findPreviousSibling(node);\n                // If there is no previous sibling, this is the first text element in the editor\n                if (!previousSibling) {\n                    return -1;\n                }\n                // If the previous sibling has text, then there are no empty blocks before this\n                else if (previousSibling.nodeValue) {\n                    return -1;\n                }\n            }\n\n            // Walk over block elements, counting number of empty blocks between last piece of text\n            // and the block the cursor is in\n            var closestBlock = MediumEditor.util.getClosestBlockContainer(cursorContainer),\n                treeWalker = doc.createTreeWalker(root, NodeFilter.SHOW_ELEMENT, filterOnlyParentElements, false),\n                emptyBlocksCount = 0;\n            while (treeWalker.nextNode()) {\n                var blockIsEmpty = treeWalker.currentNode.textContent === '';\n                if (blockIsEmpty || emptyBlocksCount > 0) {\n                    emptyBlocksCount += 1;\n                }\n                if (treeWalker.currentNode === closestBlock) {\n                    return emptyBlocksCount;\n                }\n                if (!blockIsEmpty) {\n                    emptyBlocksCount = 0;\n                }\n            }\n\n            return emptyBlocksCount;\n        },\n\n        // Returns true if the selection range begins with an image tag\n        // Returns false if the range starts with any non empty text nodes\n        doesRangeStartWithImages: function (range, doc) {\n            if (range.startOffset !== 0 || range.startContainer.nodeType !== 1) {\n                return false;\n            }\n\n            if (range.startContainer.nodeName.toLowerCase() === 'img') {\n                return true;\n            }\n\n            var img = range.startContainer.querySelector('img');\n            if (!img) {\n                return false;\n            }\n\n            var treeWalker = doc.createTreeWalker(range.startContainer, NodeFilter.SHOW_ALL, null, false);\n            while (treeWalker.nextNode()) {\n                var next = treeWalker.currentNode;\n                // If we hit the image, then there isn't any text before the image so\n                // the image is at the beginning of the range\n                if (next === img) {\n                    break;\n                }\n                // If we haven't hit the iamge, but found text that contains content\n                // then the range doesn't start with an image\n                if (next.nodeValue) {\n                    return false;\n                }\n            }\n\n            return true;\n        },\n\n        getTrailingImageCount: function (root, selectionState, endContainer, endOffset) {\n            // If the endOffset of a range is 0, the endContainer doesn't contain images\n            // If the endContainer is a text node, there are no trailing images\n            if (endOffset === 0 || endContainer.nodeType !== 1) {\n                return 0;\n            }\n\n            // If the endContainer isn't an image, and doesn't have an image descendants\n            // there are no trailing images\n            if (endContainer.nodeName.toLowerCase() !== 'img' && !endContainer.querySelector('img')) {\n                return 0;\n            }\n\n            var lastNode = endContainer.childNodes[endOffset - 1];\n            while (lastNode.hasChildNodes()) {\n                lastNode = lastNode.lastChild;\n            }\n\n            var node = root,\n                nodeStack = [],\n                charIndex = 0,\n                foundStart = false,\n                foundEnd = false,\n                stop = false,\n                nextCharIndex,\n                trailingImages = 0;\n\n            while (!stop && node) {\n                // Only iterate over elements and text nodes\n                if (node.nodeType > 3) {\n                    node = nodeStack.pop();\n                    continue;\n                }\n\n                if (node.nodeType === 3 && !foundEnd) {\n                    trailingImages = 0;\n                    nextCharIndex = charIndex + node.length;\n                    if (!foundStart && selectionState.start >= charIndex && selectionState.start <= nextCharIndex) {\n                        foundStart = true;\n                    }\n                    if (foundStart && selectionState.end >= charIndex && selectionState.end <= nextCharIndex) {\n                        foundEnd = true;\n                    }\n                    charIndex = nextCharIndex;\n                } else {\n                    if (node.nodeName.toLowerCase() === 'img') {\n                        trailingImages++;\n                    }\n\n                    if (node === lastNode) {\n                        stop = true;\n                    } else if (node.nodeType === 1) {\n                        // this is an element\n                        // add all its children to the stack\n                        var i = node.childNodes.length - 1;\n                        while (i >= 0) {\n                            nodeStack.push(node.childNodes[i]);\n                            i -= 1;\n                        }\n                    }\n                }\n\n                if (!stop) {\n                    node = nodeStack.pop();\n                }\n            }\n\n            return trailingImages;\n        },\n\n        // determine if the current selection contains any 'content'\n        // content being any non-white space text or an image\n        selectionContainsContent: function (doc) {\n            var sel = doc.getSelection();\n\n            // collapsed selection or selection withour range doesn't contain content\n            if (!sel || sel.isCollapsed || !sel.rangeCount) {\n                return false;\n            }\n\n            // if toString() contains any text, the selection contains some content\n            if (sel.toString().trim() !== '') {\n                return true;\n            }\n\n            // if selection contains only image(s), it will return empty for toString()\n            // so check for an image manually\n            var selectionNode = this.getSelectedParentElement(sel.getRangeAt(0));\n            if (selectionNode) {\n                if (selectionNode.nodeName.toLowerCase() === 'img' ||\n                    (selectionNode.nodeType === 1 && selectionNode.querySelector('img'))) {\n                    return true;\n                }\n            }\n\n            return false;\n        },\n\n        selectionInContentEditableFalse: function (contentWindow) {\n            // determine if the current selection is exclusively inside\n            // a contenteditable=\"false\", though treat the case of an\n            // explicit contenteditable=\"true\" inside a \"false\" as false.\n            var sawtrue,\n                sawfalse = this.findMatchingSelectionParent(function (el) {\n                    var ce = el && el.getAttribute('contenteditable');\n                    if (ce === 'true') {\n                        sawtrue = true;\n                    }\n                    return el.nodeName !== '#text' && ce === 'false';\n                }, contentWindow);\n\n            return !sawtrue && sawfalse;\n        },\n\n        // http://stackoverflow.com/questions/4176923/html-of-selected-text\n        // by Tim Down\n        getSelectionHtml: function getSelectionHtml(doc) {\n            var i,\n                html = '',\n                sel = doc.getSelection(),\n                len,\n                container;\n            if (sel.rangeCount) {\n                container = doc.createElement('div');\n                for (i = 0, len = sel.rangeCount; i < len; i += 1) {\n                    container.appendChild(sel.getRangeAt(i).cloneContents());\n                }\n                html = container.innerHTML;\n            }\n            return html;\n        },\n\n        /**\n         *  Find the caret position within an element irrespective of any inline tags it may contain.\n         *\n         *  @param {DOMElement} An element containing the cursor to find offsets relative to.\n         *  @param {Range} A Range representing cursor position. Will window.getSelection if none is passed.\n         *  @return {Object} 'left' and 'right' attributes contain offsets from begining and end of Element\n         */\n        getCaretOffsets: function getCaretOffsets(element, range) {\n            var preCaretRange, postCaretRange;\n\n            if (!range) {\n                range = window.getSelection().getRangeAt(0);\n            }\n\n            preCaretRange = range.cloneRange();\n            postCaretRange = range.cloneRange();\n\n            preCaretRange.selectNodeContents(element);\n            preCaretRange.setEnd(range.endContainer, range.endOffset);\n\n            postCaretRange.selectNodeContents(element);\n            postCaretRange.setStart(range.endContainer, range.endOffset);\n\n            return {\n                left: preCaretRange.toString().length,\n                right: postCaretRange.toString().length\n            };\n        },\n\n        // http://stackoverflow.com/questions/15867542/range-object-get-selection-parent-node-chrome-vs-firefox\n        rangeSelectsSingleNode: function (range) {\n            var startNode = range.startContainer;\n            return startNode === range.endContainer &&\n                startNode.hasChildNodes() &&\n                range.endOffset === range.startOffset + 1;\n        },\n\n        getSelectedParentElement: function (range) {\n            if (!range) {\n                return null;\n            }\n\n            // Selection encompasses a single element\n            if (this.rangeSelectsSingleNode(range) && range.startContainer.childNodes[range.startOffset].nodeType !== 3) {\n                return range.startContainer.childNodes[range.startOffset];\n            }\n\n            // Selection range starts inside a text node, so get its parent\n            if (range.startContainer.nodeType === 3) {\n                return range.startContainer.parentNode;\n            }\n\n            // Selection starts inside an element\n            return range.startContainer;\n        },\n\n        getSelectedElements: function (doc) {\n            var selection = doc.getSelection(),\n                range,\n                toRet,\n                currNode;\n\n            if (!selection.rangeCount || selection.isCollapsed || !selection.getRangeAt(0).commonAncestorContainer) {\n                return [];\n            }\n\n            range = selection.getRangeAt(0);\n\n            if (range.commonAncestorContainer.nodeType === 3) {\n                toRet = [];\n                currNode = range.commonAncestorContainer;\n                while (currNode.parentNode && currNode.parentNode.childNodes.length === 1) {\n                    toRet.push(currNode.parentNode);\n                    currNode = currNode.parentNode;\n                }\n\n                return toRet;\n            }\n\n            return [].filter.call(range.commonAncestorContainer.getElementsByTagName('*'), function (el) {\n                return (typeof selection.containsNode === 'function') ? selection.containsNode(el, true) : true;\n            });\n        },\n\n        selectNode: function (node, doc) {\n            var range = doc.createRange();\n            range.selectNodeContents(node);\n            this.selectRange(doc, range);\n        },\n\n        select: function (doc, startNode, startOffset, endNode, endOffset) {\n            var range = doc.createRange();\n            range.setStart(startNode, startOffset);\n            if (endNode) {\n                range.setEnd(endNode, endOffset);\n            } else {\n                range.collapse(true);\n            }\n            this.selectRange(doc, range);\n            return range;\n        },\n\n        /**\n         *  Clear the current highlighted selection and set the caret to the start or the end of that prior selection, defaults to end.\n         *\n         *  @param {DomDocument} doc            Current document\n         *  @param {boolean} moveCursorToStart  A boolean representing whether or not to set the caret to the beginning of the prior selection.\n         */\n        clearSelection: function (doc, moveCursorToStart) {\n            if (moveCursorToStart) {\n                doc.getSelection().collapseToStart();\n            } else {\n                doc.getSelection().collapseToEnd();\n            }\n        },\n\n        /**\n         * Move cursor to the given node with the given offset.\n         *\n         * @param  {DomDocument} doc     Current document\n         * @param  {DomElement}  node    Element where to jump\n         * @param  {integer}     offset  Where in the element should we jump, 0 by default\n         */\n        moveCursor: function (doc, node, offset) {\n            this.select(doc, node, offset);\n        },\n\n        getSelectionRange: function (ownerDocument) {\n            var selection = ownerDocument.getSelection();\n            if (selection.rangeCount === 0) {\n                return null;\n            }\n            return selection.getRangeAt(0);\n        },\n\n        selectRange: function (ownerDocument, range) {\n            var selection = ownerDocument.getSelection();\n\n            selection.removeAllRanges();\n            selection.addRange(range);\n        },\n\n        // http://stackoverflow.com/questions/1197401/how-can-i-get-the-element-the-caret-is-in-with-javascript-when-using-contentedi\n        // by You\n        getSelectionStart: function (ownerDocument) {\n            var node = ownerDocument.getSelection().anchorNode,\n                startNode = (node && node.nodeType === 3 ? node.parentNode : node);\n\n            return startNode;\n        }\n    };\n\n    MediumEditor.selection = Selection;\n}());\n\n(function () {\n    'use strict';\n\n    function isElementDescendantOfExtension(extensions, element) {\n        if (!extensions) {\n            return false;\n        }\n\n        return extensions.some(function (extension) {\n            if (typeof extension.getInteractionElements !== 'function') {\n                return false;\n            }\n\n            var extensionElements = extension.getInteractionElements();\n            if (!extensionElements) {\n                return false;\n            }\n\n            if (!Array.isArray(extensionElements)) {\n                extensionElements = [extensionElements];\n            }\n            return extensionElements.some(function (el) {\n                return MediumEditor.util.isDescendant(el, element, true);\n            });\n        });\n    }\n\n    var Events = function (instance) {\n        this.base = instance;\n        this.options = this.base.options;\n        this.events = [];\n        this.disabledEvents = {};\n        this.customEvents = {};\n        this.listeners = {};\n    };\n\n    Events.prototype = {\n        InputEventOnContenteditableSupported: !MediumEditor.util.isIE && !MediumEditor.util.isEdge,\n\n        // Helpers for event handling\n\n        attachDOMEvent: function (targets, event, listener, useCapture) {\n            var win = this.base.options.contentWindow,\n                doc = this.base.options.ownerDocument;\n\n            targets = MediumEditor.util.isElement(targets) || [win, doc].indexOf(targets) > -1 ? [targets] : targets;\n\n            Array.prototype.forEach.call(targets, function (target) {\n                target.addEventListener(event, listener, useCapture);\n                this.events.push([target, event, listener, useCapture]);\n            }.bind(this));\n        },\n\n        detachDOMEvent: function (targets, event, listener, useCapture) {\n            var index, e,\n                win = this.base.options.contentWindow,\n                doc = this.base.options.ownerDocument;\n\n            if (targets) {\n                targets = MediumEditor.util.isElement(targets) || [win, doc].indexOf(targets) > -1 ? [targets] : targets;\n\n                Array.prototype.forEach.call(targets, function (target) {\n                    index = this.indexOfListener(target, event, listener, useCapture);\n                    if (index !== -1) {\n                        e = this.events.splice(index, 1)[0];\n                        e[0].removeEventListener(e[1], e[2], e[3]);\n                    }\n                }.bind(this));\n            }\n        },\n\n        indexOfListener: function (target, event, listener, useCapture) {\n            var i, n, item;\n            for (i = 0, n = this.events.length; i < n; i = i + 1) {\n                item = this.events[i];\n                if (item[0] === target && item[1] === event && item[2] === listener && item[3] === useCapture) {\n                    return i;\n                }\n            }\n            return -1;\n        },\n\n        detachAllDOMEvents: function () {\n            var e = this.events.pop();\n            while (e) {\n                e[0].removeEventListener(e[1], e[2], e[3]);\n                e = this.events.pop();\n            }\n        },\n\n        detachAllEventsFromElement: function (element) {\n            var filtered = this.events.filter(function (e) {\n                return e && e[0].getAttribute && e[0].getAttribute('medium-editor-index') === element.getAttribute('medium-editor-index');\n            });\n\n            for (var i = 0, len = filtered.length; i < len; i++) {\n                var e = filtered[i];\n                this.detachDOMEvent(e[0], e[1], e[2], e[3]);\n            }\n        },\n\n        // Attach all existing handlers to a new element\n        attachAllEventsToElement: function (element) {\n            if (this.listeners['editableInput']) {\n                this.contentCache[element.getAttribute('medium-editor-index')] = element.innerHTML;\n            }\n\n            if (this.eventsCache) {\n                this.eventsCache.forEach(function (e) {\n                    this.attachDOMEvent(element, e['name'], e['handler'].bind(this));\n                }, this);\n            }\n        },\n\n        enableCustomEvent: function (event) {\n            if (this.disabledEvents[event] !== undefined) {\n                delete this.disabledEvents[event];\n            }\n        },\n\n        disableCustomEvent: function (event) {\n            this.disabledEvents[event] = true;\n        },\n\n        // custom events\n        attachCustomEvent: function (event, listener) {\n            this.setupListener(event);\n            if (!this.customEvents[event]) {\n                this.customEvents[event] = [];\n            }\n            this.customEvents[event].push(listener);\n        },\n\n        detachCustomEvent: function (event, listener) {\n            var index = this.indexOfCustomListener(event, listener);\n            if (index !== -1) {\n                this.customEvents[event].splice(index, 1);\n                // TODO: If array is empty, should detach internal listeners via destroyListener()\n            }\n        },\n\n        indexOfCustomListener: function (event, listener) {\n            if (!this.customEvents[event] || !this.customEvents[event].length) {\n                return -1;\n            }\n\n            return this.customEvents[event].indexOf(listener);\n        },\n\n        detachAllCustomEvents: function () {\n            this.customEvents = {};\n            // TODO: Should detach internal listeners here via destroyListener()\n        },\n\n        triggerCustomEvent: function (name, data, editable) {\n            if (this.customEvents[name] && !this.disabledEvents[name]) {\n                this.customEvents[name].forEach(function (listener) {\n                    listener(data, editable);\n                });\n            }\n        },\n\n        // Cleaning up\n\n        destroy: function () {\n            this.detachAllDOMEvents();\n            this.detachAllCustomEvents();\n            this.detachExecCommand();\n\n            if (this.base.elements) {\n                this.base.elements.forEach(function (element) {\n                    element.removeAttribute('data-medium-focused');\n                });\n            }\n        },\n\n        // Listening to calls to document.execCommand\n\n        // Attach a listener to be notified when document.execCommand is called\n        attachToExecCommand: function () {\n            if (this.execCommandListener) {\n                return;\n            }\n\n            // Store an instance of the listener so:\n            // 1) We only attach to execCommand once\n            // 2) We can remove the listener later\n            this.execCommandListener = function (execInfo) {\n                this.handleDocumentExecCommand(execInfo);\n            }.bind(this);\n\n            // Ensure that execCommand has been wrapped correctly\n            this.wrapExecCommand();\n\n            // Add listener to list of execCommand listeners\n            this.options.ownerDocument.execCommand.listeners.push(this.execCommandListener);\n        },\n\n        // Remove our listener for calls to document.execCommand\n        detachExecCommand: function () {\n            var doc = this.options.ownerDocument;\n            if (!this.execCommandListener || !doc.execCommand.listeners) {\n                return;\n            }\n\n            // Find the index of this listener in the array of listeners so it can be removed\n            var index = doc.execCommand.listeners.indexOf(this.execCommandListener);\n            if (index !== -1) {\n                doc.execCommand.listeners.splice(index, 1);\n            }\n\n            // If the list of listeners is now empty, put execCommand back to its original state\n            if (!doc.execCommand.listeners.length) {\n                this.unwrapExecCommand();\n            }\n        },\n\n        // Wrap document.execCommand in a custom method so we can listen to calls to it\n        wrapExecCommand: function () {\n            var doc = this.options.ownerDocument;\n\n            // Ensure all instance of MediumEditor only wrap execCommand once\n            if (doc.execCommand.listeners) {\n                return;\n            }\n\n            // Helper method to call all listeners to execCommand\n            var callListeners = function (args, result) {\n                if (doc.execCommand.listeners) {\n                    doc.execCommand.listeners.forEach(function (listener) {\n                        listener({\n                            command: args[0],\n                            value: args[2],\n                            args: args,\n                            result: result\n                        });\n                    });\n                }\n            },\n\n                // Create a wrapper method for execCommand which will:\n                // 1) Call document.execCommand with the correct arguments\n                // 2) Loop through any listeners and notify them that execCommand was called\n                //    passing extra info on the call\n                // 3) Return the result\n                wrapper = function () {\n                    var result = doc.execCommand.orig.apply(this, arguments);\n\n                    if (!doc.execCommand.listeners) {\n                        return result;\n                    }\n\n                    var args = Array.prototype.slice.call(arguments);\n                    callListeners(args, result);\n\n                    return result;\n                };\n\n            // Store a reference to the original execCommand\n            wrapper.orig = doc.execCommand;\n\n            // Attach an array for storing listeners\n            wrapper.listeners = [];\n\n            // Helper for notifying listeners\n            wrapper.callListeners = callListeners;\n\n            // Overwrite execCommand\n            doc.execCommand = wrapper;\n        },\n\n        // Revert document.execCommand back to its original self\n        unwrapExecCommand: function () {\n            var doc = this.options.ownerDocument;\n            if (!doc.execCommand.orig) {\n                return;\n            }\n\n            // Use the reference to the original execCommand to revert back\n            doc.execCommand = doc.execCommand.orig;\n        },\n\n        // Listening to browser events to emit events medium-editor cares about\n        setupListener: function (name) {\n            if (this.listeners[name]) {\n                return;\n            }\n\n            switch (name) {\n                case 'externalInteraction':\n                    // Detecting when user has interacted with elements outside of MediumEditor\n                    this.attachDOMEvent(this.options.ownerDocument.body, 'mousedown', this.handleBodyMousedown.bind(this), true);\n                    this.attachDOMEvent(this.options.ownerDocument.body, 'click', this.handleBodyClick.bind(this), true);\n                    this.attachDOMEvent(this.options.ownerDocument.body, 'focus', this.handleBodyFocus.bind(this), true);\n                    break;\n                case 'blur':\n                    // Detecting when focus is lost\n                    this.setupListener('externalInteraction');\n                    break;\n                case 'focus':\n                    // Detecting when focus moves into some part of MediumEditor\n                    this.setupListener('externalInteraction');\n                    break;\n                case 'editableInput':\n                    // setup cache for knowing when the content has changed\n                    this.contentCache = {};\n                    this.base.elements.forEach(function (element) {\n                        this.contentCache[element.getAttribute('medium-editor-index')] = element.innerHTML;\n                    }, this);\n\n                    // Attach to the 'oninput' event, handled correctly by most browsers\n                    if (this.InputEventOnContenteditableSupported) {\n                        this.attachToEachElement('input', this.handleInput);\n                    }\n\n                    // For browsers which don't support the input event on contenteditable (IE)\n                    // we'll attach to 'selectionchange' on the document and 'keypress' on the editables\n                    if (!this.InputEventOnContenteditableSupported) {\n                        this.setupListener('editableKeypress');\n                        this.keypressUpdateInput = true;\n                        this.attachDOMEvent(document, 'selectionchange', this.handleDocumentSelectionChange.bind(this));\n                        // Listen to calls to execCommand\n                        this.attachToExecCommand();\n                    }\n                    break;\n                case 'editableClick':\n                    // Detecting click in the contenteditables\n                    this.attachToEachElement('click', this.handleClick);\n                    break;\n                case 'editableBlur':\n                    // Detecting blur in the contenteditables\n                    this.attachToEachElement('blur', this.handleBlur);\n                    break;\n                case 'editableKeypress':\n                    // Detecting keypress in the contenteditables\n                    this.attachToEachElement('keypress', this.handleKeypress);\n                    break;\n                case 'editableKeyup':\n                    // Detecting keyup in the contenteditables\n                    this.attachToEachElement('keyup', this.handleKeyup);\n                    break;\n                case 'editableKeydown':\n                    // Detecting keydown on the contenteditables\n                    this.attachToEachElement('keydown', this.handleKeydown);\n                    break;\n                case 'editableKeydownSpace':\n                    // Detecting keydown for SPACE on the contenteditables\n                    this.setupListener('editableKeydown');\n                    break;\n                case 'editableKeydownEnter':\n                    // Detecting keydown for ENTER on the contenteditables\n                    this.setupListener('editableKeydown');\n                    break;\n                case 'editableKeydownTab':\n                    // Detecting keydown for TAB on the contenteditable\n                    this.setupListener('editableKeydown');\n                    break;\n                case 'editableKeydownDelete':\n                    // Detecting keydown for DELETE/BACKSPACE on the contenteditables\n                    this.setupListener('editableKeydown');\n                    break;\n                case 'editableMouseover':\n                    // Detecting mouseover on the contenteditables\n                    this.attachToEachElement('mouseover', this.handleMouseover);\n                    break;\n                case 'editableDrag':\n                    // Detecting dragover and dragleave on the contenteditables\n                    this.attachToEachElement('dragover', this.handleDragging);\n                    this.attachToEachElement('dragleave', this.handleDragging);\n                    break;\n                case 'editableDrop':\n                    // Detecting drop on the contenteditables\n                    this.attachToEachElement('drop', this.handleDrop);\n                    break;\n                // TODO: We need to have a custom 'paste' event separate from 'editablePaste'\n                // Need to think about the way to introduce this without breaking folks\n                case 'editablePaste':\n                    // Detecting paste on the contenteditables\n                    this.attachToEachElement('paste', this.handlePaste);\n                    break;\n            }\n            this.listeners[name] = true;\n        },\n\n        attachToEachElement: function (name, handler) {\n            // build our internal cache to know which element got already what handler attached\n            if (!this.eventsCache) {\n                this.eventsCache = [];\n            }\n\n            this.base.elements.forEach(function (element) {\n                this.attachDOMEvent(element, name, handler.bind(this));\n            }, this);\n\n            this.eventsCache.push({ 'name': name, 'handler': handler });\n        },\n\n        cleanupElement: function (element) {\n            var index = element.getAttribute('medium-editor-index');\n            if (index) {\n                this.detachAllEventsFromElement(element);\n                if (this.contentCache) {\n                    delete this.contentCache[index];\n                }\n            }\n        },\n\n        focusElement: function (element) {\n            element.focus();\n            this.updateFocus(element, { target: element, type: 'focus' });\n        },\n\n        updateFocus: function (target, eventObj) {\n            var hadFocus = this.base.getFocusedElement(),\n                toFocus;\n\n            // For clicks, we need to know if the mousedown that caused the click happened inside the existing focused element\n            // or one of the extension elements.  If so, we don't want to focus another element\n            if (hadFocus &&\n                eventObj.type === 'click' &&\n                this.lastMousedownTarget &&\n                (MediumEditor.util.isDescendant(hadFocus, this.lastMousedownTarget, true) ||\n                    isElementDescendantOfExtension(this.base.extensions, this.lastMousedownTarget))) {\n                toFocus = hadFocus;\n            }\n\n            if (!toFocus) {\n                this.base.elements.some(function (element) {\n                    // If the target is part of an editor element, this is the element getting focus\n                    if (!toFocus && (MediumEditor.util.isDescendant(element, target, true))) {\n                        toFocus = element;\n                    }\n\n                    // bail if we found an element that's getting focus\n                    return !!toFocus;\n                }, this);\n            }\n\n            // Check if the target is external (not part of the editor, toolbar, or any other extension)\n            var externalEvent = !MediumEditor.util.isDescendant(hadFocus, target, true) &&\n                !isElementDescendantOfExtension(this.base.extensions, target);\n\n            if (toFocus !== hadFocus) {\n                // If element has focus, and focus is going outside of editor\n                // Don't blur focused element if clicking on editor, toolbar, or anchorpreview\n                if (hadFocus && externalEvent) {\n                    // Trigger blur on the editable that has lost focus\n                    hadFocus.removeAttribute('data-medium-focused');\n                    this.triggerCustomEvent('blur', eventObj, hadFocus);\n                }\n\n                // If focus is going into an editor element\n                if (toFocus) {\n                    // Trigger focus on the editable that now has focus\n                    toFocus.setAttribute('data-medium-focused', true);\n                    this.triggerCustomEvent('focus', eventObj, toFocus);\n                }\n            }\n\n            if (externalEvent) {\n                this.triggerCustomEvent('externalInteraction', eventObj);\n            }\n        },\n\n        updateInput: function (target, eventObj) {\n            if (!this.contentCache) {\n                return;\n            }\n            // An event triggered which signifies that the user may have changed someting\n            // Look in our cache of input for the contenteditables to see if something changed\n            var index = target.getAttribute('medium-editor-index'),\n                html = target.innerHTML;\n\n            if (html !== this.contentCache[index]) {\n                // The content has changed since the last time we checked, fire the event\n                this.triggerCustomEvent('editableInput', eventObj, target);\n            }\n            this.contentCache[index] = html;\n        },\n\n        handleDocumentSelectionChange: function (event) {\n            // When selectionchange fires, target and current target are set\n            // to document, since this is where the event is handled\n            // However, currentTarget will have an 'activeElement' property\n            // which will point to whatever element has focus.\n            if (event.currentTarget && event.currentTarget.activeElement) {\n                var activeElement = event.currentTarget.activeElement,\n                    currentTarget;\n                // We can look at the 'activeElement' to determine if the selectionchange has\n                // happened within a contenteditable owned by this instance of MediumEditor\n                this.base.elements.some(function (element) {\n                    if (MediumEditor.util.isDescendant(element, activeElement, true)) {\n                        currentTarget = element;\n                        return true;\n                    }\n                    return false;\n                }, this);\n\n                // We know selectionchange fired within one of our contenteditables\n                if (currentTarget) {\n                    this.updateInput(currentTarget, { target: activeElement, currentTarget: currentTarget });\n                }\n            }\n        },\n\n        handleDocumentExecCommand: function () {\n            // document.execCommand has been called\n            // If one of our contenteditables currently has focus, we should\n            // attempt to trigger the 'editableInput' event\n            var target = this.base.getFocusedElement();\n            if (target) {\n                this.updateInput(target, { target: target, currentTarget: target });\n            }\n        },\n\n        handleBodyClick: function (event) {\n            this.updateFocus(event.target, event);\n        },\n\n        handleBodyFocus: function (event) {\n            this.updateFocus(event.target, event);\n        },\n\n        handleBodyMousedown: function (event) {\n            this.lastMousedownTarget = event.target;\n        },\n\n        handleInput: function (event) {\n            this.updateInput(event.currentTarget, event);\n        },\n\n        handleClick: function (event) {\n            this.triggerCustomEvent('editableClick', event, event.currentTarget);\n        },\n\n        handleBlur: function (event) {\n            this.triggerCustomEvent('editableBlur', event, event.currentTarget);\n        },\n\n        handleKeypress: function (event) {\n            this.triggerCustomEvent('editableKeypress', event, event.currentTarget);\n\n            // If we're doing manual detection of the editableInput event we need\n            // to check for input changes during 'keypress'\n            if (this.keypressUpdateInput) {\n                var eventObj = { target: event.target, currentTarget: event.currentTarget };\n\n                // In IE, we need to let the rest of the event stack complete before we detect\n                // changes to input, so using setTimeout here\n                setTimeout(function () {\n                    this.updateInput(eventObj.currentTarget, eventObj);\n                }.bind(this), 0);\n            }\n        },\n\n        handleKeyup: function (event) {\n            this.triggerCustomEvent('editableKeyup', event, event.currentTarget);\n        },\n\n        handleMouseover: function (event) {\n            this.triggerCustomEvent('editableMouseover', event, event.currentTarget);\n        },\n\n        handleDragging: function (event) {\n            this.triggerCustomEvent('editableDrag', event, event.currentTarget);\n        },\n\n        handleDrop: function (event) {\n            this.triggerCustomEvent('editableDrop', event, event.currentTarget);\n        },\n\n        handlePaste: function (event) {\n            this.triggerCustomEvent('editablePaste', event, event.currentTarget);\n        },\n\n        handleKeydown: function (event) {\n\n            this.triggerCustomEvent('editableKeydown', event, event.currentTarget);\n\n            if (MediumEditor.util.isKey(event, MediumEditor.util.keyCode.SPACE)) {\n                return this.triggerCustomEvent('editableKeydownSpace', event, event.currentTarget);\n            }\n\n            if (MediumEditor.util.isKey(event, MediumEditor.util.keyCode.ENTER) || (event.ctrlKey && MediumEditor.util.isKey(event, MediumEditor.util.keyCode.M))) {\n                return this.triggerCustomEvent('editableKeydownEnter', event, event.currentTarget);\n            }\n\n            if (MediumEditor.util.isKey(event, MediumEditor.util.keyCode.TAB)) {\n                return this.triggerCustomEvent('editableKeydownTab', event, event.currentTarget);\n            }\n\n            if (MediumEditor.util.isKey(event, [MediumEditor.util.keyCode.DELETE, MediumEditor.util.keyCode.BACKSPACE])) {\n                return this.triggerCustomEvent('editableKeydownDelete', event, event.currentTarget);\n            }\n        }\n    };\n\n    MediumEditor.Events = Events;\n}());\n\n(function () {\n    'use strict';\n\n    var Button = MediumEditor.Extension.extend({\n\n        /* Button Options */\n\n        /* action: [string]\n         * The action argument to pass to MediumEditor.execAction()\n         * when the button is clicked\n         */\n        action: undefined,\n\n        /* aria: [string]\n         * The value to add as the aria-label attribute of the button\n         * element displayed in the toolbar.\n         * This is also used as the tooltip for the button\n         */\n        aria: undefined,\n\n        /* tagNames: [Array]\n         * NOTE: This is not used if useQueryState is set to true.\n         *\n         * Array of element tag names that would indicate that this\n         * button has already been applied. If this action has already\n         * been applied, the button will be displayed as 'active' in the toolbar\n         *\n         * Example:\n         * For 'bold', if the text is ever within a <b> or <strong>\n         * tag that indicates the text is already bold. So the array\n         * of tagNames for bold would be: ['b', 'strong']\n         */\n        tagNames: undefined,\n\n        /* style: [Object]\n         * NOTE: This is not used if useQueryState is set to true.\n         *\n         * A pair of css property & value(s) that indicate that this\n         * button has already been applied. If this action has already\n         * been applied, the button will be displayed as 'active' in the toolbar\n         * Properties of the object:\n         *   prop [String]: name of the css property\n         *   value [String]: value(s) of the css property\n         *                   multiple values can be separated by a '|'\n         *\n         * Example:\n         * For 'bold', if the text is ever within an element with a 'font-weight'\n         * style property set to '700' or 'bold', that indicates the text\n         * is already bold.  So the style object for bold would be:\n         * { prop: 'font-weight', value: '700|bold' }\n         */\n        style: undefined,\n\n        /* useQueryState: [boolean]\n         * Enables/disables whether this button should use the built-in\n         * document.queryCommandState() method to determine whether\n         * the action has already been applied.  If the action has already\n         * been applied, the button will be displayed as 'active' in the toolbar\n         *\n         * Example:\n         * For 'bold', if this is set to true, the code will call:\n         * document.queryCommandState('bold') which will return true if the\n         * browser thinks the text is already bold, and false otherwise\n         */\n        useQueryState: undefined,\n\n        /* contentDefault: [string]\n         * Default innerHTML to put inside the button\n         */\n        contentDefault: undefined,\n\n        /* contentFA: [string]\n         * The innerHTML to use for the content of the button\n         * if the `buttonLabels` option for MediumEditor is set to 'fontawesome'\n         */\n        contentFA: undefined,\n\n        /* classList: [Array]\n         * An array of classNames (strings) to be added to the button\n         */\n        classList: undefined,\n\n        /* attrs: [object]\n         * A set of key-value pairs to add to the button as custom attributes\n         */\n        attrs: undefined,\n\n        // The button constructor can optionally accept the name of a built-in button\n        // (ie 'bold', 'italic', etc.)\n        // When the name of a button is passed, it will initialize itself with the\n        // configuration for that button\n        constructor: function (options) {\n            if (Button.isBuiltInButton(options)) {\n                MediumEditor.Extension.call(this, this.defaults[options]);\n            } else {\n                MediumEditor.Extension.call(this, options);\n            }\n        },\n\n        init: function () {\n            MediumEditor.Extension.prototype.init.apply(this, arguments);\n\n            this.button = this.createButton();\n            this.on(this.button, 'click', this.handleClick.bind(this));\n        },\n\n        /* getButton: [function ()]\n         *\n         * If implemented, this function will be called when\n         * the toolbar is being created.  The DOM Element returned\n         * by this function will be appended to the toolbar along\n         * with any other buttons.\n         */\n        getButton: function () {\n            return this.button;\n        },\n\n        getAction: function () {\n            return (typeof this.action === 'function') ? this.action(this.base.options) : this.action;\n        },\n\n        getAria: function () {\n            return (typeof this.aria === 'function') ? this.aria(this.base.options) : this.aria;\n        },\n\n        getTagNames: function () {\n            return (typeof this.tagNames === 'function') ? this.tagNames(this.base.options) : this.tagNames;\n        },\n\n        createButton: function () {\n            var button = this.document.createElement('button'),\n                content = this.contentDefault,\n                ariaLabel = this.getAria(),\n                buttonLabels = this.getEditorOption('buttonLabels');\n            // Add class names\n            button.classList.add('medium-editor-action');\n            button.classList.add('medium-editor-action-' + this.name);\n            if (this.classList) {\n                this.classList.forEach(function (className) {\n                    button.classList.add(className);\n                });\n            }\n\n            // Add attributes\n            button.setAttribute('data-action', this.getAction());\n            if (ariaLabel) {\n                button.setAttribute('title', ariaLabel);\n                button.setAttribute('aria-label', ariaLabel);\n            }\n            if (this.attrs) {\n                Object.keys(this.attrs).forEach(function (attr) {\n                    button.setAttribute(attr, this.attrs[attr]);\n                }, this);\n            }\n\n            if (buttonLabels === 'fontawesome' && this.contentFA) {\n                content = this.contentFA;\n            }\n            button.innerHTML = content;\n            return button;\n        },\n\n        handleClick: function (event) {\n            event.preventDefault();\n            event.stopPropagation();\n\n            var action = this.getAction();\n\n            if (action) {\n                this.execAction(action);\n            }\n        },\n\n        isActive: function () {\n            return this.button.classList.contains(this.getEditorOption('activeButtonClass'));\n        },\n\n        setInactive: function () {\n            this.button.classList.remove(this.getEditorOption('activeButtonClass'));\n            delete this.knownState;\n        },\n\n        setActive: function () {\n            this.button.classList.add(this.getEditorOption('activeButtonClass'));\n            delete this.knownState;\n        },\n\n        queryCommandState: function () {\n            var queryState = null;\n            if (this.useQueryState) {\n                queryState = this.base.queryCommandState(this.getAction());\n            }\n            return queryState;\n        },\n\n        isAlreadyApplied: function (node) {\n            var isMatch = false,\n                tagNames = this.getTagNames(),\n                styleVals,\n                computedStyle;\n\n            if (this.knownState === false || this.knownState === true) {\n                return this.knownState;\n            }\n\n            if (tagNames && tagNames.length > 0) {\n                isMatch = tagNames.indexOf(node.nodeName.toLowerCase()) !== -1;\n            }\n\n            if (!isMatch && this.style) {\n                styleVals = this.style.value.split('|');\n                computedStyle = this.window.getComputedStyle(node, null).getPropertyValue(this.style.prop);\n                styleVals.forEach(function (val) {\n                    if (!this.knownState) {\n                        isMatch = (computedStyle.indexOf(val) !== -1);\n                        // text-decoration is not inherited by default\n                        // so if the computed style for text-decoration doesn't match\n                        // don't write to knownState so we can fallback to other checks\n                        if (isMatch || this.style.prop !== 'text-decoration') {\n                            this.knownState = isMatch;\n                        }\n                    }\n                }, this);\n            }\n\n            return isMatch;\n        }\n    });\n\n    Button.isBuiltInButton = function (name) {\n        return (typeof name === 'string') && MediumEditor.extensions.button.prototype.defaults.hasOwnProperty(name);\n    };\n\n    MediumEditor.extensions.button = Button;\n}());\n\n(function () {\n    'use strict';\n\n    /* MediumEditor.extensions.button.defaults: [Object]\n     * Set of default config options for all of the built-in MediumEditor buttons\n     */\n    MediumEditor.extensions.button.prototype.defaults = {\n        'bold': {\n            name: 'bold',\n            action: 'bold',\n            aria: 'bold',\n            tagNames: ['b', 'strong'],\n            style: {\n                prop: 'font-weight',\n                value: '700|bold'\n            },\n            useQueryState: true,\n            contentDefault: '<b>B</b>',\n            contentFA: '<i class=\"fa fa-bold\"></i>'\n        },\n        'italic': {\n            name: 'italic',\n            action: 'italic',\n            aria: 'italic',\n            tagNames: ['i', 'em'],\n            style: {\n                prop: 'font-style',\n                value: 'italic'\n            },\n            useQueryState: true,\n            contentDefault: '<b><i>I</i></b>',\n            contentFA: '<i class=\"fa fa-italic\"></i>'\n        },\n        'underline': {\n            name: 'underline',\n            action: 'underline',\n            aria: 'underline',\n            tagNames: ['u'],\n            style: {\n                prop: 'text-decoration',\n                value: 'underline'\n            },\n            useQueryState: true,\n            contentDefault: '<b><u>U</u></b>',\n            contentFA: '<i class=\"fa fa-underline\"></i>'\n        },\n        'strikethrough': {\n            name: 'strikethrough',\n            action: 'strikethrough',\n            aria: 'strike through',\n            tagNames: ['strike'],\n            style: {\n                prop: 'text-decoration',\n                value: 'line-through'\n            },\n            useQueryState: true,\n            contentDefault: '<s>A</s>',\n            contentFA: '<i class=\"fa fa-strikethrough\"></i>'\n        },\n        'superscript': {\n            name: 'superscript',\n            action: 'superscript',\n            aria: 'superscript',\n            tagNames: ['sup'],\n            /* firefox doesn't behave the way we want it to, so we CAN'T use queryCommandState for superscript\n               https://github.com/guardian/scribe/blob/master/BROWSERINCONSISTENCIES.md#documentquerycommandstate */\n            // useQueryState: true\n            contentDefault: '<b>x<sup>1</sup></b>',\n            contentFA: '<i class=\"fa fa-superscript\"></i>'\n        },\n        'subscript': {\n            name: 'subscript',\n            action: 'subscript',\n            aria: 'subscript',\n            tagNames: ['sub'],\n            /* firefox doesn't behave the way we want it to, so we CAN'T use queryCommandState for subscript\n               https://github.com/guardian/scribe/blob/master/BROWSERINCONSISTENCIES.md#documentquerycommandstate */\n            // useQueryState: true\n            contentDefault: '<b>x<sub>1</sub></b>',\n            contentFA: '<i class=\"fa fa-subscript\"></i>'\n        },\n        'image': {\n            name: 'image',\n            action: 'image',\n            aria: 'image',\n            tagNames: ['img'],\n            contentDefault: '<b>image</b>',\n            contentFA: '<i class=\"fa fa-picture-o\"></i>'\n        },\n        'html': {\n            name: 'html',\n            action: 'html',\n            aria: 'evaluate html',\n            tagNames: ['iframe', 'object'],\n            contentDefault: '<b>html</b>',\n            contentFA: '<i class=\"fa fa-code\"></i>'\n        },\n        'orderedlist': {\n            name: 'orderedlist',\n            action: 'insertorderedlist',\n            aria: 'ordered list',\n            tagNames: ['ol'],\n            useQueryState: true,\n            contentDefault: '<b>1.</b>',\n            contentFA: '<i class=\"fa fa-list-ol\"></i>'\n        },\n        'unorderedlist': {\n            name: 'unorderedlist',\n            action: 'insertunorderedlist',\n            aria: 'unordered list',\n            tagNames: ['ul'],\n            useQueryState: true,\n            contentDefault: '<b>&bull;</b>',\n            contentFA: '<i class=\"fa fa-list-ul\"></i>'\n        },\n        'indent': {\n            name: 'indent',\n            action: 'indent',\n            aria: 'indent',\n            tagNames: [],\n            contentDefault: '<b>&rarr;</b>',\n            contentFA: '<i class=\"fa fa-indent\"></i>'\n        },\n        'outdent': {\n            name: 'outdent',\n            action: 'outdent',\n            aria: 'outdent',\n            tagNames: [],\n            contentDefault: '<b>&larr;</b>',\n            contentFA: '<i class=\"fa fa-outdent\"></i>'\n        },\n        'justifyCenter': {\n            name: 'justifyCenter',\n            action: 'justifyCenter',\n            aria: 'center justify',\n            tagNames: [],\n            style: {\n                prop: 'text-align',\n                value: 'center'\n            },\n            contentDefault: '<b>C</b>',\n            contentFA: '<i class=\"fa fa-align-center\"></i>'\n        },\n        'justifyFull': {\n            name: 'justifyFull',\n            action: 'justifyFull',\n            aria: 'full justify',\n            tagNames: [],\n            style: {\n                prop: 'text-align',\n                value: 'justify'\n            },\n            contentDefault: '<b>J</b>',\n            contentFA: '<i class=\"fa fa-align-justify\"></i>'\n        },\n        'justifyLeft': {\n            name: 'justifyLeft',\n            action: 'justifyLeft',\n            aria: 'left justify',\n            tagNames: [],\n            style: {\n                prop: 'text-align',\n                value: 'left'\n            },\n            contentDefault: '<b>L</b>',\n            contentFA: '<i class=\"fa fa-align-left\"></i>'\n        },\n        'justifyRight': {\n            name: 'justifyRight',\n            action: 'justifyRight',\n            aria: 'right justify',\n            tagNames: [],\n            style: {\n                prop: 'text-align',\n                value: 'right'\n            },\n            contentDefault: '<b>R</b>',\n            contentFA: '<i class=\"fa fa-align-right\"></i>'\n        },\n        // Known inline elements that are not removed, or not removed consistantly across browsers:\n        // <span>, <label>, <br>\n        'removeFormat': {\n            name: 'removeFormat',\n            aria: 'remove formatting',\n            action: 'removeFormat',\n            contentDefault: '<b>X</b>',\n            contentFA: '<i class=\"fa fa-eraser\"></i>'\n        },\n\n        /***** Buttons for appending block elements (append-<element> action) *****/\n\n        'quote': {\n            name: 'quote',\n            action: 'append-blockquote',\n            aria: 'blockquote',\n            tagNames: ['blockquote'],\n            contentDefault: '<b>&ldquo;</b>',\n            contentFA: '<i class=\"fa fa-quote-right\"></i>'\n        },\n        'pre': {\n            name: 'pre',\n            action: 'append-pre',\n            aria: 'preformatted text',\n            tagNames: ['pre'],\n            contentDefault: '<b>0101</b>',\n            contentFA: '<i class=\"fa fa-code fa-lg\"></i>'\n        },\n        'h1': {\n            name: 'h1',\n            action: 'append-h1',\n            aria: 'header type one',\n            tagNames: ['h1'],\n            contentDefault: '<b>H1</b>',\n            contentFA: '<i class=\"fa fa-header\"><sup>1</sup>'\n        },\n        'h2': {\n            name: 'h2',\n            action: 'append-h2',\n            aria: 'header type two',\n            tagNames: ['h2'],\n            contentDefault: '<b>H2</b>',\n            contentFA: '<i class=\"fa fa-header\"><sup>2</sup>'\n        },\n        'h3': {\n            name: 'h3',\n            action: 'append-h3',\n            aria: 'header type three',\n            tagNames: ['h3'],\n            contentDefault: '<b>H3</b>',\n            contentFA: '<i class=\"fa fa-header\"><sup>3</sup>'\n        },\n        'h4': {\n            name: 'h4',\n            action: 'append-h4',\n            aria: 'header type four',\n            tagNames: ['h4'],\n            contentDefault: '<b>H4</b>',\n            contentFA: '<i class=\"fa fa-header\"><sup>4</sup>'\n        },\n        'h5': {\n            name: 'h5',\n            action: 'append-h5',\n            aria: 'header type five',\n            tagNames: ['h5'],\n            contentDefault: '<b>H5</b>',\n            contentFA: '<i class=\"fa fa-header\"><sup>5</sup>'\n        },\n        'h6': {\n            name: 'h6',\n            action: 'append-h6',\n            aria: 'header type six',\n            tagNames: ['h6'],\n            contentDefault: '<b>H6</b>',\n            contentFA: '<i class=\"fa fa-header\"><sup>6</sup>'\n        }\n    };\n\n})();\n\n(function () {\n    'use strict';\n\n    /* Base functionality for an extension which will display\n     * a 'form' inside the toolbar\n     */\n    var FormExtension = MediumEditor.extensions.button.extend({\n\n        init: function () {\n            MediumEditor.extensions.button.prototype.init.apply(this, arguments);\n        },\n\n        // default labels for the form buttons\n        formSaveLabel: '&#10003;',\n        formCloseLabel: '&times;',\n\n        /* activeClass: [string]\n         * set class which added to shown form\n         */\n        activeClass: 'medium-editor-toolbar-form-active',\n\n        /* hasForm: [boolean]\n         *\n         * Setting this to true will cause getForm() to be called\n         * when the toolbar is created, so the form can be appended\n         * inside the toolbar container\n         */\n        hasForm: true,\n\n        /* getForm: [function ()]\n         *\n         * When hasForm is true, this function must be implemented\n         * and return a DOM Element which will be appended to\n         * the toolbar container. The form should start hidden, and\n         * the extension can choose when to hide/show it\n         */\n        getForm: function () {},\n\n        /* isDisplayed: [function ()]\n         *\n         * This function should return true/false reflecting\n         * whether the form is currently displayed\n         */\n        isDisplayed: function () {\n            if (this.hasForm) {\n                return this.getForm().classList.contains(this.activeClass);\n            }\n            return false;\n        },\n\n        /* hideForm: [function ()]\n         *\n         * This function should show the form element inside\n         * the toolbar container\n         */\n        showForm: function () {\n            if (this.hasForm) {\n                this.getForm().classList.add(this.activeClass);\n            }\n        },\n\n        /* hideForm: [function ()]\n         *\n         * This function should hide the form element inside\n         * the toolbar container\n         */\n        hideForm: function () {\n            if (this.hasForm) {\n                this.getForm().classList.remove(this.activeClass);\n            }\n        },\n\n        /************************ Helpers ************************\n         * The following are helpers that are either set by MediumEditor\n         * during initialization, or are helper methods which either\n         * route calls to the MediumEditor instance or provide common\n         * functionality for all form extensions\n         *********************************************************/\n\n        /* showToolbarDefaultActions: [function ()]\n         *\n         * Helper method which will turn back the toolbar after canceling\n         * the customized form\n         */\n        showToolbarDefaultActions: function () {\n            var toolbar = this.base.getExtensionByName('toolbar');\n            if (toolbar) {\n                toolbar.showToolbarDefaultActions();\n            }\n        },\n\n        /* hideToolbarDefaultActions: [function ()]\n         *\n         * Helper function which will hide the default contents of the\n         * toolbar, but leave the toolbar container in the same state\n         * to allow a form to display its custom contents inside the toolbar\n         */\n        hideToolbarDefaultActions: function () {\n            var toolbar = this.base.getExtensionByName('toolbar');\n            if (toolbar) {\n                toolbar.hideToolbarDefaultActions();\n            }\n        },\n\n        /* setToolbarPosition: [function ()]\n         *\n         * Helper function which will update the size and position\n         * of the toolbar based on the toolbar content and the current\n         * position of the user's selection\n         */\n        setToolbarPosition: function () {\n            var toolbar = this.base.getExtensionByName('toolbar');\n            if (toolbar) {\n                toolbar.setToolbarPosition();\n            }\n        }\n    });\n\n    MediumEditor.extensions.form = FormExtension;\n})();\n(function () {\n    'use strict';\n\n    var AnchorForm = MediumEditor.extensions.form.extend({\n        /* Anchor Form Options */\n\n        /* customClassOption: [string]  (previously options.anchorButton + options.anchorButtonClass)\n         * Custom class name the user can optionally have added to their created links (ie 'button').\n         * If passed as a non-empty string, a checkbox will be displayed allowing the user to choose\n         * whether to have the class added to the created link or not.\n         */\n        customClassOption: null,\n\n        /* customClassOptionText: [string]\n         * text to be shown in the checkbox when the __customClassOption__ is being used.\n         */\n        customClassOptionText: 'Button',\n\n        /* linkValidation: [boolean]  (previously options.checkLinkFormat)\n         * enables/disables check for common URL protocols on anchor links.\n         */\n        linkValidation: false,\n\n        /* placeholderText: [string]  (previously options.anchorInputPlaceholder)\n         * text to be shown as placeholder of the anchor input.\n         */\n        placeholderText: 'Paste or type a link',\n\n        /* targetCheckbox: [boolean]  (previously options.anchorTarget)\n         * enables/disables displaying a \"Open in new window\" checkbox, which when checked\n         * changes the `target` attribute of the created link.\n         */\n        targetCheckbox: false,\n\n        /* targetCheckboxText: [string]  (previously options.anchorInputCheckboxLabel)\n         * text to be shown in the checkbox enabled via the __targetCheckbox__ option.\n         */\n        targetCheckboxText: 'Open in new window',\n\n        // Options for the Button base class\n        name: 'anchor',\n        action: 'createLink',\n        aria: 'link',\n        tagNames: ['a'],\n        contentDefault: '<b>#</b>',\n        contentFA: '<i class=\"fa fa-link\"></i>',\n\n        init: function () {\n            MediumEditor.extensions.form.prototype.init.apply(this, arguments);\n\n            this.subscribe('editableKeydown', this.handleKeydown.bind(this));\n        },\n\n        // Called when the button the toolbar is clicked\n        // Overrides ButtonExtension.handleClick\n        handleClick: function (event) {\n            event.preventDefault();\n            event.stopPropagation();\n\n            var range = MediumEditor.selection.getSelectionRange(this.document);\n\n            if (range.startContainer.nodeName.toLowerCase() === 'a' ||\n                range.endContainer.nodeName.toLowerCase() === 'a' ||\n                MediumEditor.util.getClosestTag(MediumEditor.selection.getSelectedParentElement(range), 'a')) {\n                return this.execAction('unlink');\n            }\n\n            if (!this.isDisplayed()) {\n                this.showForm();\n            }\n\n            return false;\n        },\n\n        // Called when user hits the defined shortcut (CTRL / COMMAND + K)\n        handleKeydown: function (event) {\n            if (MediumEditor.util.isKey(event, MediumEditor.util.keyCode.K) && MediumEditor.util.isMetaCtrlKey(event) && !event.shiftKey) {\n                this.handleClick(event);\n            }\n        },\n\n        // Called by medium-editor to append form to the toolbar\n        getForm: function () {\n            if (!this.form) {\n                this.form = this.createForm();\n            }\n            return this.form;\n        },\n\n        getTemplate: function () {\n            var template = [\n                '<input type=\"text\" class=\"medium-editor-toolbar-input\" placeholder=\"', this.placeholderText, '\">'\n            ];\n\n            template.push(\n                '<a href=\"#\" class=\"medium-editor-toolbar-save\">',\n                this.getEditorOption('buttonLabels') === 'fontawesome' ? '<i class=\"fa fa-check\"></i>' : this.formSaveLabel,\n                '</a>'\n            );\n\n            template.push('<a href=\"#\" class=\"medium-editor-toolbar-close\">',\n                this.getEditorOption('buttonLabels') === 'fontawesome' ? '<i class=\"fa fa-times\"></i>' : this.formCloseLabel,\n                '</a>');\n\n            // both of these options are slightly moot with the ability to\n            // override the various form buildup/serialize functions.\n\n            if (this.targetCheckbox) {\n                // fixme: ideally, this targetCheckboxText would be a formLabel too,\n                // figure out how to deprecate? also consider `fa-` icon default implcations.\n                template.push(\n                    '<div class=\"medium-editor-toolbar-form-row\">',\n                    '<input type=\"checkbox\" class=\"medium-editor-toolbar-anchor-target\" id=\"medium-editor-toolbar-anchor-target-field-' + this.getEditorId() + '\">',\n                    '<label for=\"medium-editor-toolbar-anchor-target-field-' + this.getEditorId() + '\">',\n                    this.targetCheckboxText,\n                    '</label>',\n                    '</div>'\n                );\n            }\n\n            if (this.customClassOption) {\n                // fixme: expose this `Button` text as a formLabel property, too\n                // and provide similar access to a `fa-` icon default.\n                template.push(\n                    '<div class=\"medium-editor-toolbar-form-row\">',\n                    '<input type=\"checkbox\" class=\"medium-editor-toolbar-anchor-button\">',\n                    '<label>',\n                    this.customClassOptionText,\n                    '</label>',\n                    '</div>'\n                );\n            }\n\n            return template.join('');\n\n        },\n\n        // Used by medium-editor when the default toolbar is to be displayed\n        isDisplayed: function () {\n            return MediumEditor.extensions.form.prototype.isDisplayed.apply(this);\n        },\n\n        hideForm: function () {\n            MediumEditor.extensions.form.prototype.hideForm.apply(this);\n            this.getInput().value = '';\n        },\n\n        showForm: function (opts) {\n            var input = this.getInput(),\n                targetCheckbox = this.getAnchorTargetCheckbox(),\n                buttonCheckbox = this.getAnchorButtonCheckbox();\n\n            opts = opts || { value: '' };\n            // TODO: This is for backwards compatability\n            // We don't need to support the 'string' argument in 6.0.0\n            if (typeof opts === 'string') {\n                opts = {\n                    value: opts\n                };\n            }\n\n            this.base.saveSelection();\n            this.hideToolbarDefaultActions();\n            MediumEditor.extensions.form.prototype.showForm.apply(this);\n            this.setToolbarPosition();\n\n            input.value = opts.value;\n            input.focus();\n\n            // If we have a target checkbox, we want it to be checked/unchecked\n            // based on whether the existing link has target=_blank\n            if (targetCheckbox) {\n                targetCheckbox.checked = opts.target === '_blank';\n            }\n\n            // If we have a custom class checkbox, we want it to be checked/unchecked\n            // based on whether an existing link already has the class\n            if (buttonCheckbox) {\n                var classList = opts.buttonClass ? opts.buttonClass.split(' ') : [];\n                buttonCheckbox.checked = (classList.indexOf(this.customClassOption) !== -1);\n            }\n        },\n\n        // Called by core when tearing down medium-editor (destroy)\n        destroy: function () {\n            if (!this.form) {\n                return false;\n            }\n\n            if (this.form.parentNode) {\n                this.form.parentNode.removeChild(this.form);\n            }\n\n            delete this.form;\n        },\n\n        // core methods\n\n        getFormOpts: function () {\n            // no notion of private functions? wanted `_getFormOpts`\n            var targetCheckbox = this.getAnchorTargetCheckbox(),\n                buttonCheckbox = this.getAnchorButtonCheckbox(),\n                opts = {\n                    value: this.getInput().value.trim()\n                };\n\n            if (this.linkValidation) {\n                opts.value = this.checkLinkFormat(opts.value);\n            }\n\n            opts.target = '_self';\n            if (targetCheckbox && targetCheckbox.checked) {\n                opts.target = '_blank';\n            }\n\n            if (buttonCheckbox && buttonCheckbox.checked) {\n                opts.buttonClass = this.customClassOption;\n            }\n\n            return opts;\n        },\n\n        doFormSave: function () {\n            var opts = this.getFormOpts();\n            this.completeFormSave(opts);\n        },\n\n        completeFormSave: function (opts) {\n            this.base.restoreSelection();\n            this.execAction(this.action, opts);\n            this.base.checkSelection();\n        },\n\n        ensureEncodedUri: function (str) {\n            return str === decodeURI(str) ? encodeURI(str) : str;\n        },\n\n        ensureEncodedUriComponent: function (str) {\n            return str === decodeURIComponent(str) ? encodeURIComponent(str) : str;\n        },\n\n        ensureEncodedParam: function (param) {\n            var split = param.split('='),\n                key = split[0],\n                val = split[1];\n\n            return key + (val === undefined ? '' : '=' + this.ensureEncodedUriComponent(val));\n        },\n\n        ensureEncodedQuery: function (queryString) {\n            return queryString.split('&').map(this.ensureEncodedParam.bind(this)).join('&');\n        },\n\n        checkLinkFormat: function (value) {\n            // Matches any alphabetical characters followed by ://\n            // Matches protocol relative \"//\"\n            // Matches common external protocols \"mailto:\" \"tel:\" \"maps:\"\n            // Matches relative hash link, begins with \"#\"\n            var urlSchemeRegex = /^([a-z]+:)?\\/\\/|^(mailto|tel|maps):|^\\#/i,\n                hasScheme = urlSchemeRegex.test(value),\n                scheme = '',\n                // telRegex is a regex for checking if the string is a telephone number\n                telRegex = /^\\+?\\s?\\(?(?:\\d\\s?\\-?\\)?){3,20}$/,\n                urlParts = value.match(/^(.*?)(?:\\?(.*?))?(?:#(.*))?$/),\n                path = urlParts[1],\n                query = urlParts[2],\n                fragment = urlParts[3];\n\n            if (telRegex.test(value)) {\n                return 'tel:' + value;\n            }\n\n            if (!hasScheme) {\n                var host = path.split('/')[0];\n                // if the host part of the path looks like a hostname\n                if (host.match(/.+(\\.|:).+/) || host === 'localhost') {\n                    scheme = 'http://';\n                }\n            }\n\n            return scheme +\n                // Ensure path is encoded\n                this.ensureEncodedUri(path) +\n                // Ensure query is encoded\n                (query === undefined ? '' : '?' + this.ensureEncodedQuery(query)) +\n                // Include fragment unencoded as encodeUriComponent is too\n                // heavy handed for the many characters allowed in a fragment\n                (fragment === undefined ? '' : '#' + fragment);\n        },\n\n        doFormCancel: function () {\n            this.base.restoreSelection();\n            this.base.checkSelection();\n        },\n\n        // form creation and event handling\n        attachFormEvents: function (form) {\n            var close = form.querySelector('.medium-editor-toolbar-close'),\n                save = form.querySelector('.medium-editor-toolbar-save'),\n                input = form.querySelector('.medium-editor-toolbar-input');\n\n            // Handle clicks on the form itself\n            this.on(form, 'click', this.handleFormClick.bind(this));\n\n            // Handle typing in the textbox\n            this.on(input, 'keyup', this.handleTextboxKeyup.bind(this));\n\n            // Handle close button clicks\n            this.on(close, 'click', this.handleCloseClick.bind(this));\n\n            // Handle save button clicks (capture)\n            this.on(save, 'click', this.handleSaveClick.bind(this), true);\n\n        },\n\n        createForm: function () {\n            var doc = this.document,\n                form = doc.createElement('div');\n\n            // Anchor Form (div)\n            form.className = 'medium-editor-toolbar-form';\n            form.id = 'medium-editor-toolbar-form-anchor-' + this.getEditorId();\n            form.innerHTML = this.getTemplate();\n            this.attachFormEvents(form);\n\n            return form;\n        },\n\n        getInput: function () {\n            return this.getForm().querySelector('input.medium-editor-toolbar-input');\n        },\n\n        getAnchorTargetCheckbox: function () {\n            return this.getForm().querySelector('.medium-editor-toolbar-anchor-target');\n        },\n\n        getAnchorButtonCheckbox: function () {\n            return this.getForm().querySelector('.medium-editor-toolbar-anchor-button');\n        },\n\n        handleTextboxKeyup: function (event) {\n            // For ENTER -> create the anchor\n            if (event.keyCode === MediumEditor.util.keyCode.ENTER) {\n                event.preventDefault();\n                this.doFormSave();\n                return;\n            }\n\n            // For ESCAPE -> close the form\n            if (event.keyCode === MediumEditor.util.keyCode.ESCAPE) {\n                event.preventDefault();\n                this.doFormCancel();\n            }\n        },\n\n        handleFormClick: function (event) {\n            // make sure not to hide form when clicking inside the form\n            event.stopPropagation();\n        },\n\n        handleSaveClick: function (event) {\n            // Clicking Save -> create the anchor\n            event.preventDefault();\n            this.doFormSave();\n        },\n\n        handleCloseClick: function (event) {\n            // Click Close -> close the form\n            event.preventDefault();\n            this.doFormCancel();\n        }\n    });\n\n    MediumEditor.extensions.anchor = AnchorForm;\n}());\n\n(function () {\n    'use strict';\n\n    var AnchorPreview = MediumEditor.Extension.extend({\n        name: 'anchor-preview',\n\n        // Anchor Preview Options\n\n        /* hideDelay: [number]  (previously options.anchorPreviewHideDelay)\n         * time in milliseconds to show the anchor tag preview after the mouse has left the anchor tag.\n         */\n        hideDelay: 500,\n\n        /* previewValueSelector: [string]\n         * the default selector to locate where to put the activeAnchor value in the preview\n         */\n        previewValueSelector: 'a',\n\n        /* showWhenToolbarIsVisible: [boolean]\n         * determines whether the anchor tag preview shows up when the toolbar is visible\n         */\n        showWhenToolbarIsVisible: false,\n\n        /* showOnEmptyLinks: [boolean]\n        * determines whether the anchor tag preview shows up on links with href=\"\" or href=\"#something\"\n        */\n        showOnEmptyLinks: true,\n\n        init: function () {\n            this.anchorPreview = this.createPreview();\n\n            this.getEditorOption('elementsContainer').appendChild(this.anchorPreview);\n\n            this.attachToEditables();\n        },\n\n        getInteractionElements: function () {\n            return this.getPreviewElement();\n        },\n\n        // TODO: Remove this function in 6.0.0\n        getPreviewElement: function () {\n            return this.anchorPreview;\n        },\n\n        createPreview: function () {\n            var el = this.document.createElement('div');\n\n            el.id = 'medium-editor-anchor-preview-' + this.getEditorId();\n            el.className = 'medium-editor-anchor-preview';\n            el.innerHTML = this.getTemplate();\n\n            this.on(el, 'click', this.handleClick.bind(this));\n\n            return el;\n        },\n\n        getTemplate: function () {\n            return '<div class=\"medium-editor-toolbar-anchor-preview\" id=\"medium-editor-toolbar-anchor-preview\">' +\n                '    <a class=\"medium-editor-toolbar-anchor-preview-inner\"></a>' +\n                '</div>';\n        },\n\n        destroy: function () {\n            if (this.anchorPreview) {\n                if (this.anchorPreview.parentNode) {\n                    this.anchorPreview.parentNode.removeChild(this.anchorPreview);\n                }\n                delete this.anchorPreview;\n            }\n        },\n\n        hidePreview: function () {\n            if (this.anchorPreview) {\n                this.anchorPreview.classList.remove('medium-editor-anchor-preview-active');\n            }\n            this.activeAnchor = null;\n        },\n\n        showPreview: function (anchorEl) {\n            if (this.anchorPreview.classList.contains('medium-editor-anchor-preview-active') ||\n                    anchorEl.getAttribute('data-disable-preview')) {\n                return true;\n            }\n\n            if (this.previewValueSelector) {\n                this.anchorPreview.querySelector(this.previewValueSelector).textContent = anchorEl.attributes.href.value;\n                this.anchorPreview.querySelector(this.previewValueSelector).href = anchorEl.attributes.href.value;\n            }\n\n            this.anchorPreview.classList.add('medium-toolbar-arrow-over');\n            this.anchorPreview.classList.remove('medium-toolbar-arrow-under');\n\n            if (!this.anchorPreview.classList.contains('medium-editor-anchor-preview-active')) {\n                this.anchorPreview.classList.add('medium-editor-anchor-preview-active');\n            }\n\n            this.activeAnchor = anchorEl;\n\n            this.positionPreview();\n            this.attachPreviewHandlers();\n\n            return this;\n        },\n\n        positionPreview: function (activeAnchor) {\n            activeAnchor = activeAnchor || this.activeAnchor;\n            var containerWidth = this.window.innerWidth,\n                buttonHeight = this.anchorPreview.offsetHeight,\n                boundary = activeAnchor.getBoundingClientRect(),\n                diffLeft = this.diffLeft,\n                diffTop = this.diffTop,\n                elementsContainer = this.getEditorOption('elementsContainer'),\n                elementsContainerAbsolute = ['absolute', 'fixed'].indexOf(window.getComputedStyle(elementsContainer).getPropertyValue('position')) > -1,\n                relativeBoundary = {},\n                halfOffsetWidth, defaultLeft, middleBoundary, elementsContainerBoundary, top;\n\n            halfOffsetWidth = this.anchorPreview.offsetWidth / 2;\n            var toolbarExtension = this.base.getExtensionByName('toolbar');\n            if (toolbarExtension) {\n                diffLeft = toolbarExtension.diffLeft;\n                diffTop = toolbarExtension.diffTop;\n            }\n            defaultLeft = diffLeft - halfOffsetWidth;\n\n            // If container element is absolute / fixed, recalculate boundaries to be relative to the container\n            if (elementsContainerAbsolute) {\n                elementsContainerBoundary = elementsContainer.getBoundingClientRect();\n                ['top', 'left'].forEach(function (key) {\n                    relativeBoundary[key] = boundary[key] - elementsContainerBoundary[key];\n                });\n\n                relativeBoundary.width = boundary.width;\n                relativeBoundary.height = boundary.height;\n                boundary = relativeBoundary;\n\n                containerWidth = elementsContainerBoundary.width;\n\n                // Adjust top position according to container scroll position\n                top = elementsContainer.scrollTop;\n            } else {\n                // Adjust top position according to window scroll position\n                top = this.window.pageYOffset;\n            }\n\n            middleBoundary = boundary.left + boundary.width / 2;\n            top += buttonHeight + boundary.top + boundary.height - diffTop - this.anchorPreview.offsetHeight;\n\n            this.anchorPreview.style.top = Math.round(top) + 'px';\n            this.anchorPreview.style.right = 'initial';\n            if (middleBoundary < halfOffsetWidth) {\n                this.anchorPreview.style.left = defaultLeft + halfOffsetWidth + 'px';\n                this.anchorPreview.style.right = 'initial';\n            } else if ((containerWidth - middleBoundary) < halfOffsetWidth) {\n                this.anchorPreview.style.left = 'auto';\n                this.anchorPreview.style.right = 0;\n            } else {\n                this.anchorPreview.style.left = defaultLeft + middleBoundary + 'px';\n                this.anchorPreview.style.right = 'initial';\n            }\n        },\n\n        attachToEditables: function () {\n            this.subscribe('editableMouseover', this.handleEditableMouseover.bind(this));\n            this.subscribe('positionedToolbar', this.handlePositionedToolbar.bind(this));\n        },\n\n        handlePositionedToolbar: function () {\n            // If the toolbar is visible and positioned, we don't need to hide the preview\n            // when showWhenToolbarIsVisible is true\n            if (!this.showWhenToolbarIsVisible) {\n                this.hidePreview();\n            }\n        },\n\n        handleClick: function (event) {\n            var anchorExtension = this.base.getExtensionByName('anchor'),\n                activeAnchor = this.activeAnchor;\n\n            if (anchorExtension && activeAnchor) {\n                event.preventDefault();\n\n                this.base.selectElement(this.activeAnchor);\n\n                // Using setTimeout + delay because:\n                // We may actually be displaying the anchor form, which should be controlled by delay\n                this.base.delay(function () {\n                    if (activeAnchor) {\n                        var opts = {\n                            value: activeAnchor.attributes.href.value,\n                            target: activeAnchor.getAttribute('target'),\n                            buttonClass: activeAnchor.getAttribute('class')\n                        };\n                        anchorExtension.showForm(opts);\n                        activeAnchor = null;\n                    }\n                }.bind(this));\n            }\n\n            this.hidePreview();\n        },\n\n        handleAnchorMouseout: function () {\n            this.anchorToPreview = null;\n            this.off(this.activeAnchor, 'mouseout', this.instanceHandleAnchorMouseout);\n            this.instanceHandleAnchorMouseout = null;\n        },\n\n        handleEditableMouseover: function (event) {\n            var target = MediumEditor.util.getClosestTag(event.target, 'a');\n\n            if (false === target) {\n                return;\n            }\n\n            // Detect empty href attributes\n            // The browser will make href=\"\" or href=\"#top\"\n            // into absolute urls when accessed as event.target.href, so check the html\n            if (!this.showOnEmptyLinks &&\n                (!/href=[\"']\\S+[\"']/.test(target.outerHTML) || /href=[\"']#\\S+[\"']/.test(target.outerHTML))) {\n                return true;\n            }\n\n            // only show when toolbar is not present\n            var toolbar = this.base.getExtensionByName('toolbar');\n            if (!this.showWhenToolbarIsVisible && toolbar && toolbar.isDisplayed && toolbar.isDisplayed()) {\n                return true;\n            }\n\n            // detach handler for other anchor in case we hovered multiple anchors quickly\n            if (this.activeAnchor && this.activeAnchor !== target) {\n                this.detachPreviewHandlers();\n            }\n\n            this.anchorToPreview = target;\n\n            this.instanceHandleAnchorMouseout = this.handleAnchorMouseout.bind(this);\n            this.on(this.anchorToPreview, 'mouseout', this.instanceHandleAnchorMouseout);\n            // Using setTimeout + delay because:\n            // - We're going to show the anchor preview according to the configured delay\n            //   if the mouse has not left the anchor tag in that time\n            this.base.delay(function () {\n                if (this.anchorToPreview) {\n                    this.showPreview(this.anchorToPreview);\n                }\n            }.bind(this));\n        },\n\n        handlePreviewMouseover: function () {\n            this.lastOver = (new Date()).getTime();\n            this.hovering = true;\n        },\n\n        handlePreviewMouseout: function (event) {\n            if (!event.relatedTarget || !/anchor-preview/.test(event.relatedTarget.className)) {\n                this.hovering = false;\n            }\n        },\n\n        updatePreview: function () {\n            if (this.hovering) {\n                return true;\n            }\n            var durr = (new Date()).getTime() - this.lastOver;\n            if (durr > this.hideDelay) {\n                // hide the preview 1/2 second after mouse leaves the link\n                this.detachPreviewHandlers();\n            }\n        },\n\n        detachPreviewHandlers: function () {\n            // cleanup\n            clearInterval(this.intervalTimer);\n            if (this.instanceHandlePreviewMouseover) {\n                this.off(this.anchorPreview, 'mouseover', this.instanceHandlePreviewMouseover);\n                this.off(this.anchorPreview, 'mouseout', this.instanceHandlePreviewMouseout);\n                if (this.activeAnchor) {\n                    this.off(this.activeAnchor, 'mouseover', this.instanceHandlePreviewMouseover);\n                    this.off(this.activeAnchor, 'mouseout', this.instanceHandlePreviewMouseout);\n                }\n            }\n\n            this.hidePreview();\n\n            this.hovering = this.instanceHandlePreviewMouseover = this.instanceHandlePreviewMouseout = null;\n        },\n\n        // TODO: break up method and extract out handlers\n        attachPreviewHandlers: function () {\n            this.lastOver = (new Date()).getTime();\n            this.hovering = true;\n\n            this.instanceHandlePreviewMouseover = this.handlePreviewMouseover.bind(this);\n            this.instanceHandlePreviewMouseout = this.handlePreviewMouseout.bind(this);\n\n            this.intervalTimer = setInterval(this.updatePreview.bind(this), 200);\n\n            this.on(this.anchorPreview, 'mouseover', this.instanceHandlePreviewMouseover);\n            this.on(this.anchorPreview, 'mouseout', this.instanceHandlePreviewMouseout);\n            this.on(this.activeAnchor, 'mouseover', this.instanceHandlePreviewMouseover);\n            this.on(this.activeAnchor, 'mouseout', this.instanceHandlePreviewMouseout);\n        }\n    });\n\n    MediumEditor.extensions.anchorPreview = AnchorPreview;\n}());\n\n(function () {\n    'use strict';\n\n    var WHITESPACE_CHARS,\n        KNOWN_TLDS_FRAGMENT,\n        LINK_REGEXP_TEXT,\n        KNOWN_TLDS_REGEXP,\n        LINK_REGEXP;\n\n    WHITESPACE_CHARS = [' ', '\\t', '\\n', '\\r', '\\u00A0', '\\u2000', '\\u2001', '\\u2002', '\\u2003',\n                                    '\\u2028', '\\u2029'];\n    KNOWN_TLDS_FRAGMENT = 'com|net|org|edu|gov|mil|aero|asia|biz|cat|coop|info|int|jobs|mobi|museum|name|post|pro|tel|travel|' +\n        '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|' +\n        '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|' +\n        '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|' +\n        '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|' +\n        '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|' +\n        '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|' +\n        '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';\n\n    LINK_REGEXP_TEXT =\n        '(' +\n        // Version of Gruber URL Regexp optimized for JS: http://stackoverflow.com/a/17733640\n        '((?:(https?://|ftps?://|nntp://)|www\\\\d{0,3}[.]|[a-z0-9.\\\\-]+[.](' + KNOWN_TLDS_FRAGMENT + ')\\\\\\/)\\\\S+(?:[^\\\\s`!\\\\[\\\\]{};:\\'\\\".,?\\u00AB\\u00BB\\u201C\\u201D\\u2018\\u2019]))' +\n        // Addition to above Regexp to support bare domains/one level subdomains with common non-i18n TLDs and without www prefix:\n        ')|(([a-z0-9\\\\-]+\\\\.)?[a-z0-9\\\\-]+\\\\.(' + KNOWN_TLDS_FRAGMENT + '))';\n\n    KNOWN_TLDS_REGEXP = new RegExp('^(' + KNOWN_TLDS_FRAGMENT + ')$', 'i');\n\n    LINK_REGEXP = new RegExp(LINK_REGEXP_TEXT, 'gi');\n\n    function nodeIsNotInsideAnchorTag(node) {\n        return !MediumEditor.util.getClosestTag(node, 'a');\n    }\n\n    var AutoLink = MediumEditor.Extension.extend({\n        init: function () {\n            MediumEditor.Extension.prototype.init.apply(this, arguments);\n\n            this.disableEventHandling = false;\n            this.subscribe('editableKeypress', this.onKeypress.bind(this));\n            this.subscribe('editableBlur', this.onBlur.bind(this));\n            // MS IE has it's own auto-URL detect feature but ours is better in some ways. Be consistent.\n            this.document.execCommand('AutoUrlDetect', false, false);\n        },\n\n        isLastInstance: function () {\n            var activeInstances = 0;\n            for (var i = 0; i < this.window._mediumEditors.length; i++) {\n                var editor = this.window._mediumEditors[i];\n                if (editor !== null && editor.getExtensionByName('autoLink') !== undefined) {\n                    activeInstances++;\n                }\n            }\n            return activeInstances === 1;\n        },\n\n        destroy: function () {\n            // Turn AutoUrlDetect back on\n            if (this.document.queryCommandSupported('AutoUrlDetect') && this.isLastInstance()) {\n                this.document.execCommand('AutoUrlDetect', false, true);\n            }\n        },\n\n        onBlur: function (blurEvent, editable) {\n            this.performLinking(editable);\n        },\n\n        onKeypress: function (keyPressEvent) {\n            if (this.disableEventHandling) {\n                return;\n            }\n\n            if (MediumEditor.util.isKey(keyPressEvent, [MediumEditor.util.keyCode.SPACE, MediumEditor.util.keyCode.ENTER])) {\n                clearTimeout(this.performLinkingTimeout);\n                // Saving/restoring the selection in the middle of a keypress doesn't work well...\n                this.performLinkingTimeout = setTimeout(function () {\n                    try {\n                        var sel = this.base.exportSelection();\n                        if (this.performLinking(keyPressEvent.target)) {\n                            // pass true for favorLaterSelectionAnchor - this is needed for links at the end of a\n                            // paragraph in MS IE, or MS IE causes the link to be deleted right after adding it.\n                            this.base.importSelection(sel, true);\n                        }\n                    } catch (e) {\n                        if (window.console) {\n                            window.console.error('Failed to perform linking', e);\n                        }\n                        this.disableEventHandling = true;\n                    }\n                }.bind(this), 0);\n            }\n        },\n\n        performLinking: function (contenteditable) {\n            /*\n            Perform linking on blockElement basis, blockElements are HTML elements with text content and without\n            child element.\n\n            Example:\n            - HTML content\n            <blockquote>\n              <p>link.</p>\n              <p>my</p>\n            </blockquote>\n\n            - blockElements\n            [<p>link.</p>, <p>my</p>]\n\n            otherwise the detection can wrongly find the end of one paragraph and the beginning of another paragraph\n            to constitute a link, such as a paragraph ending \"link.\" and the next paragraph beginning with \"my\" is\n            interpreted into \"link.my\" and the code tries to create a link across blockElements - which doesn't work\n            and is terrible.\n            (Medium deletes the spaces/returns between P tags so the textContent ends up without paragraph spacing)\n            */\n            var blockElements = MediumEditor.util.splitByBlockElements(contenteditable),\n                documentModified = false;\n            if (blockElements.length === 0) {\n                blockElements = [contenteditable];\n            }\n            for (var i = 0; i < blockElements.length; i++) {\n                documentModified = this.removeObsoleteAutoLinkSpans(blockElements[i]) || documentModified;\n                documentModified = this.performLinkingWithinElement(blockElements[i]) || documentModified;\n            }\n            this.base.events.updateInput(contenteditable, { target: contenteditable, currentTarget: contenteditable });\n            return documentModified;\n        },\n\n        removeObsoleteAutoLinkSpans: function (element) {\n            if (!element || element.nodeType === 3) {\n                return false;\n            }\n\n            var spans = element.querySelectorAll('span[data-auto-link=\"true\"]'),\n                documentModified = false;\n\n            for (var i = 0; i < spans.length; i++) {\n                var textContent = spans[i].textContent;\n                if (textContent.indexOf('://') === -1) {\n                    textContent = MediumEditor.util.ensureUrlHasProtocol(textContent);\n                }\n                if (spans[i].getAttribute('data-href') !== textContent && nodeIsNotInsideAnchorTag(spans[i])) {\n                    documentModified = true;\n                    var trimmedTextContent = textContent.replace(/\\s+$/, '');\n                    if (spans[i].getAttribute('data-href') === trimmedTextContent) {\n                        var charactersTrimmed = textContent.length - trimmedTextContent.length,\n                            subtree = MediumEditor.util.splitOffDOMTree(spans[i], this.splitTextBeforeEnd(spans[i], charactersTrimmed));\n                        spans[i].parentNode.insertBefore(subtree, spans[i].nextSibling);\n                    } else {\n                        // Some editing has happened to the span, so just remove it entirely. The user can put it back\n                        // around just the href content if they need to prevent it from linking\n                        MediumEditor.util.unwrap(spans[i], this.document);\n                    }\n                }\n            }\n            return documentModified;\n        },\n\n        splitTextBeforeEnd: function (element, characterCount) {\n            var treeWalker = this.document.createTreeWalker(element, NodeFilter.SHOW_TEXT, null, false),\n                lastChildNotExhausted = true;\n\n            // Start the tree walker at the last descendant of the span\n            while (lastChildNotExhausted) {\n                lastChildNotExhausted = treeWalker.lastChild() !== null;\n            }\n\n            var currentNode,\n                currentNodeValue,\n                previousNode;\n            while (characterCount > 0 && previousNode !== null) {\n                currentNode = treeWalker.currentNode;\n                currentNodeValue = currentNode.nodeValue;\n                if (currentNodeValue.length > characterCount) {\n                    previousNode = currentNode.splitText(currentNodeValue.length - characterCount);\n                    characterCount = 0;\n                } else {\n                    previousNode = treeWalker.previousNode();\n                    characterCount -= currentNodeValue.length;\n                }\n            }\n            return previousNode;\n        },\n\n        performLinkingWithinElement: function (element) {\n            var matches = this.findLinkableText(element),\n                linkCreated = false;\n\n            for (var matchIndex = 0; matchIndex < matches.length; matchIndex++) {\n                var matchingTextNodes = MediumEditor.util.findOrCreateMatchingTextNodes(this.document, element,\n                        matches[matchIndex]);\n                if (this.shouldNotLink(matchingTextNodes)) {\n                    continue;\n                }\n                this.createAutoLink(matchingTextNodes, matches[matchIndex].href);\n            }\n            return linkCreated;\n        },\n\n        shouldNotLink: function (textNodes) {\n            var shouldNotLink = false;\n            for (var i = 0; i < textNodes.length && shouldNotLink === false; i++) {\n                // Do not link if the text node is either inside an anchor or inside span[data-auto-link]\n                shouldNotLink = !!MediumEditor.util.traverseUp(textNodes[i], function (node) {\n                    return node.nodeName.toLowerCase() === 'a' ||\n                        (node.getAttribute && node.getAttribute('data-auto-link') === 'true');\n                });\n            }\n            return shouldNotLink;\n        },\n\n        findLinkableText: function (contenteditable) {\n            var textContent = contenteditable.textContent,\n                match = null,\n                matches = [];\n\n            while ((match = LINK_REGEXP.exec(textContent)) !== null) {\n                var matchOk = true,\n                    matchEnd = match.index + match[0].length;\n                // If the regexp detected something as a link that has text immediately preceding/following it, bail out.\n                matchOk = (match.index === 0 || WHITESPACE_CHARS.indexOf(textContent[match.index - 1]) !== -1) &&\n                    (matchEnd === textContent.length || WHITESPACE_CHARS.indexOf(textContent[matchEnd]) !== -1);\n                // If the regexp detected a bare domain that doesn't use one of our expected TLDs, bail out.\n                matchOk = matchOk && (match[0].indexOf('/') !== -1 ||\n                    KNOWN_TLDS_REGEXP.test(match[0].split('.').pop().split('?').shift()));\n\n                if (matchOk) {\n                    matches.push({\n                        href: match[0],\n                        start: match.index,\n                        end: matchEnd\n                    });\n                }\n            }\n            return matches;\n        },\n\n        createAutoLink: function (textNodes, href) {\n            href = MediumEditor.util.ensureUrlHasProtocol(href);\n            var anchor = MediumEditor.util.createLink(this.document, textNodes, href, this.getEditorOption('targetBlank') ? '_blank' : null),\n                span = this.document.createElement('span');\n            span.setAttribute('data-auto-link', 'true');\n            span.setAttribute('data-href', href);\n            anchor.insertBefore(span, anchor.firstChild);\n            while (anchor.childNodes.length > 1) {\n                span.appendChild(anchor.childNodes[1]);\n            }\n        }\n\n    });\n\n    MediumEditor.extensions.autoLink = AutoLink;\n}());\n\n(function () {\n    'use strict';\n\n    var CLASS_DRAG_OVER = 'medium-editor-dragover';\n\n    function clearClassNames(element) {\n        var editable = MediumEditor.util.getContainerEditorElement(element),\n            existing = Array.prototype.slice.call(editable.parentElement.querySelectorAll('.' + CLASS_DRAG_OVER));\n\n        existing.forEach(function (el) {\n            el.classList.remove(CLASS_DRAG_OVER);\n        });\n    }\n\n    var FileDragging = MediumEditor.Extension.extend({\n        name: 'fileDragging',\n\n        allowedTypes: ['image'],\n\n        init: function () {\n            MediumEditor.Extension.prototype.init.apply(this, arguments);\n\n            this.subscribe('editableDrag', this.handleDrag.bind(this));\n            this.subscribe('editableDrop', this.handleDrop.bind(this));\n        },\n\n        handleDrag: function (event) {\n            event.preventDefault();\n            event.dataTransfer.dropEffect = 'copy';\n\n            var target = event.target.classList ? event.target : event.target.parentElement;\n\n            // Ensure the class gets removed from anything that had it before\n            clearClassNames(target);\n\n            if (event.type === 'dragover') {\n                target.classList.add(CLASS_DRAG_OVER);\n            }\n        },\n\n        handleDrop: function (event) {\n            // Prevent file from opening in the current window\n            event.preventDefault();\n            event.stopPropagation();\n            // Select the dropping target, and set the selection to the end of the target\n            // https://github.com/yabwe/medium-editor/issues/980\n            this.base.selectElement(event.target);\n            var selection = this.base.exportSelection();\n            selection.start = selection.end;\n            this.base.importSelection(selection);\n            // IE9 does not support the File API, so prevent file from opening in the window\n            // but also don't try to actually get the file\n            if (event.dataTransfer.files) {\n                Array.prototype.slice.call(event.dataTransfer.files).forEach(function (file) {\n                    if (this.isAllowedFile(file)) {\n                        if (file.type.match('image')) {\n                            this.insertImageFile(file);\n                        }\n                    }\n                }, this);\n            }\n\n            // Make sure we remove our class from everything\n            clearClassNames(event.target);\n        },\n\n        isAllowedFile: function (file) {\n            return this.allowedTypes.some(function (fileType) {\n                return !!file.type.match(fileType);\n            });\n        },\n\n        insertImageFile: function (file) {\n            if (typeof FileReader !== 'function') {\n                return;\n            }\n            var fileReader = new FileReader();\n            fileReader.readAsDataURL(file);\n\n            // attach the onload event handler, makes it easier to listen in with jasmine\n            fileReader.addEventListener('load', function (e) {\n                var addImageElement = this.document.createElement('img');\n                addImageElement.src = e.target.result;\n                MediumEditor.util.insertHTMLCommand(this.document, addImageElement.outerHTML);\n            }.bind(this));\n        }\n    });\n\n    MediumEditor.extensions.fileDragging = FileDragging;\n}());\n\n(function () {\n    'use strict';\n\n    var KeyboardCommands = MediumEditor.Extension.extend({\n        name: 'keyboard-commands',\n\n        /* KeyboardCommands Options */\n\n        /* commands: [Array]\n         * Array of objects describing each command and the combination of keys that will trigger it\n         * Required for each object:\n         *   command [String] (argument passed to editor.execAction())\n         *   key [String] (keyboard character that triggers this command)\n         *   meta [boolean] (whether the ctrl/meta key has to be active or inactive)\n         *   shift [boolean] (whether the shift key has to be active or inactive)\n         *   alt [boolean] (whether the alt key has to be active or inactive)\n         */\n        commands: [\n            {\n                command: 'bold',\n                key: 'B',\n                meta: true,\n                shift: false,\n                alt: false\n            },\n            {\n                command: 'italic',\n                key: 'I',\n                meta: true,\n                shift: false,\n                alt: false\n            },\n            {\n                command: 'underline',\n                key: 'U',\n                meta: true,\n                shift: false,\n                alt: false\n            }\n        ],\n\n        init: function () {\n            MediumEditor.Extension.prototype.init.apply(this, arguments);\n\n            this.subscribe('editableKeydown', this.handleKeydown.bind(this));\n            this.keys = {};\n            this.commands.forEach(function (command) {\n                var keyCode = command.key.charCodeAt(0);\n                if (!this.keys[keyCode]) {\n                    this.keys[keyCode] = [];\n                }\n                this.keys[keyCode].push(command);\n            }, this);\n        },\n\n        handleKeydown: function (event) {\n            var keyCode = MediumEditor.util.getKeyCode(event);\n            if (!this.keys[keyCode]) {\n                return;\n            }\n\n            var isMeta = MediumEditor.util.isMetaCtrlKey(event),\n                isShift = !!event.shiftKey,\n                isAlt = !!event.altKey;\n\n            this.keys[keyCode].forEach(function (data) {\n                if (data.meta === isMeta &&\n                    data.shift === isShift &&\n                    (data.alt === isAlt ||\n                     undefined === data.alt)) { // TODO deprecated: remove check for undefined === data.alt when jumping to 6.0.0\n                    event.preventDefault();\n                    event.stopPropagation();\n\n                    // command can be a function to execute\n                    if (typeof data.command === 'function') {\n                        data.command.apply(this);\n                    }\n                    // command can be false so the shortcut is just disabled\n                    else if (false !== data.command) {\n                        this.execAction(data.command);\n                    }\n                }\n            }, this);\n        }\n    });\n\n    MediumEditor.extensions.keyboardCommands = KeyboardCommands;\n}());\n\n(function () {\n    'use strict';\n\n    var FontNameForm = MediumEditor.extensions.form.extend({\n\n        name: 'fontname',\n        action: 'fontName',\n        aria: 'change font name',\n        contentDefault: '&#xB1;', // ±\n        contentFA: '<i class=\"fa fa-font\"></i>',\n\n        fonts: ['', 'Arial', 'Verdana', 'Times New Roman'],\n\n        init: function () {\n            MediumEditor.extensions.form.prototype.init.apply(this, arguments);\n        },\n\n        // Called when the button the toolbar is clicked\n        // Overrides ButtonExtension.handleClick\n        handleClick: function (event) {\n            event.preventDefault();\n            event.stopPropagation();\n\n            if (!this.isDisplayed()) {\n                // Get FontName of current selection (convert to string since IE returns this as number)\n                var fontName = this.document.queryCommandValue('fontName') + '';\n                this.showForm(fontName);\n            }\n\n            return false;\n        },\n\n        // Called by medium-editor to append form to the toolbar\n        getForm: function () {\n            if (!this.form) {\n                this.form = this.createForm();\n            }\n            return this.form;\n        },\n\n        // Used by medium-editor when the default toolbar is to be displayed\n        isDisplayed: function () {\n            return this.getForm().style.display === 'block';\n        },\n\n        hideForm: function () {\n            this.getForm().style.display = 'none';\n            this.getSelect().value = '';\n        },\n\n        showForm: function (fontName) {\n            var select = this.getSelect();\n\n            this.base.saveSelection();\n            this.hideToolbarDefaultActions();\n            this.getForm().style.display = 'block';\n            this.setToolbarPosition();\n\n            select.value = fontName || '';\n            select.focus();\n        },\n\n        // Called by core when tearing down medium-editor (destroy)\n        destroy: function () {\n            if (!this.form) {\n                return false;\n            }\n\n            if (this.form.parentNode) {\n                this.form.parentNode.removeChild(this.form);\n            }\n\n            delete this.form;\n        },\n\n        // core methods\n\n        doFormSave: function () {\n            this.base.restoreSelection();\n            this.base.checkSelection();\n        },\n\n        doFormCancel: function () {\n            this.base.restoreSelection();\n            this.clearFontName();\n            this.base.checkSelection();\n        },\n\n        // form creation and event handling\n        createForm: function () {\n            var doc = this.document,\n                form = doc.createElement('div'),\n                select = doc.createElement('select'),\n                close = doc.createElement('a'),\n                save = doc.createElement('a'),\n                option;\n\n            // Font Name Form (div)\n            form.className = 'medium-editor-toolbar-form';\n            form.id = 'medium-editor-toolbar-form-fontname-' + this.getEditorId();\n\n            // Handle clicks on the form itself\n            this.on(form, 'click', this.handleFormClick.bind(this));\n\n            // Add font names\n            for (var i = 0; i<this.fonts.length; i++) {\n                option = doc.createElement('option');\n                option.innerHTML = this.fonts[i];\n                option.value = this.fonts[i];\n                select.appendChild(option);\n            }\n\n            select.className = 'medium-editor-toolbar-select';\n            form.appendChild(select);\n\n            // Handle typing in the textbox\n            this.on(select, 'change', this.handleFontChange.bind(this));\n\n            // Add save buton\n            save.setAttribute('href', '#');\n            save.className = 'medium-editor-toobar-save';\n            save.innerHTML = this.getEditorOption('buttonLabels') === 'fontawesome' ?\n                             '<i class=\"fa fa-check\"></i>' :\n                             '&#10003;';\n            form.appendChild(save);\n\n            // Handle save button clicks (capture)\n            this.on(save, 'click', this.handleSaveClick.bind(this), true);\n\n            // Add close button\n            close.setAttribute('href', '#');\n            close.className = 'medium-editor-toobar-close';\n            close.innerHTML = this.getEditorOption('buttonLabels') === 'fontawesome' ?\n                              '<i class=\"fa fa-times\"></i>' :\n                              '&times;';\n            form.appendChild(close);\n\n            // Handle close button clicks\n            this.on(close, 'click', this.handleCloseClick.bind(this));\n\n            return form;\n        },\n\n        getSelect: function () {\n            return this.getForm().querySelector('select.medium-editor-toolbar-select');\n        },\n\n        clearFontName: function () {\n            MediumEditor.selection.getSelectedElements(this.document).forEach(function (el) {\n                if (el.nodeName.toLowerCase() === 'font' && el.hasAttribute('face')) {\n                    el.removeAttribute('face');\n                }\n            });\n        },\n\n        handleFontChange: function () {\n            var font = this.getSelect().value;\n            if (font === '') {\n                this.clearFontName();\n            } else {\n                this.execAction('fontName', { value: font });\n            }\n        },\n\n        handleFormClick: function (event) {\n            // make sure not to hide form when clicking inside the form\n            event.stopPropagation();\n        },\n\n        handleSaveClick: function (event) {\n            // Clicking Save -> create the font size\n            event.preventDefault();\n            this.doFormSave();\n        },\n\n        handleCloseClick: function (event) {\n            // Click Close -> close the form\n            event.preventDefault();\n            this.doFormCancel();\n        }\n    });\n\n    MediumEditor.extensions.fontName = FontNameForm;\n}());\n\n(function () {\n    'use strict';\n\n    var FontSizeForm = MediumEditor.extensions.form.extend({\n\n        name: 'fontsize',\n        action: 'fontSize',\n        aria: 'increase/decrease font size',\n        contentDefault: '&#xB1;', // ±\n        contentFA: '<i class=\"fa fa-text-height\"></i>',\n\n        init: function () {\n            MediumEditor.extensions.form.prototype.init.apply(this, arguments);\n        },\n\n        // Called when the button the toolbar is clicked\n        // Overrides ButtonExtension.handleClick\n        handleClick: function (event) {\n            event.preventDefault();\n            event.stopPropagation();\n\n            if (!this.isDisplayed()) {\n                // Get fontsize of current selection (convert to string since IE returns this as number)\n                var fontSize = this.document.queryCommandValue('fontSize') + '';\n                this.showForm(fontSize);\n            }\n\n            return false;\n        },\n\n        // Called by medium-editor to append form to the toolbar\n        getForm: function () {\n            if (!this.form) {\n                this.form = this.createForm();\n            }\n            return this.form;\n        },\n\n        // Used by medium-editor when the default toolbar is to be displayed\n        isDisplayed: function () {\n            return this.getForm().style.display === 'block';\n        },\n\n        hideForm: function () {\n            this.getForm().style.display = 'none';\n            this.getInput().value = '';\n        },\n\n        showForm: function (fontSize) {\n            var input = this.getInput();\n\n            this.base.saveSelection();\n            this.hideToolbarDefaultActions();\n            this.getForm().style.display = 'block';\n            this.setToolbarPosition();\n\n            input.value = fontSize || '';\n            input.focus();\n        },\n\n        // Called by core when tearing down medium-editor (destroy)\n        destroy: function () {\n            if (!this.form) {\n                return false;\n            }\n\n            if (this.form.parentNode) {\n                this.form.parentNode.removeChild(this.form);\n            }\n\n            delete this.form;\n        },\n\n        // core methods\n\n        doFormSave: function () {\n            this.base.restoreSelection();\n            this.base.checkSelection();\n        },\n\n        doFormCancel: function () {\n            this.base.restoreSelection();\n            this.clearFontSize();\n            this.base.checkSelection();\n        },\n\n        // form creation and event handling\n        createForm: function () {\n            var doc = this.document,\n                form = doc.createElement('div'),\n                input = doc.createElement('input'),\n                close = doc.createElement('a'),\n                save = doc.createElement('a');\n\n            // Font Size Form (div)\n            form.className = 'medium-editor-toolbar-form';\n            form.id = 'medium-editor-toolbar-form-fontsize-' + this.getEditorId();\n\n            // Handle clicks on the form itself\n            this.on(form, 'click', this.handleFormClick.bind(this));\n\n            // Add font size slider\n            input.setAttribute('type', 'range');\n            input.setAttribute('min', '1');\n            input.setAttribute('max', '7');\n            input.className = 'medium-editor-toolbar-input';\n            form.appendChild(input);\n\n            // Handle typing in the textbox\n            this.on(input, 'change', this.handleSliderChange.bind(this));\n\n            // Add save buton\n            save.setAttribute('href', '#');\n            save.className = 'medium-editor-toobar-save';\n            save.innerHTML = this.getEditorOption('buttonLabels') === 'fontawesome' ?\n                             '<i class=\"fa fa-check\"></i>' :\n                             '&#10003;';\n            form.appendChild(save);\n\n            // Handle save button clicks (capture)\n            this.on(save, 'click', this.handleSaveClick.bind(this), true);\n\n            // Add close button\n            close.setAttribute('href', '#');\n            close.className = 'medium-editor-toobar-close';\n            close.innerHTML = this.getEditorOption('buttonLabels') === 'fontawesome' ?\n                              '<i class=\"fa fa-times\"></i>' :\n                              '&times;';\n            form.appendChild(close);\n\n            // Handle close button clicks\n            this.on(close, 'click', this.handleCloseClick.bind(this));\n\n            return form;\n        },\n\n        getInput: function () {\n            return this.getForm().querySelector('input.medium-editor-toolbar-input');\n        },\n\n        clearFontSize: function () {\n            MediumEditor.selection.getSelectedElements(this.document).forEach(function (el) {\n                if (el.nodeName.toLowerCase() === 'font' && el.hasAttribute('size')) {\n                    el.removeAttribute('size');\n                }\n            });\n        },\n\n        handleSliderChange: function () {\n            var size = this.getInput().value;\n            if (size === '4') {\n                this.clearFontSize();\n            } else {\n                this.execAction('fontSize', { value: size });\n            }\n        },\n\n        handleFormClick: function (event) {\n            // make sure not to hide form when clicking inside the form\n            event.stopPropagation();\n        },\n\n        handleSaveClick: function (event) {\n            // Clicking Save -> create the font size\n            event.preventDefault();\n            this.doFormSave();\n        },\n\n        handleCloseClick: function (event) {\n            // Click Close -> close the form\n            event.preventDefault();\n            this.doFormCancel();\n        }\n    });\n\n    MediumEditor.extensions.fontSize = FontSizeForm;\n}());\n(function () {\n    'use strict';\n\n    /* Helpers and internal variables that don't need to be members of actual paste handler */\n\n    var pasteBinDefaultContent = '%ME_PASTEBIN%',\n        lastRange = null,\n        keyboardPasteEditable = null,\n        stopProp = function (event) {\n            event.stopPropagation();\n        };\n\n    /*jslint regexp: true*/\n    /*\n        jslint does not allow character negation, because the negation\n        will not match any unicode characters. In the regexes in this\n        block, negation is used specifically to match the end of an html\n        tag, and in fact unicode characters *should* be allowed.\n    */\n    function createReplacements() {\n        return [\n            // Remove anything but the contents within the BODY element\n            [new RegExp(/^[\\s\\S]*<body[^>]*>\\s*|\\s*<\\/body[^>]*>[\\s\\S]*$/g), ''],\n\n            // cleanup comments added by Chrome when pasting html\n            [new RegExp(/<!--StartFragment-->|<!--EndFragment-->/g), ''],\n\n            // Trailing BR elements\n            [new RegExp(/<br>$/i), ''],\n\n            // replace two bogus tags that begin pastes from google docs\n            [new RegExp(/<[^>]*docs-internal-guid[^>]*>/gi), ''],\n            [new RegExp(/<\\/b>(<br[^>]*>)?$/gi), ''],\n\n             // un-html spaces and newlines inserted by OS X\n            [new RegExp(/<span class=\"Apple-converted-space\">\\s+<\\/span>/g), ' '],\n            [new RegExp(/<br class=\"Apple-interchange-newline\">/g), '<br>'],\n\n            // replace google docs italics+bold with a span to be replaced once the html is inserted\n            [new RegExp(/<span[^>]*(font-style:italic;font-weight:(bold|700)|font-weight:(bold|700);font-style:italic)[^>]*>/gi), '<span class=\"replace-with italic bold\">'],\n\n            // replace google docs italics with a span to be replaced once the html is inserted\n            [new RegExp(/<span[^>]*font-style:italic[^>]*>/gi), '<span class=\"replace-with italic\">'],\n\n            //[replace google docs bolds with a span to be replaced once the html is inserted\n            [new RegExp(/<span[^>]*font-weight:(bold|700)[^>]*>/gi), '<span class=\"replace-with bold\">'],\n\n             // replace manually entered b/i/a tags with real ones\n            [new RegExp(/&lt;(\\/?)(i|b|a)&gt;/gi), '<$1$2>'],\n\n             // replace manually a tags with real ones, converting smart-quotes from google docs\n            [new RegExp(/&lt;a(?:(?!href).)+href=(?:&quot;|&rdquo;|&ldquo;|\"|“|”)(((?!&quot;|&rdquo;|&ldquo;|\"|“|”).)*)(?:&quot;|&rdquo;|&ldquo;|\"|“|”)(?:(?!&gt;).)*&gt;/gi), '<a href=\"$1\">'],\n\n            // Newlines between paragraphs in html have no syntactic value,\n            // but then have a tendency to accidentally become additional paragraphs down the line\n            [new RegExp(/<\\/p>\\n+/gi), '</p>'],\n            [new RegExp(/\\n+<p/gi), '<p'],\n\n            // Microsoft Word makes these odd tags, like <o:p></o:p>\n            [new RegExp(/<\\/?o:[a-z]*>/gi), ''],\n\n            // Microsoft Word adds some special elements around list items\n            [new RegExp(/<!\\[if !supportLists\\]>(((?!<!).)*)<!\\[endif]\\>/gi), '$1']\n        ];\n    }\n    /*jslint regexp: false*/\n\n    /**\n     * Gets various content types out of the Clipboard API. It will also get the\n     * plain text using older IE and WebKit API.\n     *\n     * @param {event} event Event fired on paste.\n     * @param {win} reference to window\n     * @param {doc} reference to document\n     * @return {Object} Object with mime types and data for those mime types.\n     */\n    function getClipboardContent(event, win, doc) {\n        var dataTransfer = event.clipboardData || win.clipboardData || doc.dataTransfer,\n            data = {};\n\n        if (!dataTransfer) {\n            return data;\n        }\n\n        // Use old WebKit/IE API\n        if (dataTransfer.getData) {\n            var legacyText = dataTransfer.getData('Text');\n            if (legacyText && legacyText.length > 0) {\n                data['text/plain'] = legacyText;\n            }\n        }\n\n        if (dataTransfer.types) {\n            for (var i = 0; i < dataTransfer.types.length; i++) {\n                var contentType = dataTransfer.types[i];\n                data[contentType] = dataTransfer.getData(contentType);\n            }\n        }\n\n        return data;\n    }\n\n    var PasteHandler = MediumEditor.Extension.extend({\n        /* Paste Options */\n\n        /* forcePlainText: [boolean]\n         * Forces pasting as plain text.\n         */\n        forcePlainText: true,\n\n        /* cleanPastedHTML: [boolean]\n         * cleans pasted content from different sources, like google docs etc.\n         */\n        cleanPastedHTML: false,\n\n        /* preCleanReplacements: [Array]\n         * custom pairs (2 element arrays) of RegExp and replacement text to use during past when\n         * __forcePlainText__ or __cleanPastedHTML__ are `true` OR when calling `cleanPaste(text)` helper method.\n         * These replacements are executed before any medium editor defined replacements.\n         */\n        preCleanReplacements: [],\n\n        /* cleanReplacements: [Array]\n         * custom pairs (2 element arrays) of RegExp and replacement text to use during paste when\n         * __forcePlainText__ or __cleanPastedHTML__ are `true` OR when calling `cleanPaste(text)` helper method.\n         * These replacements are executed after any medium editor defined replacements.\n         */\n        cleanReplacements: [],\n\n        /* cleanAttrs:: [Array]\n         * list of element attributes to remove during paste when __cleanPastedHTML__ is `true` or when\n         * calling `cleanPaste(text)` or `pasteHTML(html, options)` helper methods.\n         */\n        cleanAttrs: ['class', 'style', 'dir'],\n\n        /* cleanTags: [Array]\n         * list of element tag names to remove during paste when __cleanPastedHTML__ is `true` or when\n         * calling `cleanPaste(text)` or `pasteHTML(html, options)` helper methods.\n         */\n        cleanTags: ['meta'],\n\n        /* unwrapTags: [Array]\n         * list of element tag names to unwrap (remove the element tag but retain its child elements)\n         * during paste when __cleanPastedHTML__ is `true` or when\n         * calling `cleanPaste(text)` or `pasteHTML(html, options)` helper methods.\n         */\n        unwrapTags: [],\n\n        init: function () {\n            MediumEditor.Extension.prototype.init.apply(this, arguments);\n\n            if (this.forcePlainText || this.cleanPastedHTML) {\n                this.subscribe('editableKeydown', this.handleKeydown.bind(this));\n                // We need access to the full event data in paste\n                // so we can't use the editablePaste event here\n                this.getEditorElements().forEach(function (element) {\n                    this.on(element, 'paste', this.handlePaste.bind(this));\n                }, this);\n                this.subscribe('addElement', this.handleAddElement.bind(this));\n            }\n        },\n\n        handleAddElement: function (event, editable) {\n            this.on(editable, 'paste', this.handlePaste.bind(this));\n        },\n\n        destroy: function () {\n            // Make sure pastebin is destroyed in case it's still around for some reason\n            if (this.forcePlainText || this.cleanPastedHTML) {\n                this.removePasteBin();\n            }\n        },\n\n        handlePaste: function (event, editable) {\n            if (event.defaultPrevented) {\n                return;\n            }\n\n            var clipboardContent = getClipboardContent(event, this.window, this.document),\n                pastedHTML = clipboardContent['text/html'],\n                pastedPlain = clipboardContent['text/plain'];\n\n            if (this.window.clipboardData && event.clipboardData === undefined && !pastedHTML) {\n                // If window.clipboardData exists, but event.clipboardData doesn't exist,\n                // we're probably in IE. IE only has two possibilities for clipboard\n                // data format: 'Text' and 'URL'.\n                //\n                // For IE, we'll fallback to 'Text' for text/html\n                pastedHTML = pastedPlain;\n            }\n\n            if (pastedHTML || pastedPlain) {\n                event.preventDefault();\n\n                this.doPaste(pastedHTML, pastedPlain, editable);\n            }\n        },\n\n        doPaste: function (pastedHTML, pastedPlain, editable) {\n            var paragraphs,\n                html = '',\n                p;\n\n            if (this.cleanPastedHTML && pastedHTML) {\n                return this.cleanPaste(pastedHTML);\n            }\n\n            if (!pastedPlain) {\n                return;\n            }\n\n            if (!(this.getEditorOption('disableReturn') || (editable && editable.getAttribute('data-disable-return')))) {\n                paragraphs = pastedPlain.split(/[\\r\\n]+/g);\n                // If there are no \\r\\n in data, don't wrap in <p>\n                if (paragraphs.length > 1) {\n                    for (p = 0; p < paragraphs.length; p += 1) {\n                        if (paragraphs[p] !== '') {\n                            html += '<p>' + MediumEditor.util.htmlEntities(paragraphs[p]) + '</p>';\n                        }\n                    }\n                } else {\n                    html = MediumEditor.util.htmlEntities(paragraphs[0]);\n                }\n            } else {\n                html = MediumEditor.util.htmlEntities(pastedPlain);\n            }\n            MediumEditor.util.insertHTMLCommand(this.document, html);\n        },\n\n        handlePasteBinPaste: function (event) {\n            if (event.defaultPrevented) {\n                this.removePasteBin();\n                return;\n            }\n\n            var clipboardContent = getClipboardContent(event, this.window, this.document),\n                pastedHTML = clipboardContent['text/html'],\n                pastedPlain = clipboardContent['text/plain'],\n                editable = keyboardPasteEditable;\n\n            // If we have valid html already, or we're not in cleanPastedHTML mode\n            // we can ignore the paste bin and just paste now\n            if (!this.cleanPastedHTML || pastedHTML) {\n                event.preventDefault();\n                this.removePasteBin();\n                this.doPaste(pastedHTML, pastedPlain, editable);\n\n                // The event handling code listens for paste on the editable element\n                // in order to trigger the editablePaste event.  Since this paste event\n                // is happening on the pastebin, the event handling code never knows about it\n                // So, we have to trigger editablePaste manually\n                this.trigger('editablePaste', { currentTarget: editable, target: editable }, editable);\n                return;\n            }\n\n            // We need to look at the paste bin, so do a setTimeout to let the paste\n            // fall through into the paste bin\n            setTimeout(function () {\n                // Only look for HTML if we're in cleanPastedHTML mode\n                if (this.cleanPastedHTML) {\n                    // If clipboard didn't have HTML, try the paste bin\n                    pastedHTML = this.getPasteBinHtml();\n                }\n\n                // If we needed the paste bin, we're done with it now, remove it\n                this.removePasteBin();\n\n                // Handle the paste with the html from the paste bin\n                this.doPaste(pastedHTML, pastedPlain, editable);\n\n                // The event handling code listens for paste on the editable element\n                // in order to trigger the editablePaste event.  Since this paste event\n                // is happening on the pastebin, the event handling code never knows about it\n                // So, we have to trigger editablePaste manually\n                this.trigger('editablePaste', { currentTarget: editable, target: editable }, editable);\n            }.bind(this), 0);\n        },\n\n        handleKeydown: function (event, editable) {\n            // if it's not Ctrl+V, do nothing\n            if (!(MediumEditor.util.isKey(event, MediumEditor.util.keyCode.V) && MediumEditor.util.isMetaCtrlKey(event))) {\n                return;\n            }\n\n            event.stopImmediatePropagation();\n\n            this.removePasteBin();\n            this.createPasteBin(editable);\n        },\n\n        createPasteBin: function (editable) {\n            var rects,\n                range = MediumEditor.selection.getSelectionRange(this.document),\n                top = this.window.pageYOffset;\n\n            keyboardPasteEditable = editable;\n\n            if (range) {\n                rects = range.getClientRects();\n\n                // on empty line, rects is empty so we grab information from the first container of the range\n                if (rects.length) {\n                    top += rects[0].top;\n                } else if (range.startContainer.getBoundingClientRect !== undefined) {\n                    top += range.startContainer.getBoundingClientRect().top;\n                } else {\n                    top += range.getBoundingClientRect().top;\n                }\n            }\n\n            lastRange = range;\n\n            var pasteBinElm = this.document.createElement('div');\n            pasteBinElm.id = this.pasteBinId = 'medium-editor-pastebin-' + (+Date.now());\n            pasteBinElm.setAttribute('style', 'border: 1px red solid; position: absolute; top: ' + top + 'px; width: 10px; height: 10px; overflow: hidden; opacity: 0');\n            pasteBinElm.setAttribute('contentEditable', true);\n            pasteBinElm.innerHTML = pasteBinDefaultContent;\n\n            this.document.body.appendChild(pasteBinElm);\n\n            // avoid .focus() to stop other event (actually the paste event)\n            this.on(pasteBinElm, 'focus', stopProp);\n            this.on(pasteBinElm, 'focusin', stopProp);\n            this.on(pasteBinElm, 'focusout', stopProp);\n\n            pasteBinElm.focus();\n\n            MediumEditor.selection.selectNode(pasteBinElm, this.document);\n\n            if (!this.boundHandlePaste) {\n                this.boundHandlePaste = this.handlePasteBinPaste.bind(this);\n            }\n\n            this.on(pasteBinElm, 'paste', this.boundHandlePaste);\n        },\n\n        removePasteBin: function () {\n            if (null !== lastRange) {\n                MediumEditor.selection.selectRange(this.document, lastRange);\n                lastRange = null;\n            }\n\n            if (null !== keyboardPasteEditable) {\n                keyboardPasteEditable = null;\n            }\n\n            var pasteBinElm = this.getPasteBin();\n            if (!pasteBinElm) {\n                return;\n            }\n\n            if (pasteBinElm) {\n                this.off(pasteBinElm, 'focus', stopProp);\n                this.off(pasteBinElm, 'focusin', stopProp);\n                this.off(pasteBinElm, 'focusout', stopProp);\n                this.off(pasteBinElm, 'paste', this.boundHandlePaste);\n                pasteBinElm.parentElement.removeChild(pasteBinElm);\n            }\n        },\n\n        getPasteBin: function () {\n            return this.document.getElementById(this.pasteBinId);\n        },\n\n        getPasteBinHtml: function () {\n            var pasteBinElm = this.getPasteBin();\n\n            if (!pasteBinElm) {\n                return false;\n            }\n\n            // WebKit has a nice bug where it clones the paste bin if you paste from for example notepad\n            // so we need to force plain text mode in this case\n            if (pasteBinElm.firstChild && pasteBinElm.firstChild.id === 'mcepastebin') {\n                return false;\n            }\n\n            var pasteBinHtml = pasteBinElm.innerHTML;\n\n            // If paste bin is empty try using plain text mode\n            // since that is better than nothing right\n            if (!pasteBinHtml || pasteBinHtml === pasteBinDefaultContent) {\n                return false;\n            }\n\n            return pasteBinHtml;\n        },\n\n        cleanPaste: function (text) {\n            var i, elList, tmp, workEl,\n                multiline = /<p|<br|<div/.test(text),\n                replacements = [].concat(\n                    this.preCleanReplacements || [],\n                    createReplacements(),\n                    this.cleanReplacements || []);\n\n            for (i = 0; i < replacements.length; i += 1) {\n                text = text.replace(replacements[i][0], replacements[i][1]);\n            }\n\n            if (!multiline) {\n                return this.pasteHTML(text);\n            }\n\n            // create a temporary div to cleanup block elements\n            tmp = this.document.createElement('div');\n\n            // double br's aren't converted to p tags, but we want paragraphs.\n            tmp.innerHTML = '<p>' + text.split('<br><br>').join('</p><p>') + '</p>';\n\n            // block element cleanup\n            elList = tmp.querySelectorAll('a,p,div,br');\n            for (i = 0; i < elList.length; i += 1) {\n                workEl = elList[i];\n\n                // Microsoft Word replaces some spaces with newlines.\n                // While newlines between block elements are meaningless, newlines within\n                // elements are sometimes actually spaces.\n                workEl.innerHTML = workEl.innerHTML.replace(/\\n/gi, ' ');\n\n                switch (workEl.nodeName.toLowerCase()) {\n                    case 'p':\n                    case 'div':\n                        this.filterCommonBlocks(workEl);\n                        break;\n                    case 'br':\n                        this.filterLineBreak(workEl);\n                        break;\n                }\n            }\n\n            this.pasteHTML(tmp.innerHTML);\n        },\n\n        pasteHTML: function (html, options) {\n            options = MediumEditor.util.defaults({}, options, {\n                cleanAttrs: this.cleanAttrs,\n                cleanTags: this.cleanTags,\n                unwrapTags: this.unwrapTags\n            });\n\n            var elList, workEl, i, fragmentBody, pasteBlock = this.document.createDocumentFragment();\n\n            pasteBlock.appendChild(this.document.createElement('body'));\n\n            fragmentBody = pasteBlock.querySelector('body');\n            fragmentBody.innerHTML = html;\n\n            this.cleanupSpans(fragmentBody);\n\n            elList = fragmentBody.querySelectorAll('*');\n            for (i = 0; i < elList.length; i += 1) {\n                workEl = elList[i];\n\n                if ('a' === workEl.nodeName.toLowerCase() && this.getEditorOption('targetBlank')) {\n                    MediumEditor.util.setTargetBlank(workEl);\n                }\n\n                MediumEditor.util.cleanupAttrs(workEl, options.cleanAttrs);\n                MediumEditor.util.cleanupTags(workEl, options.cleanTags);\n                MediumEditor.util.unwrapTags(workEl, options.unwrapTags);\n            }\n\n            MediumEditor.util.insertHTMLCommand(this.document, fragmentBody.innerHTML.replace(/&nbsp;/g, ' '));\n        },\n\n        // TODO (6.0): Make this an internal helper instead of member of paste handler\n        isCommonBlock: function (el) {\n            return (el && (el.nodeName.toLowerCase() === 'p' || el.nodeName.toLowerCase() === 'div'));\n        },\n\n        // TODO (6.0): Make this an internal helper instead of member of paste handler\n        filterCommonBlocks: function (el) {\n            if (/^\\s*$/.test(el.textContent) && el.parentNode) {\n                el.parentNode.removeChild(el);\n            }\n        },\n\n        // TODO (6.0): Make this an internal helper instead of member of paste handler\n        filterLineBreak: function (el) {\n            if (this.isCommonBlock(el.previousElementSibling)) {\n                // remove stray br's following common block elements\n                this.removeWithParent(el);\n            } else if (this.isCommonBlock(el.parentNode) && (el.parentNode.firstChild === el || el.parentNode.lastChild === el)) {\n                // remove br's just inside open or close tags of a div/p\n                this.removeWithParent(el);\n            } else if (el.parentNode && el.parentNode.childElementCount === 1 && el.parentNode.textContent === '') {\n                // and br's that are the only child of elements other than div/p\n                this.removeWithParent(el);\n            }\n        },\n\n        // TODO (6.0): Make this an internal helper instead of member of paste handler\n        // remove an element, including its parent, if it is the only element within its parent\n        removeWithParent: function (el) {\n            if (el && el.parentNode) {\n                if (el.parentNode.parentNode && el.parentNode.childElementCount === 1) {\n                    el.parentNode.parentNode.removeChild(el.parentNode);\n                } else {\n                    el.parentNode.removeChild(el);\n                }\n            }\n        },\n\n        // TODO (6.0): Make this an internal helper instead of member of paste handler\n        cleanupSpans: function (containerEl) {\n            var i,\n                el,\n                newEl,\n                spans = containerEl.querySelectorAll('.replace-with'),\n                isCEF = function (el) {\n                    return (el && el.nodeName !== '#text' && el.getAttribute('contenteditable') === 'false');\n                };\n\n            for (i = 0; i < spans.length; i += 1) {\n                el = spans[i];\n                newEl = this.document.createElement(el.classList.contains('bold') ? 'b' : 'i');\n\n                if (el.classList.contains('bold') && el.classList.contains('italic')) {\n                    // add an i tag as well if this has both italics and bold\n                    newEl.innerHTML = '<i>' + el.innerHTML + '</i>';\n                } else {\n                    newEl.innerHTML = el.innerHTML;\n                }\n                el.parentNode.replaceChild(newEl, el);\n            }\n\n            spans = containerEl.querySelectorAll('span');\n            for (i = 0; i < spans.length; i += 1) {\n                el = spans[i];\n\n                // bail if span is in contenteditable = false\n                if (MediumEditor.util.traverseUp(el, isCEF)) {\n                    return false;\n                }\n\n                // remove empty spans, replace others with their contents\n                MediumEditor.util.unwrap(el, this.document);\n            }\n        }\n    });\n\n    MediumEditor.extensions.paste = PasteHandler;\n}());\n\n(function () {\n    'use strict';\n\n    var Placeholder = MediumEditor.Extension.extend({\n        name: 'placeholder',\n\n        /* Placeholder Options */\n\n        /* text: [string]\n         * Text to display in the placeholder\n         */\n        text: 'Type your text',\n\n        /* hideOnClick: [boolean]\n         * Should we hide the placeholder on click (true) or when user starts typing (false)\n         */\n        hideOnClick: true,\n\n        init: function () {\n            MediumEditor.Extension.prototype.init.apply(this, arguments);\n\n            this.initPlaceholders();\n            this.attachEventHandlers();\n        },\n\n        initPlaceholders: function () {\n            this.getEditorElements().forEach(this.initElement, this);\n        },\n\n        handleAddElement: function (event, editable) {\n            this.initElement(editable);\n        },\n\n        initElement: function (el) {\n            if (!el.getAttribute('data-placeholder')) {\n                el.setAttribute('data-placeholder', this.text);\n            }\n            this.updatePlaceholder(el);\n        },\n\n        destroy: function () {\n            this.getEditorElements().forEach(this.cleanupElement, this);\n        },\n\n        handleRemoveElement: function (event, editable) {\n            this.cleanupElement(editable);\n        },\n\n        cleanupElement: function (el) {\n            if (el.getAttribute('data-placeholder') === this.text) {\n                el.removeAttribute('data-placeholder');\n            }\n        },\n\n        showPlaceholder: function (el) {\n            if (el) {\n                // https://github.com/yabwe/medium-editor/issues/234\n                // In firefox, styling the placeholder with an absolutely positioned\n                // pseudo element causes the cursor to appear in a bad location\n                // when the element is completely empty, so apply a different class to\n                // style it with a relatively positioned pseudo element\n                if (MediumEditor.util.isFF && el.childNodes.length === 0) {\n                    el.classList.add('medium-editor-placeholder-relative');\n                    el.classList.remove('medium-editor-placeholder');\n                } else {\n                    el.classList.add('medium-editor-placeholder');\n                    el.classList.remove('medium-editor-placeholder-relative');\n                }\n            }\n        },\n\n        hidePlaceholder: function (el) {\n            if (el) {\n                el.classList.remove('medium-editor-placeholder');\n                el.classList.remove('medium-editor-placeholder-relative');\n            }\n        },\n\n        updatePlaceholder: function (el, dontShow) {\n            // If the element has content, hide the placeholder\n            if (el.querySelector('img, blockquote, ul, ol, table') || (el.textContent.replace(/^\\s+|\\s+$/g, '') !== '')) {\n                return this.hidePlaceholder(el);\n            }\n\n            if (!dontShow) {\n                this.showPlaceholder(el);\n            }\n        },\n\n        attachEventHandlers: function () {\n            if (this.hideOnClick) {\n                // For the 'hideOnClick' option, the placeholder should always be hidden on focus\n                this.subscribe('focus', this.handleFocus.bind(this));\n            }\n\n            // If the editor has content, it should always hide the placeholder\n            this.subscribe('editableInput', this.handleInput.bind(this));\n\n            // When the editor loses focus, check if the placeholder should be visible\n            this.subscribe('blur', this.handleBlur.bind(this));\n\n            // Need to know when elements are added/removed from the editor\n            this.subscribe('addElement', this.handleAddElement.bind(this));\n            this.subscribe('removeElement', this.handleRemoveElement.bind(this));\n        },\n\n        handleInput: function (event, element) {\n            // If the placeholder should be hidden on focus and the\n            // element has focus, don't show the placeholder\n            var dontShow = this.hideOnClick && (element === this.base.getFocusedElement());\n\n            // Editor's content has changed, check if the placeholder should be hidden\n            this.updatePlaceholder(element, dontShow);\n        },\n\n        handleFocus: function (event, element) {\n            // Editor has focus, hide the placeholder\n            this.hidePlaceholder(element);\n        },\n\n        handleBlur: function (event, element) {\n            // Editor has lost focus, check if the placeholder should be shown\n            this.updatePlaceholder(element);\n        }\n    });\n\n    MediumEditor.extensions.placeholder = Placeholder;\n}());\n\n(function () {\n    'use strict';\n\n    var Toolbar = MediumEditor.Extension.extend({\n        name: 'toolbar',\n\n        /* Toolbar Options */\n\n        /* align: ['left'|'center'|'right']\n         * When the __static__ option is true, this aligns the static toolbar\n         * relative to the medium-editor element.\n         */\n        align: 'center',\n\n        /* allowMultiParagraphSelection: [boolean]\n         * enables/disables whether the toolbar should be displayed when\n         * selecting multiple paragraphs/block elements\n         */\n        allowMultiParagraphSelection: true,\n\n        /* buttons: [Array]\n         * the names of the set of buttons to display on the toolbar.\n         */\n        buttons: ['bold', 'italic', 'underline', 'anchor', 'h2', 'h3', 'quote'],\n\n        /* diffLeft: [Number]\n         * value in pixels to be added to the X axis positioning of the toolbar.\n         */\n        diffLeft: 0,\n\n        /* diffTop: [Number]\n         * value in pixels to be added to the Y axis positioning of the toolbar.\n         */\n        diffTop: -10,\n\n        /* firstButtonClass: [string]\n         * CSS class added to the first button in the toolbar.\n         */\n        firstButtonClass: 'medium-editor-button-first',\n\n        /* lastButtonClass: [string]\n         * CSS class added to the last button in the toolbar.\n         */\n        lastButtonClass: 'medium-editor-button-last',\n\n        /* standardizeSelectionStart: [boolean]\n         * enables/disables standardizing how the beginning of a range is decided\n         * between browsers whenever the selected text is analyzed for updating toolbar buttons status.\n         */\n        standardizeSelectionStart: false,\n\n        /* static: [boolean]\n         * enable/disable the toolbar always displaying in the same location\n         * relative to the medium-editor element.\n         */\n        static: false,\n\n        /* sticky: [boolean]\n         * When the __static__ option is true, this enables/disables the toolbar\n         * \"sticking\" to the viewport and staying visible on the screen while\n         * the page scrolls.\n         */\n        sticky: false,\n\n        /* stickyTopOffset: [Number]\n         * Value in pixel of the top offset above the toolbar\n         */\n        stickyTopOffset: 0,\n\n        /* updateOnEmptySelection: [boolean]\n         * When the __static__ option is true, this enables/disables updating\n         * the state of the toolbar buttons even when the selection is collapsed\n         * (there is no selection, just a cursor).\n         */\n        updateOnEmptySelection: false,\n\n        /* relativeContainer: [node]\n         * appending the toolbar to a given node instead of body\n         */\n        relativeContainer: null,\n\n        init: function () {\n            MediumEditor.Extension.prototype.init.apply(this, arguments);\n\n            this.initThrottledMethods();\n\n            if (!this.relativeContainer) {\n                this.getEditorOption('elementsContainer').appendChild(this.getToolbarElement());\n            } else {\n                this.relativeContainer.appendChild(this.getToolbarElement());\n            }\n        },\n\n        // Helper method to execute method for every extension, but ignoring the toolbar extension\n        forEachExtension: function (iterator, context) {\n            return this.base.extensions.forEach(function (command) {\n                if (command === this) {\n                    return;\n                }\n                return iterator.apply(context || this, arguments);\n            }, this);\n        },\n\n        // Toolbar creation/deletion\n\n        createToolbar: function () {\n            var toolbar = this.document.createElement('div');\n\n            toolbar.id = 'medium-editor-toolbar-' + this.getEditorId();\n            toolbar.className = 'medium-editor-toolbar';\n\n            if (this.static) {\n                toolbar.className += ' static-toolbar';\n            } else if (this.relativeContainer) {\n                toolbar.className += ' medium-editor-relative-toolbar';\n            } else {\n                toolbar.className += ' medium-editor-stalker-toolbar';\n            }\n\n            toolbar.appendChild(this.createToolbarButtons());\n\n            // Add any forms that extensions may have\n            this.forEachExtension(function (extension) {\n                if (extension.hasForm) {\n                    toolbar.appendChild(extension.getForm());\n                }\n            });\n\n            this.attachEventHandlers();\n\n            return toolbar;\n        },\n\n        createToolbarButtons: function () {\n            var ul = this.document.createElement('ul'),\n                li,\n                btn,\n                buttons,\n                extension,\n                buttonName,\n                buttonOpts;\n\n            ul.id = 'medium-editor-toolbar-actions' + this.getEditorId();\n            ul.className = 'medium-editor-toolbar-actions';\n            ul.style.display = 'block';\n\n            this.buttons.forEach(function (button) {\n                if (typeof button === 'string') {\n                    buttonName = button;\n                    buttonOpts = null;\n                } else {\n                    buttonName = button.name;\n                    buttonOpts = button;\n                }\n\n                // If the button already exists as an extension, it'll be returned\n                // othwerise it'll create the default built-in button\n                extension = this.base.addBuiltInExtension(buttonName, buttonOpts);\n\n                if (extension && typeof extension.getButton === 'function') {\n                    btn = extension.getButton(this.base);\n                    li = this.document.createElement('li');\n                    if (MediumEditor.util.isElement(btn)) {\n                        li.appendChild(btn);\n                    } else {\n                        li.innerHTML = btn;\n                    }\n                    ul.appendChild(li);\n                }\n            }, this);\n\n            buttons = ul.querySelectorAll('button');\n            if (buttons.length > 0) {\n                buttons[0].classList.add(this.firstButtonClass);\n                buttons[buttons.length - 1].classList.add(this.lastButtonClass);\n            }\n\n            return ul;\n        },\n\n        destroy: function () {\n            if (this.toolbar) {\n                if (this.toolbar.parentNode) {\n                    this.toolbar.parentNode.removeChild(this.toolbar);\n                }\n                delete this.toolbar;\n            }\n        },\n\n        // Toolbar accessors\n\n        getInteractionElements: function () {\n            return this.getToolbarElement();\n        },\n\n        getToolbarElement: function () {\n            if (!this.toolbar) {\n                this.toolbar = this.createToolbar();\n            }\n\n            return this.toolbar;\n        },\n\n        getToolbarActionsElement: function () {\n            return this.getToolbarElement().querySelector('.medium-editor-toolbar-actions');\n        },\n\n        // Toolbar event handlers\n\n        initThrottledMethods: function () {\n            // throttledPositionToolbar is throttled because:\n            // - It will be called when the browser is resizing, which can fire many times very quickly\n            // - For some event (like resize) a slight lag in UI responsiveness is OK and provides performance benefits\n            this.throttledPositionToolbar = MediumEditor.util.throttle(function () {\n                if (this.base.isActive) {\n                    this.positionToolbarIfShown();\n                }\n            }.bind(this));\n        },\n\n        attachEventHandlers: function () {\n            // MediumEditor custom events for when user beings and ends interaction with a contenteditable and its elements\n            this.subscribe('blur', this.handleBlur.bind(this));\n            this.subscribe('focus', this.handleFocus.bind(this));\n\n            // Updating the state of the toolbar as things change\n            this.subscribe('editableClick', this.handleEditableClick.bind(this));\n            this.subscribe('editableKeyup', this.handleEditableKeyup.bind(this));\n\n            // Handle mouseup on document for updating the selection in the toolbar\n            this.on(this.document.documentElement, 'mouseup', this.handleDocumentMouseup.bind(this));\n\n            // Add a scroll event for sticky toolbar\n            if (this.static && this.sticky) {\n                // On scroll (capture), re-position the toolbar\n                this.on(this.window, 'scroll', this.handleWindowScroll.bind(this), true);\n            }\n\n            // On resize, re-position the toolbar\n            this.on(this.window, 'resize', this.handleWindowResize.bind(this));\n        },\n\n        handleWindowScroll: function () {\n            this.positionToolbarIfShown();\n        },\n\n        handleWindowResize: function () {\n            this.throttledPositionToolbar();\n        },\n\n        handleDocumentMouseup: function (event) {\n            // Do not trigger checkState when mouseup fires over the toolbar\n            if (event &&\n                    event.target &&\n                    MediumEditor.util.isDescendant(this.getToolbarElement(), event.target)) {\n                return false;\n            }\n            this.checkState();\n        },\n\n        handleEditableClick: function () {\n            // Delay the call to checkState to handle bug where selection is empty\n            // immediately after clicking inside a pre-existing selection\n            setTimeout(function () {\n                this.checkState();\n            }.bind(this), 0);\n        },\n\n        handleEditableKeyup: function () {\n            this.checkState();\n        },\n\n        handleBlur: function () {\n            // Kill any previously delayed calls to hide the toolbar\n            clearTimeout(this.hideTimeout);\n\n            // Blur may fire even if we have a selection, so we want to prevent any delayed showToolbar\n            // calls from happening in this specific case\n            clearTimeout(this.delayShowTimeout);\n\n            // Delay the call to hideToolbar to handle bug with multiple editors on the page at once\n            this.hideTimeout = setTimeout(function () {\n                this.hideToolbar();\n            }.bind(this), 1);\n        },\n\n        handleFocus: function () {\n            this.checkState();\n        },\n\n        // Hiding/showing toolbar\n\n        isDisplayed: function () {\n            return this.getToolbarElement().classList.contains('medium-editor-toolbar-active');\n        },\n\n        showToolbar: function () {\n            clearTimeout(this.hideTimeout);\n            if (!this.isDisplayed()) {\n                this.getToolbarElement().classList.add('medium-editor-toolbar-active');\n                this.trigger('showToolbar', {}, this.base.getFocusedElement());\n            }\n        },\n\n        hideToolbar: function () {\n            if (this.isDisplayed()) {\n                this.getToolbarElement().classList.remove('medium-editor-toolbar-active');\n                this.trigger('hideToolbar', {}, this.base.getFocusedElement());\n            }\n        },\n\n        isToolbarDefaultActionsDisplayed: function () {\n            return this.getToolbarActionsElement().style.display === 'block';\n        },\n\n        hideToolbarDefaultActions: function () {\n            if (this.isToolbarDefaultActionsDisplayed()) {\n                this.getToolbarActionsElement().style.display = 'none';\n            }\n        },\n\n        showToolbarDefaultActions: function () {\n            this.hideExtensionForms();\n\n            if (!this.isToolbarDefaultActionsDisplayed()) {\n                this.getToolbarActionsElement().style.display = 'block';\n            }\n\n            // Using setTimeout + options.delay because:\n            // We will actually be displaying the toolbar, which should be controlled by options.delay\n            this.delayShowTimeout = this.base.delay(function () {\n                this.showToolbar();\n            }.bind(this));\n        },\n\n        hideExtensionForms: function () {\n            // Hide all extension forms\n            this.forEachExtension(function (extension) {\n                if (extension.hasForm && extension.isDisplayed()) {\n                    extension.hideForm();\n                }\n            });\n        },\n\n        // Responding to changes in user selection\n\n        // Checks for existance of multiple block elements in the current selection\n        multipleBlockElementsSelected: function () {\n            var regexEmptyHTMLTags = /<[^\\/>][^>]*><\\/[^>]+>/gim, // http://stackoverflow.com/questions/3129738/remove-empty-tags-using-regex\n                regexBlockElements = new RegExp('<(' + MediumEditor.util.blockContainerElementNames.join('|') + ')[^>]*>', 'g'),\n                selectionHTML = MediumEditor.selection.getSelectionHtml(this.document).replace(regexEmptyHTMLTags, ''), // Filter out empty blocks from selection\n                hasMultiParagraphs = selectionHTML.match(regexBlockElements); // Find how many block elements are within the html\n\n            return !!hasMultiParagraphs && hasMultiParagraphs.length > 1;\n        },\n\n        modifySelection: function () {\n            var selection = this.window.getSelection(),\n                selectionRange = selection.getRangeAt(0);\n\n            /*\n            * In firefox, there are cases (ie doubleclick of a word) where the selectionRange start\n            * will be at the very end of an element.  In other browsers, the selectionRange start\n            * would instead be at the very beginning of an element that actually has content.\n            * example:\n            *   <span>foo</span><span>bar</span>\n            *\n            * If the text 'bar' is selected, most browsers will have the selectionRange start at the beginning\n            * of the 'bar' span.  However, there are cases where firefox will have the selectionRange start\n            * at the end of the 'foo' span.  The contenteditable behavior will be ok, but if there are any\n            * properties on the 'bar' span, they won't be reflected accurately in the toolbar\n            * (ie 'Bold' button wouldn't be active)\n            *\n            * So, for cases where the selectionRange start is at the end of an element/node, find the next\n            * adjacent text node that actually has content in it, and move the selectionRange start there.\n            */\n            if (this.standardizeSelectionStart &&\n                    selectionRange.startContainer.nodeValue &&\n                    (selectionRange.startOffset === selectionRange.startContainer.nodeValue.length)) {\n                var adjacentNode = MediumEditor.util.findAdjacentTextNodeWithContent(MediumEditor.selection.getSelectionElement(this.window), selectionRange.startContainer, this.document);\n                if (adjacentNode) {\n                    var offset = 0;\n                    while (adjacentNode.nodeValue.substr(offset, 1).trim().length === 0) {\n                        offset = offset + 1;\n                    }\n                    selectionRange = MediumEditor.selection.select(this.document, adjacentNode, offset,\n                        selectionRange.endContainer, selectionRange.endOffset);\n                }\n            }\n        },\n\n        checkState: function () {\n            if (this.base.preventSelectionUpdates) {\n                return;\n            }\n\n            // If no editable has focus OR selection is inside contenteditable = false\n            // hide toolbar\n            if (!this.base.getFocusedElement() ||\n                    MediumEditor.selection.selectionInContentEditableFalse(this.window)) {\n                return this.hideToolbar();\n            }\n\n            // If there's no selection element, selection element doesn't belong to this editor\n            // or toolbar is disabled for this selection element\n            // hide toolbar\n            var selectionElement = MediumEditor.selection.getSelectionElement(this.window);\n            if (!selectionElement ||\n                    this.getEditorElements().indexOf(selectionElement) === -1 ||\n                    selectionElement.getAttribute('data-disable-toolbar')) {\n                return this.hideToolbar();\n            }\n\n            // Now we know there's a focused editable with a selection\n\n            // If the updateOnEmptySelection option is true, show the toolbar\n            if (this.updateOnEmptySelection && this.static) {\n                return this.showAndUpdateToolbar();\n            }\n\n            // If we don't have a 'valid' selection -> hide toolbar\n            if (!MediumEditor.selection.selectionContainsContent(this.document) ||\n                (this.allowMultiParagraphSelection === false && this.multipleBlockElementsSelected())) {\n                return this.hideToolbar();\n            }\n\n            this.showAndUpdateToolbar();\n        },\n\n        // Updating the toolbar\n\n        showAndUpdateToolbar: function () {\n            this.modifySelection();\n            this.setToolbarButtonStates();\n            this.trigger('positionToolbar', {}, this.base.getFocusedElement());\n            this.showToolbarDefaultActions();\n            this.setToolbarPosition();\n        },\n\n        setToolbarButtonStates: function () {\n            this.forEachExtension(function (extension) {\n                if (typeof extension.isActive === 'function' &&\n                    typeof extension.setInactive === 'function') {\n                    extension.setInactive();\n                }\n            });\n\n            this.checkActiveButtons();\n        },\n\n        checkActiveButtons: function () {\n            var manualStateChecks = [],\n                queryState = null,\n                selectionRange = MediumEditor.selection.getSelectionRange(this.document),\n                parentNode,\n                updateExtensionState = function (extension) {\n                    if (typeof extension.checkState === 'function') {\n                        extension.checkState(parentNode);\n                    } else if (typeof extension.isActive === 'function' &&\n                               typeof extension.isAlreadyApplied === 'function' &&\n                               typeof extension.setActive === 'function') {\n                        if (!extension.isActive() && extension.isAlreadyApplied(parentNode)) {\n                            extension.setActive();\n                        }\n                    }\n                };\n\n            if (!selectionRange) {\n                return;\n            }\n\n            // Loop through all extensions\n            this.forEachExtension(function (extension) {\n                // For those extensions where we can use document.queryCommandState(), do so\n                if (typeof extension.queryCommandState === 'function') {\n                    queryState = extension.queryCommandState();\n                    // If queryCommandState returns a valid value, we can trust the browser\n                    // and don't need to do our manual checks\n                    if (queryState !== null) {\n                        if (queryState && typeof extension.setActive === 'function') {\n                            extension.setActive();\n                        }\n                        return;\n                    }\n                }\n                // We can't use queryCommandState for this extension, so add to manualStateChecks\n                manualStateChecks.push(extension);\n            });\n\n            parentNode = MediumEditor.selection.getSelectedParentElement(selectionRange);\n\n            // Make sure the selection parent isn't outside of the contenteditable\n            if (!this.getEditorElements().some(function (element) {\n                    return MediumEditor.util.isDescendant(element, parentNode, true);\n                })) {\n                return;\n            }\n\n            // Climb up the DOM and do manual checks for whether a certain extension is currently enabled for this node\n            while (parentNode) {\n                manualStateChecks.forEach(updateExtensionState);\n\n                // we can abort the search upwards if we leave the contentEditable element\n                if (MediumEditor.util.isMediumEditorElement(parentNode)) {\n                    break;\n                }\n                parentNode = parentNode.parentNode;\n            }\n        },\n\n        // Positioning toolbar\n\n        positionToolbarIfShown: function () {\n            if (this.isDisplayed()) {\n                this.setToolbarPosition();\n            }\n        },\n\n        setToolbarPosition: function () {\n            var container = this.base.getFocusedElement(),\n                selection = this.window.getSelection();\n\n            // If there isn't a valid selection, bail\n            if (!container) {\n                return this;\n            }\n\n            if (this.static || !selection.isCollapsed) {\n                this.showToolbar();\n\n                // we don't need any absolute positioning if relativeContainer is set\n                if (!this.relativeContainer) {\n                    if (this.static) {\n                        this.positionStaticToolbar(container);\n                    } else {\n                        this.positionToolbar(selection);\n                    }\n                }\n\n                this.trigger('positionedToolbar', {}, this.base.getFocusedElement());\n            }\n        },\n\n        positionStaticToolbar: function (container) {\n            // position the toolbar at left 0, so we can get the real width of the toolbar\n            this.getToolbarElement().style.left = '0';\n\n            // document.documentElement for IE 9\n            var scrollTop = (this.document.documentElement && this.document.documentElement.scrollTop) || this.document.body.scrollTop,\n                windowWidth = this.window.innerWidth,\n                toolbarElement = this.getToolbarElement(),\n                containerRect = container.getBoundingClientRect(),\n                containerTop = containerRect.top + scrollTop,\n                containerCenter = (containerRect.left + (containerRect.width / 2)),\n                toolbarHeight = toolbarElement.offsetHeight,\n                toolbarWidth = toolbarElement.offsetWidth,\n                halfOffsetWidth = toolbarWidth / 2,\n                targetLeft;\n\n            if (this.sticky) {\n                // If it's beyond the height of the editor, position it at the bottom of the editor\n                if (scrollTop > (containerTop + container.offsetHeight - toolbarHeight - this.stickyTopOffset)) {\n                    toolbarElement.style.top = (containerTop + container.offsetHeight - toolbarHeight) + 'px';\n                    toolbarElement.classList.remove('medium-editor-sticky-toolbar');\n                // Stick the toolbar to the top of the window\n                } else if (scrollTop > (containerTop - toolbarHeight - this.stickyTopOffset)) {\n                    toolbarElement.classList.add('medium-editor-sticky-toolbar');\n                    toolbarElement.style.top = this.stickyTopOffset + 'px';\n                // Normal static toolbar position\n                } else {\n                    toolbarElement.classList.remove('medium-editor-sticky-toolbar');\n                    toolbarElement.style.top = containerTop - toolbarHeight + 'px';\n                }\n            } else {\n                toolbarElement.style.top = containerTop - toolbarHeight + 'px';\n            }\n\n            switch (this.align) {\n                case 'left':\n                    targetLeft = containerRect.left;\n                    break;\n\n                case 'right':\n                    targetLeft = containerRect.right - toolbarWidth;\n                    break;\n\n                case 'center':\n                    targetLeft = containerCenter - halfOffsetWidth;\n                    break;\n            }\n\n            if (targetLeft < 0) {\n                targetLeft = 0;\n            } else if ((targetLeft + toolbarWidth) > windowWidth) {\n                targetLeft = (windowWidth - Math.ceil(toolbarWidth) - 1);\n            }\n\n            toolbarElement.style.left = targetLeft + 'px';\n        },\n\n        positionToolbar: function (selection) {\n            // position the toolbar at left 0, so we can get the real width of the toolbar\n            this.getToolbarElement().style.left = '0';\n            this.getToolbarElement().style.right = 'initial';\n\n            var range = selection.getRangeAt(0),\n                boundary = range.getBoundingClientRect();\n\n            // Handle selections with just images\n            if (!boundary || ((boundary.height === 0 && boundary.width === 0) && range.startContainer === range.endContainer)) {\n                // If there's a nested image, use that for the bounding rectangle\n                if (range.startContainer.nodeType === 1 && range.startContainer.querySelector('img')) {\n                    boundary = range.startContainer.querySelector('img').getBoundingClientRect();\n                } else {\n                    boundary = range.startContainer.getBoundingClientRect();\n                }\n            }\n\n            var containerWidth = this.window.innerWidth,\n                toolbarElement = this.getToolbarElement(),\n                toolbarHeight = toolbarElement.offsetHeight,\n                toolbarWidth = toolbarElement.offsetWidth,\n                halfOffsetWidth = toolbarWidth / 2,\n                buttonHeight = 50,\n                defaultLeft = this.diffLeft - halfOffsetWidth,\n                elementsContainer = this.getEditorOption('elementsContainer'),\n                elementsContainerAbsolute = ['absolute', 'fixed'].indexOf(window.getComputedStyle(elementsContainer).getPropertyValue('position')) > -1,\n                positions = {},\n                relativeBoundary = {},\n                middleBoundary, elementsContainerBoundary;\n\n            // If container element is absolute / fixed, recalculate boundaries to be relative to the container\n            if (elementsContainerAbsolute) {\n                elementsContainerBoundary = elementsContainer.getBoundingClientRect();\n                ['top', 'left'].forEach(function (key) {\n                    relativeBoundary[key] = boundary[key] - elementsContainerBoundary[key];\n                });\n\n                relativeBoundary.width = boundary.width;\n                relativeBoundary.height = boundary.height;\n                boundary = relativeBoundary;\n\n                containerWidth = elementsContainerBoundary.width;\n\n                // Adjust top position according to container scroll position\n                positions.top = elementsContainer.scrollTop;\n            } else {\n                // Adjust top position according to window scroll position\n                positions.top = this.window.pageYOffset;\n            }\n\n            middleBoundary = boundary.left + boundary.width / 2;\n            positions.top += boundary.top - toolbarHeight;\n\n            if (boundary.top < buttonHeight) {\n                toolbarElement.classList.add('medium-toolbar-arrow-over');\n                toolbarElement.classList.remove('medium-toolbar-arrow-under');\n                positions.top += buttonHeight + boundary.height - this.diffTop;\n            } else {\n                toolbarElement.classList.add('medium-toolbar-arrow-under');\n                toolbarElement.classList.remove('medium-toolbar-arrow-over');\n                positions.top += this.diffTop;\n            }\n\n            if (middleBoundary < halfOffsetWidth) {\n                positions.left = defaultLeft + halfOffsetWidth;\n                positions.right = 'initial';\n            } else if ((containerWidth - middleBoundary) < halfOffsetWidth) {\n                positions.left = 'auto';\n                positions.right = 0;\n            } else {\n                positions.left = defaultLeft + middleBoundary;\n                positions.right = 'initial';\n            }\n\n            ['top', 'left', 'right'].forEach(function (key) {\n                toolbarElement.style[key] = positions[key] + (isNaN(positions[key]) ? '' : 'px');\n            });\n        }\n    });\n\n    MediumEditor.extensions.toolbar = Toolbar;\n}());\n\n(function () {\n    'use strict';\n\n    var ImageDragging = MediumEditor.Extension.extend({\n        init: function () {\n            MediumEditor.Extension.prototype.init.apply(this, arguments);\n\n            this.subscribe('editableDrag', this.handleDrag.bind(this));\n            this.subscribe('editableDrop', this.handleDrop.bind(this));\n        },\n\n        handleDrag: function (event) {\n            var className = 'medium-editor-dragover';\n            event.preventDefault();\n            event.dataTransfer.dropEffect = 'copy';\n\n            if (event.type === 'dragover') {\n                event.target.classList.add(className);\n            } else if (event.type === 'dragleave') {\n                event.target.classList.remove(className);\n            }\n        },\n\n        handleDrop: function (event) {\n            var className = 'medium-editor-dragover',\n                files;\n            event.preventDefault();\n            event.stopPropagation();\n\n            // IE9 does not support the File API, so prevent file from opening in a new window\n            // but also don't try to actually get the file\n            if (event.dataTransfer.files) {\n                files = Array.prototype.slice.call(event.dataTransfer.files, 0);\n                files.some(function (file) {\n                    if (file.type.match('image')) {\n                        var fileReader, id;\n                        fileReader = new FileReader();\n                        fileReader.readAsDataURL(file);\n\n                        id = 'medium-img-' + (+new Date());\n                        MediumEditor.util.insertHTMLCommand(this.document, '<img class=\"medium-editor-image-loading\" id=\"' + id + '\" />');\n\n                        fileReader.onload = function () {\n                            var img = this.document.getElementById(id);\n                            if (img) {\n                                img.removeAttribute('id');\n                                img.removeAttribute('class');\n                                img.src = fileReader.result;\n                            }\n                        }.bind(this);\n                    }\n                }.bind(this));\n            }\n            event.target.classList.remove(className);\n        }\n    });\n\n    MediumEditor.extensions.imageDragging = ImageDragging;\n}());\n\n(function () {\n    'use strict';\n\n    // Event handlers that shouldn't be exposed externally\n\n    function handleDisableExtraSpaces(event) {\n        var node = MediumEditor.selection.getSelectionStart(this.options.ownerDocument),\n            textContent = node.textContent,\n            caretPositions = MediumEditor.selection.getCaretOffsets(node);\n\n        if ((textContent[caretPositions.left - 1] === undefined) || (textContent[caretPositions.left - 1].trim() === '') || (textContent[caretPositions.left] !== undefined && textContent[caretPositions.left].trim() === '')) {\n            event.preventDefault();\n        }\n    }\n\n    function handleDisabledEnterKeydown(event, element) {\n        if (this.options.disableReturn || element.getAttribute('data-disable-return')) {\n            event.preventDefault();\n        } else if (this.options.disableDoubleReturn || element.getAttribute('data-disable-double-return')) {\n            var node = MediumEditor.selection.getSelectionStart(this.options.ownerDocument);\n\n            // if current text selection is empty OR previous sibling text is empty OR it is not a list\n            if ((node && node.textContent.trim() === '' && node.nodeName.toLowerCase() !== 'li') ||\n                (node.previousElementSibling && node.previousElementSibling.nodeName.toLowerCase() !== 'br' &&\n                 node.previousElementSibling.textContent.trim() === '')) {\n                event.preventDefault();\n            }\n        }\n    }\n\n    function handleTabKeydown(event) {\n        // Override tab only for pre nodes\n        var node = MediumEditor.selection.getSelectionStart(this.options.ownerDocument),\n            tag = node && node.nodeName.toLowerCase();\n\n        if (tag === 'pre') {\n            event.preventDefault();\n            MediumEditor.util.insertHTMLCommand(this.options.ownerDocument, '    ');\n        }\n\n        // Tab to indent list structures!\n        if (MediumEditor.util.isListItem(node)) {\n            event.preventDefault();\n\n            // If Shift is down, outdent, otherwise indent\n            if (event.shiftKey) {\n                this.options.ownerDocument.execCommand('outdent', false, null);\n            } else {\n                this.options.ownerDocument.execCommand('indent', false, null);\n            }\n        }\n    }\n\n    function handleBlockDeleteKeydowns(event) {\n        var p, node = MediumEditor.selection.getSelectionStart(this.options.ownerDocument),\n            tagName = node.nodeName.toLowerCase(),\n            isEmpty = /^(\\s+|<br\\/?>)?$/i,\n            isHeader = /h\\d/i;\n\n        if (MediumEditor.util.isKey(event, [MediumEditor.util.keyCode.BACKSPACE, MediumEditor.util.keyCode.ENTER]) &&\n                // has a preceeding sibling\n                node.previousElementSibling &&\n                // in a header\n                isHeader.test(tagName) &&\n                // at the very end of the block\n                MediumEditor.selection.getCaretOffsets(node).left === 0) {\n            if (MediumEditor.util.isKey(event, MediumEditor.util.keyCode.BACKSPACE) && isEmpty.test(node.previousElementSibling.innerHTML)) {\n                // backspacing the begining of a header into an empty previous element will\n                // change the tagName of the current node to prevent one\n                // instead delete previous node and cancel the event.\n                node.previousElementSibling.parentNode.removeChild(node.previousElementSibling);\n                event.preventDefault();\n            } else if (!this.options.disableDoubleReturn && MediumEditor.util.isKey(event, MediumEditor.util.keyCode.ENTER)) {\n                // hitting return in the begining of a header will create empty header elements before the current one\n                // instead, make \"<p><br></p>\" element, which are what happens if you hit return in an empty paragraph\n                p = this.options.ownerDocument.createElement('p');\n                p.innerHTML = '<br>';\n                node.previousElementSibling.parentNode.insertBefore(p, node);\n                event.preventDefault();\n            }\n        } else if (MediumEditor.util.isKey(event, MediumEditor.util.keyCode.DELETE) &&\n                    // between two sibling elements\n                    node.nextElementSibling &&\n                    node.previousElementSibling &&\n                    // not in a header\n                    !isHeader.test(tagName) &&\n                    // in an empty tag\n                    isEmpty.test(node.innerHTML) &&\n                    // when the next tag *is* a header\n                    isHeader.test(node.nextElementSibling.nodeName.toLowerCase())) {\n            // hitting delete in an empty element preceding a header, ex:\n            //  <p>[CURSOR]</p><h1>Header</h1>\n            // Will cause the h1 to become a paragraph.\n            // Instead, delete the paragraph node and move the cursor to the begining of the h1\n\n            // remove node and move cursor to start of header\n            MediumEditor.selection.moveCursor(this.options.ownerDocument, node.nextElementSibling);\n\n            node.previousElementSibling.parentNode.removeChild(node);\n\n            event.preventDefault();\n        } else if (MediumEditor.util.isKey(event, MediumEditor.util.keyCode.BACKSPACE) &&\n                tagName === 'li' &&\n                // hitting backspace inside an empty li\n                isEmpty.test(node.innerHTML) &&\n                // is first element (no preceeding siblings)\n                !node.previousElementSibling &&\n                // parent also does not have a sibling\n                !node.parentElement.previousElementSibling &&\n                // is not the only li in a list\n                node.nextElementSibling &&\n                node.nextElementSibling.nodeName.toLowerCase() === 'li') {\n            // backspacing in an empty first list element in the first list (with more elements) ex:\n            //  <ul><li>[CURSOR]</li><li>List Item 2</li></ul>\n            // will remove the first <li> but add some extra element before (varies based on browser)\n            // Instead, this will:\n            // 1) remove the list element\n            // 2) create a paragraph before the list\n            // 3) move the cursor into the paragraph\n\n            // create a paragraph before the list\n            p = this.options.ownerDocument.createElement('p');\n            p.innerHTML = '<br>';\n            node.parentElement.parentElement.insertBefore(p, node.parentElement);\n\n            // move the cursor into the new paragraph\n            MediumEditor.selection.moveCursor(this.options.ownerDocument, p);\n\n            // remove the list element\n            node.parentElement.removeChild(node);\n\n            event.preventDefault();\n        } else if (MediumEditor.util.isKey(event, MediumEditor.util.keyCode.BACKSPACE) &&\n                (MediumEditor.util.getClosestTag(node, 'blockquote') !== false) &&\n                MediumEditor.selection.getCaretOffsets(node).left === 0) {\n\n            // when cursor is at the begining of the element and the element is <blockquote>\n            // then pressing backspace key should change the <blockquote> to a <p> tag\n            event.preventDefault();\n            MediumEditor.util.execFormatBlock(this.options.ownerDocument, 'p');\n        } else if (MediumEditor.util.isKey(event, MediumEditor.util.keyCode.ENTER) &&\n                (MediumEditor.util.getClosestTag(node, 'blockquote') !== false) &&\n                MediumEditor.selection.getCaretOffsets(node).right === 0) {\n\n            // when cursor is at the end of <blockquote>,\n            // then pressing enter key should create <p> tag, not <blockquote>\n            p = this.options.ownerDocument.createElement('p');\n            p.innerHTML = '<br>';\n            node.parentElement.insertBefore(p, node.nextSibling);\n\n            // move the cursor into the new paragraph\n            MediumEditor.selection.moveCursor(this.options.ownerDocument, p);\n\n            event.preventDefault();\n        } else if (MediumEditor.util.isKey(event, MediumEditor.util.keyCode.BACKSPACE) &&\n                MediumEditor.util.isMediumEditorElement(node.parentElement) &&\n                !node.previousElementSibling &&\n                node.nextElementSibling &&\n                isEmpty.test(node.innerHTML)) {\n\n            // when cursor is in the first element, it's empty and user presses backspace,\n            // do delete action instead to get rid of the first element and move caret to 2nd\n            event.preventDefault();\n            MediumEditor.selection.moveCursor(this.options.ownerDocument, node.nextSibling);\n            node.parentElement.removeChild(node);\n        }\n    }\n\n    function handleKeyup(event) {\n        var node = MediumEditor.selection.getSelectionStart(this.options.ownerDocument),\n            tagName;\n\n        if (!node) {\n            return;\n        }\n\n        // https://github.com/yabwe/medium-editor/issues/994\n        // Firefox thrown an error when calling `formatBlock` on an empty editable blockContainer that's not a <div>\n        if (MediumEditor.util.isMediumEditorElement(node) && node.children.length === 0 && !MediumEditor.util.isBlockContainer(node)) {\n            this.options.ownerDocument.execCommand('formatBlock', false, 'p');\n        }\n\n        // https://github.com/yabwe/medium-editor/issues/834\n        // https://github.com/yabwe/medium-editor/pull/382\n        // Don't call format block if this is a block element (ie h1, figCaption, etc.)\n        if (MediumEditor.util.isKey(event, MediumEditor.util.keyCode.ENTER) &&\n            !MediumEditor.util.isListItem(node) &&\n            !MediumEditor.util.isBlockContainer(node)) {\n\n            tagName = node.nodeName.toLowerCase();\n            // For anchor tags, unlink\n            if (tagName === 'a') {\n                this.options.ownerDocument.execCommand('unlink', false, null);\n            } else if (!event.shiftKey && !event.ctrlKey) {\n                this.options.ownerDocument.execCommand('formatBlock', false, 'p');\n            }\n        }\n    }\n\n    function handleEditableInput(event, editable) {\n        var textarea = editable.parentNode.querySelector('textarea[medium-editor-textarea-id=\"' + editable.getAttribute('medium-editor-textarea-id') + '\"]');\n        if (textarea) {\n            textarea.value = editable.innerHTML.trim();\n        }\n    }\n\n    // Internal helper methods which shouldn't be exposed externally\n\n    function addToEditors(win) {\n        if (!win._mediumEditors) {\n            // To avoid breaking users who are assuming that the unique id on\n            // medium-editor elements will start at 1, inserting a 'null' in the\n            // array so the unique-id can always map to the index of the editor instance\n            win._mediumEditors = [null];\n        }\n\n        // If this already has a unique id, re-use it\n        if (!this.id) {\n            this.id = win._mediumEditors.length;\n        }\n\n        win._mediumEditors[this.id] = this;\n    }\n\n    function removeFromEditors(win) {\n        if (!win._mediumEditors || !win._mediumEditors[this.id]) {\n            return;\n        }\n\n        /* Setting the instance to null in the array instead of deleting it allows:\n         * 1) Each instance to preserve its own unique-id, even after being destroyed\n         *    and initialized again\n         * 2) The unique-id to always correspond to an index in the array of medium-editor\n         *    instances. Thus, we will be able to look at a contenteditable, and determine\n         *    which instance it belongs to, by indexing into the global array.\n         */\n        win._mediumEditors[this.id] = null;\n    }\n\n    function createElementsArray(selector, doc, filterEditorElements) {\n        var elements = [];\n\n        if (!selector) {\n            selector = [];\n        }\n        // If string, use as query selector\n        if (typeof selector === 'string') {\n            selector = doc.querySelectorAll(selector);\n        }\n        // If element, put into array\n        if (MediumEditor.util.isElement(selector)) {\n            selector = [selector];\n        }\n\n        if (filterEditorElements) {\n            // Remove elements that have already been initialized by the editor\n            // selecotr might not be an array (ie NodeList) so use for loop\n            for (var i = 0; i < selector.length; i++) {\n                var el = selector[i];\n                if (MediumEditor.util.isElement(el) &&\n                    !el.getAttribute('data-medium-editor-element') &&\n                    !el.getAttribute('medium-editor-textarea-id')) {\n                    elements.push(el);\n                }\n            }\n        } else {\n            // Convert NodeList (or other array like object) into an array\n            elements = Array.prototype.slice.apply(selector);\n        }\n\n        return elements;\n    }\n\n    function cleanupTextareaElement(element) {\n        var textarea = element.parentNode.querySelector('textarea[medium-editor-textarea-id=\"' + element.getAttribute('medium-editor-textarea-id') + '\"]');\n        if (textarea) {\n            // Un-hide the textarea\n            textarea.classList.remove('medium-editor-hidden');\n            textarea.removeAttribute('medium-editor-textarea-id');\n        }\n        if (element.parentNode) {\n            element.parentNode.removeChild(element);\n        }\n    }\n\n    function setExtensionDefaults(extension, defaults) {\n        Object.keys(defaults).forEach(function (prop) {\n            if (extension[prop] === undefined) {\n                extension[prop] = defaults[prop];\n            }\n        });\n        return extension;\n    }\n\n    function initExtension(extension, name, instance) {\n        var extensionDefaults = {\n            'window': instance.options.contentWindow,\n            'document': instance.options.ownerDocument,\n            'base': instance\n        };\n\n        // Add default options into the extension\n        extension = setExtensionDefaults(extension, extensionDefaults);\n\n        // Call init on the extension\n        if (typeof extension.init === 'function') {\n            extension.init();\n        }\n\n        // Set extension name (if not already set)\n        if (!extension.name) {\n            extension.name = name;\n        }\n        return extension;\n    }\n\n    function isToolbarEnabled() {\n        // If any of the elements don't have the toolbar disabled\n        // We need a toolbar\n        if (this.elements.every(function (element) {\n                return !!element.getAttribute('data-disable-toolbar');\n            })) {\n            return false;\n        }\n\n        return this.options.toolbar !== false;\n    }\n\n    function isAnchorPreviewEnabled() {\n        // If toolbar is disabled, don't add\n        if (!isToolbarEnabled.call(this)) {\n            return false;\n        }\n\n        return this.options.anchorPreview !== false;\n    }\n\n    function isPlaceholderEnabled() {\n        return this.options.placeholder !== false;\n    }\n\n    function isAutoLinkEnabled() {\n        return this.options.autoLink !== false;\n    }\n\n    function isImageDraggingEnabled() {\n        return this.options.imageDragging !== false;\n    }\n\n    function isKeyboardCommandsEnabled() {\n        return this.options.keyboardCommands !== false;\n    }\n\n    function shouldUseFileDraggingExtension() {\n        // Since the file-dragging extension replaces the image-dragging extension,\n        // we need to check if the user passed an overrided image-dragging extension.\n        // If they have, to avoid breaking users, we won't use file-dragging extension.\n        return !this.options.extensions['imageDragging'];\n    }\n\n    function createContentEditable(textarea) {\n        var div = this.options.ownerDocument.createElement('div'),\n            now = Date.now(),\n            uniqueId = 'medium-editor-' + now,\n            atts = textarea.attributes;\n\n        // Some browsers can move pretty fast, since we're using a timestamp\n        // to make a unique-id, ensure that the id is actually unique on the page\n        while (this.options.ownerDocument.getElementById(uniqueId)) {\n            now++;\n            uniqueId = 'medium-editor-' + now;\n        }\n\n        div.className = textarea.className;\n        div.id = uniqueId;\n        div.innerHTML = textarea.value;\n\n        textarea.setAttribute('medium-editor-textarea-id', uniqueId);\n\n        // re-create all attributes from the textearea to the new created div\n        for (var i = 0, n = atts.length; i < n; i++) {\n            // do not re-create existing attributes\n            if (!div.hasAttribute(atts[i].nodeName)) {\n                div.setAttribute(atts[i].nodeName, atts[i].value);\n            }\n        }\n\n        // If textarea has a form, listen for reset on the form to clear\n        // the content of the created div\n        if (textarea.form) {\n            this.on(textarea.form, 'reset', function (event) {\n                if (!event.defaultPrevented) {\n                    this.resetContent(this.options.ownerDocument.getElementById(uniqueId));\n                }\n            }.bind(this));\n        }\n\n        textarea.classList.add('medium-editor-hidden');\n        textarea.parentNode.insertBefore(\n            div,\n            textarea\n        );\n\n        return div;\n    }\n\n    function initElement(element, editorId) {\n        if (!element.getAttribute('data-medium-editor-element')) {\n            if (element.nodeName.toLowerCase() === 'textarea') {\n                element = createContentEditable.call(this, element);\n\n                // Make sure we only attach to editableInput once for <textarea> elements\n                if (!this.instanceHandleEditableInput) {\n                    this.instanceHandleEditableInput = handleEditableInput.bind(this);\n                    this.subscribe('editableInput', this.instanceHandleEditableInput);\n                }\n            }\n\n            if (!this.options.disableEditing && !element.getAttribute('data-disable-editing')) {\n                element.setAttribute('contentEditable', true);\n                element.setAttribute('spellcheck', this.options.spellcheck);\n            }\n\n            // Make sure we only attach to editableKeydownEnter once for disable-return options\n            if (!this.instanceHandleEditableKeydownEnter) {\n                if (element.getAttribute('data-disable-return') || element.getAttribute('data-disable-double-return')) {\n                    this.instanceHandleEditableKeydownEnter = handleDisabledEnterKeydown.bind(this);\n                    this.subscribe('editableKeydownEnter', this.instanceHandleEditableKeydownEnter);\n                }\n            }\n\n            // if we're not disabling return, add a handler to help handle cleanup\n            // for certain cases when enter is pressed\n            if (!this.options.disableReturn && !element.getAttribute('data-disable-return')) {\n                this.on(element, 'keyup', handleKeyup.bind(this));\n            }\n\n            var elementId = MediumEditor.util.guid();\n\n            element.setAttribute('data-medium-editor-element', true);\n            element.classList.add('medium-editor-element');\n            element.setAttribute('role', 'textbox');\n            element.setAttribute('aria-multiline', true);\n            element.setAttribute('data-medium-editor-editor-index', editorId);\n            // TODO: Merge data-medium-editor-element and medium-editor-index attributes for 6.0.0\n            // medium-editor-index is not named correctly anymore and can be re-purposed to signify\n            // whether the element has been initialized or not\n            element.setAttribute('medium-editor-index', elementId);\n            initialContent[elementId] = element.innerHTML;\n\n            this.events.attachAllEventsToElement(element);\n        }\n\n        return element;\n    }\n\n    function attachHandlers() {\n        // attach to tabs\n        this.subscribe('editableKeydownTab', handleTabKeydown.bind(this));\n\n        // Bind keys which can create or destroy a block element: backspace, delete, return\n        this.subscribe('editableKeydownDelete', handleBlockDeleteKeydowns.bind(this));\n        this.subscribe('editableKeydownEnter', handleBlockDeleteKeydowns.bind(this));\n\n        // Bind double space event\n        if (this.options.disableExtraSpaces) {\n            this.subscribe('editableKeydownSpace', handleDisableExtraSpaces.bind(this));\n        }\n\n        // Make sure we only attach to editableKeydownEnter once for disable-return options\n        if (!this.instanceHandleEditableKeydownEnter) {\n            // disabling return or double return\n            if (this.options.disableReturn || this.options.disableDoubleReturn) {\n                this.instanceHandleEditableKeydownEnter = handleDisabledEnterKeydown.bind(this);\n                this.subscribe('editableKeydownEnter', this.instanceHandleEditableKeydownEnter);\n            }\n        }\n    }\n\n    function initExtensions() {\n\n        this.extensions = [];\n\n        // Passed in extensions\n        Object.keys(this.options.extensions).forEach(function (name) {\n            // Always save the toolbar extension for last\n            if (name !== 'toolbar' && this.options.extensions[name]) {\n                this.extensions.push(initExtension(this.options.extensions[name], name, this));\n            }\n        }, this);\n\n        // 4 Cases for imageDragging + fileDragging extensons:\n        //\n        // 1. ImageDragging ON + No Custom Image Dragging Extension:\n        //    * Use fileDragging extension (default options)\n        // 2. ImageDragging OFF + No Custom Image Dragging Extension:\n        //    * Use fileDragging extension w/ images turned off\n        // 3. ImageDragging ON + Custom Image Dragging Extension:\n        //    * Don't use fileDragging (could interfere with custom image dragging extension)\n        // 4. ImageDragging OFF + Custom Image Dragging:\n        //    * Don't use fileDragging (could interfere with custom image dragging extension)\n        if (shouldUseFileDraggingExtension.call(this)) {\n            var opts = this.options.fileDragging;\n            if (!opts) {\n                opts = {};\n\n                // Image is in the 'allowedTypes' list by default.\n                // If imageDragging is off override the 'allowedTypes' list with an empty one\n                if (!isImageDraggingEnabled.call(this)) {\n                    opts.allowedTypes = [];\n                }\n            }\n            this.addBuiltInExtension('fileDragging', opts);\n        }\n\n        // Built-in extensions\n        var builtIns = {\n            paste: true,\n            'anchor-preview': isAnchorPreviewEnabled.call(this),\n            autoLink: isAutoLinkEnabled.call(this),\n            keyboardCommands: isKeyboardCommandsEnabled.call(this),\n            placeholder: isPlaceholderEnabled.call(this)\n        };\n        Object.keys(builtIns).forEach(function (name) {\n            if (builtIns[name]) {\n                this.addBuiltInExtension(name);\n            }\n        }, this);\n\n        // Users can pass in a custom toolbar extension\n        // so check for that first and if it's not present\n        // just create the default toolbar\n        var toolbarExtension = this.options.extensions['toolbar'];\n        if (!toolbarExtension && isToolbarEnabled.call(this)) {\n            // Backwards compatability\n            var toolbarOptions = MediumEditor.util.extend({}, this.options.toolbar, {\n                allowMultiParagraphSelection: this.options.allowMultiParagraphSelection // deprecated\n            });\n            toolbarExtension = new MediumEditor.extensions.toolbar(toolbarOptions);\n        }\n\n        // If the toolbar is not disabled, so we actually have an extension\n        // initialize it and add it to the extensions array\n        if (toolbarExtension) {\n            this.extensions.push(initExtension(toolbarExtension, 'toolbar', this));\n        }\n    }\n\n    function mergeOptions(defaults, options) {\n        var deprecatedProperties = [\n            ['allowMultiParagraphSelection', 'toolbar.allowMultiParagraphSelection']\n        ];\n        // warn about using deprecated properties\n        if (options) {\n            deprecatedProperties.forEach(function (pair) {\n                if (options.hasOwnProperty(pair[0]) && options[pair[0]] !== undefined) {\n                    MediumEditor.util.deprecated(pair[0], pair[1], 'v6.0.0');\n                }\n            });\n        }\n\n        return MediumEditor.util.defaults({}, options, defaults);\n    }\n\n    function execActionInternal(action, opts) {\n        /*jslint regexp: true*/\n        var appendAction = /^append-(.+)$/gi,\n            justifyAction = /justify([A-Za-z]*)$/g, /* Detecting if is justifyCenter|Right|Left */\n            match,\n            cmdValueArgument;\n        /*jslint regexp: false*/\n\n        // Actions starting with 'append-' should attempt to format a block of text ('formatBlock') using a specific\n        // type of block element (ie append-blockquote, append-h1, append-pre, etc.)\n        match = appendAction.exec(action);\n        if (match) {\n            return MediumEditor.util.execFormatBlock(this.options.ownerDocument, match[1]);\n        }\n\n        if (action === 'fontSize') {\n            // TODO: Deprecate support for opts.size in 6.0.0\n            if (opts.size) {\n                MediumEditor.util.deprecated('.size option for fontSize command', '.value', '6.0.0');\n            }\n            cmdValueArgument = opts.value || opts.size;\n            return this.options.ownerDocument.execCommand('fontSize', false, cmdValueArgument);\n        }\n\n        if (action === 'fontName') {\n            // TODO: Deprecate support for opts.name in 6.0.0\n            if (opts.name) {\n                MediumEditor.util.deprecated('.name option for fontName command', '.value', '6.0.0');\n            }\n            cmdValueArgument = opts.value || opts.name;\n            return this.options.ownerDocument.execCommand('fontName', false, cmdValueArgument);\n        }\n\n        if (action === 'createLink') {\n            return this.createLink(opts);\n        }\n\n        if (action === 'image') {\n            var src = this.options.contentWindow.getSelection().toString().trim();\n            return this.options.ownerDocument.execCommand('insertImage', false, src);\n        }\n\n        if (action === 'html') {\n            var html = this.options.contentWindow.getSelection().toString().trim();\n            return MediumEditor.util.insertHTMLCommand(this.options.ownerDocument, html);\n        }\n\n        /* Issue: https://github.com/yabwe/medium-editor/issues/595\n         * If the action is to justify the text */\n        if (justifyAction.exec(action)) {\n            var result = this.options.ownerDocument.execCommand(action, false, null),\n                parentNode = MediumEditor.selection.getSelectedParentElement(MediumEditor.selection.getSelectionRange(this.options.ownerDocument));\n            if (parentNode) {\n                cleanupJustifyDivFragments.call(this, MediumEditor.util.getTopBlockContainer(parentNode));\n            }\n\n            return result;\n        }\n\n        cmdValueArgument = opts && opts.value;\n        return this.options.ownerDocument.execCommand(action, false, cmdValueArgument);\n    }\n\n    /* If we've just justified text within a container block\n     * Chrome may have removed <br> elements and instead wrapped lines in <div> elements\n     * with a text-align property.  If so, we want to fix this\n     */\n    function cleanupJustifyDivFragments(blockContainer) {\n        if (!blockContainer) {\n            return;\n        }\n\n        var textAlign,\n            childDivs = Array.prototype.slice.call(blockContainer.childNodes).filter(function (element) {\n                var isDiv = element.nodeName.toLowerCase() === 'div';\n                if (isDiv && !textAlign) {\n                    textAlign = element.style.textAlign;\n                }\n                return isDiv;\n            });\n\n        /* If we found child <div> elements with text-align style attributes\n         * we should fix this by:\n         *\n         * 1) Unwrapping each <div> which has a text-align style\n         * 2) Insert a <br> element after each set of 'unwrapped' div children\n         * 3) Set the text-align style of the parent block element\n         */\n        if (childDivs.length) {\n            // Since we're mucking with the HTML, preserve selection\n            this.saveSelection();\n            childDivs.forEach(function (div) {\n                if (div.style.textAlign === textAlign) {\n                    var lastChild = div.lastChild;\n                    if (lastChild) {\n                        // Instead of a div, extract the child elements and add a <br>\n                        MediumEditor.util.unwrap(div, this.options.ownerDocument);\n                        var br = this.options.ownerDocument.createElement('BR');\n                        lastChild.parentNode.insertBefore(br, lastChild.nextSibling);\n                    }\n                }\n            }, this);\n            blockContainer.style.textAlign = textAlign;\n            // We're done, so restore selection\n            this.restoreSelection();\n        }\n    }\n\n    var initialContent = {};\n\n    MediumEditor.prototype = {\n        // NOT DOCUMENTED - exposed for backwards compatability\n        init: function (elements, options) {\n            this.options = mergeOptions.call(this, this.defaults, options);\n            this.origElements = elements;\n\n            if (!this.options.elementsContainer) {\n                this.options.elementsContainer = this.options.ownerDocument.body;\n            }\n\n            return this.setup();\n        },\n\n        setup: function () {\n            if (this.isActive) {\n                return;\n            }\n\n            addToEditors.call(this, this.options.contentWindow);\n            this.events = new MediumEditor.Events(this);\n            this.elements = [];\n\n            this.addElements(this.origElements);\n\n            if (this.elements.length === 0) {\n                return;\n            }\n\n            this.isActive = true;\n\n            // Call initialization helpers\n            initExtensions.call(this);\n            attachHandlers.call(this);\n        },\n\n        destroy: function () {\n            if (!this.isActive) {\n                return;\n            }\n\n            this.isActive = false;\n\n            this.extensions.forEach(function (extension) {\n                if (typeof extension.destroy === 'function') {\n                    extension.destroy();\n                }\n            }, this);\n\n            this.events.destroy();\n\n            this.elements.forEach(function (element) {\n                // Reset elements content, fix for issue where after editor destroyed the red underlines on spelling errors are left\n                if (this.options.spellcheck) {\n                    element.innerHTML = element.innerHTML;\n                }\n\n                // cleanup extra added attributes\n                element.removeAttribute('contentEditable');\n                element.removeAttribute('spellcheck');\n                element.removeAttribute('data-medium-editor-element');\n                element.classList.remove('medium-editor-element');\n                element.removeAttribute('role');\n                element.removeAttribute('aria-multiline');\n                element.removeAttribute('medium-editor-index');\n                element.removeAttribute('data-medium-editor-editor-index');\n\n                // Remove any elements created for textareas\n                if (element.getAttribute('medium-editor-textarea-id')) {\n                    cleanupTextareaElement(element);\n                }\n            }, this);\n            this.elements = [];\n            this.instanceHandleEditableKeydownEnter = null;\n            this.instanceHandleEditableInput = null;\n\n            removeFromEditors.call(this, this.options.contentWindow);\n        },\n\n        on: function (target, event, listener, useCapture) {\n            this.events.attachDOMEvent(target, event, listener, useCapture);\n\n            return this;\n        },\n\n        off: function (target, event, listener, useCapture) {\n            this.events.detachDOMEvent(target, event, listener, useCapture);\n\n            return this;\n        },\n\n        subscribe: function (event, listener) {\n            this.events.attachCustomEvent(event, listener);\n\n            return this;\n        },\n\n        unsubscribe: function (event, listener) {\n            this.events.detachCustomEvent(event, listener);\n\n            return this;\n        },\n\n        trigger: function (name, data, editable) {\n            this.events.triggerCustomEvent(name, data, editable);\n\n            return this;\n        },\n\n        delay: function (fn) {\n            var self = this;\n            return setTimeout(function () {\n                if (self.isActive) {\n                    fn();\n                }\n            }, this.options.delay);\n        },\n\n        serialize: function () {\n            var i,\n                elementid,\n                content = {},\n                len = this.elements.length;\n\n            for (i = 0; i < len; i += 1) {\n                elementid = (this.elements[i].id !== '') ? this.elements[i].id : 'element-' + i;\n                content[elementid] = {\n                    value: this.elements[i].innerHTML.trim()\n                };\n            }\n            return content;\n        },\n\n        getExtensionByName: function (name) {\n            var extension;\n            if (this.extensions && this.extensions.length) {\n                this.extensions.some(function (ext) {\n                    if (ext.name === name) {\n                        extension = ext;\n                        return true;\n                    }\n                    return false;\n                });\n            }\n            return extension;\n        },\n\n        /**\n         * NOT DOCUMENTED - exposed as a helper for other extensions to use\n         */\n        addBuiltInExtension: function (name, opts) {\n            var extension = this.getExtensionByName(name),\n                merged;\n            if (extension) {\n                return extension;\n            }\n\n            switch (name) {\n                case 'anchor':\n                    merged = MediumEditor.util.extend({}, this.options.anchor, opts);\n                    extension = new MediumEditor.extensions.anchor(merged);\n                    break;\n                case 'anchor-preview':\n                    extension = new MediumEditor.extensions.anchorPreview(this.options.anchorPreview);\n                    break;\n                case 'autoLink':\n                    extension = new MediumEditor.extensions.autoLink();\n                    break;\n                case 'fileDragging':\n                    extension = new MediumEditor.extensions.fileDragging(opts);\n                    break;\n                case 'fontname':\n                    extension = new MediumEditor.extensions.fontName(this.options.fontName);\n                    break;\n                case 'fontsize':\n                    extension = new MediumEditor.extensions.fontSize(opts);\n                    break;\n                case 'keyboardCommands':\n                    extension = new MediumEditor.extensions.keyboardCommands(this.options.keyboardCommands);\n                    break;\n                case 'paste':\n                    extension = new MediumEditor.extensions.paste(this.options.paste);\n                    break;\n                case 'placeholder':\n                    extension = new MediumEditor.extensions.placeholder(this.options.placeholder);\n                    break;\n                default:\n                    // All of the built-in buttons for MediumEditor are extensions\n                    // so check to see if the extension we're creating is a built-in button\n                    if (MediumEditor.extensions.button.isBuiltInButton(name)) {\n                        if (opts) {\n                            merged = MediumEditor.util.defaults({}, opts, MediumEditor.extensions.button.prototype.defaults[name]);\n                            extension = new MediumEditor.extensions.button(merged);\n                        } else {\n                            extension = new MediumEditor.extensions.button(name);\n                        }\n                    }\n            }\n\n            if (extension) {\n                this.extensions.push(initExtension(extension, name, this));\n            }\n\n            return extension;\n        },\n\n        stopSelectionUpdates: function () {\n            this.preventSelectionUpdates = true;\n        },\n\n        startSelectionUpdates: function () {\n            this.preventSelectionUpdates = false;\n        },\n\n        checkSelection: function () {\n            var toolbar = this.getExtensionByName('toolbar');\n            if (toolbar) {\n                toolbar.checkState();\n            }\n            return this;\n        },\n\n        // Wrapper around document.queryCommandState for checking whether an action has already\n        // been applied to the current selection\n        queryCommandState: function (action) {\n            var fullAction = /^full-(.+)$/gi,\n                match,\n                queryState = null;\n\n            // Actions starting with 'full-' need to be modified since this is a medium-editor concept\n            match = fullAction.exec(action);\n            if (match) {\n                action = match[1];\n            }\n\n            try {\n                queryState = this.options.ownerDocument.queryCommandState(action);\n            } catch (exc) {\n                queryState = null;\n            }\n\n            return queryState;\n        },\n\n        execAction: function (action, opts) {\n            /*jslint regexp: true*/\n            var fullAction = /^full-(.+)$/gi,\n                match,\n                result;\n            /*jslint regexp: false*/\n\n            // Actions starting with 'full-' should be applied to to the entire contents of the editable element\n            // (ie full-bold, full-append-pre, etc.)\n            match = fullAction.exec(action);\n            if (match) {\n                // Store the current selection to be restored after applying the action\n                this.saveSelection();\n                // Select all of the contents before calling the action\n                this.selectAllContents();\n                result = execActionInternal.call(this, match[1], opts);\n                // Restore the previous selection\n                this.restoreSelection();\n            } else {\n                result = execActionInternal.call(this, action, opts);\n            }\n\n            // do some DOM clean-up for known browser issues after the action\n            if (action === 'insertunorderedlist' || action === 'insertorderedlist') {\n                MediumEditor.util.cleanListDOM(this.options.ownerDocument, this.getSelectedParentElement());\n            }\n\n            this.checkSelection();\n            return result;\n        },\n\n        getSelectedParentElement: function (range) {\n            if (range === undefined) {\n                range = this.options.contentWindow.getSelection().getRangeAt(0);\n            }\n            return MediumEditor.selection.getSelectedParentElement(range);\n        },\n\n        selectAllContents: function () {\n            var currNode = MediumEditor.selection.getSelectionElement(this.options.contentWindow);\n\n            if (currNode) {\n                // Move to the lowest descendant node that still selects all of the contents\n                while (currNode.children.length === 1) {\n                    currNode = currNode.children[0];\n                }\n\n                this.selectElement(currNode);\n            }\n        },\n\n        selectElement: function (element) {\n            MediumEditor.selection.selectNode(element, this.options.ownerDocument);\n\n            var selElement = MediumEditor.selection.getSelectionElement(this.options.contentWindow);\n            if (selElement) {\n                this.events.focusElement(selElement);\n            }\n        },\n\n        getFocusedElement: function () {\n            var focused;\n            this.elements.some(function (element) {\n                // Find the element that has focus\n                if (!focused && element.getAttribute('data-medium-focused')) {\n                    focused = element;\n                }\n\n                // bail if we found the element that had focus\n                return !!focused;\n            }, this);\n\n            return focused;\n        },\n\n        // Export the state of the selection in respect to one of this\n        // instance of MediumEditor's elements\n        exportSelection: function () {\n            var selectionElement = MediumEditor.selection.getSelectionElement(this.options.contentWindow),\n                editableElementIndex = this.elements.indexOf(selectionElement),\n                selectionState = null;\n\n            if (editableElementIndex >= 0) {\n                selectionState = MediumEditor.selection.exportSelection(selectionElement, this.options.ownerDocument);\n            }\n\n            if (selectionState !== null && editableElementIndex !== 0) {\n                selectionState.editableElementIndex = editableElementIndex;\n            }\n\n            return selectionState;\n        },\n\n        saveSelection: function () {\n            this.selectionState = this.exportSelection();\n        },\n\n        // Restore a selection based on a selectionState returned by a call\n        // to MediumEditor.exportSelection\n        importSelection: function (selectionState, favorLaterSelectionAnchor) {\n            if (!selectionState) {\n                return;\n            }\n\n            var editableElement = this.elements[selectionState.editableElementIndex || 0];\n            MediumEditor.selection.importSelection(selectionState, editableElement, this.options.ownerDocument, favorLaterSelectionAnchor);\n        },\n\n        restoreSelection: function () {\n            this.importSelection(this.selectionState);\n        },\n\n        createLink: function (opts) {\n            var currentEditor = MediumEditor.selection.getSelectionElement(this.options.contentWindow),\n                customEvent = {},\n                targetUrl;\n\n            // Make sure the selection is within an element this editor is tracking\n            if (this.elements.indexOf(currentEditor) === -1) {\n                return;\n            }\n\n            try {\n                this.events.disableCustomEvent('editableInput');\n                // TODO: Deprecate support for opts.url in 6.0.0\n                if (opts.url) {\n                    MediumEditor.util.deprecated('.url option for createLink', '.value', '6.0.0');\n                }\n                targetUrl = opts.url || opts.value;\n                if (targetUrl && targetUrl.trim().length > 0) {\n                    var currentSelection = this.options.contentWindow.getSelection();\n                    if (currentSelection) {\n                        var currRange = currentSelection.getRangeAt(0),\n                            commonAncestorContainer = currRange.commonAncestorContainer,\n                            exportedSelection,\n                            startContainerParentElement,\n                            endContainerParentElement,\n                            textNodes;\n\n                        // If the selection is contained within a single text node\n                        // and the selection starts at the beginning of the text node,\n                        // MSIE still says the startContainer is the parent of the text node.\n                        // If the selection is contained within a single text node, we\n                        // want to just use the default browser 'createLink', so we need\n                        // to account for this case and adjust the commonAncestorContainer accordingly\n                        if (currRange.endContainer.nodeType === 3 &&\n                            currRange.startContainer.nodeType !== 3 &&\n                            currRange.startOffset === 0 &&\n                            currRange.startContainer.firstChild === currRange.endContainer) {\n                            commonAncestorContainer = currRange.endContainer;\n                        }\n\n                        startContainerParentElement = MediumEditor.util.getClosestBlockContainer(currRange.startContainer);\n                        endContainerParentElement = MediumEditor.util.getClosestBlockContainer(currRange.endContainer);\n\n                        // If the selection is not contained within a single text node\n                        // but the selection is contained within the same block element\n                        // we want to make sure we create a single link, and not multiple links\n                        // which can happen with the built in browser functionality\n                        if (commonAncestorContainer.nodeType !== 3 && commonAncestorContainer.textContent.length !== 0 && startContainerParentElement === endContainerParentElement) {\n                            var parentElement = (startContainerParentElement || currentEditor),\n                                fragment = this.options.ownerDocument.createDocumentFragment();\n\n                            // since we are going to create a link from an extracted text,\n                            // be sure that if we are updating a link, we won't let an empty link behind (see #754)\n                            // (Workaroung for Chrome)\n                            this.execAction('unlink');\n\n                            exportedSelection = this.exportSelection();\n                            fragment.appendChild(parentElement.cloneNode(true));\n\n                            if (currentEditor === parentElement) {\n                                // We have to avoid the editor itself being wiped out when it's the only block element,\n                                // as our reference inside this.elements gets detached from the page when insertHTML runs.\n                                // If we just use [parentElement, 0] and [parentElement, parentElement.childNodes.length]\n                                // as the range boundaries, this happens whenever parentElement === currentEditor.\n                                // The tradeoff to this workaround is that a orphaned tag can sometimes be left behind at\n                                // the end of the editor's content.\n                                // In Gecko:\n                                // as an empty <strong></strong> if parentElement.lastChild is a <strong> tag.\n                                // In WebKit:\n                                // an invented <br /> tag at the end in the same situation\n                                MediumEditor.selection.select(\n                                    this.options.ownerDocument,\n                                    parentElement.firstChild,\n                                    0,\n                                    parentElement.lastChild,\n                                    parentElement.lastChild.nodeType === 3 ?\n                                    parentElement.lastChild.nodeValue.length : parentElement.lastChild.childNodes.length\n                                );\n                            } else {\n                                MediumEditor.selection.select(\n                                    this.options.ownerDocument,\n                                    parentElement,\n                                    0,\n                                    parentElement,\n                                    parentElement.childNodes.length\n                                );\n                            }\n\n                            var modifiedExportedSelection = this.exportSelection();\n\n                            textNodes = MediumEditor.util.findOrCreateMatchingTextNodes(\n                                this.options.ownerDocument,\n                                fragment,\n                                {\n                                    start: exportedSelection.start - modifiedExportedSelection.start,\n                                    end: exportedSelection.end - modifiedExportedSelection.start,\n                                    editableElementIndex: exportedSelection.editableElementIndex\n                                }\n                            );\n                            // If textNodes are not present, when changing link on images\n                            // ex: <a><img src=\"http://image.test.com\"></a>, change fragment to currRange.startContainer\n                            // and set textNodes array to [imageElement, imageElement]\n                            if (textNodes.length === 0) {\n                                fragment = this.options.ownerDocument.createDocumentFragment();\n                                fragment.appendChild(commonAncestorContainer.cloneNode(true));\n                                textNodes = [fragment.firstChild.firstChild, fragment.firstChild.lastChild];\n                            }\n\n                            // Creates the link in the document fragment\n                            MediumEditor.util.createLink(this.options.ownerDocument, textNodes, targetUrl.trim());\n\n                            // Chrome trims the leading whitespaces when inserting HTML, which messes up restoring the selection.\n                            var leadingWhitespacesCount = (fragment.firstChild.innerHTML.match(/^\\s+/) || [''])[0].length;\n\n                            // Now move the created link back into the original document in a way to preserve undo/redo history\n                            MediumEditor.util.insertHTMLCommand(this.options.ownerDocument, fragment.firstChild.innerHTML.replace(/^\\s+/, ''));\n                            exportedSelection.start -= leadingWhitespacesCount;\n                            exportedSelection.end -= leadingWhitespacesCount;\n\n                            this.importSelection(exportedSelection);\n                        } else {\n                            this.options.ownerDocument.execCommand('createLink', false, targetUrl);\n                        }\n\n                        if (this.options.targetBlank || opts.target === '_blank') {\n                            MediumEditor.util.setTargetBlank(MediumEditor.selection.getSelectionStart(this.options.ownerDocument), targetUrl);\n                        } else {\n                            MediumEditor.util.removeTargetBlank(MediumEditor.selection.getSelectionStart(this.options.ownerDocument), targetUrl);\n                        }\n\n                        if (opts.buttonClass) {\n                            MediumEditor.util.addClassToAnchors(MediumEditor.selection.getSelectionStart(this.options.ownerDocument), opts.buttonClass);\n                        }\n                    }\n                }\n                // Fire input event for backwards compatibility if anyone was listening directly to the DOM input event\n                if (this.options.targetBlank || opts.target === '_blank' || opts.buttonClass) {\n                    customEvent = this.options.ownerDocument.createEvent('HTMLEvents');\n                    customEvent.initEvent('input', true, true, this.options.contentWindow);\n                    for (var i = 0, len = this.elements.length; i < len; i += 1) {\n                        this.elements[i].dispatchEvent(customEvent);\n                    }\n                }\n            } finally {\n                this.events.enableCustomEvent('editableInput');\n            }\n            // Fire our custom editableInput event\n            this.events.triggerCustomEvent('editableInput', customEvent, currentEditor);\n        },\n\n        cleanPaste: function (text) {\n            this.getExtensionByName('paste').cleanPaste(text);\n        },\n\n        pasteHTML: function (html, options) {\n            this.getExtensionByName('paste').pasteHTML(html, options);\n        },\n\n        setContent: function (html, index) {\n            index = index || 0;\n\n            if (this.elements[index]) {\n                var target = this.elements[index];\n                target.innerHTML = html;\n                this.checkContentChanged(target);\n            }\n        },\n\n        getContent: function (index) {\n            index = index || 0;\n\n            if (this.elements[index]) {\n                return this.elements[index].innerHTML.trim();\n            }\n            return null;\n        },\n\n        checkContentChanged: function (editable) {\n            editable = editable || MediumEditor.selection.getSelectionElement(this.options.contentWindow);\n            this.events.updateInput(editable, { target: editable, currentTarget: editable });\n        },\n\n        resetContent: function (element) {\n            // For all elements that exist in the this.elements array, we can assume:\n            // - Its initial content has been set in the initialContent object\n            // - It has a medium-editor-index attribute which is the key value in the initialContent object\n\n            if (element) {\n                var index = this.elements.indexOf(element);\n                if (index !== -1) {\n                    this.setContent(initialContent[element.getAttribute('medium-editor-index')], index);\n                }\n                return;\n            }\n\n            this.elements.forEach(function (el, idx) {\n                this.setContent(initialContent[el.getAttribute('medium-editor-index')], idx);\n            }, this);\n        },\n\n        addElements: function (selector) {\n            // Convert elements into an array\n            var elements = createElementsArray(selector, this.options.ownerDocument, true);\n\n            // Do we have elements to add now?\n            if (elements.length === 0) {\n                return false;\n            }\n\n            elements.forEach(function (element) {\n                // Initialize all new elements (we check that in those functions don't worry)\n                element = initElement.call(this, element, this.id);\n\n                // Add new elements to our internal elements array\n                this.elements.push(element);\n\n                // Trigger event so extensions can know when an element has been added\n                this.trigger('addElement', { target: element, currentTarget: element }, element);\n            }, this);\n        },\n\n        removeElements: function (selector) {\n            // Convert elements into an array\n            var elements = createElementsArray(selector, this.options.ownerDocument),\n                toRemove = elements.map(function (el) {\n                    // For textareas, make sure we're looking at the editor div and not the textarea itself\n                    if (el.getAttribute('medium-editor-textarea-id') && el.parentNode) {\n                        return el.parentNode.querySelector('div[medium-editor-textarea-id=\"' + el.getAttribute('medium-editor-textarea-id') + '\"]');\n                    } else {\n                        return el;\n                    }\n                });\n\n            this.elements = this.elements.filter(function (element) {\n                // If this is an element we want to remove\n                if (toRemove.indexOf(element) !== -1) {\n                    this.events.cleanupElement(element);\n                    if (element.getAttribute('medium-editor-textarea-id')) {\n                        cleanupTextareaElement(element);\n                    }\n                    // Trigger event so extensions can clean-up elements that are being removed\n                    this.trigger('removeElement', { target: element, currentTarget: element }, element);\n                    return false;\n                }\n                return true;\n            }, this);\n        }\n    };\n\n    MediumEditor.getEditorFromElement = function (element) {\n        var index = element.getAttribute('data-medium-editor-editor-index'),\n            win = element && element.ownerDocument && (element.ownerDocument.defaultView || element.ownerDocument.parentWindow);\n        if (win && win._mediumEditors && win._mediumEditors[index]) {\n            return win._mediumEditors[index];\n        }\n        return null;\n    };\n}());\n\n(function () {\n    // summary: The default options hash used by the Editor\n\n    MediumEditor.prototype.defaults = {\n        activeButtonClass: 'medium-editor-button-active',\n        buttonLabels: false,\n        delay: 0,\n        disableReturn: false,\n        disableDoubleReturn: false,\n        disableExtraSpaces: false,\n        disableEditing: false,\n        autoLink: false,\n        elementsContainer: false,\n        contentWindow: window,\n        ownerDocument: document,\n        targetBlank: false,\n        extensions: {},\n        spellcheck: true\n    };\n})();\n\nMediumEditor.parseVersionString = function (release) {\n    var split = release.split('-'),\n        version = split[0].split('.'),\n        preRelease = (split.length > 1) ? split[1] : '';\n    return {\n        major: parseInt(version[0], 10),\n        minor: parseInt(version[1], 10),\n        revision: parseInt(version[2], 10),\n        preRelease: preRelease,\n        toString: function () {\n            return [version[0], version[1], version[2]].join('.') + (preRelease ? '-' + preRelease : '');\n        }\n    };\n};\n\nMediumEditor.version = MediumEditor.parseVersionString.call(this, ({\n    // grunt-bump looks for this:\n    'version': '5.23.3'\n}).version);\n\n    return MediumEditor;\n}()));\n"
  },
  {
    "path": "index.js",
    "content": "var connect = require('connect');\nvar serveStatic = require('serve-static');\nconnect().use(serveStatic(__dirname)).listen(8088);\n"
  },
  {
    "path": "karma.conf.js",
    "content": "/* global module */\n\nmodule.exports = function (config) {\n    config.set({\n\n        browserStack: {\n            apiClientEndpoint: 'https://api.browserstack.com',\n            timeout: 600\n        },\n\n        customLaunchers: {\n            WIN81Chrome: {\n                'base': 'BrowserStack',\n                'os': 'Windows',\n                'os_version': '8.1',\n                'browser': 'chrome'\n            },\n            WIN81Firefox: {\n                'base': 'BrowserStack',\n                'os': 'Windows',\n                'os_version': '8.1',\n                'browser': 'firefox'\n            },\n            WIN81Edge: {\n                'base': 'BrowserStack',\n                'os': 'Windows',\n                'os_version': '8.1',\n                'browser': 'edge'\n            },\n            WIN10Chrome: {\n                'base': 'BrowserStack',\n                'os': 'Windows',\n                'os_version': '10',\n                'browser': 'chrome'\n            },\n            WIN10Firefox: {\n                'base': 'BrowserStack',\n                'os': 'Windows',\n                'os_version': '10',\n                'browser': 'firefox'\n            },\n            WIN10Edge: {\n                'base': 'BrowserStack',\n                'os': 'Windows',\n                'os_version': '10',\n                'browser': 'edge'\n            },\n            OSXYosemiteSafari: {\n                'base': 'BrowserStack',\n                'os': 'OS X',\n                'os_version': 'Yosemite',\n                'browser': 'safari'\n            },\n            OSXElCapitanChrome: {\n                'base': 'BrowserStack',\n                'os': 'OS X',\n                'os_version': 'El Capitan',\n                'browser': 'chrome'\n            },\n            OSXElCapitanSafari: {\n                'base': 'BrowserStack',\n                'os': 'OS X',\n                'os_version': 'El Capitan',\n                'browser': 'safari'\n            },\n            OSXElCapitanFirefox: {\n                'base': 'BrowserStack',\n                'os': 'OS X',\n                'os_version': 'El Capitan',\n                'browser': 'firefox'\n            }\n        },\n\n        basePath: '',\n        frameworks: ['jasmine'],\n\n        files: [\n            'dist/css/*.css',\n            'node_modules/lodash/lodash.js',\n            'src/js/polyfills.js',\n            'src/js/globals.js',\n            'src/js/util.js',\n            'src/js/extension.js',\n            'src/js/selection.js',\n            'src/js/events.js',\n            'src/js/extensions/button.js',\n            'src/js/defaults/buttons.js',\n            'src/js/extensions/form.js',\n            'src/js/extensions/anchor.js',\n            'src/js/extensions/anchor-preview.js',\n            'src/js/extensions/auto-link.js',\n            'src/js/extensions/file-dragging.js',\n            'src/js/extensions/keyboard-commands.js',\n            'src/js/extensions/fontname.js',\n            'src/js/extensions/fontsize.js',\n            'src/js/extensions/paste.js',\n            'src/js/extensions/placeholder.js',\n            'src/js/extensions/toolbar.js',\n            'src/js/extensions/deprecated/image-dragging.js',\n            'src/js/core.js',\n            'src/js/defaults/options.js',\n            'src/js/version.js',\n            'spec/helpers/util.js',\n            'spec/*.spec.js'\n        ],\n\n        exclude: [\n            'src/js/extensions/deprecated/*'\n        ],\n\n        preprocessors: {\n        },\n\n        plugins: [\n            'karma-jasmine',\n            'karma-spec-reporter',\n            'karma-jasmine-html-reporter',\n            'karma-browserstack-launcher',\n            'karma-phantomjs-launcher',\n            'karma-firefox-launcher',\n            'karma-chrome-launcher',\n            'karma-coverage',\n            'karma-coveralls'\n        ],\n\n        reporters: ['coverage', 'coveralls', 'BrowserStack', 'dots', 'spec', 'kjhtml'],\n\n        coverageReporter: {\n            type: 'lcov',\n            dir: 'coverage/'\n        },\n\n        port: 9876,\n\n        logLevel: config.LOG_ERROR,\n        colors: true,\n\n        autoWatch: false,\n\n        browsers: ['WIN10Edge', 'WIN10Chrome', 'WIN10Firefox', 'OSXElCapitanChrome', 'OSXElCapitanFirefox', 'OSXYosemiteSafari'],\n\n        client: {\n            clearContext: false\n        },\n\n        singleRun: true,\n\n        concurrency: Infinity\n    });\n};"
  },
  {
    "path": "karma.dev.conf.js",
    "content": "/* global module */\n\nmodule.exports = function (config) {\n    config.set({\n\n        basePath: '',\n        frameworks: ['jasmine'],\n\n        files: [\n            'dist/css/*.css',\n            'node_modules/lodash/lodash.js',\n            'src/js/polyfills.js',\n            'src/js/globals.js',\n            'src/js/util.js',\n            'src/js/extension.js',\n            'src/js/selection.js',\n            'src/js/events.js',\n            'src/js/extensions/button.js',\n            'src/js/defaults/buttons.js',\n            'src/js/extensions/form.js',\n            'src/js/extensions/anchor.js',\n            'src/js/extensions/anchor-preview.js',\n            'src/js/extensions/auto-link.js',\n            'src/js/extensions/file-dragging.js',\n            'src/js/extensions/keyboard-commands.js',\n            'src/js/extensions/fontname.js',\n            'src/js/extensions/fontsize.js',\n            'src/js/extensions/paste.js',\n            'src/js/extensions/placeholder.js',\n            'src/js/extensions/toolbar.js',\n            'src/js/extensions/deprecated/image-dragging.js',\n            'src/js/core.js',\n            'src/js/defaults/options.js',\n            'src/js/version.js',\n            'spec/helpers/util.js',\n            'spec/*.spec.js'\n        ],\n\n        exclude: [\n            'src/js/extensions/deprecated/*'\n        ],\n\n        preprocessors: {\n        },\n\n        browsers: [\n            'Chrome'\n        ],\n        plugins: [\n            'karma-jasmine',\n            'karma-spec-reporter',\n            'karma-jasmine-html-reporter',\n            'karma-browserstack-launcher',\n            'karma-phantomjs-launcher',\n            'karma-chrome-launcher'\n        ],\n        reporters: ['progress', 'BrowserStack', 'dots', 'spec', 'kjhtml'],\n\n        port: 9876,\n\n        logLevel: config.LOG_INFO,\n        colors: true,\n\n        autoWatch: false,\n\n        client: {\n            clearContext: false\n        },\n\n        singleRun: true,\n\n        concurrency: Infinity\n    });\n};"
  },
  {
    "path": "package.json",
    "content": "{\n  \"name\": \"medium-editor\",\n  \"version\": \"5.23.3\",\n  \"author\": \"Davi Ferreira <hi@daviferreira.com>\",\n  \"contributors\": [\n    {\n      \"name\": \"Nate Mielnik\",\n      \"email\": \"nathan@outlook.com\"\n    },\n    {\n      \"name\": \"Noah Chase\",\n      \"email\": \"nchase@gmail.com\"\n    },\n    {\n      \"name\": \"Jeremy Benoist\",\n      \"email\": \"jeremy.benoist@gmail.com\"\n    }\n  ],\n  \"description\": \"Medium.com WYSIWYG editor clone.\",\n  \"main\": \"dist/js/medium-editor.js\",\n  \"repository\": {\n    \"type\": \"git\",\n    \"url\": \"https://github.com/yabwe/medium-editor\"\n  },\n  \"bugs\": {\n    \"url\": \"https://github.com/yabwe/medium-editor/issues\",\n    \"email\": \"hi@daviferreira.com\"\n  },\n  \"homepage\": \"http://yabwe.github.io/medium-editor/\",\n  \"keywords\": [\n    \"contenteditable\",\n    \"editor\",\n    \"medium\",\n    \"wysiwyg\",\n    \"rich-text\"\n  ],\n  \"publishConfig\": {\n    \"registry\": \"http://registry.npmjs.org/\"\n  },\n  \"license\": \"MIT\",\n  \"devDependencies\": {\n    \"brfs\": \"2.0.2\",\n    \"connect\": \"3.7.0\",\n    \"grunt\": \"1.2.1\",\n    \"grunt-autoprefixer\": \"3.0.4\",\n    \"grunt-bump\": \"0.8.0\",\n    \"grunt-cli\": \"1.3.2\",\n    \"grunt-contrib-concat\": \"1.0.1\",\n    \"grunt-contrib-connect\": \"2.1.0\",\n    \"grunt-contrib-csslint\": \"2.0.0\",\n    \"grunt-contrib-cssmin\": \"3.0.0\",\n    \"grunt-contrib-jasmine\": \"1.0.3\",\n    \"grunt-contrib-jshint\": \"2.1.0\",\n    \"grunt-contrib-uglify\": \"4.0.1\",\n    \"grunt-contrib-watch\": \"1.1.0\",\n    \"grunt-coveralls\": \"2.0.0\",\n    \"grunt-jscs\": \"3.0.1\",\n    \"grunt-karma\": \"4.0.0\",\n    \"grunt-plato\": \"1.4.0\",\n    \"grunt-sass\": \"3.1.0\",\n    \"grunt-template-jasmine-istanbul\": \"0.4.0\",\n    \"jasmine\": \"3.6.1\",\n    \"jasmine-console-reporter\": \"3.1.0\",\n    \"jasmine-core\": \"3.6.0\",\n    \"jshint-stylish\": \"2.2.1\",\n    \"karma\": \"5.1.0\",\n    \"karma-browserstack-launcher\": \"1.6.0\",\n    \"karma-chrome-launcher\": \"3.1.0\",\n    \"karma-coverage\": \"2.0.3\",\n    \"karma-coveralls\": \"2.1.0\",\n    \"karma-firefox-launcher\": \"1.3.0\",\n    \"karma-jasmine\": \"3.3.1\",\n    \"karma-jasmine-html-reporter\": \"1.5.4\",\n    \"karma-phantomjs-launcher\": \"1.0.4\",\n    \"karma-spec-reporter\": \"0.0.32\",\n    \"load-grunt-tasks\": \"5.1.0\",\n    \"lodash\": \"4.17.19\",\n    \"open-cli\": \"6.0.1\",\n    \"phantomjs-prebuilt\": \"2.1.16\",\n    \"serve-static\": \"1.14.1\",\n    \"time-grunt\": \"2.0.0\"\n  },\n  \"scripts\": {\n    \"test\": \"node node_modules/grunt-cli/bin/grunt test --verbose\",\n    \"test:ci\": \"node node_modules/grunt-cli/bin/grunt travis --verbose\",\n    \"start\": \"open-cli ./demo/index.html\",\n    \"build\": \"node node_modules/grunt-cli/bin/grunt\"\n  }\n}\n"
  },
  {
    "path": "spec/anchor-preview.spec.js",
    "content": "/*global fireEvent, selectElementContentsAndFire */\n\ndescribe('Anchor Preview TestCase', function () {\n    'use strict';\n\n    beforeEach(function () {\n        setupTestHelpers.call(this);\n        this.el = this.createElement('div', 'editor',\n            'lorem ' +\n            '<a id=\"test-link\" href=\"http://test.com\">ipsum</a> ' +\n            'preview <span id=\"another-element\">&nbsp;</span> ' +\n            '<a id=\"test-empty-link\" href=\"\">ipsum</a> ' +\n            '<a id=\"test-link-disable-preview\" data-disable-preview=\"true\" href=\"http://test.com\">ipsum</a> ' +\n            '<a id=\"test-markup-link\" href=\"http://test.com\"><b>ipsum</b></a> ' +\n            '<a id=\"test-symbol-link\" href=\"http://[{~#custom#~}].com\"></a> ' +\n            '<a id=\"text-target-blank-link\" target=\"_blank\" href=\"http://test.com\">ipsum</a> ' +\n            '<a id=\"text-custom-class-link\" class=\"custom-class\" href=\"http://test.com\">ipsum</a>');\n    });\n\n    afterEach(function () {\n        this.cleanupTest();\n    });\n\n    describe('anchor preview element', function () {\n        it('should be displayed on hover of a link element', function () {\n            var editor = this.newMediumEditor('.editor', {\n                    delay: 200\n                }),\n                toolbar = editor.getExtensionByName('toolbar'),\n                sel = window.getSelection(),\n                anchorPreview = editor.getExtensionByName('anchor-preview'),\n                nextRange;\n\n            // show preview\n            spyOn(MediumEditor.extensions.anchorPreview.prototype, 'showPreview').and.callThrough();\n            fireEvent(document.getElementById('test-link'), 'mouseover');\n\n            // preview shows only after delay\n            expect(anchorPreview.showPreview).not.toHaveBeenCalled();\n            jasmine.clock().tick(250);\n            expect(anchorPreview.showPreview).toHaveBeenCalled();\n\n            // link is set in preview\n            expect(anchorPreview.getPreviewElement().querySelector('a').innerHTML).toBe(document.getElementById('test-link').attributes.href.value);\n\n            // load into editor\n            spyOn(MediumEditor.extensions.anchor.prototype, 'showForm').and.callThrough();\n            fireEvent(anchorPreview.getPreviewElement(), 'click');\n            jasmine.clock().tick(300);\n            expect(editor.getExtensionByName('anchor').showForm).toHaveBeenCalled();\n\n            // selecting other text should close the toolbar\n            spyOn(MediumEditor.extensions.toolbar.prototype, 'hideToolbar').and.callThrough();\n            nextRange = document.createRange();\n            nextRange.selectNodeContents(document.getElementById('another-element'));\n            sel.removeAllRanges();\n            sel.addRange(nextRange);\n            fireEvent(document.getElementById('another-element'), 'click');\n            jasmine.clock().tick(200);\n            expect(toolbar.hideToolbar).toHaveBeenCalled();\n        });\n\n        it('should be displayed on hover of a link element with markup inside', function () {\n            var editor = this.newMediumEditor('.editor', {\n                    delay: 200\n                }),\n                anchorPreview = editor.getExtensionByName('anchor-preview');\n\n            // show preview\n            spyOn(MediumEditor.extensions.anchorPreview.prototype, 'showPreview').and.callThrough();\n            fireEvent(document.getElementById('test-markup-link'), 'mouseover');\n\n            // preview shows only after delay\n            expect(anchorPreview.showPreview).not.toHaveBeenCalled();\n            jasmine.clock().tick(250);\n            expect(anchorPreview.showPreview).toHaveBeenCalled();\n\n            // link is set in preview\n            expect(anchorPreview.getPreviewElement().querySelector('a').innerHTML).toBe(document.getElementById('test-markup-link').attributes.href.value);\n        });\n\n        it('should show the unencoded link', function () {\n            var editor = this.newMediumEditor('.editor'),\n                anchorPreview = editor.getExtensionByName('anchor-preview');\n\n            // show preview\n            fireEvent(document.getElementById('test-symbol-link'), 'mouseover');\n\n            // preview shows only after delay\n            jasmine.clock().tick(200);\n\n            // link is set in preview\n            expect(anchorPreview.getPreviewElement().querySelector('a').innerHTML).toBe(document.getElementById('test-symbol-link').attributes.href.value);\n        });\n\n        it('should display different urls when hovering over different links consecutively', function () {\n            var editor = this.newMediumEditor('.editor', {\n                    delay: 300\n                }),\n                anchorPreview = editor.getExtensionByName('anchor-preview');\n\n            // show preview for first link\n            fireEvent(document.getElementById('test-link'), 'mouseover');\n\n            // preview shows only after delay\n            jasmine.clock().tick(300);\n            expect(anchorPreview.getPreviewElement().querySelector('a').innerHTML).toBe(document.getElementById('test-link').attributes.href.value);\n\n            // show preview for second link\n            fireEvent(document.getElementById('test-symbol-link'), 'mouseover');\n            expect(anchorPreview.getPreviewElement().classList.contains('medium-editor-anchor-preview-active')).toBe(false);\n\n            // wait for delay\n            jasmine.clock().tick(300);\n            expect(anchorPreview.getPreviewElement().querySelector('a').innerHTML).toBe(document.getElementById('test-symbol-link').attributes.href.value);\n        });\n\n        it('should display the anchor form in the toolbar when clicked', function () {\n            var editor = this.newMediumEditor('.editor', {\n                    anchor: {\n                        targetCheckbox: true,\n                        targetCheckboxText: 'Open in new window',\n                        customClassOption: 'custom-class',\n                        customClassOptionText: 'Custom Class'\n                    }\n                }),\n                anchorPreview = editor.getExtensionByName('anchor-preview'),\n                anchor = editor.getExtensionByName('anchor'),\n                toolbar = editor.getExtensionByName('toolbar');\n\n            // show preview\n            fireEvent(document.getElementById('test-link'), 'mouseover');\n\n            // load into editor\n            jasmine.clock().tick(1);\n            fireEvent(anchorPreview.getPreviewElement(), 'click');\n            jasmine.clock().tick(200);\n\n            expect(toolbar.isDisplayed()).toBe(true);\n            expect(anchor.isDisplayed()).toBe(true);\n\n            // textbox should contain the url\n            expect(anchor.getInput().value).toEqual('http://test.com');\n\n            // the checkboxes should be unchecked\n            expect(anchor.getAnchorTargetCheckbox().checked).toBe(false);\n            expect(anchor.getAnchorButtonCheckbox().checked).toBe(false);\n        });\n\n        it('should display the anchor form with target checkbox checked in the toolbar when clicked', function () {\n            var editor = this.newMediumEditor('.editor', {\n                    anchor: {\n                        targetCheckbox: true,\n                        targetCheckboxText: 'Open in new window',\n                        customClassOption: 'custom-class',\n                        customClassOptionText: 'Custom Class'\n                    }\n                }),\n                anchorPreview = editor.getExtensionByName('anchor-preview'),\n                anchor = editor.getExtensionByName('anchor'),\n                toolbar = editor.getExtensionByName('toolbar');\n\n            // show preview\n            fireEvent(document.getElementById('text-target-blank-link'), 'mouseover');\n\n            // load into editor\n            jasmine.clock().tick(1);\n            fireEvent(anchorPreview.getPreviewElement(), 'click');\n            jasmine.clock().tick(200);\n\n            expect(toolbar.isDisplayed()).toBe(true);\n            expect(anchor.isDisplayed()).toBe(true);\n\n            // the checkboxes should be unchecked\n            expect(anchor.getAnchorTargetCheckbox().checked).toBe(true);\n            expect(anchor.getAnchorButtonCheckbox().checked).toBe(false);\n        });\n\n        it('should display the anchor form with custom class checkbox checked in the toolbar when clicked', function () {\n            var editor = this.newMediumEditor('.editor', {\n                    anchor: {\n                        targetCheckbox: true,\n                        targetCheckboxText: 'Open in new window',\n                        customClassOption: 'custom-class',\n                        customClassOptionText: 'Custom Class'\n                    }\n                }),\n                anchorPreview = editor.getExtensionByName('anchor-preview'),\n                anchor = editor.getExtensionByName('anchor'),\n                toolbar = editor.getExtensionByName('toolbar');\n\n            // show preview\n            fireEvent(document.getElementById('text-custom-class-link'), 'mouseover');\n\n            // load into editor\n            jasmine.clock().tick(1);\n            fireEvent(anchorPreview.getPreviewElement(), 'click');\n            jasmine.clock().tick(200);\n\n            expect(toolbar.isDisplayed()).toBe(true);\n            expect(anchor.isDisplayed()).toBe(true);\n\n            // the checkboxes should be unchecked\n            expect(anchor.getAnchorTargetCheckbox().checked).toBe(false);\n            expect(anchor.getAnchorButtonCheckbox().checked).toBe(true);\n        });\n\n        it('should be displayed by default when the hovered link is empty', function () {\n            var editor = this.newMediumEditor('.editor', {\n                    delay: 200\n                }),\n                anchorPreview = editor.getExtensionByName('anchor-preview');\n\n            // show preview\n            spyOn(MediumEditor.extensions.anchorPreview.prototype, 'showPreview').and.callThrough();\n            fireEvent(document.getElementById('test-empty-link'), 'mouseover');\n\n            // preview shows only after delay\n            jasmine.clock().tick(250);\n            expect(anchorPreview.showPreview).toHaveBeenCalled();\n        });\n\n        it('should NOT be displayed when the hovered link is empty and the option showOnEmptyLinks is set to false', function () {\n            var editor = this.newMediumEditor('.editor', {\n                delay: 200,\n                anchorPreview: {\n                    showOnEmptyLinks: false\n                }\n            }),\n            anchorPreview = editor.getExtensionByName('anchor-preview');\n\n            // show preview\n            spyOn(MediumEditor.extensions.anchorPreview.prototype, 'showPreview').and.callThrough();\n            fireEvent(document.getElementById('test-empty-link'), 'mouseover');\n\n            // preview shows only after delay\n            jasmine.clock().tick(250);\n            expect(anchorPreview.showPreview).not.toHaveBeenCalled();\n        });\n\n        it('should be displayed when the link has data attribute to disable preview', function () {\n            var editor = this.newMediumEditor('.editor', {\n                    delay: 200\n                }),\n                anchorPreview = editor.getExtensionByName('anchor-preview');\n\n            // show preview\n            spyOn(MediumEditor.extensions.anchorPreview.prototype, 'showPreview').and.callThrough();\n            fireEvent(document.getElementById('test-link-disable-preview'), 'mouseover');\n\n            // preview shows only after delay\n            jasmine.clock().tick(250);\n            expect(anchorPreview.showPreview).toHaveBeenCalled();\n\n            // showPreview is called but the preview isn't displayed\n            expect(anchorPreview.getPreviewElement().classList.contains('medium-toolbar-arrow-over')).toBe(false);\n        });\n\n        it('should be displayed when the option showWhenToolbarIsVisible is set to true and toolbar is visible', function () {\n            var editor = this.newMediumEditor('.editor', {\n                    delay: 200,\n                    anchorPreview: {\n                        showWhenToolbarIsVisible: true\n                    },\n                    toolbar: {\n                        static: true\n                    }\n                }),\n                anchorPreview = editor.getExtensionByName('anchor-preview'),\n                toolbar = editor.getExtensionByName('toolbar');\n\n            selectElementContentsAndFire(editor.elements[0].firstChild);\n\n            // show preview\n            spyOn(MediumEditor.extensions.anchorPreview.prototype, 'showPreview').and.callThrough();\n            fireEvent(document.getElementById('test-link'), 'mouseover');\n\n            // preview shows only after delay\n            jasmine.clock().tick(250);\n            expect(anchorPreview.showPreview).toHaveBeenCalled();\n            expect(toolbar.isDisplayed()).toBe(true);\n            expect(anchorPreview.getPreviewElement().classList.contains('medium-editor-anchor-preview-active')).toBe(true);\n        });\n\n        it('should NOT be displayed when the option showWhenToolbarIsVisible is set to false and toolbar is visible', function () {\n            var editor = this.newMediumEditor('.editor', {\n                    delay: 200,\n                    anchorPreview: {\n                        showWhenToolbarIsVisible: false\n                    },\n                    toolbar: {\n                        static: true\n                    }\n                }),\n                anchorPreview = editor.getExtensionByName('anchor-preview'),\n                toolbar = editor.getExtensionByName('toolbar');\n\n            selectElementContentsAndFire(editor.elements[0].firstChild);\n\n            // show preview\n            spyOn(MediumEditor.extensions.anchorPreview.prototype, 'showPreview').and.callThrough();\n            fireEvent(document.getElementById('test-link'), 'mouseover');\n\n            // preview shows only after delay\n            jasmine.clock().tick(250);\n            expect(anchorPreview.showPreview).not.toHaveBeenCalled();\n            expect(toolbar.isDisplayed()).toBe(true);\n            expect(anchorPreview.getPreviewElement().classList.contains('medium-editor-anchor-preview-active')).toBe(false);\n        });\n\n        // https://github.com/yabwe/medium-editor/issues/1047\n        it('should display the anchor form in the toolbar when clicked when showWhenToolbarIsVisible is set to true and toolbar is visible', function () {\n            var editor = this.newMediumEditor('.editor', {\n                    anchorPreview: {\n                        showWhenToolbarIsVisible: true\n                    },\n                    toolbar: {\n                        static: true\n                    }\n                }),\n                anchorPreview = editor.getExtensionByName('anchor-preview'),\n                anchor = editor.getExtensionByName('anchor'),\n                toolbar = editor.getExtensionByName('toolbar');\n\n            // show toolbar\n            selectElementContentsAndFire(editor.elements[0].firstChild);\n            jasmine.clock().tick(1);\n            expect(toolbar.isDisplayed()).toBe(true);\n\n            // show preview\n            fireEvent(document.getElementById('test-link'), 'mouseover');\n\n            // load into editor\n            jasmine.clock().tick(1);\n            expect(anchorPreview.getPreviewElement().classList.contains('medium-editor-anchor-preview-active')).toBe(true);\n\n            var clickEvent = {\n                defaultPrevented: false,\n                preventDefault: function () {\n                    this.defaultPrevented = true;\n                }\n            };\n\n            // trigger all events toolbar is listening to on clicks\n            fireEvent(anchorPreview.getPreviewElement(), 'mousedown');\n            fireEvent(anchorPreview.getPreviewElement(), 'mouseup');\n            anchorPreview.handleClick(clickEvent);\n            jasmine.clock().tick(1);\n\n            // click on the link should have called `preventDefault` to stop from navigating away\n            expect(clickEvent.defaultPrevented).toBe(true, 'link navigation was not prevented on click of the anchor-preview');\n\n            // anchor form should be visible in toolbar\n            expect(toolbar.isDisplayed()).toBe(true);\n            expect(anchor.isDisplayed()).toBe(true, 'anchor form to edit link is not visible');\n            expect(anchorPreview.getPreviewElement().classList.contains('medium-editor-anchor-preview-active')).toBe(false,\n                'anchor-preview is still visible after being clicked');\n        });\n\n        it('should NOT be present when anchorPreview option is set to false', function () {\n            var editor = this.newMediumEditor('.editor', {\n                    anchorPreview: false\n                }),\n                anchorPreview = editor.getExtensionByName('anchor-preview');\n\n            expect(anchorPreview).toBeUndefined();\n            expect(document.querySelector('.medium-editor-anchor-preview')).toBeNull();\n        });\n\n        it('should not be present when toolbar option is disabled', function () {\n            var editor = this.newMediumEditor('.editor', {\n                    toolbar: false\n                }),\n                anchorPreview = editor.getExtensionByName('anchor-preview');\n\n            expect(anchorPreview).toBeUndefined();\n            expect(document.querySelector('.medium-editor-anchor-preview')).toBeNull();\n        });\n\n        it('should be removed from the document when editor is destroyed', function () {\n            var editor = this.newMediumEditor('.editor'),\n                anchorPreview = editor.getExtensionByName('anchor-preview');\n\n            spyOn(MediumEditor.extensions.anchorPreview.prototype, 'destroy').and.callThrough();\n            expect(document.querySelector('.medium-editor-anchor-preview')).not.toBeNull();\n            expect(document.querySelector('.medium-editor-anchor-preview-active')).toBeNull();\n\n            // show preview\n            fireEvent(document.getElementById('test-link'), 'mouseover');\n\n            jasmine.clock().tick(1);\n            expect(document.querySelector('.medium-editor-anchor-preview-active')).not.toBeNull();\n\n            // destroy\n            editor.destroy();\n            expect(anchorPreview.destroy).toHaveBeenCalled();\n            expect(document.querySelector('.medium-editor-anchor-preview-active')).toBeNull();\n            expect(document.querySelector('.medium-editor-anchor-preview')).toBeNull();\n        });\n\n        it('should NOT be in the DOM when a custom anchorPreview extension is provided', function () {\n            this.newMediumEditor('.editor', {\n                extensions: {\n                    'anchor-preview': {}\n                }\n            });\n\n            expect(document.querySelector('.medium-editor-anchor-preview')).toBeNull();\n        });\n\n        it('should correctly set preview position even if elementsContainer is absolute', function () {\n            var container = document.createElement('div'),\n                editor, anchorPreview;\n\n            container.style.position = 'absolute';\n            container.style.left = '200px';\n            container.style.top = '200px';\n            document.body.appendChild(container);\n\n            editor = this.newMediumEditor('.editor', {\n                elementsContainer: container,\n                anchorPreview: {\n                    showWhenToolbarIsVisible: true\n                },\n                toolbar: {\n                    static: true\n                }\n            });\n            anchorPreview = editor.getExtensionByName('anchor-preview').getPreviewElement();\n\n            selectElementContentsAndFire(editor.elements[0].firstChild);\n\n            // show preview\n            fireEvent(document.getElementById('test-link'), 'mouseover');\n\n            // preview shows only after delay\n            jasmine.clock().tick(501);\n            expect(anchorPreview.classList.contains('medium-editor-anchor-preview-active')).toBe(true);\n            expect(parseInt(anchorPreview.style.left, 10)).toBeLessThan(200);\n            expect(parseInt(anchorPreview.style.top, 10)).toBeLessThan(200);\n\n            document.body.removeChild(container);\n        });\n    });\n\n});\n"
  },
  {
    "path": "spec/anchor.spec.js",
    "content": "/*global fireEvent, selectElementContents,\n         selectElementContentsAndFire, getEdgeVersion */\n\ndescribe('Anchor Button TestCase', function () {\n    'use strict';\n\n    beforeEach(function () {\n        setupTestHelpers.call(this);\n        this.el = this.createElement('div', 'editor', 'lorem ipsum');\n    });\n\n    afterEach(function () {\n        this.cleanupTest();\n    });\n\n    describe('Anchor Form', function () {\n        it('should add class for visible state and remove it for invisivble', function () {\n            var editor = this.newMediumEditor('.editor', {\n                    buttonLabels: 'fontawesome'\n                }),\n                anchorExtension = editor.getExtensionByName('anchor'),\n                toolbar = editor.getExtensionByName('toolbar'),\n                activeClass = anchorExtension.activeClass;\n\n            selectElementContentsAndFire(editor.elements[0]);\n            var button = toolbar.getToolbarElement().querySelector('[data-action=\"createLink\"]');\n            fireEvent(button, 'click');\n            expect(anchorExtension.getForm().classList.contains(activeClass)).toBe(true);\n\n            fireEvent(anchorExtension.getForm().querySelector('a.medium-editor-toolbar-save'), 'click');\n            expect(anchorExtension.getForm().classList.contains(activeClass)).toBe(false);\n        });\n\n        it('should not hide the toolbar when mouseup fires inside the anchor form', function () {\n            var editor = this.newMediumEditor('.editor', {\n                    buttonLabels: 'fontawesome'\n                }),\n                anchorExtension = editor.getExtensionByName('anchor'),\n                toolbar = editor.getExtensionByName('toolbar');\n\n            selectElementContentsAndFire(editor.elements[0]);\n            var button = toolbar.getToolbarElement().querySelector('[data-action=\"createLink\"]');\n            fireEvent(button, 'click');\n\n            expect(toolbar.isDisplayed()).toBe(true);\n            expect(anchorExtension.isDisplayed()).toBe(true);\n\n            fireEvent(anchorExtension.getInput(), 'mouseup');\n            expect(toolbar.isDisplayed()).toBe(true);\n            expect(anchorExtension.isDisplayed()).toBe(true);\n        });\n\n        it('should show the form on shortcut', function () {\n            var editor = this.newMediumEditor('.editor'),\n                anchorExtension = editor.getExtensionByName('anchor'),\n                toolbar = editor.getExtensionByName('toolbar'),\n                code = 'K'.charCodeAt(0);\n\n            selectElementContentsAndFire(editor.elements[0]);\n            fireEvent(editor.elements[0], 'keydown', {\n                keyCode: code,\n                ctrlKey: true,\n                metaKey: true\n            });\n\n            expect(toolbar.isDisplayed()).toBe(true);\n            expect(anchorExtension.isDisplayed()).toBe(true);\n        });\n    });\n\n    describe('Link Creation', function () {\n        it('should create a link when user presses enter', function () {\n            spyOn(MediumEditor.prototype, 'createLink').and.callThrough();\n            var editor = this.newMediumEditor('.editor'),\n                toolbar = editor.getExtensionByName('toolbar'),\n                button, input;\n\n            selectElementContents(editor.elements[0]);\n            button = toolbar.getToolbarElement().querySelector('[data-action=\"createLink\"]');\n            fireEvent(button, 'click');\n            input = editor.getExtensionByName('anchor').getInput();\n            input.value = 'http://test.com';\n            fireEvent(input, 'keyup', {\n                keyCode: MediumEditor.util.keyCode.ENTER\n            });\n            expect(editor.createLink).toHaveBeenCalled();\n            // A trailing <br> may be added when insertHTML is used to add the link internally.\n            expect(this.el.innerHTML.indexOf('<a href=\"http://test.com\">lorem ipsum</a>')).toBe(0);\n        });\n\n        it('should remove the extra white spaces in the link when user presses enter', function () {\n            spyOn(MediumEditor.prototype, 'createLink').and.callThrough();\n            var editor = this.newMediumEditor('.editor'),\n                toolbar = editor.getExtensionByName('toolbar'),\n                button, input;\n\n            selectElementContents(editor.elements[0]);\n            button = toolbar.getToolbarElement().querySelector('[data-action=\"createLink\"]');\n            fireEvent(button, 'click');\n            input = editor.getExtensionByName('anchor').getInput();\n            input.value = '    test   ';\n            fireEvent(input, 'keyup', {\n                keyCode: MediumEditor.util.keyCode.ENTER\n            });\n            expect(editor.createLink).toHaveBeenCalled();\n            // A trailing <br> may be added when insertHTML is used to add the link internally.\n            expect(this.el.innerHTML.indexOf('<a href=\"test\">lorem ipsum</a>')).toBe(0);\n        });\n\n        it('should not set any href if all user passes is spaces in the link when user presses enter', function () {\n            spyOn(MediumEditor.prototype, 'createLink').and.callThrough();\n            var editor = this.newMediumEditor('.editor'),\n                toolbar = editor.getExtensionByName('toolbar'),\n                button, input;\n\n            selectElementContents(editor.elements[0]);\n            button = toolbar.getToolbarElement().querySelector('[data-action=\"createLink\"]');\n            fireEvent(button, 'click');\n            input = editor.getExtensionByName('anchor').getInput();\n            input.value = '    ';\n            fireEvent(input, 'keyup', {\n                keyCode: MediumEditor.util.keyCode.ENTER\n            });\n            //Since user only passes empty string in the url, there should be no <a> tag created for the element.\n            expect(this.el.innerHTML.indexOf('<a href=\"\">lorem ipsum</a>')).toBe(-1);\n        });\n\n        it('should create only one anchor tag when the user selects across a boundary', function () {\n            this.el.innerHTML = 'Hello world, this <strong>will become a link, but this part won\\'t.</strong>';\n\n            spyOn(MediumEditor.prototype, 'createLink').and.callThrough();\n            var editor = this.newMediumEditor('.editor'),\n                toolbar = editor.getExtensionByName('toolbar'),\n                button, input;\n\n            MediumEditor.selection.select(document, this.el.childNodes[0], 'Hello world, '.length,\n                this.el.childNodes[1].childNodes[0], 'will become a link'.length);\n            button = toolbar.getToolbarElement().querySelector('[data-action=\"createLink\"]');\n            fireEvent(button, 'click');\n            input = editor.getExtensionByName('anchor').getInput();\n            input.value = 'http://test.com';\n            fireEvent(input, 'keyup', {\n                keyCode: MediumEditor.util.keyCode.ENTER\n            });\n            expect(editor.createLink).toHaveBeenCalled();\n            expect(this.el.innerHTML).toMatch(/^Hello world, <a href=\"http:\\/\\/test\\.com\\/?\">this <strong>will become a link<\\/strong><\\/a><strong>, but this part won\\'t\\.<\\/strong>(<br>|<strong><\\/strong>)?$/);\n        });\n\n        it('should create a link when the user selects text within two paragraphs', function () {\n            this.el.innerHTML = '<p>Hello <span>world</span>.</p>' +\n                '<p><strong>Let us make a link</strong> across paragraphs.</p>';\n\n            spyOn(MediumEditor.prototype, 'createLink').and.callThrough();\n            var editor = this.newMediumEditor('.editor'),\n                toolbar = editor.getExtensionByName('toolbar'),\n                button, input;\n\n            MediumEditor.selection.select(document, this.el.querySelector('span'), 0,\n                this.el.querySelector('strong'), 1);\n            button = toolbar.getToolbarElement().querySelector('[data-action=\"createLink\"]');\n            fireEvent(button, 'click');\n            input = editor.getExtensionByName('anchor').getInput();\n            input.value = 'http://test.com';\n            fireEvent(input, 'keyup', {\n                keyCode: MediumEditor.util.keyCode.ENTER\n            });\n            expect(editor.createLink).toHaveBeenCalled();\n\n            var anchors = this.el.querySelectorAll('a');\n            // Edge creates 3 links, other browsers create 2\n            expect(anchors.length).toBeGreaterThan(1);\n            expect(anchors.length).toBeLessThan(4);\n\n            var linkText = '';\n            Array.prototype.slice.call(anchors).forEach(function (anchor) {\n                linkText += anchor.textContent;\n            });\n            expect(linkText).toBe('world.Let us make a link');\n\n            var spans = this.el.querySelectorAll('span');\n            expect(spans.length).toBe(1);\n            expect(spans[0].textContent).toBe('world');\n\n            var strongs = this.el.querySelectorAll('strong');\n            expect(strongs.length).toBe(1);\n            expect(strongs[0].textContent).toBe('Let us make a link');\n        });\n\n        it('shouldn\\'t create a link when user presses enter without value', function () {\n            spyOn(MediumEditor.prototype, 'createLink').and.callThrough();\n            var editor = this.newMediumEditor('.editor'),\n                toolbar = editor.getExtensionByName('toolbar'),\n                button,\n                input;\n\n            selectElementContents(editor.elements[0]);\n            button = toolbar.getToolbarElement().querySelector('[data-action=\"createLink\"]');\n            fireEvent(button, 'click');\n            input = editor.getExtensionByName('anchor').getInput();\n            input.value = '';\n            fireEvent(input, 'keyup', {\n                keyCode: MediumEditor.util.keyCode.ENTER\n            });\n            expect(editor.elements[0].querySelector('a')).toBeNull();\n        });\n\n        it('should add http:// if need be and linkValidation option is set to true', function () {\n            var editor = this.newMediumEditor('.editor', {\n                anchor: {\n                    linkValidation: true\n                }\n            }),\n                link,\n                anchorExtension = editor.getExtensionByName('anchor');\n\n            selectElementContentsAndFire(editor.elements[0]);\n            anchorExtension.showForm('test.com');\n            fireEvent(anchorExtension.getForm().querySelector('a.medium-editor-toolbar-save'), 'click');\n\n            link = editor.elements[0].querySelector('a');\n            expect(link).not.toBeNull();\n            expect(link.href).toBe('http://test.com/');\n        });\n\n        it('should add tel: if need be and linkValidation option is set to true', function () {\n            var editor = this.newMediumEditor('.editor', {\n                anchor: {\n                    linkValidation: true\n                }\n            }),\n                link,\n                anchorExtension = editor.getExtensionByName('anchor');\n\n            selectElementContentsAndFire(editor.elements[0]);\n            anchorExtension.showForm('347-999-9999');\n            fireEvent(anchorExtension.getForm().querySelector('a.medium-editor-toolbar-save'), 'click');\n\n            link = editor.elements[0].querySelector('a');\n            expect(link).not.toBeNull();\n            expect(link.href).toBe('tel:347-999-9999');\n        });\n\n        it('should not change protocol when a valid one is included', function () {\n            var editor = this.newMediumEditor('.editor', {\n                anchor: {\n                    linkValidation: true\n                }\n            }),\n                validUrl = 'mailto:test.com',\n                link,\n                anchorExtension = editor.getExtensionByName('anchor');\n\n            selectElementContentsAndFire(editor.elements[0]);\n            anchorExtension.showForm(validUrl);\n            fireEvent(anchorExtension.getForm().querySelector('a.medium-editor-toolbar-save'), 'click');\n\n            link = editor.elements[0].querySelector('a');\n            expect(link).not.toBeNull();\n            expect(link.href).toBe(validUrl);\n        });\n\n        it('should not change protocol when a tel scheme is included', function () {\n            var editor = this.newMediumEditor('.editor', {\n                anchor: {\n                    linkValidation: true\n                }\n            }),\n                // Specifically using a bad phone number to illustrate that what follows tel is not checked\n                validUrl = 'tel:abc123!@#',\n                link,\n                anchorExtension = editor.getExtensionByName('anchor');\n\n            selectElementContentsAndFire(editor.elements[0]);\n            anchorExtension.showForm(validUrl);\n            fireEvent(anchorExtension.getForm().querySelector('a.medium-editor-toolbar-save'), 'click');\n\n            link = editor.elements[0].querySelector('a');\n            expect(link).not.toBeNull();\n            expect(link.href).toBe(validUrl);\n        });\n\n        it('should not change protocol when a maps scheme is included', function () {\n            var editor = this.newMediumEditor('.editor', {\n                anchor: {\n                    linkValidation: true\n                }\n            }),\n                // Specifically using a non-sensical maps string to illustrate that what follows maps is not checked\n                validUrl = 'maps:abc123!@#',\n                link,\n                anchorExtension = editor.getExtensionByName('anchor');\n\n            selectElementContentsAndFire(editor.elements[0]);\n            anchorExtension.showForm(validUrl);\n            fireEvent(anchorExtension.getForm().querySelector('a.medium-editor-toolbar-save'), 'click');\n\n            link = editor.elements[0].querySelector('a');\n            expect(link).not.toBeNull();\n            expect(link.href).toBe(validUrl);\n        });\n\n        it('should not change protocol for protocol-relative URLs', function () {\n            var editor = this.newMediumEditor('.editor', {\n                anchor: {\n                    linkValidation: true\n                }\n            }),\n                validUrl = '//test.com/',\n                link,\n                anchorExtension = editor.getExtensionByName('anchor');\n\n            selectElementContentsAndFire(editor.elements[0]);\n            anchorExtension.showForm(validUrl);\n            fireEvent(anchorExtension.getForm().querySelector('a.medium-editor-toolbar-save'), 'click');\n\n            link = editor.elements[0].querySelector('a');\n            expect(link).not.toBeNull();\n            expect(link.href).toBe(window.location.protocol + validUrl);\n        });\n\n        it('should not change protocol for any alphabetic scheme', function () {\n            var editor = this.newMediumEditor('.editor', {\n                anchor: {\n                    linkValidation: true\n                }\n            }),\n                link,\n                validUrl = 'abcDEFgHi://test.com/',\n                anchorExtension = editor.getExtensionByName('anchor');\n\n            selectElementContentsAndFire(editor.elements[0]);\n            anchorExtension.showForm(validUrl);\n            fireEvent(anchorExtension.getForm().querySelector('a.medium-editor-toolbar-save'), 'click');\n\n            link = editor.elements[0].querySelector('a');\n            expect(link).not.toBeNull();\n            expect(link.href).toBe(validUrl.toLowerCase());\n        });\n\n        it('should not change fragment identifier when link begins with hash', function () {\n            var editor = this.newMediumEditor('.editor', {\n                    anchor: {\n                        linkValidation: true\n                    }\n                }),\n                validHashLink = '#!$&\\'()*+,;=123abcDEF-._~:@/?',\n                link,\n                anchorExtension = editor.getExtensionByName('anchor');\n\n            selectElementContentsAndFire(editor.elements[0]);\n            anchorExtension.showForm(validHashLink);\n            fireEvent(anchorExtension.getForm().querySelector('a.medium-editor-toolbar-save'), 'click');\n\n            link = editor.elements[0].querySelector('a');\n            expect(link).not.toBeNull();\n            expect(link.getAttribute('href')).toBe(validHashLink);\n        });\n\n        it('should not add a scheme to an absolute path', function () {\n            var editor = this.newMediumEditor('.editor', {\n                    anchor: {\n                        linkValidation: true\n                    }\n                }),\n                absolutePath = '/test',\n                link,\n                anchorExtension = editor.getExtensionByName('anchor');\n\n            selectElementContentsAndFire(editor.elements[0]);\n            anchorExtension.showForm(absolutePath);\n            fireEvent(anchorExtension.getForm().querySelector('a.medium-editor-toolbar-save'), 'click');\n\n            link = editor.elements[0].querySelector('a');\n            expect(link).not.toBeNull();\n            expect(link.getAttribute('href')).toBe(absolutePath);\n        });\n\n        it('should not add a scheme to an obviously relative path', function () {\n            var editor = this.newMediumEditor('.editor', {\n                    anchor: {\n                        linkValidation: true\n                    }\n                }),\n                relativePath = 'test/file.html',\n                link,\n                anchorExtension = editor.getExtensionByName('anchor');\n\n            selectElementContentsAndFire(editor.elements[0]);\n            anchorExtension.showForm(relativePath);\n            fireEvent(anchorExtension.getForm().querySelector('a.medium-editor-toolbar-save'), 'click');\n\n            link = editor.elements[0].querySelector('a');\n            expect(link).not.toBeNull();\n            expect(link.getAttribute('href')).toBe(relativePath);\n        });\n\n        it('should add a scheme to a localhost url', function () {\n            var editor = this.newMediumEditor('.editor', {\n                    anchor: {\n                        linkValidation: true\n                    }\n                }),\n                localhostUrl = 'http://localhost',\n                link,\n                anchorExtension = editor.getExtensionByName('anchor');\n\n            selectElementContentsAndFire(editor.elements[0]);\n            anchorExtension.showForm('localhost');\n            fireEvent(anchorExtension.getForm().querySelector('a.medium-editor-toolbar-save'), 'click');\n\n            link = editor.elements[0].querySelector('a');\n            expect(link).not.toBeNull();\n            expect(link.getAttribute('href')).toBe(localhostUrl);\n        });\n\n        it('should change spaces to %20 for a valid url if linkValidation option is set to true', function () {\n            var editor = this.newMediumEditor('.editor', {\n                    anchor: {\n                        linkValidation: true\n                    }\n                }),\n                link,\n                anchorExtension = editor.getExtensionByName('anchor'),\n                expectedOpts = {\n                    value: 'http://te%20s%20t.com/',\n                    target: '_self'\n                };\n\n            spyOn(editor, 'execAction').and.callThrough();\n\n            selectElementContentsAndFire(editor.elements[0]);\n            anchorExtension.showForm('te s t.com/');\n            fireEvent(anchorExtension.getForm().querySelector('a.medium-editor-toolbar-save'), 'click');\n\n            expect(editor.execAction).toHaveBeenCalledWith('createLink', expectedOpts);\n\n            link = editor.elements[0].querySelector('a');\n            expect(link).not.toBeNull();\n            expect(link.href).toBe(expectedOpts.value);\n        });\n\n        it('should not change # to %23 for a valid url if linkValidation option is set to true', function () {\n            var editor = this.newMediumEditor('.editor', {\n                    anchor: {\n                        linkValidation: true\n                    }\n                }),\n                link,\n                anchorExtension = editor.getExtensionByName('anchor'),\n                expectedOpts = {\n                    value: 'http://example.com/?query=value#anchor',\n                    target: '_self'\n                };\n\n            spyOn(editor, 'execAction').and.callThrough();\n\n            selectElementContentsAndFire(editor.elements[0]);\n            anchorExtension.showForm(expectedOpts.value);\n            fireEvent(anchorExtension.getForm().querySelector('a.medium-editor-toolbar-save'), 'click');\n\n            expect(editor.execAction).toHaveBeenCalledWith('createLink', expectedOpts);\n\n            link = editor.elements[0].querySelector('a');\n            expect(link).not.toBeNull();\n            expect(link.href).toBe(expectedOpts.value);\n        });\n\n        it('should not encode an encoded URL if linkValidation option is set to true', function () {\n            var editor = this.newMediumEditor('.editor', {\n                    anchor: {\n                        linkValidation: true\n                    }\n                }),\n                link,\n                anchorExtension = editor.getExtensionByName('anchor'),\n                expectedOpts = {\n                    value: 'http://a%20b.com/',\n                    target: '_self'\n                };\n\n            spyOn(editor, 'execAction').and.callThrough();\n            selectElementContentsAndFire(editor.elements[0]);\n            anchorExtension.showForm('a%20b.com/');\n            fireEvent(anchorExtension.getForm().querySelector('a.medium-editor-toolbar-save'), 'click');\n\n            expect(editor.execAction).toHaveBeenCalledWith('createLink', expectedOpts);\n\n            link = editor.elements[0].querySelector('a');\n            expect(link).not.toBeNull();\n            expect(link.href).toBe(expectedOpts.value);\n        });\n\n        it('should encode query params if linkValidation option is set to true', function () {\n            var editor = this.newMediumEditor('.editor', {\n                    anchor: {\n                        linkValidation: true\n                    }\n                }),\n                link,\n                anchorExtension = editor.getExtensionByName('anchor'),\n                expectedOpts = {\n                    value: 'http://a.com/?q=http%3A%2F%2Fb.com&q2=http%3A%2F%2Fc.com',\n                    target: '_self'\n                };\n\n            spyOn(editor, 'execAction').and.callThrough();\n\n            selectElementContentsAndFire(editor.elements[0]);\n            anchorExtension.showForm('a.com/?q=http://b.com&q2=http://c.com');\n            fireEvent(anchorExtension.getForm().querySelector('a.medium-editor-toolbar-save'), 'click');\n\n            expect(editor.execAction).toHaveBeenCalledWith('createLink', expectedOpts);\n\n            link = editor.elements[0].querySelector('a');\n            expect(link).not.toBeNull();\n            expect(link.href).toBe(expectedOpts.value);\n        });\n\n        it('should not encode an encoded query param if linkValidation option is set to true', function () {\n            var editor = this.newMediumEditor('.editor', {\n                    anchor: {\n                        linkValidation: true\n                    }\n                }),\n                link,\n                anchorExtension = editor.getExtensionByName('anchor'),\n                expectedOpts = {\n                    value: 'http://a.com/?q=http%3A%2F%2Fb.com&q2=http%3A%2F%2Fc.com',\n                    target: '_self'\n                };\n\n            spyOn(editor, 'execAction').and.callThrough();\n\n            selectElementContentsAndFire(editor.elements[0]);\n            anchorExtension.showForm('a.com/?q=http%3A%2F%2Fb.com&q2=http://c.com');\n            fireEvent(anchorExtension.getForm().querySelector('a.medium-editor-toolbar-save'), 'click');\n\n            expect(editor.execAction).toHaveBeenCalledWith('createLink', expectedOpts);\n\n            link = editor.elements[0].querySelector('a');\n            expect(link).not.toBeNull();\n            expect(link.href).toBe(expectedOpts.value);\n        });\n\n        it('should not change spaces to %20 if linkValidation is set to false', function () {\n            var editor = this.newMediumEditor('.editor', {\n                anchor: {\n                    linkValidation: false\n                }\n            }),\n                link,\n                anchorExtension = editor.getExtensionByName('anchor'),\n                expectedOpts = {\n                    value: 'http://te s t.com/',\n                    target: '_self'\n                };\n\n            spyOn(editor, 'execAction').and.callThrough();\n\n            selectElementContentsAndFire(editor.elements[0]);\n            anchorExtension.showForm(expectedOpts.value);\n            fireEvent(anchorExtension.getForm().querySelector('a.medium-editor-toolbar-save'), 'click');\n\n            // Chrome, Edge, and IE will automatically escape the href once it's set on the link\n            // So for this case, we'll only look at the call to execAction to see what URL it was trying to set\n            // since the value of the link's href could be different values\n            expect(editor.execAction).toHaveBeenCalledWith('createLink', expectedOpts);\n\n            link = editor.elements[0].querySelector('a');\n            expect(link).not.toBeNull();\n        });\n\n        it('should add target=\"_blank\" when \"open in a new window\" checkbox is checked', function () {\n            var editor = this.newMediumEditor('.editor', {\n                anchor: {\n                    targetCheckbox: true\n                }\n            }),\n                anchorExtension = editor.getExtensionByName('anchor'),\n                targetCheckbox,\n                link;\n\n            selectElementContentsAndFire(editor.elements[0]);\n            anchorExtension.showForm('http://test.com');\n            expect(anchorExtension.isDisplayed()).toBe(true);\n            targetCheckbox = anchorExtension.getForm().querySelector('input.medium-editor-toolbar-anchor-target');\n            expect().not.toBeNull(targetCheckbox);\n            targetCheckbox.checked = true;\n            fireEvent(anchorExtension.getForm().querySelector('a.medium-editor-toolbar-save'), 'click');\n            expect(anchorExtension.isDisplayed()).toBe(false);\n\n            link = editor.elements[0].querySelector('a');\n            expect(link).not.toBeNull();\n            expect(link.target).toBe('_blank');\n            expect(link.rel).toBe('noopener noreferrer');\n        });\n\n        it('should add target=\"_blank\" when respective option is set to true', function () {\n            var editor = this.newMediumEditor('.editor', {\n                targetBlank: true\n            }),\n                link,\n                anchorExtension = editor.getExtensionByName('anchor');\n\n            selectElementContentsAndFire(editor.elements[0]);\n            anchorExtension.showForm('http://test.com');\n            fireEvent(anchorExtension.getForm().querySelector('a.medium-editor-toolbar-save'), 'click');\n\n            link = editor.elements[0].querySelector('a');\n            expect(link).not.toBeNull();\n            expect(link.target).toBe('_blank');\n            expect(link.rel).toBe('noopener noreferrer');\n        });\n\n        it('should create a button when user selects this option and presses enter', function () {\n            spyOn(MediumEditor.prototype, 'createLink').and.callThrough();\n            var editor = this.newMediumEditor('.editor', {\n                    anchor: {\n                        customClassOption: 'btn btn-default'\n                    }\n                }),\n                save,\n                input,\n                button,\n                link,\n                opts,\n                anchorExtension = editor.getExtensionByName('anchor'),\n                toolbar = editor.getExtensionByName('toolbar');\n\n            selectElementContents(editor.elements[0]);\n            save = toolbar.getToolbarElement().querySelector('[data-action=\"createLink\"]');\n            fireEvent(save, 'click');\n\n            input = anchorExtension.getInput();\n            input.value = 'http://test.com';\n\n            button = anchorExtension.getForm().querySelector('input.medium-editor-toolbar-anchor-button');\n            button.setAttribute('type', 'checkbox');\n            button.checked = true;\n\n            fireEvent(input, 'keyup', {\n                keyCode: MediumEditor.util.keyCode.ENTER\n            });\n            opts = {\n                value: 'http://test.com',\n                target: '_self',\n                buttonClass: 'btn btn-default'\n            };\n            expect(editor.createLink).toHaveBeenCalledWith(opts);\n\n            link = editor.elements[0].querySelector('a');\n            expect(link).not.toBeNull();\n            expect(link.classList.contains('btn')).toBe(true);\n            expect(link.classList.contains('btn-default')).toBe(true);\n        });\n\n        it('should create a button when user selects this option, even if selected text carries tags ' +\n                'and the selection doesn\\'t exactly match those tags', function () {\n            spyOn(MediumEditor.prototype, 'createLink').and.callThrough();\n            this.el.innerHTML = '<p>Hello <b>world</b>&nbsp;!</p>';\n\n            var editor = this.newMediumEditor('.editor', {\n                    anchor: {\n                        customClassOption: 'btn btn-default'\n                    }\n                }),\n                p = this.el.querySelector('p'),\n                b = p.childNodes[1],\n                emptySpacePart = p.childNodes[2],\n                save,\n                input,\n                button,\n                link,\n                opts,\n                anchorExtension = editor.getExtensionByName('anchor'),\n                toolbar = editor.getExtensionByName('toolbar');\n\n            MediumEditor.selection.select(document, b, 0, emptySpacePart, 1); //select world including space\n            save = toolbar.getToolbarElement().querySelector('[data-action=\"createLink\"]');\n\n            input = anchorExtension.getInput();\n            input.value = 'http://test.com';\n\n            button = anchorExtension.getForm().querySelector('input.medium-editor-toolbar-anchor-button');\n            button.setAttribute('type', 'checkbox');\n            button.checked = true;\n\n            fireEvent(anchorExtension.getForm().querySelector('a.medium-editor-toolbar-save'), 'click');\n\n            opts = {\n                value: 'http://test.com',\n                target: '_self',\n                buttonClass: 'btn btn-default'\n            };\n            expect(editor.createLink).toHaveBeenCalledWith(opts);\n\n            link = editor.elements[0].querySelector('a');\n            expect(link).not.toBeNull('Link does not exist');\n            expect(link.classList.contains('btn')).toBe(true, 'Link does not contain class btn');\n            expect(link.classList.contains('btn-default')).toBe(true, 'Link does not contain class btn-default');\n        });\n\n        it('should remove the target _blank from the anchor tag when the open in a new window checkbox,' +\n                ' is unchecked and the form is saved', function () {\n            var editor = this.newMediumEditor('.editor', {\n                    anchor: {\n                        targetCheckbox: true\n                    }\n                }),\n                anchorExtension = editor.getExtensionByName('anchor'),\n                targetCheckbox,\n                link;\n\n            selectElementContentsAndFire(editor.elements[0]);\n            anchorExtension.showForm('http://test.com');\n            expect(anchorExtension.isDisplayed()).toBe(true);\n            targetCheckbox = anchorExtension.getForm().querySelector('input.medium-editor-toolbar-anchor-target');\n            targetCheckbox.checked = true;\n            fireEvent(anchorExtension.getForm().querySelector('a.medium-editor-toolbar-save'), 'click');\n            link = editor.elements[0].querySelector('a');\n            expect(link.target).toBe('_blank');\n            expect(link.rel).toBe('noopener noreferrer');\n\n            selectElementContentsAndFire(editor.elements[0]);\n            anchorExtension.showForm('http://test.com');\n            targetCheckbox = anchorExtension.getForm().querySelector('input.medium-editor-toolbar-anchor-target');\n            targetCheckbox.checked = false;\n            fireEvent(anchorExtension.getForm().querySelector('a.medium-editor-toolbar-save'), 'click');\n            link = editor.elements[0].querySelector('a');\n            expect(link.target).toBe('');\n        });\n\n        it('should fire editableInput only once when the user creates a link open to a new window,' +\n                ' and it should fire at the end of the DOM and selection modifications', function () {\n            spyOn(MediumEditor.prototype, 'createLink').and.callThrough();\n            this.el.innerHTML = '<p>Lorem ipsum et dolitur sunt.</p>';\n            var editor = this.newMediumEditor('.editor', {\n                    anchor: {\n                        targetCheckbox: true\n                    }\n                }),\n                p = this.el.lastChild,\n                anchorExtension = editor.getExtensionByName('anchor'),\n                toolbar = editor.getExtensionByName('toolbar'),\n                selectionWhenEventsFired = [],\n                listener = function () {\n                    selectionWhenEventsFired.push(window.getSelection().toString());\n                };\n\n            MediumEditor.selection.select(document, p.firstChild, 'Lorem '.length, p.firstChild, 'Lorem ipsum'.length);\n            fireEvent(editor.elements[0], 'focus');\n            jasmine.clock().tick(1);\n\n            // Click the 'anchor' button in the toolbar\n            fireEvent(toolbar.getToolbarElement().querySelector('[data-action=\"createLink\"]'), 'click');\n\n            // Input a url and save\n            var input = anchorExtension.getInput(),\n                checkbox = anchorExtension.getAnchorTargetCheckbox();\n            input.value = 'http://www.example.com';\n            checkbox.checked = true;\n            editor.subscribe('editableInput', listener);\n            fireEvent(input, 'keyup', {\n                keyCode: MediumEditor.util.keyCode.ENTER\n            });\n\n            expect(editor.createLink).toHaveBeenCalledWith({\n                value: 'http://www.example.com',\n                target: '_blank'\n            });\n            expect(window.getSelection().toString()).toBe('ipsum', 'selected text should remain selected');\n            expect(selectionWhenEventsFired.length).toBe(1, 'only one editableInput event should have been registered');\n            expect(selectionWhenEventsFired[0]).toBe('ipsum', 'selected text should have been the same when event fired');\n        });\n\n        // https://github.com/yabwe/medium-editor/issues/757\n        it('should not select empty paragraphs when link is created at beginning of paragraph after empty paragraphs', function () {\n            spyOn(MediumEditor.prototype, 'createLink').and.callThrough();\n            this.el.innerHTML = '<p>Some text</p><p><br/></p><p><br/></p><p>link text more text</p>';\n            var editor = this.newMediumEditor('.editor'),\n                lastP = this.el.lastChild,\n                anchorExtension = editor.getExtensionByName('anchor'),\n                toolbar = editor.getExtensionByName('toolbar');\n\n            // Select the text 'link text' in the last paragraph\n            MediumEditor.selection.select(document, lastP.firstChild, 0, lastP.firstChild, 'link text'.length);\n            fireEvent(editor.elements[0], 'focus');\n            jasmine.clock().tick(1);\n\n            // Click the 'anchor' button in the toolbar\n            fireEvent(toolbar.getToolbarElement().querySelector('[data-action=\"createLink\"]'), 'click');\n\n            // Input a url and save\n            var input = anchorExtension.getInput();\n            input.value = 'http://www.example.com';\n            fireEvent(input, 'keyup', {\n                keyCode: MediumEditor.util.keyCode.ENTER\n            });\n\n            expect(editor.createLink).toHaveBeenCalledWith({\n                value: 'http://www.example.com',\n                target: '_self'\n            });\n\n            // Make sure the <p> wasn't removed, and the <a> was added to the end\n            expect(this.el.lastChild).toBe(lastP);\n            expect(lastP.firstChild.nodeName.toLowerCase()).toBe('a');\n\n            // Make sure selection is only the link\n            var range = window.getSelection().getRangeAt(0);\n            expect(MediumEditor.util.isDescendant(lastP, range.startContainer, true)).toBe(true, 'The start of the selection is incorrect');\n            expect(range.startOffset).toBe(0);\n            expect(MediumEditor.util.isDescendant(lastP.firstChild, range.endContainer, true)).toBe(true, 'The end of the selection is not contained within the link');\n        });\n\n        // https://github.com/yabwe/medium-editor/issues/757\n        it('should not select empty paragraphs when link is created at beginning of paragraph after another paragraph', function () {\n            spyOn(MediumEditor.prototype, 'createLink').and.callThrough();\n            this.el.innerHTML = '<p>Some text</p><p>link text more text</p>';\n            var editor = this.newMediumEditor('.editor'),\n                lastP = this.el.lastChild,\n                anchorExtension = editor.getExtensionByName('anchor'),\n                toolbar = editor.getExtensionByName('toolbar');\n\n            // Select the text 'link text' in the last paragraph\n            MediumEditor.selection.select(document, lastP.firstChild, 0, lastP.firstChild, 'link text'.length);\n            fireEvent(editor.elements[0], 'focus');\n            jasmine.clock().tick(1);\n\n            // Click the 'anchor' button in the toolbar\n            fireEvent(toolbar.getToolbarElement().querySelector('[data-action=\"createLink\"]'), 'click');\n\n            // Input a url and save\n            var input = anchorExtension.getInput();\n            input.value = 'http://www.example.com';\n            fireEvent(input, 'keyup', {\n                keyCode: MediumEditor.util.keyCode.ENTER\n            });\n\n            expect(editor.createLink).toHaveBeenCalledWith({\n                value: 'http://www.example.com',\n                target: '_self'\n            });\n\n            // Make sure the <p> wasn't removed, and the <a> was added to the end\n            expect(this.el.lastChild).toBe(lastP);\n            expect(lastP.firstChild.nodeName.toLowerCase()).toBe('a');\n\n            // Make sure selection is only the link\n            var range = window.getSelection().getRangeAt(0);\n            expect(MediumEditor.util.isDescendant(lastP, range.startContainer, true)).toBe(true, 'The start of the selection is incorrect');\n            expect(range.startOffset).toBe(0);\n            expect(MediumEditor.util.isDescendant(lastP.firstChild, range.endContainer, true)).toBe(true, 'The end of the selection is not contained within the link');\n        });\n\n        it('should not remove the <p> container when adding a link inside a top-level <p> with a single text-node child', function () {\n            spyOn(MediumEditor.prototype, 'createLink').and.callThrough();\n            this.el.innerHTML = '<p>Some text</p><p><br/></p><p><br/></p><p>some text link text</p>';\n            var editor = this.newMediumEditor('.editor'),\n                lastP = this.el.lastChild,\n                anchorExtension = editor.getExtensionByName('anchor'),\n                toolbar = editor.getExtensionByName('toolbar');\n\n            // Select the text 'link text' in the last paragraph\n            MediumEditor.selection.select(document, lastP.firstChild, 'some text '.length, lastP.firstChild, 'some text link text'.length);\n            fireEvent(editor.elements[0], 'focus');\n            jasmine.clock().tick(1);\n\n            // Click the 'anchor' button in the toolbar\n            fireEvent(toolbar.getToolbarElement().querySelector('[data-action=\"createLink\"]'), 'click');\n\n            // Input a url and save\n            var input = anchorExtension.getInput();\n            input.value = 'http://www.example.com';\n            fireEvent(input, 'keyup', {\n                keyCode: MediumEditor.util.keyCode.ENTER\n            });\n\n            expect(editor.createLink).toHaveBeenCalledWith({\n                value: 'http://www.example.com',\n                target: '_self'\n            });\n\n            // Make sure the <p> wasn't removed, and the <a> was added to the end\n            expect(this.el.lastChild).toBe(lastP);\n            expect(lastP.lastChild.nodeName.toLowerCase()).toBe('a');\n\n            // Make sure selection is only the link\n            var range = window.getSelection().getRangeAt(0);\n            if (range.startContainer === lastP.lastChild.firstChild) {\n                expect(range.startOffset).toBe(0, 'The start of the selection is not at the front of the link');\n            } else {\n                expect(range.startContainer).toBe(lastP.firstChild);\n                expect(range.startOffset).toBe('some text '.length, 'The start of the selection is not at the front of the link');\n            }\n            expect(MediumEditor.util.isDescendant(lastP.lastChild, range.endContainer, true)).toBe(true, 'The end of the selection is incorrect');\n        });\n\n        // https://github.com/yabwe/medium-editor/issues/803\n        it('should update the href of a link containing only an image', function () {\n            this.el.innerHTML = '<a href=\"#\"><img src=\"../demo/img/medium-editor.jpg\"></a>';\n\n            spyOn(MediumEditor.prototype, 'createLink').and.callThrough();\n            var editor = this.newMediumEditor('.editor'),\n                toolbar = editor.getExtensionByName('toolbar'),\n                button, input,\n                aTag = this.el.childNodes[0];\n\n            selectElementContents(aTag);\n            button = toolbar.getToolbarElement().querySelector('[data-action=\"createLink\"]');\n            fireEvent(button, 'click');\n            input = editor.getExtensionByName('anchor').getInput();\n            input.value = 'http://www.google.com';\n            fireEvent(input, 'keyup', {\n                keyCode: MediumEditor.util.keyCode.ENTER\n            });\n            expect(editor.createLink).toHaveBeenCalled();\n            // This appears to be broken in Edge < 13, but works correctly in Edge 13 or higher\n            // So for the sake of sanity, disabling this check for Edge 12.\n            // TODO: Find a better way to fix this issue if Edge 12 is going to matter\n            var edgeVersion = getEdgeVersion();\n            if (!edgeVersion || edgeVersion >= 13) {\n                expect(this.el.innerHTML).toContain('<a href=\"http://www.google.com\"><img src=\"../demo/img/medium-editor.jpg\"></a>');\n            }\n        });\n    });\n\n    describe('Cancel', function () {\n        it('should close the link form when user clicks on cancel', function () {\n            spyOn(MediumEditor.extensions.toolbar.prototype, 'showAndUpdateToolbar').and.callThrough();\n            var editor = this.newMediumEditor('.editor'),\n                button,\n                cancel,\n                anchorExtension = editor.getExtensionByName('anchor'),\n                toolbar = editor.getExtensionByName('toolbar');\n\n            selectElementContentsAndFire(editor.elements[0]);\n            button = toolbar.getToolbarElement().querySelector('[data-action=\"createLink\"]');\n            cancel = anchorExtension.getForm().querySelector('a.medium-editor-toolbar-close');\n            fireEvent(button, 'click');\n            expect(anchorExtension.isDisplayed()).toBe(true);\n            fireEvent(cancel, 'click');\n            expect(toolbar.showAndUpdateToolbar).toHaveBeenCalled();\n            expect(anchorExtension.isDisplayed()).toBe(false);\n        });\n\n        it('should close the link form when user presses escape', function () {\n            var editor = this.newMediumEditor('.editor'),\n                anchorExtension = editor.getExtensionByName('anchor'),\n                toolbar = editor.getExtensionByName('toolbar');\n\n            selectElementContentsAndFire(editor.elements[0]);\n            fireEvent(toolbar.getToolbarElement().querySelector('[data-action=\"createLink\"]'), 'click');\n            expect(anchorExtension.isDisplayed()).toBe(true);\n            fireEvent(anchorExtension.getInput(), 'keyup', {\n                keyCode: MediumEditor.util.keyCode.ESCAPE\n            });\n            expect(anchorExtension.isDisplayed()).toBe(false);\n        });\n    });\n\n    describe('Click', function () {\n        it('should display the anchor form when toolbar is visible', function () {\n            spyOn(MediumEditor.extensions.anchor.prototype, 'showForm').and.callThrough();\n            var button,\n                editor = this.newMediumEditor('.editor'),\n                anchorExtension = editor.getExtensionByName('anchor'),\n                toolbar = editor.getExtensionByName('toolbar');\n\n            selectElementContentsAndFire(editor.elements[0]);\n            button = toolbar.getToolbarElement().querySelector('[data-action=\"createLink\"]');\n            fireEvent(button, 'click');\n            expect(toolbar.getToolbarActionsElement().style.display).toBe('none');\n            expect(anchorExtension.isDisplayed()).toBe(true);\n            expect(anchorExtension.showForm).toHaveBeenCalled();\n        });\n\n        it('should unlink when selection is a link', function () {\n            this.el.innerHTML = '<a href=\"#\">link</a>';\n            var button,\n                editor = this.newMediumEditor('.editor'),\n                toolbar = editor.getExtensionByName('toolbar');\n            spyOn(document, 'execCommand').and.callThrough();\n\n            selectElementContentsAndFire(editor.elements[0]);\n            button = toolbar.getToolbarElement().querySelector('[data-action=\"createLink\"]');\n            fireEvent(button, 'click');\n            expect(this.el.innerHTML).toBe('link');\n            expect(document.execCommand).toHaveBeenCalled();\n        });\n\n        // https://github.com/yabwe/medium-editor/issues/751\n        it('should allow more than one link creation within a paragraph', function () {\n            spyOn(MediumEditor.extensions.anchor.prototype, 'showForm').and.callThrough();\n            this.el.innerHTML = '<p><a href=\"#\">beginning</a> some text middle some text end</p>';\n            var editor = this.newMediumEditor('.editor'),\n                anchorExtension = editor.getExtensionByName('anchor'),\n                toolbar = editor.getExtensionByName('toolbar'),\n                para = this.el.firstChild;\n\n            // Select the text 'middle'\n            MediumEditor.selection.select(document, para.childNodes[1], 11, para.childNodes[1], 18);\n            fireEvent(editor.elements[0], 'focus');\n            jasmine.clock().tick(1);\n\n            // Click the 'anchor' button in the toolbar\n            fireEvent(toolbar.getToolbarElement().querySelector('[data-action=\"createLink\"]'), 'click');\n\n            expect(toolbar.getToolbarActionsElement().style.display).toBe('none');\n            expect(anchorExtension.isDisplayed()).toBe(true);\n            expect(anchorExtension.showForm).toHaveBeenCalled();\n            expect(anchorExtension.getInput().value).toBe('');\n        });\n    });\n\n});\n"
  },
  {
    "path": "spec/auto-link.spec.js",
    "content": "/*global fireEvent, selectElementContentsAndFire */\n\ndescribe('Autolink', function () {\n    'use strict';\n\n    describe('extension', function () {\n\n        beforeEach(function () {\n            setupTestHelpers.call(this);\n            this.el = this.createElement('div', 'editor', '');\n        });\n\n        afterEach(function () {\n            this.cleanupTest();\n        });\n\n        it('should turn off browser auto-link during initialization', function () {\n            var autoUrlDetectTurnedOn = true,\n                origExecCommand = document.execCommand;\n            spyOn(document, 'execCommand').and.callFake(function (command, showUi, val) {\n                if (command === 'AutoUrlDetect') {\n                    autoUrlDetectTurnedOn = val;\n                }\n                return origExecCommand.apply(document, arguments);\n            });\n            this.newMediumEditor('.editor', {\n                autoLink: true\n            });\n            expect(autoUrlDetectTurnedOn).toBe(false);\n        });\n\n        it('should reset browser auto-link (if supported) during destroy', function () {\n            var autoUrlDetectTurnedOn = true,\n                origExecCommand = document.execCommand,\n                origQCS = document.queryCommandSupported;\n            spyOn(document, 'execCommand').and.callFake(function (command, showUi, val) {\n                if (command === 'AutoUrlDetect') {\n                    autoUrlDetectTurnedOn = val;\n                }\n                return origExecCommand.apply(document, arguments);\n            });\n            spyOn(document, 'queryCommandSupported').and.callFake(function (command) {\n                if (command === 'AutoUrlDetect') {\n                    return true;\n                }\n                return origQCS.apply(document, arguments);\n            });\n\n            var editor = this.newMediumEditor('.editor', {\n                autoLink: true\n            });\n\n            expect(autoUrlDetectTurnedOn).toBe(false);\n            editor.destroy();\n            expect(autoUrlDetectTurnedOn).toBe(true);\n        });\n\n        it('should not reset multiple browser auto-links (if supported) during destroy', function () {\n            var autoUrlDetectTurnedOn = true,\n                origExecCommand = document.execCommand,\n                origQCS = document.queryCommandSupported;\n            this.el = this.createElement('div', 'editor2', '');\n            spyOn(document, 'execCommand').and.callFake(function (command, showUi, val) {\n                if (command === 'AutoUrlDetect') {\n                    autoUrlDetectTurnedOn = val;\n                }\n                return origExecCommand.apply(document, arguments);\n            });\n            spyOn(document, 'queryCommandSupported').and.callFake(function (command) {\n                if (command === 'AutoUrlDetect') {\n                    return true;\n                }\n                return origQCS.apply(document, arguments);\n            });\n\n            var\n                editor = this.newMediumEditor('.editor', {\n                    autoLink: true\n                }),\n                editor2 = this.newMediumEditor('.editor2', {\n                    autoLink: true\n                });\n\n            expect(autoUrlDetectTurnedOn).toBe(false);\n            editor.destroy();\n            expect(autoUrlDetectTurnedOn).toBe(false);\n            editor2.destroy();\n            expect(autoUrlDetectTurnedOn).toBe(true);\n        });\n    });\n\n    describe('integration', function () {\n\n        beforeEach(function () {\n            setupTestHelpers.call(this);\n            this.el = this.createElement('div', 'editor', '');\n        });\n\n        afterEach(function () {\n            this.cleanupTest();\n        });\n\n        describe('auto-linking typed-in text', function () {\n\n            beforeEach(function () {\n                this.editor = this.newMediumEditor('.editor', {\n                    autoLink: true\n                });\n            });\n\n            var links = [\n                'http://www.royal.gov.uk',\n                'http://www.bbc.co.uk',\n                'http://mountaindew.com',\n                'http://coca-cola.com',\n                'http://example.com',\n                'http://wwww.example.com', // with more \"w\"s it's still a valid subdomain\n                'http://www.example.com',\n                'http://www.example.com/foo/bar',\n                'http://www.example.com?foo=bar',\n                'http://www.example.com/baz?foo=bar',\n                'http://www.example.com/baz?foo=bar#buzz',\n                'http://www.example.com/#buzz',\n                'http://about.museum',\n                'http://getty.art.museum/visit/center/art.html',\n                'http://en.wikipedia.org/wiki/List_of_diplomatic_missions_of_China'\n            ],\n                notLinks = [\n                'http:google.com',\n                'http:/example.com',\n                'app.can',\n                'sadasda.sdfasf.sdfas',\n                'www.example.combasic',\n                // Our algorithm assumes that '.' is punctuation, not part of the URL.\n                'en.wikipedia.org/wiki/Embassy_of_China_in_Washington,_D.C.'\n            ];\n\n            function triggerAutolinking(element, key) {\n                var keyPressed = key || MediumEditor.util.keyCode.SPACE;\n                fireEvent(element, 'keypress', {\n                    keyCode: keyPressed\n                });\n                jasmine.clock().tick(1);\n            }\n\n            function generateLinkTest(link, href) {\n                return function () {\n                    var selection = window.getSelection(),\n                        newRange = document.createRange();\n                    this.el.innerHTML = '<p>' + link + ' </p>';\n                    selection.removeAllRanges();\n                    newRange.setStart(this.el.firstChild.childNodes[0], link.length + 1);\n                    newRange.setEnd(this.el.firstChild.childNodes[0], link.length + 1);\n                    selection.addRange(newRange);\n\n                    triggerAutolinking(this.el);\n\n                    var anchors = this.el.getElementsByTagName('a');\n                    expect(anchors.length).toBe(1);\n                    expect('href: ' + anchors[0].getAttribute('href')).toBe('href: ' + href);\n                    expect('Text content: ' + anchors[0].textContent).toBe('Text content: ' + link);\n                    expect(anchors[0].firstChild.getAttribute('data-auto-link')).toBe('true');\n                };\n            }\n\n            links.forEach(function (link) {\n                it('should auto-link \"' + link + '\" when typed in',\n                    generateLinkTest(link, link));\n\n                it('should auto-link \"' + link.toUpperCase() + '\" when typed in',\n                    generateLinkTest(link.toUpperCase(), link.toUpperCase()));\n\n                var noProtocolLink = link.slice('http://'.length);\n                it('should auto-link \"' + noProtocolLink + '\" when typed in',\n                    generateLinkTest(noProtocolLink, link));\n            });\n\n            function generateNotLinkTest(link) {\n                return function () {\n                    var selection = window.getSelection(),\n                        newRange = document.createRange();\n                    this.el.innerHTML = '<p>' + link + ' </p>';\n                    selection.removeAllRanges();\n                    newRange.setStart(this.el.firstChild.childNodes[0], link.length + 1);\n                    newRange.setEnd(this.el.firstChild.childNodes[0], link.length + 1);\n                    selection.addRange(newRange);\n\n                    triggerAutolinking(this.el);\n                    var anchors = this.el.getElementsByTagName('a');\n                    expect(anchors.length).toBe(0, '# of anchors');\n                };\n            }\n\n            notLinks.forEach(function (link) {\n                it('should not auto-link \"' + link + '\" when typed in', generateNotLinkTest(link));\n            });\n\n            it('should auto-link text on its own', function () {\n                this.el.innerHTML = 'http://www.example.com';\n\n                selectElementContentsAndFire(this.el);\n                triggerAutolinking(this.el);\n                var links = this.el.getElementsByTagName('a');\n                expect(links.length).toBe(1);\n                expect(links[0].getAttribute('href')).toBe('http://www.example.com');\n                expect(links[0].firstChild.getAttribute('data-auto-link')).toBe('true');\n                expect(links[0].textContent).toBe('http://www.example.com');\n            });\n\n            it('should auto-link text on all SPACE or ENTER', function () {\n                var links;\n                this.el.innerHTML = 'http://www.example.enter';\n\n                selectElementContentsAndFire(this.el);\n                triggerAutolinking(this.el, MediumEditor.util.keyCode.ENTER);\n                links = this.el.getElementsByTagName('a');\n                expect(links.length).toBe(1, 'links length after ENTER');\n                expect(links[0].getAttribute('href')).toBe('http://www.example.enter');\n                expect(links[0].firstChild.getAttribute('data-auto-link')).toBe('true');\n                expect(links[0].textContent).toBe('http://www.example.enter');\n\n                this.el.innerHTML = 'http://www.example.space';\n\n                selectElementContentsAndFire(this.el);\n                triggerAutolinking(this.el, MediumEditor.util.keyCode.SPACE);\n                links = this.el.getElementsByTagName('a');\n                expect(links.length).toBe(1, 'links length after SPACE');\n                expect(links[0].getAttribute('href')).toBe('http://www.example.space');\n                expect(links[0].firstChild.getAttribute('data-auto-link')).toBe('true');\n                expect(links[0].textContent).toBe('http://www.example.space');\n            });\n\n            it('should auto-link text on blur', function () {\n                var links;\n                this.el.innerHTML = 'http://www.example.blur';\n\n                selectElementContentsAndFire(this.el);\n\n                fireEvent(this.el, 'blur');\n                jasmine.clock().tick(1);\n\n                links = this.el.getElementsByTagName('a');\n                expect(links.length).toBe(1);\n                expect(links[0].getAttribute('href')).toBe('http://www.example.blur');\n                expect(links[0].firstChild.getAttribute('data-auto-link')).toBe('true');\n                expect(links[0].textContent).toBe('http://www.example.blur');\n            });\n\n            it('should fire editableInput after autolinking', function () {\n                var spy = jasmine.createSpy('handler');\n                this.el.innerHTML = 'http://www.example.com';\n\n                selectElementContentsAndFire(this.el);\n                this.editor.subscribe('editableInput', spy);\n                expect(spy).not.toHaveBeenCalled();\n\n                fireEvent(this.el, 'blur');\n                jasmine.clock().tick(1);\n\n                expect(spy).toHaveBeenCalled();\n            });\n\n            it('should auto-link link within basic text', function () {\n                this.el.innerHTML = 'Text with http://www.example.com inside!';\n\n                selectElementContentsAndFire(this.el);\n                triggerAutolinking(this.el);\n                var links = this.el.getElementsByTagName('a');\n                expect(links.length).toBe(1);\n                expect(links[0].getAttribute('href')).toBe('http://www.example.com');\n                expect(links[0].getAttribute('href')).toBe(links[0].textContent);\n                expect(this.el.childNodes[0].nodeValue).toBe('Text with ');\n                expect(this.el.childNodes[this.el.childNodes.length - 1].nodeValue).toBe(' inside!');\n            });\n\n            it('should auto-link basic text within a parent element', function () {\n                this.el.innerHTML = '<span>Text with http://www.example.com inside!</span>';\n\n                selectElementContentsAndFire(this.el);\n                triggerAutolinking(this.el);\n                var links = this.el.getElementsByTagName('a');\n                expect(links.length).toBe(1);\n                expect(this.el.firstChild.nodeName.toLowerCase()).toBe('span');\n                expect(this.el.firstChild.textContent).toBe('Text with http://www.example.com inside!');\n                expect(this.el.firstChild.getElementsByTagName('a').length).toBe(1);\n                expect(links[0].getAttribute('href')).toBe('http://www.example.com');\n                expect(links[0].textContent).toBe('http://www.example.com');\n                expect(links[0].firstChild.getAttribute('data-auto-link')).toBe('true');\n            });\n\n            it('should auto-link text that is partially styled and preserve the SPAN and B tags', function () {\n                var selection = window.getSelection(),\n                    newRange = document.createRange();\n                this.el.innerHTML = '<p><span class=\"a\"><b>Here is the link: http://www.</b>exa</span>mple.com </p>';\n                selection.removeAllRanges();\n                newRange.setStart(this.el.firstChild.lastChild, this.el.firstChild.lastChild.nodeValue.length);\n                newRange.setEnd(this.el.firstChild.lastChild, this.el.firstChild.lastChild.nodeValue.length);\n                selection.addRange(newRange);\n\n                triggerAutolinking(this.el);\n\n                expect(this.el.innerHTML.indexOf('<p><span class=\"a\"><b>Here is the link: </b></span>')).toBe(0);\n\n                var links = this.el.getElementsByTagName('a');\n                expect(links.length).toBe(1);\n                expect(links[0].getAttribute('href')).toBe('http://www.example.com');\n                expect(links[0].firstChild.getAttribute('data-auto-link')).toBe('true');\n                expect(links[0].firstChild.nodeName.toLowerCase()).toBe('span');\n                expect(links[0].firstChild.innerHTML).toBe('<span class=\"a\"><b>http://www.</b>exa</span>mple.com');\n                expect(this.el.firstChild.lastChild.nodeValue).toBe(' ');\n            });\n\n            it('should auto-link text that is partially styled with a profusion of mixed bold sections', function () {\n                var selection = window.getSelection(),\n                    newRange = document.createRange();\n                this.el.innerHTML = '<p><b>Here is the link: http://www.</b>exampl<b>e</b>.com </p>';\n                selection.removeAllRanges();\n                newRange.setStart(this.el.firstChild.lastChild, this.el.firstChild.lastChild.nodeValue.length);\n                newRange.setEnd(this.el.firstChild.lastChild, this.el.firstChild.lastChild.nodeValue.length);\n                selection.addRange(newRange);\n\n                triggerAutolinking(this.el);\n\n                expect(this.el.innerHTML.indexOf('<p><b>Here is the link: </b>')).toBe(0);\n                var links = this.el.getElementsByTagName('a');\n                expect(links.length).toBe(1);\n                expect(links[0].firstChild.getAttribute('data-auto-link')).toBe('true');\n                expect(links[0].firstChild.nodeName.toLowerCase()).toBe('span');\n                expect(links[0].firstChild.innerHTML).toBe('<b>http://www.</b>exampl<b>e</b>.com');\n                expect(links[0].getAttribute('href')).toBe('http://www.example.com');\n                expect(links[0].firstChild.getAttribute('data-auto-link')).toBe('true');\n                expect(this.el.firstChild.lastChild.nodeValue).toBe(' ');\n            });\n\n            it('should auto-link text in a really hideous example', function () {\n                this.el.innerHTML = '' +\n                '<span>' +\n                    '<b>Link: http</b>' +\n                    '<i>://</i>' +\n                '</span>' +\n                '<span>' +\n                    '<b>www</b>' +\n                    '<u>.google.com</u>' +\n                '</span>' +\n                '<span>' +\n                    '<b>/wow </b>' +\n                    '<i>impressive</i>' +\n                '</span>';\n\n                selectElementContentsAndFire(this.el.firstChild);\n\n                triggerAutolinking(this.el);\n\n                var links = this.el.getElementsByTagName('a');\n                expect(links.length).toBe(1);\n                expect(links[0].firstChild.getAttribute('data-auto-link')).toBe('true');\n                expect(links[0].firstChild.getAttribute('data-href')).toBe('http://www.google.com/wow');\n                links[0].firstChild.removeAttribute('data-href'); // to make the next innerHTML check work consistently\n\n                var expectedOutput = '' +\n                '<span>' +\n                    '<b>Link: </b>' +\n                '</span>' +\n                '<a href=\"http://www.google.com/wow\">' +\n                    '<span data-auto-link=\"true\">' +\n                        '<span>' +\n                            '<b>http</b>' +\n                            '<i>://</i>' +\n                        '</span>' +\n                        '<span>' +\n                            '<b>www</b>' +\n                            '<u>.google.com</u>' +\n                        '</span>' +\n                        '<span>' +\n                            '<b>/wow</b>' +\n                        '</span>' +\n                    '</span>' +\n                '</a>' +\n                '<span>' +\n                    '<b> </b>' +\n                    '<i>impressive</i>' +\n                '</span>';\n\n                expect(this.el.innerHTML).toBe(expectedOutput);\n            });\n\n            it('should not auto-link text inside links', function () {\n                this.el.innerHTML = 'Click this http://www.example.com link';\n\n                selectElementContentsAndFire(this.el.firstChild);\n\n                triggerAutolinking(this.el);\n                var links = this.el.getElementsByTagName('a');\n                expect(links.length).toBe(1);\n                expect(links[0].getAttribute('href')).toBe('http://www.example.com');\n                expect(links[0].firstChild.getAttribute('data-auto-link')).toBe('true');\n\n                triggerAutolinking(this.el);\n                links = this.el.getElementsByTagName('a');\n                expect(links.length).toBe(1);\n                expect(links[0].getAttribute('href')).toBe('http://www.example.com');\n                expect(links[0].firstChild.getAttribute('data-auto-link')).toBe('true');\n            });\n\n            it('should not auto-link text inside a span with data-auto-link=true', function () {\n                this.el.innerHTML = 'Click this <span data-href=\"http://www.example.com\" data-auto-link=\"true\">' +\n                    'http://www.example.com</span> link';\n\n                selectElementContentsAndFire(this.el.firstChild);\n\n                triggerAutolinking(this.el);\n                expect(this.el.getElementsByTagName('a').length).toBe(0, 'should not create a link');\n                expect(this.el.getElementsByTagName('span').length).toBe(1, 'span should remain in place');\n            });\n\n            it('should not auto-link text containing a span with data-auto-link=true', function () {\n                this.el.innerHTML = 'Click this <span data-href=\"http://www.example.com\" data-auto-link=\"true\">' +\n                    'www.example.com</span>foo/bar/baz link';\n\n                selectElementContentsAndFire(this.el.firstChild);\n\n                triggerAutolinking(this.el);\n                expect(this.el.getElementsByTagName('a').length).toBe(0, 'should not create a link');\n                expect(this.el.getElementsByTagName('span').length).toBe(1, 'span should remain in place');\n            });\n\n            it('should remove a span with data-auto-link=true when the text no longer matches the original link', function () {\n                this.el.innerHTML = 'Click this <span data-auto-link=\"true\" data-href=\"http://www.example.com\">' +\n                    'foo</span> link';\n\n                triggerAutolinking(this.el);\n                expect(this.el.getElementsByTagName('span').length).toBe(0, 'span should have been removed');\n            });\n\n            it('should create a link with data-auto-link=true when the text no longer matches the original link' +\n                    ' and it has been unlinked', function () {\n                this.el.innerHTML = 'Click this <span data-auto-link=\"true\" data-href=\"http://www.example.com\">' +\n                    'www.example.co.uk</span> link';\n\n                triggerAutolinking(this.el);\n                expect(this.el.getElementsByTagName('a').length).toBe(1, 'link should have been added');\n                expect(this.el.getElementsByTagName('a')[0].getAttribute('href')).toBe('http://www.example.co.uk',\n                    'link should have been added');\n            });\n\n            it('should create a link with target=\"_blank\" when respective option is set to true', function () {\n                this.el = this.createElement('div', 'editor-blank', '');\n                this.newMediumEditor('.editor-blank', {\n                    autoLink: true,\n                    targetBlank: true\n                });\n\n                this.el.innerHTML = 'http://www.example.com';\n                selectElementContentsAndFire(this.el);\n                triggerAutolinking(this.el);\n                var links = this.el.getElementsByTagName('a');\n                expect(links.length).toBe(1);\n                expect(links[0].getAttribute('href')).toBe('http://www.example.com');\n                expect(links[0].target).toBe('_blank');\n            });\n\n            it('should stop attempting to auto-link on keypress if an error is encountered', function () {\n                var spy = spyOn(MediumEditor.extensions.autoLink.prototype, 'performLinking');\n\n                this.el.innerHTML = '<span><a href=\"http://www.google.com>http://www.google.com</a></span>';\n\n                // This will cause an error\n                triggerAutolinking(this.el);\n                expect(spy.calls.count()).toBe(1);\n\n                // The previous error should prevent performLiking from being called again\n                triggerAutolinking(this.el);\n                expect(spy.calls.count()).toBe(2);\n            });\n\n            it('should create a link for a url within a list item', function () {\n                this.el.innerHTML = '<p>This is my list of links:</p><ol><li>http://www.example.com</li></ol><p>It is very impressive</p>';\n\n                selectElementContentsAndFire(this.el);\n                triggerAutolinking(this.el);\n                var links = this.el.getElementsByTagName('a'),\n                    li = this.el.querySelector('li');\n                expect(links.length).toBe(1, 'A single link was not automatically created');\n                expect(li.firstChild).toBe(links[0]);\n                expect(li.textContent).toBe('http://www.example.com');\n                expect(links[0].getAttribute('href')).toBe('http://www.example.com');\n                expect(links[0].firstChild.getAttribute('data-auto-link')).toBe('true');\n            });\n\n            // https://github.com/yabwe/medium-editor/issues/790\n            it('should not create a link when text in consecutive list items could be a valid url when combined', function () {\n                this.el.innerHTML = '<ul><li>text ending in a period.</li><li>name - text starting with a TLD</li></ul>';\n\n                selectElementContentsAndFire(this.el);\n                triggerAutolinking(this.el);\n                var links = this.el.getElementsByTagName('a');\n                expect(links.length).toBe(0, 'A link was created without a valid url being in the text');\n                expect(this.el.innerHTML).toBe('<ul><li>text ending in a period.</li><li>name - text starting with a TLD</li></ul>',\n                    'Content does not contain a valid url, but auto-link caused the content to change');\n            });\n\n            // https://github.com/yabwe/medium-editor/issues/790\n            it('should not create a link which spans multiple list items', function () {\n                this.el.innerHTML = '<blockquote><ol><li>abc</li><li>www.example.com</li></ol></blockquote>';\n\n                selectElementContentsAndFire(this.el);\n                triggerAutolinking(this.el);\n                var links = this.el.getElementsByTagName('a'),\n                    lastLi = this.el.querySelector('ol').lastChild;\n                expect(links.length).toBe(1, 'There should have been exactly 1 link created');\n                expect(links[0].getAttribute('href')).toBe('http://www.example.com');\n                expect(lastLi.firstChild).toBe(links[0]);\n                expect(lastLi.textContent).toBe('www.example.com');\n            });\n\n            it('should not create a link which spans multiple list items', function () {\n                this.el.innerHTML = '<blockquote><ol><li>www</li><li>.example.com</li></ol></blockquote>';\n\n                selectElementContentsAndFire(this.el);\n                triggerAutolinking(this.el);\n                var links = this.el.getElementsByTagName('a');\n                expect(links.length).toBe(0, 'There should not have been any links created');\n            });\n        });\n    });\n\n});\n"
  },
  {
    "path": "spec/buttons.spec.js",
    "content": "/*global fireEvent, selectElementContentsAndFire,\n         isIE, isOldIE */\n\ndescribe('Buttons TestCase', function () {\n    'use strict';\n\n    var textarea;\n    beforeAll(function () {\n        textarea = document.createElement('textarea');\n        textarea.innerHTML = 'Ignore me please, placed here to make create an image test pass in Gecko';\n        document.body.appendChild(textarea);\n        textarea.focus();\n    });\n\n    afterAll(function () {\n        document.body.removeChild(textarea);\n    });\n\n    beforeEach(function () {\n        setupTestHelpers.call(this);\n        this.el = this.createElement('div', 'editor', 'lorem ipsum');\n    });\n\n    afterEach(function () {\n        this.cleanupTest();\n    });\n\n    describe('Button click', function () {\n        it('should set active class on click', function () {\n            var button,\n                editor = this.newMediumEditor('.editor'),\n                toolbar = editor.getExtensionByName('toolbar');\n\n            selectElementContentsAndFire(editor.elements[0]);\n            button = toolbar.getToolbarElement().querySelector('[data-action=\"bold\"]');\n            fireEvent(button, 'click');\n            expect(button.className).toContain('medium-editor-button-active');\n        });\n\n        it('should check for selection when selection is undefined', function () {\n            spyOn(MediumEditor.prototype, 'checkSelection');\n            var button,\n                editor = this.newMediumEditor('.editor'),\n                toolbar = editor.getExtensionByName('toolbar');\n\n            selectElementContentsAndFire(editor.elements[0]);\n            button = toolbar.getToolbarElement().querySelector('[data-action=\"bold\"]');\n            fireEvent(button, 'click');\n            expect(editor.checkSelection).toHaveBeenCalled();\n        });\n\n        it('should remove active class if button has it', function () {\n            this.el.innerHTML = '<b>lorem ipsum</b>';\n            var button,\n                editor = this.newMediumEditor('.editor'),\n                toolbar = editor.getExtensionByName('toolbar');\n\n            selectElementContentsAndFire(editor.elements[0]);\n            button = toolbar.getToolbarElement().querySelector('[data-action=\"bold\"]');\n            expect(button.className).toContain('medium-editor-button-active');\n            fireEvent(button, 'click');\n            expect(button.className).not.toContain('medium-editor-button-active');\n        });\n\n        it('should execute the button action', function () {\n            spyOn(MediumEditor.prototype, 'execAction');\n            var button,\n                editor = this.newMediumEditor('.editor'),\n                toolbar = editor.getExtensionByName('toolbar');\n\n            selectElementContentsAndFire(editor.elements[0]);\n            button = toolbar.getToolbarElement().querySelector('[data-action=\"bold\"]');\n            fireEvent(button, 'click');\n            expect(editor.execAction).toHaveBeenCalledWith('bold');\n        });\n    });\n\n    describe('Button default config', function () {\n        it('should be accesible via defaults property of the button prototype', function () {\n            expect(MediumEditor.extensions.button.prototype.defaults['bold']).toBeTruthy();\n            expect(MediumEditor.extensions.button.prototype.defaults['anchor']).toBeFalsy();\n        });\n\n        it('should be check-able via static Button.isBuiltInButton() method', function () {\n            expect(MediumEditor.extensions.button.isBuiltInButton('bold')).toBe(true);\n            expect(MediumEditor.extensions.button.isBuiltInButton('anchor')).toBe(false);\n        });\n    });\n\n    describe('Button constructor', function () {\n        it('should accept a set of config options', function () {\n            var italicConfig = MediumEditor.extensions.button.prototype.defaults['italic'],\n                italicButton = new MediumEditor.extensions.button(italicConfig);\n\n            Object.keys(italicConfig).forEach(function (prop) {\n                expect(italicButton[prop]).toBe(italicConfig[prop]);\n            });\n        });\n\n        it('should accept a built-in button name', function () {\n            var italicButtonOne = new MediumEditor.extensions.button(MediumEditor.extensions.button.prototype.defaults['italic']),\n                italicButtonTwo = new MediumEditor.extensions.button('italic');\n\n            expect(italicButtonOne).toEqual(italicButtonTwo);\n        });\n    });\n\n    describe('Button options', function () {\n        it('should support overriding defaults', function () {\n            this.el.innerHTML = '<h2>lorem</h2><h3>ipsum</h3>';\n            var editor = this.newMediumEditor('.editor', {\n                    toolbar: {\n                        buttons: [\n                            'bold',\n                            {\n                                name: 'h1',\n                                action: 'append-h2',\n                                aria: 'fake h1',\n                                tagNames: ['h2'],\n                                contentDefault: '<b>H1</b>',\n                                classList: ['customClassName'],\n                                attrs: {\n                                    'data-custom-attr': 'custom-value'\n                                }\n                            },\n                            {\n                                name: 'h2',\n                                getAction: function () {\n                                    return 'append-h3';\n                                },\n                                getAria: function () {\n                                    return 'fake h2';\n                                },\n                                getTagNames: function () {\n                                    return ['h3'];\n                                },\n                                contentDefault: '<b>H2</b>'\n                            }\n                        ]\n                    }\n                }),\n                headerOneButton = editor.getExtensionByName('h1'),\n                headerTwoButton = editor.getExtensionByName('h2'),\n                toolbar = editor.getExtensionByName('toolbar');\n\n            expect(toolbar.getToolbarElement().querySelectorAll('button').length).toBe(3);\n\n            var button = toolbar.getToolbarElement().querySelector('.medium-editor-action-h1'),\n                buttonTwo = toolbar.getToolbarElement().querySelector('.medium-editor-action-h2');\n            expect(button).toBe(headerOneButton.getButton());\n            expect(button.getAttribute('aria-label')).toBe('fake h1');\n            expect(button.getAttribute('title')).toBe('fake h1');\n            expect(button.getAttribute('data-custom-attr')).toBe('custom-value');\n            expect(button.classList.contains('customClassName')).toBe(true);\n            expect(button.innerHTML).toBe('<b>H1</b>');\n\n            selectElementContentsAndFire(editor.elements[0].querySelector('h2').firstChild);\n            expect(button.classList.contains('medium-editor-button-active')).toBe(true);\n            expect(buttonTwo.classList.contains('medium-editor-button-active')).toBe(false);\n\n            expect(buttonTwo).toBe(headerTwoButton.getButton());\n            expect(buttonTwo.getAttribute('aria-label')).toBe('fake h2');\n            expect(buttonTwo.getAttribute('title')).toBe('fake h2');\n            expect(buttonTwo.innerHTML).toBe('<b>H2</b>');\n\n            selectElementContentsAndFire(editor.elements[0].querySelector('h3'), { eventToFire: 'mouseup' });\n            expect(button.classList.contains('medium-editor-button-active')).toBe(false);\n            expect(buttonTwo.classList.contains('medium-editor-button-active')).toBe(true);\n        });\n    });\n\n    describe('Buttons with various labels', function () {\n        var defaultLabels = {},\n            fontAwesomeLabels = {},\n            customLabels = {},\n            allButtons = [],\n            customButtons = [],\n            buttonsData = MediumEditor.extensions.button.prototype.defaults,\n            currButton,\n            tempEl;\n\n        Object.keys(buttonsData).forEach(function (buttonName) {\n            allButtons.push(buttonName);\n            currButton = buttonsData[buttonName];\n            // If the labels contain HTML entities, we need to escape them\n            tempEl = document.createElement('div');\n\n            // Default Labels\n            tempEl.innerHTML = currButton.contentDefault;\n            defaultLabels[buttonName] = {\n                action: currButton.action,\n                label: tempEl.innerHTML\n            };\n\n            // fontawesome labels\n            tempEl.innerHTML = currButton.contentFA;\n            fontAwesomeLabels[buttonName] = tempEl.innerHTML;\n\n            // custom labels (using aria label as a test)\n            customLabels[buttonName] = currButton.aria;\n            customButtons.push({\n                name: buttonName,\n                contentDefault: currButton.aria\n            });\n        });\n\n        // Add in anchor button\n        allButtons.push('anchor');\n        tempEl = document.createElement('div');\n        tempEl.innerHTML = MediumEditor.extensions.anchor.prototype.contentDefault;\n        defaultLabels['anchor'] = {\n            label: tempEl.innerHTML\n        };\n        tempEl.innerHTML = MediumEditor.extensions.anchor.prototype.contentFA;\n        fontAwesomeLabels['anchor'] = tempEl.innerHTML;\n        customLabels['anchor'] = MediumEditor.extensions.anchor.prototype.aria;\n        customButtons.push({\n            name: 'anchor',\n            contentDefault: customLabels['anchor']\n        });\n\n        // Add in fontsize button\n        allButtons.push('fontsize');\n        tempEl.innerHTML = MediumEditor.extensions.fontSize.prototype.contentDefault;\n        defaultLabels['fontsize'] = {\n            label: tempEl.innerHTML\n        };\n        tempEl.innerHTML = MediumEditor.extensions.fontSize.prototype.contentFA;\n        fontAwesomeLabels['fontsize'] = tempEl.innerHTML;\n        customLabels['fontsize'] = MediumEditor.extensions.fontSize.prototype.aria;\n        customButtons.push({\n            name: 'fontsize',\n            contentDefault: customLabels['fontsize']\n        });\n\n        it('should have aria-label and title attributes set', function () {\n            var button,\n                editor = this.newMediumEditor('.editor', {\n                    toolbar: {\n                        buttons: allButtons\n                    }\n                }),\n                toolbar = editor.getExtensionByName('toolbar');\n            Object.keys(customLabels).forEach(function (buttonName) {\n                button = toolbar.getToolbarElement().querySelector('.medium-editor-action-' + buttonName);\n                expect(button).not.toBeUndefined();\n                expect(button.getAttribute('aria-label')).toBe(customLabels[buttonName]);\n                expect(button.getAttribute('title')).toBe(customLabels[buttonName]);\n            });\n        });\n\n        it('should contain default content if no custom labels are provided', function () {\n            var button,\n                editor = this.newMediumEditor('.editor', {\n                    toolbar: {\n                        buttons: allButtons\n                    }\n                }),\n                toolbar = editor.getExtensionByName('toolbar');\n            expect(toolbar.getToolbarElement().querySelectorAll('button').length).toBe(allButtons.length);\n            selectElementContentsAndFire(editor.elements[0]);\n\n            Object.keys(defaultLabels).forEach(function (buttonName) {\n                button = toolbar.getToolbarElement().querySelector('.medium-editor-action-' + buttonName);\n                expect(button).not.toBeUndefined();\n                expect(button.innerHTML).toBe(defaultLabels[buttonName].label);\n            });\n        });\n\n        it('should contain fontawesome labels and execute the button action when clicked', function () {\n            spyOn(MediumEditor.prototype, 'execAction');\n            var action,\n                button,\n                editor = this.newMediumEditor('.editor', {\n                    toolbar: {\n                        buttons: allButtons\n                    },\n                    buttonLabels: 'fontawesome'\n                }),\n                toolbar = editor.getExtensionByName('toolbar');\n            expect(toolbar.getToolbarElement().querySelectorAll('button').length).toBe(allButtons.length);\n            selectElementContentsAndFire(editor.elements[0]);\n\n            Object.keys(fontAwesomeLabels).forEach(function (buttonName) {\n                action = defaultLabels[buttonName].action;\n                button = toolbar.getToolbarElement().querySelector('.medium-editor-action-' + buttonName);\n                expect(button).not.toBeUndefined();\n                fireEvent(button, 'click');\n                if (action) {\n                    expect(editor.execAction).toHaveBeenCalledWith(action);\n                }\n                expect(button.innerHTML).toBe(fontAwesomeLabels[buttonName]);\n            });\n        });\n\n        it('should contain custom labels and execute the button action when clicked', function () {\n            spyOn(MediumEditor.prototype, 'execAction');\n            var action,\n                button,\n                editor = this.newMediumEditor('.editor', {\n                    toolbar: {\n                        buttons: customButtons\n                    }\n                }),\n                toolbar = editor.getExtensionByName('toolbar');\n            expect(toolbar.getToolbarElement().querySelectorAll('button').length).toBe(allButtons.length);\n            selectElementContentsAndFire(editor.elements[0]);\n\n            Object.keys(customLabels).forEach(function (buttonName) {\n                action = defaultLabels[buttonName].action;\n                button = toolbar.getToolbarElement().querySelector('.medium-editor-action-' + buttonName);\n                expect(button).not.toBeUndefined();\n                fireEvent(button, 'click');\n                if (action) {\n                    expect(editor.execAction).toHaveBeenCalledWith(action);\n                }\n                expect(button.innerHTML).toBe(customLabels[buttonName]);\n            });\n        });\n    });\n\n    describe('AppendEl', function () {\n        it('should call the document.execCommand method when button action is append', function () {\n            var button,\n                editor = this.newMediumEditor('.editor'),\n                toolbar = editor.getExtensionByName('toolbar');\n            spyOn(document, 'execCommand');\n\n            selectElementContentsAndFire(editor.elements[0]);\n            button = toolbar.getToolbarElement().querySelector('[data-action=\"append-h3\"]');\n            fireEvent(button, 'click');\n            if (isIE()) {\n                expect(document.execCommand).toHaveBeenCalledWith('formatBlock', false, '<h3>');\n            } else {\n                expect(document.execCommand).toHaveBeenCalledWith('formatBlock', false, 'h3');\n            }\n        });\n\n        it('should create an h3 element when h3 is clicked', function () {\n            this.el.innerHTML = '<p><b>lorem ipsum</b></p>';\n            var button,\n                editor = this.newMediumEditor('.editor'),\n                toolbar = editor.getExtensionByName('toolbar');\n\n            selectElementContentsAndFire(editor.elements[0]);\n            button = toolbar.getToolbarElement().querySelector('[data-action=\"append-h3\"]');\n            fireEvent(button, 'click');\n            // depending on the styling you have,\n            // IE might strip the <b> out when it applies the H3 here.\n            // so, make the <b> match optional in the output:\n            expect(this.el.innerHTML).toMatch(/<h3>(<b>)?lorem ipsum(<\\/b>)?<\\/h3>/);\n        });\n\n        it('should get back to a p element if parent element is the same as the action', function () {\n            this.el.innerHTML = '<h3><b>lorem ipsum</b></h3>';\n            var button,\n                editor = this.newMediumEditor('.editor'),\n                toolbar = editor.getExtensionByName('toolbar');\n\n            selectElementContentsAndFire(editor.elements[0].firstChild);\n            button = toolbar.getToolbarElement().querySelector('[data-action=\"append-h3\"]');\n            fireEvent(button, 'click');\n            expect(this.el.innerHTML).toBe('<p><b>lorem ipsum</b></p>');\n        });\n    });\n\n    describe('First and Last', function () {\n        it('should add a special class to the first and last buttons', function () {\n            var editor = this.newMediumEditor('.editor'),\n                toolbar = editor.getExtensionByName('toolbar'),\n                buttons = toolbar.getToolbarElement().querySelectorAll('button');\n            expect(buttons[0].className).toContain('medium-editor-button-first');\n            expect(buttons[1].className).not.toContain('medium-editor-button-first');\n            expect(buttons[1].className).not.toContain('medium-editor-button-last');\n            expect(buttons[buttons.length - 1].className).toContain('medium-editor-button-last');\n        });\n    });\n\n    describe('Bold', function () {\n        it('button should be active if the selection already has the element', function () {\n            var editor = this.newMediumEditor('.editor', {\n                    toolbar: {\n                        buttons: ['bold']\n                    }\n                }),\n                toolbar = editor.getExtensionByName('toolbar'),\n                button = toolbar.getToolbarElement().querySelector('[data-action=\"bold\"]');\n            this.el.innerHTML = '<b>lorem ipsum</b>';\n\n            selectElementContentsAndFire(editor.elements[0]);\n            expect(button.classList.contains('medium-editor-button-active')).toBe(true);\n\n            fireEvent(button, 'click');\n            expect(button.classList.contains('medium-editor-button-active')).toBe(false);\n\n            this.el.innerHTML = '<strong>lorem ipsum</strong>';\n            selectElementContentsAndFire(editor.elements[0], { eventToFire: 'mouseup' });\n            expect(button.classList.contains('medium-editor-button-active')).toBe(true);\n        });\n\n        it('button should be active if the selection is bold and queryCommandState fails', function () {\n            var editor = this.newMediumEditor('.editor', {\n                    toolbar: {\n                        buttons: ['bold']\n                    }\n                }),\n                toolbar = editor.getExtensionByName('toolbar'),\n                button = toolbar.getToolbarElement().querySelector('[data-action=\"bold\"]');\n\n            spyOn(document, 'queryCommandState').and.throwError('DOM ERROR');\n            this.el.innerHTML = '<b><i><u>lorem ipsum</u></i></b>';\n\n            selectElementContentsAndFire(editor.elements[0].querySelector('u'));\n            expect(button.classList.contains('medium-editor-button-active')).toBe(true);\n\n            fireEvent(button, 'click');\n            expect(button.classList.contains('medium-editor-button-active')).toBe(false);\n        });\n\n        it('button should be active for other cases when text is bold', function () {\n            var editor = this.newMediumEditor('.editor', {\n                    toolbar: {\n                        buttons: ['bold']\n                    }\n                }),\n                toolbar = editor.getExtensionByName('toolbar'),\n                button = toolbar.getToolbarElement().querySelector('[data-action=\"bold\"]');\n            this.el.innerHTML = '<p><span id=\"bold-span\" style=\"font-weight: bold\">lorem ipsum</span></p>';\n\n            selectElementContentsAndFire(document.getElementById('bold-span'));\n            expect(button.classList.contains('medium-editor-button-active')).toBe(true);\n\n            fireEvent(button, 'click');\n\n            this.el.innerHTML = stripAttrIfEmpty(this.el, 'style');\n\n            // style=\"font-weight: bold\" prevents IE9+10 from doing anything when 'bold' is triggered\n            // but it should work in other browsers\n            expect(!isOldIE() && button.classList.contains('medium-editor-button-active')).toBe(false);\n            expect(!isOldIE() && (this.el.innerHTML !== '<p><span id=\"bold-span\">lorem ipsum</span></p>')).toBe(false);\n        });\n    });\n\n    describe('Italics', function () {\n        it('should call the execCommand for native actions', function () {\n            var button,\n                editor = this.newMediumEditor('.editor'),\n                toolbar = editor.getExtensionByName('toolbar');\n            spyOn(document, 'execCommand').and.callThrough();\n\n            selectElementContentsAndFire(editor.elements[0]);\n            button = toolbar.getToolbarElement().querySelector('[data-action=\"italic\"]');\n            fireEvent(button, 'click');\n            expect(document.execCommand).toHaveBeenCalled();\n            // IE won't generate an `<i>` tag here. it generates an `<em>`:\n            expect(this.el.innerHTML).toMatch(/(<i>|<em>)lorem ipsum(<\\/i>|<\\/em>)/);\n        });\n\n        it('button should be active if the selection already has the element', function () {\n            var editor = this.newMediumEditor('.editor', {\n                    toolbar: {\n                        buttons: ['italic']\n                    }\n                }),\n                toolbar = editor.getExtensionByName('toolbar'),\n                button = toolbar.getToolbarElement().querySelector('[data-action=\"italic\"]');\n            this.el.innerHTML = '<i>lorem ipsum</i>';\n\n            selectElementContentsAndFire(editor.elements[0]);\n            expect(button.classList.contains('medium-editor-button-active')).toBe(true);\n\n            fireEvent(button, 'click');\n            expect(button.classList.contains('medium-editor-button-active')).toBe(false);\n\n            this.el.innerHTML = '<em>lorem ipsum</em>';\n            selectElementContentsAndFire(editor.elements[0], { eventToFire: 'mouseup' });\n            expect(button.classList.contains('medium-editor-button-active')).toBe(true);\n        });\n\n        it('button should be active if the selection is italic and queryCommandState fails', function () {\n            var editor = this.newMediumEditor('.editor', {\n                    toolbar: {\n                        buttons: ['italic']\n                    }\n                }),\n                toolbar = editor.getExtensionByName('toolbar'),\n                button = toolbar.getToolbarElement().querySelector('[data-action=\"italic\"]');\n\n            spyOn(document, 'queryCommandState').and.throwError('DOM ERROR');\n            this.el.innerHTML = '<i><b><u>lorem ipsum</u></b></i>';\n\n            selectElementContentsAndFire(editor.elements[0].querySelector('u'));\n            expect(button.classList.contains('medium-editor-button-active')).toBe(true);\n\n            fireEvent(button, 'click');\n            expect(button.classList.contains('medium-editor-button-active')).toBe(false);\n        });\n\n        it('button should be active for other cases when text is italic', function () {\n            var editor = this.newMediumEditor('.editor', {\n                    toolbar: {\n                        buttons: ['italic']\n                    }\n                }),\n                toolbar = editor.getExtensionByName('toolbar'),\n                button = toolbar.getToolbarElement().querySelector('[data-action=\"italic\"]');\n            this.el.innerHTML = '<p><span id=\"italic-span\" style=\"font-style: italic\">lorem ipsum</span></p>';\n\n            selectElementContentsAndFire(document.getElementById('italic-span'));\n            expect(button.classList.contains('medium-editor-button-active')).toBe(true);\n\n            fireEvent(button, 'click');\n\n            this.el.innerHTML = stripAttrIfEmpty(this.el, 'style');\n\n            // style=\"font-style: italic\" prevents IE9+10 from doing anything when 'italic' is triggered\n            // but it should work in other browsers\n            expect(!isOldIE() && button.classList.contains('medium-editor-button-active')).toBe(false);\n            expect(!isOldIE() && (this.el.innerHTML !== '<p><span id=\"italic-span\">lorem ipsum</span></p>')).toBe(false);\n        });\n    });\n\n    describe('Underline', function () {\n        it('button should be active if the selection already has the element', function () {\n            var editor = this.newMediumEditor('.editor', {\n                    toolbar: {\n                        buttons: ['underline']\n                    }\n                }),\n                toolbar = editor.getExtensionByName('toolbar'),\n                button = toolbar.getToolbarElement().querySelector('[data-action=\"underline\"]');\n            this.el.innerHTML = '<u>lorem ipsum</u>';\n\n            selectElementContentsAndFire(editor.elements[0]);\n            expect(button.classList.contains('medium-editor-button-active')).toBe(true);\n\n            fireEvent(button, 'click');\n            expect(button.classList.contains('medium-editor-button-active')).toBe(false);\n            expect(this.el.innerHTML).toBe('lorem ipsum');\n        });\n\n        it('button should be active if the selection is underlined and queryCommandState fails', function () {\n            var editor = this.newMediumEditor('.editor', {\n                    toolbar: {\n                        buttons: ['underline']\n                    }\n                }),\n                toolbar = editor.getExtensionByName('toolbar'),\n                button = toolbar.getToolbarElement().querySelector('[data-action=\"underline\"]');\n\n            spyOn(document, 'queryCommandState').and.throwError('DOM ERROR');\n            this.el.innerHTML = '<u><b><i>lorem ipsum</i></b></u>';\n\n            selectElementContentsAndFire(editor.elements[0].querySelector('i'));\n            expect(button.classList.contains('medium-editor-button-active')).toBe(true);\n\n            fireEvent(button, 'click');\n            expect(button.classList.contains('medium-editor-button-active')).toBe(false);\n        });\n\n        it('button should be active for other cases when text is underlined', function () {\n            var editor = this.newMediumEditor('.editor', {\n                    toolbar: {\n                        buttons: ['underline']\n                    }\n                }),\n                toolbar = editor.getExtensionByName('toolbar'),\n                button = toolbar.getToolbarElement().querySelector('[data-action=\"underline\"]');\n            this.el.innerHTML = '<p><span id=\"underline-span\" style=\"text-decoration: underline\">lorem ipsum</span></p>';\n\n            selectElementContentsAndFire(document.getElementById('underline-span'));\n            expect(button.classList.contains('medium-editor-button-active')).toBe(true);\n\n            fireEvent(button, 'click');\n\n            this.el.innerHTML = stripAttrIfEmpty(this.el, 'style');\n\n            // style=\"text-decoration: underline\" prevents IE9+10 from doing anything when 'underline' is triggered\n            // but it should work in other browsers\n            expect(!isOldIE() && button.classList.contains('medium-editor-button-active')).toBe(false);\n            expect(!isOldIE() && (this.el.innerHTML !== '<p><span id=\"underline-span\">lorem ipsum</span></p>')).toBe(false);\n        });\n    });\n\n    describe('Strikethrough', function () {\n        it('button should be active if the selection already has the element', function () {\n            var editor = this.newMediumEditor('.editor', {\n                    toolbar: {\n                        buttons: ['strikethrough']\n                    }\n                }),\n                toolbar = editor.getExtensionByName('toolbar'),\n                button = toolbar.getToolbarElement().querySelector('[data-action=\"strikethrough\"]');\n            this.el.innerHTML = '<s>lorem ipsum</s>';\n\n            selectElementContentsAndFire(editor.elements[0]);\n            expect(button.classList.contains('medium-editor-button-active')).toBe(true);\n\n            fireEvent(button, 'click');\n            expect(button.classList.contains('medium-editor-button-active')).toBe(false);\n            expect(this.el.innerHTML).toBe('lorem ipsum');\n        });\n\n        it('button should be active if the selection is strikethrough and queryCommandState fails', function () {\n            var editor = this.newMediumEditor('.editor', {\n                    toolbar: {\n                        buttons: ['strikethrough']\n                    }\n                }),\n                toolbar = editor.getExtensionByName('toolbar'),\n                button = toolbar.getToolbarElement().querySelector('[data-action=\"strikethrough\"]');\n\n            spyOn(document, 'queryCommandState').and.throwError('DOM ERROR');\n            this.el.innerHTML = '<s><b><i>lorem ipsum</i></b></s>';\n\n            selectElementContentsAndFire(editor.elements[0].querySelector('i'));\n            expect(button.classList.contains('medium-editor-button-active')).toBe(true);\n\n            fireEvent(button, 'click');\n            expect(button.classList.contains('medium-editor-button-active')).toBe(false);\n        });\n\n        it('button should be active for other cases when text is strikethrough', function () {\n            var editor = this.newMediumEditor('.editor', {\n                    toolbar: {\n                        buttons: ['strikethrough']\n                    }\n                }),\n                toolbar = editor.getExtensionByName('toolbar'),\n                button = toolbar.getToolbarElement().querySelector('[data-action=\"strikethrough\"]');\n            this.el.innerHTML = '<p><span id=\"strike-span\" style=\"text-decoration: line-through\">lorem ipsum</span></p>';\n\n            selectElementContentsAndFire(document.getElementById('strike-span'));\n            expect(button.classList.contains('medium-editor-button-active')).toBe(true);\n\n            fireEvent(button, 'click');\n\n            this.el.innerHTML = stripAttrIfEmpty(this.el, 'style');\n\n            // style=\"text-decoration: line-through\" prevents IE9+10 from doing anything when 'strikethrough' is triggered\n            // but it should work in other browsers\n            expect(!isOldIE() && button.classList.contains('medium-editor-button-active')).toBe(false);\n            expect(!isOldIE() && (this.el.innerHTML !== '<p><span id=\"strike-span\">lorem ipsum</span></p>')).toBe(false);\n        });\n    });\n\n    describe('Superscript', function () {\n        it('button should be active if the selection already has the element', function () {\n            var editor = this.newMediumEditor('.editor', {\n                    toolbar: {\n                        buttons: ['superscript']\n                    }\n                }),\n                toolbar = editor.getExtensionByName('toolbar'),\n                button = toolbar.getToolbarElement().querySelector('[data-action=\"superscript\"]');\n            this.el.innerHTML = '<sup>lorem ipsum</sub>';\n\n            selectElementContentsAndFire(editor.elements[0]);\n            expect(button.classList.contains('medium-editor-button-active')).toBe(true);\n\n            fireEvent(button, 'click');\n            expect(button.classList.contains('medium-editor-button-active')).toBe(false);\n            expect(this.el.innerHTML).toBe('lorem ipsum');\n        });\n    });\n\n    describe('Subscript', function () {\n        it('button should be active if the selection already has the element', function () {\n            var editor = this.newMediumEditor('.editor', {\n                    toolbar: {\n                        buttons: ['subscript']\n                    }\n                }),\n                toolbar = editor.getExtensionByName('toolbar'),\n                button = toolbar.getToolbarElement().querySelector('[data-action=\"subscript\"]');\n            this.el.innerHTML = '<sub>lorem ipsum</sub>';\n\n            selectElementContentsAndFire(editor.elements[0]);\n            expect(button.classList.contains('medium-editor-button-active')).toBe(true);\n\n            fireEvent(button, 'click');\n            expect(button.classList.contains('medium-editor-button-active')).toBe(false);\n            expect(this.el.innerHTML).toBe('lorem ipsum');\n        });\n    });\n\n    describe('Anchor', function () {\n        it('button should be active if the selection already has the element', function () {\n            var editor = this.newMediumEditor('.editor', {\n                    toolbar: {\n                        buttons: ['anchor']\n                    }\n                }),\n                toolbar = editor.getExtensionByName('toolbar'),\n                button = toolbar.getToolbarElement().querySelector('[data-action=\"createLink\"]');\n            this.el.innerHTML = '<p><span id=\"span-lorem\">lorem</span> <a href=\"#\" id=\"link\">ipsum</a></p>';\n\n            selectElementContentsAndFire(document.getElementById('link'));\n            expect(button.classList.contains('medium-editor-button-active')).toBe(true);\n\n            selectElementContentsAndFire(document.getElementById('span-lorem'), { eventToFire: 'mouseup' });\n            expect(button.classList.contains('medium-editor-button-active')).toBe(false);\n        });\n\n        it('button should call the anchorExtension.showForm() method', function () {\n            spyOn(MediumEditor.extensions.anchor.prototype, 'showForm');\n            var button,\n                editor = this.newMediumEditor('.editor'),\n                toolbar = editor.getExtensionByName('toolbar');\n\n            selectElementContentsAndFire(editor.elements[0]);\n            button = toolbar.getToolbarElement().querySelector('[data-action=\"createLink\"]');\n            fireEvent(button, 'click');\n            expect(editor.getExtensionByName('anchor').showForm).toHaveBeenCalled();\n        });\n    });\n\n    describe('Quote', function () {\n        it('button should not be active if the selection is not inside a blockquote', function () {\n            var editor = this.newMediumEditor('.editor', {\n                    toolbar: {\n                        buttons: ['quote']\n                    }\n                }),\n                toolbar = editor.getExtensionByName('toolbar'),\n                button = toolbar.getToolbarElement().querySelector('[data-action=\"append-blockquote\"]');\n            this.el.innerHTML = '<span id=\"span-lorem\">lorem ipsum</span>';\n\n            selectElementContentsAndFire(document.getElementById('span-lorem'));\n            expect(button.classList.contains('medium-editor-button-active')).toBe(false);\n\n            fireEvent(button, 'click');\n            selectElementContentsAndFire(document.getElementById('span-lorem'));\n            expect(button.classList.contains('medium-editor-button-active')).toBe(true);\n        });\n    });\n\n    describe('Image', function () {\n        it('should create an image', function () {\n            var editor = this.newMediumEditor('.editor', {\n                    toolbar: {\n                        buttons: ['image']\n                    }\n                }),\n                toolbar = editor.getExtensionByName('toolbar'),\n                button = toolbar.getToolbarElement().querySelector('[data-action=\"image\"]');\n            spyOn(document, 'execCommand').and.callThrough();\n            this.el.innerHTML = '<span id=\"span-image\">http://i.imgur.com/twlXfUq.jpg  \\n\\n</span>';\n\n            selectElementContentsAndFire(document.getElementById('span-image'));\n\n            fireEvent(button, 'click');\n\n            expect(this.el.innerHTML).toContain('<img src=\"http://i.imgur.com/twlXfUq.jpg\">');\n            expect(document.execCommand).toHaveBeenCalledWith('insertImage', false, 'http://i.imgur.com/twlXfUq.jpg');\n        });\n    });\n\n    describe('Html', function () {\n        it('should create an evaluated html tag', function () {\n            var editor = this.newMediumEditor('.editor', {\n                    toolbar: {\n                        buttons: ['html']\n                    }\n                }),\n                toolbar = editor.getExtensionByName('toolbar'),\n                button = toolbar.getToolbarElement().querySelector('[data-action=\"html\"]');\n            spyOn(document, 'execCommand').and.callThrough();\n            this.el.innerHTML = '<span id=\"span-html\">&lt;iframe width=\"854\" height=\"480\" src=\"https://www.youtube.com/embed/QHH3iSeDBLo\" frameborder=\"0\" allowfullscreen&gt;&lt;/iframe&gt;  \\n\\n</span>';\n\n            selectElementContentsAndFire(document.getElementById('span-html'));\n\n            fireEvent(button, 'click');\n\n            expect(this.el.innerHTML).toMatch(/<iframe(?: width=\"854\"| height=\"480\"| src=\"https:\\/\\/www.youtube.com\\/embed\\/QHH3iSeDBLo\"| frameborder=\"0\"| allowfullscreen=\"\"){5}>(<br>)?<\\/iframe>/gi);\n        });\n    });\n\n    describe('OrderedList', function () {\n        it('button should be active if the selection already has the element', function () {\n            var editor = this.newMediumEditor('.editor', {\n                    toolbar: {\n                        buttons: ['orderedlist', 'unorderedlist']\n                    }\n                }),\n                toolbar = editor.getExtensionByName('toolbar'),\n                button = toolbar.getToolbarElement().querySelector('[data-action=\"insertorderedlist\"]');\n            this.el.innerHTML = '<ol><li id=\"li-lorem\">lorem ipsum</li></ol>';\n\n            selectElementContentsAndFire(document.getElementById('li-lorem'));\n            expect(button.classList.contains('medium-editor-button-active')).toBe(true);\n            // Unordered list should not be active\n            expect(toolbar.getToolbarElement().querySelector('[data-action=\"insertunorderedlist\"]').classList.contains('medium-editor-button-active')).toBe(false);\n\n            fireEvent(button, 'click');\n            expect(button.classList.contains('medium-editor-button-active')).toBe(false);\n            // Unordered list should not be active\n            expect(toolbar.getToolbarElement().querySelector('[data-action=\"insertunorderedlist\"]').classList.contains('medium-editor-button-active')).toBe(false);\n        });\n    });\n\n    describe('UnorderedList', function () {\n        it('button should be active if the selection already has the element', function () {\n            var editor = this.newMediumEditor('.editor', {\n                    toolbar: {\n                        buttons: ['unorderedlist', 'orderedlist']\n                    }\n                }),\n                toolbar = editor.getExtensionByName('toolbar'),\n                button = toolbar.getToolbarElement().querySelector('[data-action=\"insertunorderedlist\"]');\n            this.el.innerHTML = '<ul><li id=\"li-lorem\">lorem ipsum</li></ul>';\n\n            selectElementContentsAndFire(document.getElementById('li-lorem'));\n            expect(button.classList.contains('medium-editor-button-active')).toBe(true);\n            // Ordered list button should not be active\n            expect(toolbar.getToolbarElement().querySelector('[data-action=\"insertorderedlist\"]').classList.contains('medium-editor-button-active')).toBe(false);\n\n            fireEvent(button, 'click');\n            expect(button.classList.contains('medium-editor-button-active')).toBe(false);\n            // Ordered list button should not be active\n            expect(toolbar.getToolbarElement().querySelector('[data-action=\"insertorderedlist\"]').classList.contains('medium-editor-button-active')).toBe(false);\n        });\n    });\n\n    describe('Pre', function () {\n        it('button should be active if the selection already has the element', function () {\n            var editor = this.newMediumEditor('.editor', {\n                    toolbar: {\n                        buttons: ['pre']\n                    }\n                }),\n                toolbar = editor.getExtensionByName('toolbar'),\n                button = toolbar.getToolbarElement().querySelector('[data-action=\"append-pre\"]');\n            this.el.innerHTML = '<pre><span id=\"span-lorem\">lorem ipsum</span></pre>';\n\n            selectElementContentsAndFire(document.getElementById('span-lorem'));\n            expect(button.classList.contains('medium-editor-button-active')).toBe(true);\n\n            fireEvent(button, 'click');\n            expect(button.classList.contains('medium-editor-button-active')).toBe(false);\n        });\n    });\n\n    describe('Justify', function () {\n        it('button should be activated based on text-align values', function () {\n            this.el.innerHTML = '<div><p id=\"justify-para-one\">lorem ipsum</p></div>';\n            document.body.style.setProperty('text-align', 'center');\n            try {\n                var editor = this.newMediumEditor('.editor', {\n                        toolbar: {\n                            buttons: ['justifyCenter', 'justifyRight']\n                        }\n                    }),\n                    toolbar = editor.getExtensionByName('toolbar'),\n                    rightButton = toolbar.getToolbarElement().querySelector('[data-action=\"justifyRight\"]'),\n                    centerButton = toolbar.getToolbarElement().querySelector('[data-action=\"justifyCenter\"]');\n\n                selectElementContentsAndFire(document.getElementById('justify-para-one'));\n                expect(rightButton.classList.contains('medium-editor-button-active')).toBe(false);\n                expect(centerButton.classList.contains('medium-editor-button-active')).toBe(true);\n\n                fireEvent(rightButton, 'click');\n                expect(rightButton.classList.contains('medium-editor-button-active')).toBe(true);\n                expect(centerButton.classList.contains('medium-editor-button-active')).toBe(false);\n            } finally {\n                document.body.style.removeProperty('text-align');\n            }\n        });\n\n        it('buttons should deactivate other justify buttons', function () {\n            this.el.innerHTML = '<p id=\"justify-para-one\">lorem ipsum</p>' +\n                                '<p id=\"justify-para-two\" align=\"left\">lorem ipsum</p>' +\n                                '<p id=\"justify-para-three\" align=\"right\">lorem ipsum</p>' +\n                                '<p id=\"justify-para-four\" align=\"center\">lorem ipsum</p>' +\n                                '<p id=\"justify-para-five\" align=\"justify\">lorem ipsum</p>';\n            var editor = this.newMediumEditor('.editor', {\n                    toolbar: {\n                        buttons: ['justifyLeft', 'justifyCenter', 'justifyRight', 'justifyFull']\n                    }\n                }),\n                toolbar = editor.getExtensionByName('toolbar'),\n                leftButton = toolbar.getToolbarElement().querySelector('[data-action=\"justifyLeft\"]'),\n                rightButton = toolbar.getToolbarElement().querySelector('[data-action=\"justifyRight\"]'),\n                centerButton = toolbar.getToolbarElement().querySelector('[data-action=\"justifyCenter\"]'),\n                fullButton = toolbar.getToolbarElement().querySelector('[data-action=\"justifyFull\"]');\n\n            // First paragraph should have nothing activated (IE will select align-left)\n            selectElementContentsAndFire(document.getElementById('justify-para-one'));\n            expect(rightButton.classList.contains('medium-editor-button-active')).toBe(false);\n            expect(centerButton.classList.contains('medium-editor-button-active')).toBe(false);\n            expect(fullButton.classList.contains('medium-editor-button-active')).toBe(false);\n\n            // Trigger justify full, only it should be active\n            fireEvent(fullButton, 'click');\n            expect(leftButton.classList.contains('medium-editor-button-active')).toBe(false);\n            expect(rightButton.classList.contains('medium-editor-button-active')).toBe(false);\n            expect(centerButton.classList.contains('medium-editor-button-active')).toBe(false);\n            expect(fullButton.classList.contains('medium-editor-button-active')).toBe(true);\n\n            // Second paragraph should have justifyLeft activated\n            selectElementContentsAndFire(document.getElementById('justify-para-two'), { eventToFire: 'mouseup' });\n            expect(leftButton.classList.contains('medium-editor-button-active')).toBe(true);\n            expect(rightButton.classList.contains('medium-editor-button-active')).toBe(false);\n            expect(centerButton.classList.contains('medium-editor-button-active')).toBe(false);\n            expect(fullButton.classList.contains('medium-editor-button-active')).toBe(false);\n\n            // Trigger justify center, only it should be active\n            fireEvent(centerButton, 'click');\n            expect(leftButton.classList.contains('medium-editor-button-active')).toBe(false);\n            expect(rightButton.classList.contains('medium-editor-button-active')).toBe(false);\n            expect(centerButton.classList.contains('medium-editor-button-active')).toBe(true);\n            expect(fullButton.classList.contains('medium-editor-button-active')).toBe(false);\n\n            // Third paragraph should have justifyRight activated\n            selectElementContentsAndFire(document.getElementById('justify-para-three'), { eventToFire: 'mouseup' });\n            expect(leftButton.classList.contains('medium-editor-button-active')).toBe(false);\n            expect(rightButton.classList.contains('medium-editor-button-active')).toBe(true);\n            expect(centerButton.classList.contains('medium-editor-button-active')).toBe(false);\n            expect(fullButton.classList.contains('medium-editor-button-active')).toBe(false);\n\n            // Trigger justify left, only it should be active\n            fireEvent(leftButton, 'click');\n            expect(leftButton.classList.contains('medium-editor-button-active')).toBe(true);\n            expect(rightButton.classList.contains('medium-editor-button-active')).toBe(false);\n            expect(centerButton.classList.contains('medium-editor-button-active')).toBe(false);\n            expect(fullButton.classList.contains('medium-editor-button-active')).toBe(false);\n\n            // Fourth paragraph should have justifyCenter activated\n            selectElementContentsAndFire(document.getElementById('justify-para-four'), { eventToFire: 'mouseup' });\n            expect(leftButton.classList.contains('medium-editor-button-active')).toBe(false);\n            expect(rightButton.classList.contains('medium-editor-button-active')).toBe(false);\n            expect(centerButton.classList.contains('medium-editor-button-active')).toBe(true);\n            expect(fullButton.classList.contains('medium-editor-button-active')).toBe(false);\n\n            // Trigger justify right, only it should be active\n            fireEvent(rightButton, 'click');\n            selectElementContentsAndFire(document.getElementById('justify-para-four'), { eventToFire: 'mouseup' });\n            expect(leftButton.classList.contains('medium-editor-button-active')).toBe(false);\n            expect(rightButton.classList.contains('medium-editor-button-active')).toBe(true);\n            expect(centerButton.classList.contains('medium-editor-button-active')).toBe(false);\n            expect(fullButton.classList.contains('medium-editor-button-active')).toBe(false);\n\n            // Fifth paragraph should have justifyFull activated\n            selectElementContentsAndFire(document.getElementById('justify-para-five'), { eventToFire: 'mouseup' });\n            expect(leftButton.classList.contains('medium-editor-button-active')).toBe(false);\n            expect(rightButton.classList.contains('medium-editor-button-active')).toBe(false);\n            expect(centerButton.classList.contains('medium-editor-button-active')).toBe(false);\n            expect(fullButton.classList.contains('medium-editor-button-active')).toBe(true);\n        });\n    });\n\n    describe('Remove Formatting', function () {\n\n        it('should unwrap basic things', function () {\n            var editor = this.newMediumEditor('.editor', {\n                    toolbar: {\n                        buttons: ['removeFormat']\n                    }\n                }),\n                toolbar = editor.getExtensionByName('toolbar'),\n                button = toolbar.getToolbarElement().querySelector('[data-action=\"removeFormat\"]');\n\n            expect(button).toBeTruthy();\n            this.el.innerHTML = '<p>foo<b>bar</b><i>baz</i><strong>bam</strong></p>';\n\n            selectElementContentsAndFire(editor.elements[0].querySelector('p'));\n            fireEvent(button, 'click');\n            expect(this.el.innerHTML).toBe('<p>foobarbazbam</p>');\n\n            this.el.innerHTML = '<div><p><b>foo</b></p><p><i>bar</i></p><ul><li>on<b>e</b></li></ul></div>';\n            selectElementContentsAndFire(editor.elements[0].querySelector('div'));\n            fireEvent(button, 'click');\n            expect(this.el.innerHTML).toBe('<div><p>foo</p><p>bar</p><ul><li>one</li></ul></div>');\n\n            // TODO: IE does not unwrap something like <p><span style='color:red'>bar</span></p>\n            this.el.innerHTML = '<div><h2>b<i>a</i>r</h2><p><em><strong><u><sub><sup>foo</sup></sub></u></strong></em></p><pre>foo<i>bar</i>baz</pre></div>';\n            selectElementContentsAndFire(editor.elements[0].querySelector('div'));\n            fireEvent(button, 'click');\n            expect(this.el.innerHTML).toBe('<div><h2>bar</h2><p>foo</p><pre>foobarbaz</pre></div>');\n\n        });\n\n    });\n\n    describe('Header', function () {\n        it('buttons should be active if the selection already has the element', function () {\n            var editor = this.newMediumEditor('.editor', {\n                    toolbar: {\n                        buttons: ['h3', 'h4']\n                    }\n                }),\n                toolbar = editor.getExtensionByName('toolbar'),\n                buttonOne = toolbar.getToolbarElement().querySelector('[data-action=\"append-h3\"]'),\n                buttonTwo = toolbar.getToolbarElement().querySelector('[data-action=\"append-h4\"]');\n\n            this.el.innerHTML = '<h2>lorem</h2><h3>ipsum</h3><h4>dolor</h4>';\n            selectElementContentsAndFire(editor.elements[0].querySelector('h2'));\n            expect(buttonOne.classList.contains('medium-editor-button-active')).toBe(false);\n            expect(buttonTwo.classList.contains('medium-editor-button-active')).toBe(false);\n\n            selectElementContentsAndFire(editor.elements[0].querySelector('h3'), { eventToFire: 'mouseup' });\n            expect(buttonOne.classList.contains('medium-editor-button-active')).toBe(true);\n            expect(buttonTwo.classList.contains('medium-editor-button-active')).toBe(false);\n\n            selectElementContentsAndFire(editor.elements[0].querySelector('h4'), { eventToFire: 'mouseup' });\n            expect(buttonOne.classList.contains('medium-editor-button-active')).toBe(false);\n            expect(buttonTwo.classList.contains('medium-editor-button-active')).toBe(true);\n        });\n\n        it('buttons should be active if the selection already custom defined element types', function () {\n            var editor = this.newMediumEditor('.editor', {\n                    toolbar: {\n                        buttons: ['h1', 'h5']\n                    }\n                }),\n                toolbar = editor.getExtensionByName('toolbar'),\n                buttonOne = toolbar.getToolbarElement().querySelector('[data-action=\"append-h1\"]'),\n                buttonTwo = toolbar.getToolbarElement().querySelector('[data-action=\"append-h5\"]');\n\n            expect(buttonOne).toBeTruthy();\n            expect(buttonTwo).toBeTruthy();\n\n            this.el.innerHTML = '<h1>lorem</h1><h3>ipsum</h3><h5>dolor</h5>';\n            selectElementContentsAndFire(editor.elements[0].querySelector('h1'));\n            expect(buttonOne.classList.contains('medium-editor-button-active')).toBe(true);\n            expect(buttonTwo.classList.contains('medium-editor-button-active')).toBe(false);\n\n            selectElementContentsAndFire(editor.elements[0].querySelector('h3'), { eventToFire: 'mouseup' });\n            expect(buttonOne.classList.contains('medium-editor-button-active')).toBe(false);\n            expect(buttonTwo.classList.contains('medium-editor-button-active')).toBe(false);\n\n            selectElementContentsAndFire(editor.elements[0].querySelector('h5'), { eventToFire: 'mouseup' });\n            expect(buttonOne.classList.contains('medium-editor-button-active')).toBe(false);\n            expect(buttonTwo.classList.contains('medium-editor-button-active')).toBe(true);\n        });\n\n        it('buttons should convert between element types and \"undo\" back to original type', function () {\n            var editor = this.newMediumEditor('.editor', {\n                    toolbar: {\n                        buttons: ['h1', 'h4']\n                    }\n                }),\n                toolbar = editor.getExtensionByName('toolbar'),\n                buttonOne = toolbar.getToolbarElement().querySelector('[data-action=\"append-h1\"]'),\n                buttonTwo = toolbar.getToolbarElement().querySelector('[data-action=\"append-h4\"]');\n\n            this.el.innerHTML = '<p>lorem ipsum dolor</p>';\n            selectElementContentsAndFire(editor.elements[0].firstChild);\n            expect(buttonOne.classList.contains('medium-editor-button-active')).toBe(false);\n            expect(buttonTwo.classList.contains('medium-editor-button-active')).toBe(false);\n\n            fireEvent(buttonOne, 'click');\n            selectElementContentsAndFire(editor.elements[0].firstChild);\n            expect(buttonOne.classList.contains('medium-editor-button-active')).toBe(true);\n            expect(buttonTwo.classList.contains('medium-editor-button-active')).toBe(false);\n            expect(editor.elements[0].innerHTML).toBe('<h1>lorem ipsum dolor</h1>');\n\n            fireEvent(buttonTwo, 'click');\n            selectElementContentsAndFire(editor.elements[0].firstChild);\n            expect(buttonOne.classList.contains('medium-editor-button-active')).toBe(false);\n            expect(buttonTwo.classList.contains('medium-editor-button-active')).toBe(true);\n            expect(editor.elements[0].innerHTML).toBe('<h4>lorem ipsum dolor</h4>');\n\n            fireEvent(buttonTwo, 'click');\n            expect(buttonOne.classList.contains('medium-editor-button-active')).toBe(false);\n            expect(buttonTwo.classList.contains('medium-editor-button-active')).toBe(false);\n            expect(editor.elements[0].innerHTML).toBe('<p>lorem ipsum dolor</p>');\n        });\n    });\n});\n\nfunction stripAttrIfEmpty(element, attribute) {\n    // we want to strip empty attributes (especially styles,\n    // because the tests create style tags, inject style content,\n    // and then remove that style content.\n    //\n    // some browsers will remove empty attributes automatically.\n    //\n    // others (Chrome, seemingly) will not:\n    return element.innerHTML.replace(attribute + '=\"\"', '');\n}\n"
  },
  {
    "path": "spec/content.spec.js",
    "content": "/*global fireEvent, firePreparedEvent,\n         prepareEvent, selectElementContents,\n         selectElementContentsAndFire,\n         placeCursorInsideElement,\n         getEdgeVersion, isFirefox */\n\ndescribe('Content TestCase', function () {\n    'use strict';\n\n    beforeEach(function () {\n        setupTestHelpers.call(this);\n        this.el = this.createElement('div', 'editor', 'lore ipsum');\n    });\n\n    afterEach(function () {\n        this.cleanupTest();\n    });\n\n    it('should remove paragraphs when a list is inserted inside of it', function () {\n        this.el.innerHTML = '<p>lorem ipsum<ul><li>dolor</li></ul></p>';\n        var editor = this.newMediumEditor('.editor', {\n                toolbar: {\n                    buttons: ['orderedlist']\n                }\n            }),\n            target = editor.elements[0].querySelector('p'),\n            toolbar = editor.getExtensionByName('toolbar'),\n            range, sel;\n        selectElementContentsAndFire(target);\n        fireEvent(toolbar.getToolbarElement().querySelector('[data-action=\"insertorderedlist\"]'), 'click');\n        expect(this.el.innerHTML).toMatch(/^<ol><li>lorem ipsum(<br>)?<\\/li><\\/ol><ul><li>dolor<\\/li><\\/ul>?/);\n\n        sel = document.getSelection();\n        expect(sel.rangeCount).toBe(1);\n        range = sel.getRangeAt(0);\n\n        // Chrome and Safari collapse the range at the end of the 'lorem ipsum' li\n        // Firefox, IE, and Edge select the 'lorem ipsum' contents\n        if (range.collapsed) {\n            expect(range.startContainer.nodeValue).toBe('lorem ipsum');\n            expect(range.endContainer.nodeValue).toBe('lorem ipsum');\n            expect(range.startOffset).toBe('lorem ipsum'.length);\n            expect(range.endOffset).toBe('lorem ipsum'.length);\n        } else {\n            expect(range.toString()).toBe('lorem ipsum');\n            expect(range.startContainer.nodeName.toLowerCase()).toBe('li');\n            expect(range.endContainer.nodeName.toLowerCase()).toBe('li');\n            expect(range.startOffset).toBe(0);\n            expect(range.endOffset).toBe(1);\n        }\n    });\n\n    describe('when the tab key is pressed', function () {\n        it('should indent when within an <li>', function () {\n            this.el.innerHTML = '<ol><li>lorem</li><li>ipsum</li></ol>';\n            var editor = this.newMediumEditor('.editor'),\n                target = editor.elements[0].querySelector('ol').lastChild;\n            spyOn(document, 'execCommand').and.callThrough();\n            selectElementContents(target);\n            fireEvent(target, 'keydown', {\n                keyCode: MediumEditor.util.keyCode.TAB\n            });\n            expect(document.execCommand).toHaveBeenCalledWith('indent', false, null);\n            // Firefox (annoyingly) throws a NS_ERROR_FAILURE when attempting to mimic this through a test case\n            // I was unable to find a workaround, and this works fine in a browser\n            // so let's just skip skip the innerHTML check in firefox\n            if (!isFirefox()) {\n                expect(this.el.innerHTML).toBe('<ol><li>lorem</li><ol><li>ipsum</li></ol></ol>');\n            }\n        });\n\n        it('with shift key, should outdent when within an <li>', function () {\n            this.el.innerHTML = '<ol><li>lorem</li><ol><li><span><span>ipsum</span></span></li></ol></ol>';\n            var editor = this.newMediumEditor('.editor'),\n                target = editor.elements[0].querySelector('ol').lastChild.firstChild.firstChild;\n            spyOn(document, 'execCommand').and.callThrough();\n            selectElementContents(target);\n            fireEvent(target, 'keydown', {\n                keyCode: MediumEditor.util.keyCode.TAB,\n                shiftKey: true\n            });\n            expect(document.execCommand).toHaveBeenCalledWith('outdent', false, null);\n            // Firefox (annoyingly) throws a NS_ERROR_FAILURE when attempting to mimic this through a test case\n            // I was unable to find a workaround, and this works fine in a browser\n            // so let's just skip skip the innerHTML check in firefox\n            if (!isFirefox()) {\n                // Webkit removes the nested spans, IE does not\n                expect(this.el.innerHTML).toMatch(/<ol><li>lorem<\\/li><li>(<span><span>)?ipsum(<br>|<\\/span><\\/span>)<\\/li><\\/ol>/);\n            }\n        });\n\n        it('should insert a space when within a pre node', function () {\n            this.el.innerHTML = '<pre>lorem ipsum</pre>';\n            var editor = this.newMediumEditor('.editor'),\n                targetNode = editor.elements[0].querySelector('pre');\n            placeCursorInsideElement(targetNode, 0);\n            fireEvent(targetNode, 'keydown', {\n                keyCode: MediumEditor.util.keyCode.TAB\n            });\n            expect(this.el.innerHTML).toBe('<pre>    lorem ipsum</pre>');\n        });\n    });\n\n    describe('when the space key is pressed', function () {\n        it('should not prevent new spaces from being inserted when disableExtraSpaces options is false', function () {\n            this.el.innerHTML = '<p>lorem ipsum</p>';\n\n            var editor = this.newMediumEditor('.editor', { disableExtraSpaces: false }),\n                evt;\n\n            placeCursorInsideElement(editor.elements[0], 0);\n\n            evt = prepareEvent(editor.elements[0], 'keydown', {\n                keyCode: MediumEditor.util.keyCode.SPACE\n            });\n\n            spyOn(evt, 'preventDefault').and.callThrough();\n\n            firePreparedEvent(evt, editor.elements[0], 'keydown');\n\n            expect(evt.preventDefault).not.toHaveBeenCalled();\n        });\n\n        it('should prevent new spaces from being inserted when disableExtraSpaces options is true', function () {\n            this.el.innerHTML = '<p>lorem ipsum</p>';\n\n            var editor = this.newMediumEditor('.editor', { disableExtraSpaces: true }),\n                evt;\n\n            placeCursorInsideElement(editor.elements[0], 0);\n\n            evt = prepareEvent(editor.elements[0], 'keydown', {\n                keyCode: MediumEditor.util.keyCode.SPACE\n            });\n\n            spyOn(evt, 'preventDefault').and.callThrough();\n\n            firePreparedEvent(evt, editor.elements[0], 'keydown');\n\n            expect(evt.preventDefault).toHaveBeenCalled();\n        });\n\n        it('should allow one space at the end of a line when disableExtraSpaces options is true', function () {\n            this.el.innerHTML = '<p>lorem ipsum</p>';\n\n            var editor = this.newMediumEditor('.editor', { disableExtraSpaces: true }),\n                evt;\n\n            placeCursorInsideElement(editor.elements[0].getElementsByTagName('p')[0], 1);\n\n            evt = prepareEvent(editor.elements[0], 'keydown', {\n                keyCode: MediumEditor.util.keyCode.SPACE\n            });\n\n            spyOn(evt, 'preventDefault').and.callThrough();\n\n            firePreparedEvent(evt, editor.elements[0], 'keydown');\n\n            expect(evt.preventDefault).not.toHaveBeenCalled();\n        });\n\n        it('should prevent more spaces from being inserted at the end of a line when disableExtraSpaces options is true', function () {\n            this.el.innerHTML = '<p>lorem ipsum    <br /></p>';\n\n            var editor = this.newMediumEditor('.editor', { disableExtraSpaces: true }),\n                evt;\n\n            placeCursorInsideElement(editor.elements[0].getElementsByTagName('p')[0], 1);\n\n            evt = prepareEvent(editor.elements[0], 'keydown', {\n                keyCode: MediumEditor.util.keyCode.SPACE\n            });\n\n            spyOn(evt, 'preventDefault').and.callThrough();\n\n            firePreparedEvent(evt, editor.elements[0], 'keydown');\n\n            expect(evt.preventDefault).toHaveBeenCalled();\n        });\n\n        //This test case replicates https://github.com/yabwe/medium-editor/issues/982\n        it('should prevent more spaces from being inserted when a space already exists and disableExtraSpaces options is true', function () {\n            this.el.innerHTML = '<p>lorem<span> ipsum</span></p>';\n\n            var editor = this.newMediumEditor('.editor', { disableExtraSpaces: true }),\n                evt;\n\n            placeCursorInsideElement(editor.elements[0].getElementsByTagName('p')[0], 1);\n\n            evt = prepareEvent(editor.elements[0], 'keydown', {\n                keyCode: MediumEditor.util.keyCode.SPACE\n            });\n\n            spyOn(evt, 'preventDefault').and.callThrough();\n\n            firePreparedEvent(evt, editor.elements[0], 'keydown');\n\n            expect(evt.preventDefault).toHaveBeenCalled();\n        });\n    });\n\n    describe('when the enter key is pressed', function () {\n        it('should prevent new lines from being inserted when disableReturn options is true', function () {\n            this.el.innerHTML = 'lorem ipsum';\n\n            var editor = this.newMediumEditor('.editor', { disableReturn: true }),\n                evt;\n\n            placeCursorInsideElement(editor.elements[0], 0);\n\n            evt = prepareEvent(editor.elements[0], 'keydown', {\n                keyCode: MediumEditor.util.keyCode.ENTER\n            });\n\n            spyOn(evt, 'preventDefault').and.callThrough();\n\n            firePreparedEvent(evt, editor.elements[0], 'keydown');\n\n            expect(evt.preventDefault).toHaveBeenCalled();\n        });\n\n        it('should prevent new lines from being inserted when data-disable-return is defined', function () {\n            this.el.innerHTML = 'lorem ipsum';\n            this.el.setAttribute('data-disable-return', true);\n\n            var editor = this.newMediumEditor('.editor'),\n                evt;\n\n            placeCursorInsideElement(editor.elements[0], 0);\n\n            evt = prepareEvent(editor.elements[0], 'keydown', {\n                keyCode: MediumEditor.util.keyCode.ENTER\n            });\n\n            spyOn(evt, 'preventDefault').and.callThrough();\n\n            firePreparedEvent(evt, editor.elements[0], 'keydown');\n\n            expect(evt.preventDefault).toHaveBeenCalled();\n        });\n\n        it('should allow to get out of list when enter is pressed twice', function () {\n            this.el.innerHTML = '<li><br></li>';\n            var editor = this.newMediumEditor('.editor', { disableDoubleReturn: true }),\n                p = editor.elements[0].querySelector('li'),\n                evt;\n\n            placeCursorInsideElement(p, 0);\n\n            evt = prepareEvent(p, 'keydown', {\n                keyCode: MediumEditor.util.keyCode.ENTER\n            });\n\n            spyOn(evt, 'preventDefault').and.callThrough();\n\n            firePreparedEvent(evt, p, 'keydown');\n\n            expect(evt.preventDefault).not.toHaveBeenCalled();\n        });\n\n        it('should allow a line to be added when pressed enter at end of the <p> tag when disableDoubleReturn is true and contains <br> as the previous sibling', function () {\n\n            this.el.innerHTML = '<p>it is a test</p><br><p>because tests are great..!!</p>';\n            var editor = this.newMediumEditor('.editor', { disableDoubleReturn: true }),\n                targetNode = editor.elements[0].querySelector('p:last-child'),\n                evt;\n\n            placeCursorInsideElement(targetNode, 0);\n\n            evt = prepareEvent(targetNode, 'keydown', {\n                keyCode: MediumEditor.util.keyCode.ENTER\n            });\n\n            spyOn(evt, 'preventDefault').and.callThrough();\n\n            firePreparedEvent(evt, targetNode, 'keydown');\n\n            expect(evt.preventDefault).not.toHaveBeenCalled();\n\n        });\n\n        it('should prevent consecutive new lines from being inserted when disableDoubleReturn is true', function () {\n            this.el.innerHTML = '<p><br></p>';\n            var editor = this.newMediumEditor('.editor', { disableDoubleReturn: true }),\n                p = editor.elements[0].querySelector('p'),\n                evt;\n\n            placeCursorInsideElement(p, 0);\n\n            evt = prepareEvent(p, 'keydown', {\n                keyCode: MediumEditor.util.keyCode.ENTER\n            });\n\n            spyOn(evt, 'preventDefault').and.callThrough();\n\n            firePreparedEvent(evt, p, 'keydown');\n\n            expect(evt.preventDefault).toHaveBeenCalled();\n        });\n\n        it('should prevent consecutive new lines from being inserted when data-disable-double-return is defined', function () {\n            this.el.innerHTML = '<p><br></p>';\n            this.el.setAttribute('data-disable-double-return', true);\n\n            var editor = this.newMediumEditor('.editor'),\n                p = editor.elements[0].querySelector('p'),\n                evt;\n\n            placeCursorInsideElement(p, 0);\n\n            evt = prepareEvent(p, 'keydown', {\n                keyCode: MediumEditor.util.keyCode.ENTER\n            });\n\n            spyOn(evt, 'preventDefault').and.callThrough();\n\n            firePreparedEvent(evt, p, 'keydown');\n\n            expect(evt.preventDefault).toHaveBeenCalled();\n        });\n\n        it('should prevent consecutive new lines from being inserted inside a sentence when disableDoubleReturn is true', function () {\n            this.el.innerHTML = '<p>hello</p><p><br></p><p>word</p>';\n            var editor = this.newMediumEditor('.editor', { disableDoubleReturn: true }),\n                p = editor.elements[0].getElementsByTagName('p')[2],\n                evt;\n\n            placeCursorInsideElement(p, 0);\n\n            evt = prepareEvent(p, 'keydown', {\n                keyCode: MediumEditor.util.keyCode.ENTER\n            });\n\n            spyOn(evt, 'preventDefault').and.callThrough();\n\n            firePreparedEvent(evt, p, 'keydown');\n\n            expect(evt.preventDefault).toHaveBeenCalled();\n        });\n\n        it('should call formatBlock when inside a non-header and non-anchor', function () {\n            this.el.innerHTML = '<p>lorem <span>ipsum</span></p>';\n            var editor = this.newMediumEditor('.editor'),\n                targetNode = editor.elements[0].querySelector('span').firstChild;\n            spyOn(document, 'execCommand').and.callThrough();\n            placeCursorInsideElement(targetNode, targetNode.textContent.length - 1);\n            fireEvent(targetNode, 'keyup', {\n                keyCode: MediumEditor.util.keyCode.ENTER\n            });\n            expect(document.execCommand).toHaveBeenCalledWith('formatBlock', false, 'p');\n        });\n\n        it('should not call formatBlock when inside an anchor', function () {\n            var html = '<p>lorem <a href=\"#\">ipsum</a></p>';\n            this.el.innerHTML = html;\n            var editor = this.newMediumEditor('.editor'),\n                targetNode = editor.elements[0].querySelector('a').firstChild;\n            spyOn(document, 'execCommand').and.callThrough();\n            placeCursorInsideElement(targetNode, targetNode.textContent.length - 1);\n            fireEvent(targetNode, 'keyup', {\n                keyCode: MediumEditor.util.keyCode.ENETER\n            });\n            expect(document.execCommand).not.toHaveBeenCalled();\n            expect(this.el.innerHTML).toBe(html);\n        });\n\n        // https://github.com/yabwe/medium-editor/issues/834\n        it('should not call formatBlock when inside a figCaption', function () {\n            var html = '<figure><img src=\"../demo/img/medium-editor.jpg\"><figcaption>ipsum</figcaption></figure>';\n            this.el.innerHTML = html;\n            var editor = this.newMediumEditor('.editor'),\n                targetNode = editor.elements[0].querySelector('figCaption').firstChild;\n            spyOn(document, 'execCommand').and.callThrough();\n            placeCursorInsideElement(targetNode, targetNode.textContent.length - 1);\n            fireEvent(targetNode, 'keyup', {\n                keyCode: MediumEditor.util.keyCode.ENETER\n            });\n            expect(document.execCommand).not.toHaveBeenCalled();\n            expect(this.el.innerHTML).toBe(html);\n        });\n\n        it('should not call formatBlock when inside a header', function () {\n            var html = '<h1>lorem ipsum</h1>';\n            this.el.innerHTML = html;\n            var editor = this.newMediumEditor('.editor'),\n                targetNode = editor.elements[0].querySelector('h1').firstChild;\n            spyOn(document, 'execCommand').and.callThrough();\n            placeCursorInsideElement(targetNode, targetNode.textContent.length - 1);\n            fireEvent(targetNode, 'keyup', {\n                keyCode: MediumEditor.util.keyCode.ENETER\n            });\n            expect(document.execCommand).not.toHaveBeenCalled();\n            expect(this.el.innerHTML).toBe(html);\n        });\n\n        it('with ctrl key, should not call formatBlock', function () {\n            this.el.innerHTML = '<p>lorem ipsum</p>';\n\n            var editor = this.newMediumEditor('.editor'),\n                p = editor.elements[0].querySelector('p');\n\n            spyOn(document, 'execCommand').and.callThrough();\n\n            placeCursorInsideElement(p, 0);\n\n            fireEvent(p, 'keyup', {\n                keyCode: MediumEditor.util.keyCode.ENTER,\n                ctrlKey: true\n            });\n\n            expect(document.execCommand).not.toHaveBeenCalledWith('formatBlock', false, 'p');\n        });\n\n        it('inside an anchor the anchors should be unlinked', function () {\n            this.el.innerHTML = '<a href=\"#\">test</a>';\n            var editor = this.newMediumEditor('.editor'),\n                target = editor.elements[0].querySelector('a');\n            spyOn(document, 'execCommand').and.callThrough();\n            placeCursorInsideElement(target, 1);\n            fireEvent(target, 'keyup', {\n                keyCode: MediumEditor.util.keyCode.ENTER\n            });\n            expect(document.execCommand).toHaveBeenCalledWith('unlink', false, null);\n        });\n\n        describe('within a blockquote element', function () {\n\n            it('at the end of the blockquote, p tag should be created next, not blockquote', function () {\n                this.el.innerHTML = '<blockquote>lorem ipsum</blockquote>';\n                var editor = this.newMediumEditor('.editor'),\n                    target = editor.elements[0].querySelector('blockquote').firstChild;\n\n                placeCursorInsideElement(target, target.textContent.length);\n                fireEvent(target, 'keydown', {\n                    keyCode: MediumEditor.util.keyCode.ENTER\n                });\n\n                expect(this.el.innerHTML).toBe('<blockquote>lorem ipsum</blockquote><p><br></p>');\n            });\n\n            it('NOT at the start of the blockqoute, no formatting should be changed', function () {\n                this.el.innerHTML = '<blockquote>lorem ipsum</blockquote>';\n                var editor = this.newMediumEditor('.editor'),\n                    target = editor.elements[0].querySelector('blockquote').firstChild;\n\n                placeCursorInsideElement(target, 0);\n                fireEvent(target, 'keydown', {\n                    keyCode: MediumEditor.util.keyCode.ENTER\n                });\n\n                expect(this.el.innerHTML).toBe('<blockquote>lorem ipsum</blockquote>');\n            });\n        });\n    });\n\n    describe('when the ctrl key and m key is pressed', function () {\n        it('should prevent new lines from being inserted when disableReturn options is true', function () {\n            this.el.innerHTML = 'lorem ipsum';\n\n            var editor = this.newMediumEditor('.editor', { disableReturn: true }),\n                evt;\n\n            placeCursorInsideElement(editor.elements[0], 0);\n\n            evt = prepareEvent(editor.elements[0], 'keydown', {\n                ctrlKey: true,\n                keyCode: MediumEditor.util.keyCode.M\n            });\n\n            spyOn(evt, 'preventDefault').and.callThrough();\n\n            firePreparedEvent(evt, editor.elements[0], 'keydown');\n\n            expect(evt.preventDefault).toHaveBeenCalled();\n        });\n\n        it('should prevent new lines from being inserted when data-disable-return is defined', function () {\n            this.el.innerHTML = 'lorem ipsum';\n            this.el.setAttribute('data-disable-return', true);\n\n            var editor = this.newMediumEditor('.editor'),\n                evt;\n\n            placeCursorInsideElement(editor.elements[0], 0);\n\n            evt = prepareEvent(editor.elements[0], 'keydown', {\n                ctrlKey: true,\n                keyCode: MediumEditor.util.keyCode.M\n            });\n\n            spyOn(evt, 'preventDefault').and.callThrough();\n\n            firePreparedEvent(evt, editor.elements[0], 'keydown');\n\n            expect(evt.preventDefault).toHaveBeenCalled();\n        });\n\n        it('should prevent consecutive new lines from being inserted when disableDoubleReturn is true', function () {\n            this.el.innerHTML = '<p><br></p>';\n            var editor = this.newMediumEditor('.editor', { disableDoubleReturn: true }),\n                p = editor.elements[0].querySelector('p'),\n                evt;\n\n            placeCursorInsideElement(p, 0);\n\n            evt = prepareEvent(p, 'keydown', {\n                ctrlKey: true,\n                keyCode: MediumEditor.util.keyCode.M\n            });\n\n            spyOn(evt, 'preventDefault').and.callThrough();\n\n            firePreparedEvent(evt, p, 'keydown');\n\n            expect(evt.preventDefault).toHaveBeenCalled();\n        });\n\n        it('should prevent consecutive new lines from being inserted when data-disable-double-return is defined', function () {\n            this.el.innerHTML = '<p><br></p>';\n            this.el.setAttribute('data-disable-double-return', true);\n\n            var editor = this.newMediumEditor('.editor'),\n                p = editor.elements[0].querySelector('p'),\n                evt;\n\n            placeCursorInsideElement(p, 0);\n\n            evt = prepareEvent(p, 'keydown', {\n                ctrlKey: true,\n                keyCode: MediumEditor.util.keyCode.M\n            });\n\n            spyOn(evt, 'preventDefault').and.callThrough();\n\n            firePreparedEvent(evt, p, 'keydown');\n\n            expect(evt.preventDefault).toHaveBeenCalled();\n        });\n\n        it('should prevent consecutive new lines from being inserted inside a sentence when disableDoubleReturn is true', function () {\n            this.el.innerHTML = '<p>hello</p><p><br></p><p>word</p>';\n            var editor = this.newMediumEditor('.editor', { disableDoubleReturn: true }),\n                p = editor.elements[0].getElementsByTagName('p')[2],\n                evt;\n\n            placeCursorInsideElement(p, 0);\n\n            evt = prepareEvent(p, 'keydown', {\n                ctrlKey: true,\n                keyCode: MediumEditor.util.keyCode.M\n            });\n\n            spyOn(evt, 'preventDefault').and.callThrough();\n\n            firePreparedEvent(evt, p, 'keydown');\n\n            expect(evt.preventDefault).toHaveBeenCalled();\n        });\n\n        it('should add a new line when selected node is an h2/h3 tag with text in it and when disableDoubleReturn is true', function () {\n            this.el.innerHTML = '<p>lorem</p><h2>ipsum<br></h2>';\n            var editor = this.newMediumEditor('.editor', { disableDoubleReturn: true }),\n                p = editor.elements[0].getElementsByTagName('h2')[0],\n                evt;\n\n            placeCursorInsideElement(p, 0);\n\n            evt = prepareEvent(p, 'keydown', {\n                keyCode: MediumEditor.util.keyCode.ENTER\n            });\n\n            spyOn(evt, 'preventDefault').and.callThrough();\n\n            firePreparedEvent(evt, p, 'keydown');\n\n            expect(evt.preventDefault).not.toHaveBeenCalled();\n        });\n\n        it('should prevent new line being added when selected node is an empty h2/h3 tag and when disableDoubleReturn is true', function () {\n            this.el.innerHTML = '<p>lorem</p><h2><br></h2>';\n            var editor = this.newMediumEditor('.editor', { disableDoubleReturn: true }),\n                p = editor.elements[0].getElementsByTagName('h2')[0],\n                evt;\n\n            placeCursorInsideElement(p, 1);\n\n            evt = prepareEvent(p, 'keydown', {\n                keyCode: MediumEditor.util.keyCode.ENTER\n            });\n\n            spyOn(evt, 'preventDefault').and.callThrough();\n\n            firePreparedEvent(evt, p, 'keydown');\n\n            expect(evt.preventDefault).toHaveBeenCalled();\n            expect(this.el.innerHTML).toBe('<p>lorem</p><h2><br></h2>');\n        });\n    });\n\n    describe('when backspace is pressed', function () {\n\n        describe('within a blockquote element', function () {\n\n            it('at the start of the blockquote, the blockquote tag should be replaced with a p tag', function () {\n                this.el.innerHTML = '<blockquote>lorem ipsum</blockquote>';\n                var editor = this.newMediumEditor('.editor'),\n                    target = editor.elements[0].querySelector('blockquote');\n\n                placeCursorInsideElement(target, 0);\n                fireEvent(target, 'keydown', {\n                    keyCode: MediumEditor.util.keyCode.BACKSPACE\n                });\n                expect(this.el.innerHTML).toBe('<p>lorem ipsum</p>');\n            });\n\n            it('NOT at the start of the blockqoute, no formatting should be changed', function () {\n                this.el.innerHTML = '<blockquote>lorem ipsum</blockquote>';\n                var editor = this.newMediumEditor('.editor'),\n                    target = editor.elements[0].querySelector('blockquote');\n\n                placeCursorInsideElement(target, 1);\n                fireEvent(target, 'keydown', {\n                    keyCode: MediumEditor.util.keyCode.BACKSPACE\n                });\n                expect(this.el.innerHTML).toBe('<blockquote>lorem ipsum</blockquote>');\n            });\n        });\n\n        describe('within an empty first list item', function () {\n            it('should insert a paragraph before the list if it is the first element in the editor', function () {\n                this.el.innerHTML = '<ul><li></li><li>lorem ipsum</li></ul>';\n                var editor = this.newMediumEditor('.editor'),\n                    target = editor.elements[0].querySelector('li'),\n                    range;\n                placeCursorInsideElement(target, 0);\n                fireEvent(target, 'keydown', {\n                    keyCode: MediumEditor.util.keyCode.BACKSPACE\n                });\n                expect(this.el.innerHTML).toBe('<p><br></p><ul><li>lorem ipsum</li></ul>');\n                range = document.getSelection().getRangeAt(0);\n                expect(range.commonAncestorContainer.nodeName.toLowerCase()).toBe('p');\n            });\n\n            it('should not insert a paragraph before the list if it is NOT the first element in the editor', function () {\n                this.el.innerHTML = '<p>lorem ipsum</p><ul><li></li><li>lorem ipsum</li></ul>';\n                var editor = this.newMediumEditor('.editor'),\n                    target = editor.elements[0].querySelector('li');\n                placeCursorInsideElement(target, 0);\n                fireEvent(target, 'keydown', {\n                    keyCode: MediumEditor.util.keyCode.BACKSPACE\n                });\n                expect(this.el.innerHTML).toMatch(/^<p>lorem ipsum<\\/p><ul><li>(<br>)?<\\/li><li>lorem ipsum<\\/li><\\/ul>$/);\n            });\n        });\n\n        describe('within an empty paragraph which is the first element of the editor', function () {\n            it('should delete the paragraph and place the caret to the next paragraph', function () {\n                this.el.innerHTML = '<p class=\"\"><br></p><p>test</p>';\n                var editor = this.newMediumEditor('.editor'),\n                    target = editor.elements[0].querySelector('p'),\n                    range;\n                placeCursorInsideElement(target, 0);\n                fireEvent(target, 'keydown', {\n                    keyCode: MediumEditor.util.keyCode.BACKSPACE\n                });\n                expect(this.el.innerHTML).toBe('<p>test</p>');\n                range = document.getSelection().getRangeAt(0);\n                expect(range.commonAncestorContainer.textContent).toBe('test');\n            });\n        });\n    });\n\n    describe('with header tags', function () {\n        it('should insert a breaking paragraph before header when hitting enter key at front of header', function () {\n            this.el.innerHTML = '<h2>lorem</h2><h3>ipsum</h3>';\n            var editor = this.newMediumEditor('.editor'),\n                targetNode = editor.elements[0].querySelector('h3');\n            placeCursorInsideElement(targetNode, 0);\n            fireEvent(targetNode, 'keydown', {\n                keyCode: MediumEditor.util.keyCode.ENTER\n            });\n            expect(this.el.innerHTML).toBe('<h2>lorem</h2><p><br></p><h3>ipsum</h3>');\n        });\n\n        it('should remove empty element if hitting delete key inside empty element before a header tag', function () {\n            this.el.innerHTML = '<h2>lorem</h2><p><br></p><h3>ipsum</h3>';\n            var editor = this.newMediumEditor('.editor'),\n                targetNode = editor.elements[0].querySelector('p');\n            selectElementContents(targetNode);\n            fireEvent(targetNode, 'keydown', {\n                keyCode: MediumEditor.util.keyCode.DELETE\n            });\n            expect(this.el.innerHTML).toBe('<h2>lorem</h2><h3>ipsum</h3>');\n        });\n\n        it('should not create a <p> tag when hitting enter', function () {\n            this.el.innerHTML = '<h2>lorem ipsum</h2>';\n            var editor = this.newMediumEditor('.editor'),\n                targetNode = editor.elements[0].querySelector('h2');\n            spyOn(document, 'execCommand').and.callThrough();\n            placeCursorInsideElement(targetNode, 0);\n            fireEvent(targetNode, 'keyup', {\n                keyCode: MediumEditor.util.keyCode.ENTER\n            });\n            expect(document.execCommand).not.toHaveBeenCalledWith('formatBlock', false, 'p');\n            expect(this.el.innerHTML).toBe('<h2>lorem ipsum</h2>');\n        });\n    });\n\n    it('should call formatBlock when a keyup results in an empty element', function () {\n        this.el.innerHTML = ' ';\n        var editor = this.newMediumEditor('.editor'),\n            target = editor.elements[0].firstChild;\n        spyOn(document, 'execCommand').and.callThrough();\n        selectElementContents(target);\n        target.parentNode.removeChild(target);\n        fireEvent(editor.elements[0], 'keyup', {\n            keyCode: MediumEditor.util.keyCode.BACKSPACE\n        });\n        expect(document.execCommand).toHaveBeenCalledWith('formatBlock', false, 'p');\n        // Webkit inserts a <p> tag, firefox & ie do not\n        expect(this.el.innerHTML).toMatch(/(<p><br><\\/p>)?/);\n    });\n\n    // https://github.com/yabwe/medium-editor/issues/994\n    it('should not throw an error when keyup occurs within a non-div editor', function () {\n        var origEC = document.execCommand,\n            errorCount = 0;\n        // Wrap document.execCommand so we can detect browser errors when it's called\n        document.execCommand = function () {\n            try {\n                origEC.apply(document, arguments);\n            } catch (err) {\n                errorCount++;\n                throw err;\n            }\n        };\n\n        this.el.parentNode.removeChild(this.el);\n        this.el = this.createElement('h1', 'editor', 'Lorem ipsum dolor sit amet');\n\n        var editor = this.newMediumEditor('h1.editor');\n        editor.elements[0].focus();\n        selectElementContentsAndFire(editor.elements[0]);\n        jasmine.clock().tick(1);\n        fireEvent(editor.elements[0], 'keyup', {\n            keyCode: MediumEditor.util.keyCode.M\n        });\n\n        // Restore original document.execCommand\n        document.execCommand = origEC;\n        expect(errorCount).toBe(0, 'there was an error thrown when calling document.execCommand()');\n    });\n\n    describe('spellcheck', function () {\n        it('should have spellcheck attribute set to true by default', function () {\n            var editor = this.newMediumEditor('.editor');\n            expect(editor.elements[0].getAttribute('spellcheck')).toBe('true');\n        });\n\n        it('should accept spellcheck as an options', function () {\n            var editor = this.newMediumEditor('.editor', { spellcheck: false });\n            expect(editor.elements[0].getAttribute('spellcheck')).toBe('false');\n        });\n    });\n\n    describe('justify actions', function () {\n        it('should not replace line breaks inside header elements with div elements', function () {\n            this.el.innerHTML = '<h2>lorem ipsum<br />lorem ipsum<br />lorem ipsum<br /></h2><ul><li>item 1</li><li>item 2</li><li>item 3</li></ul>';\n            var editor = this.newMediumEditor('.editor'),\n                h2 = this.el.querySelector('h2');\n            selectElementContentsAndFire(h2.firstChild);\n            editor.execAction('justifyRight');\n            h2 = this.el.querySelector('h2');\n            expect(h2.querySelectorAll('br').length).toBe(3, 'Some of the <br> elements have been removed from the <h2>');\n            expect(h2.querySelectorAll('div').length).toBe(0, 'Some <br> elements were replaced with <div> elements within the <h2>');\n        });\n\n        it('should not replace line breaks inside blockquote elements with div elements', function () {\n            this.el.innerHTML = '<ul><li>item 1</li><li>item 2</li></ul><blockquote>lorem ipsum<br />lorem ipsum<br />lorem ipsum<br /></blockquote><ul><li>item 1</li><li>item 2</li><li>item 3</li></ul>';\n            var editor = this.newMediumEditor('.editor'),\n                blockquote = this.el.querySelector('blockquote');\n            selectElementContentsAndFire(blockquote);\n            editor.execAction('justifyCenter');\n            blockquote = this.el.querySelector('blockquote');\n            // Edge adds another <br /> automatically for some reason...\n            expect(blockquote.querySelectorAll('br').length).toBeGreaterThan(2, 'Some of the <br> elements have been removed from the <blockquote>');\n            expect(blockquote.querySelectorAll('div').length).toBe(0, 'Some <br> elements were replaced with <div> elements within the <blckquote>');\n        });\n\n        it('should not replace line breaks inside pre elements with div elements', function () {\n            this.el.innerHTML = '<ul><li>item 1</li><li>item 2</li></ul><pre>lorem ipsum<br />lorem ipsum<br />lorem ipsum<br /></pre><ul><li>item 1</li><li>item 2</li><li>item 3</li></ul>';\n            var editor = this.newMediumEditor('.editor'),\n                pre = this.el.querySelector('pre');\n            selectElementContentsAndFire(pre);\n            editor.execAction('justifyCenter');\n            pre = this.el.querySelector('pre');\n            expect(pre.querySelectorAll('br').length).toBe(3, 'Some of the <br> elements have been removed from the <pre>');\n            expect(pre.querySelectorAll('div').length).toBe(0, 'Some <br> elements were replaced with <div> elements within the <pre>');\n        });\n\n        it('should not replace line breaks inside p elements with div elements', function () {\n            this.el.innerHTML = '<ul><li>item 1</li><li>item 2</li></ul><p>lorem ipsum<br />lorem ipsum<br />lorem ipsum<br /></p><ul><li>item 1</li><li>item 2</li><li>item 3</li></ul>';\n            var editor = this.newMediumEditor('.editor'),\n                para = this.el.querySelector('p');\n            selectElementContentsAndFire(para);\n            editor.execAction('justifyCenter');\n            para = this.el.querySelector('p');\n            expect(para.querySelectorAll('br').length).toBe(3, 'Some of the <br> elements have been removed from the <p>');\n            expect(para.querySelectorAll('div').length).toBe(0, 'Some <br> elements were replaced with <div> elements within the <p>');\n        });\n    });\n\n    describe('when list element is unlisted', function () {\n        it('should fix markup when one list element is unlisted', function () {\n            this.el.innerHTML = '<ul><li>lorem</li><li>ipsum</li><li>dolor</li></ul>';\n            var editor = this.newMediumEditor('.editor', {\n                    toolbar: {\n                        buttons: ['unorderedlist']\n                    }\n                }),\n                target = editor.elements[0].querySelector('li'),\n                toolbar = editor.getExtensionByName('toolbar');\n\n            selectElementContentsAndFire(target);\n            fireEvent(toolbar.getToolbarElement().querySelector('[data-action=\"insertunorderedlist\"]'), 'click');\n\n            if (getEdgeVersion() > 0) {\n                // Edge wraps elements in div\n                expect(this.el.innerHTML).toBe('<div>lorem</div><ul><li>ipsum</li><li>dolor</li></ul>');\n            } else {\n                // Other browsers should wrap them in p\n                expect(this.el.innerHTML).toBe('<p>lorem</p><ul><li>ipsum</li><li>dolor</li></ul>');\n            }\n        });\n\n        it('should fix markup when miltiple list elements are unlisted', function () {\n            this.el.innerHTML = '<ol><li>lorem</li><li>ipsum</li><li>dolor</li></ol>';\n            var editor = this.newMediumEditor('.editor', {\n                    toolbar: {\n                        buttons: ['orderedlist']\n                    }\n                }),\n                toolbar = editor.getExtensionByName('toolbar'),\n                selection = document.getSelection(),\n                range = document.createRange();\n\n            range.setStart(this.el.querySelectorAll('li')[0].firstChild, 0);\n            range.setEnd(this.el.querySelectorAll('li')[1].firstChild, 5);\n            selection.removeAllRanges();\n            selection.addRange(range);\n\n            fireEvent(toolbar.getToolbarElement().querySelector('[data-action=\"insertorderedlist\"]'), 'click');\n            if (getEdgeVersion() > 0) {\n                // Edge wraps elements in div\n                expect(this.el.innerHTML).toBe('<div>lorem</div><div>ipsum</div><ol><li>dolor</li></ol>');\n            } else {\n                // Other browsers should wrap them in p\n                expect(this.el.innerHTML).toBe('<p>lorem</p><p>ipsum</p><ol><li>dolor</li></ol>');\n            }\n        });\n\n        it('should fix markup when all list elements are unlisted', function () {\n            this.el.innerHTML = '<ul><li>lorem</li><li>ipsum</li><li>dolor</li></ul>';\n            var editor = this.newMediumEditor('.editor', {\n                    toolbar: {\n                        buttons: ['unorderedlist']\n                    }\n                }),\n                target = editor.elements[0].querySelector('ul'),\n                toolbar = editor.getExtensionByName('toolbar');\n\n            selectElementContentsAndFire(target);\n            fireEvent(toolbar.getToolbarElement().querySelector('[data-action=\"insertunorderedlist\"]'), 'click');\n            if (getEdgeVersion() > 0) {\n                // Edge wraps elements in div\n                expect(this.el.innerHTML).toBe('<div>lorem</div><div>ipsum</div><div>dolor</div>');\n            } else {\n                // Other browsers should wrap them in p\n                expect(this.el.innerHTML).toBe('<p>lorem</p><p>ipsum</p><p>dolor</p>');\n            }\n        });\n    });\n});\n"
  },
  {
    "path": "spec/core-api.spec.js",
    "content": "/*global fireEvent, selectElementContents,\n  selectElementContentsAndFire */\n\ndescribe('Core-API', function () {\n    'use strict';\n\n    beforeEach(function () {\n        setupTestHelpers.call(this);\n        this.el = this.createElement('div', 'editor', 'lore ipsum');\n    });\n\n    afterEach(function () {\n        this.cleanupTest();\n    });\n\n    describe('getFocusedElement', function () {\n        it('should return the element which currently has a data-medium-focused attribute', function () {\n            var elementOne = this.createElement('div', 'editor', 'lorem ipsum'),\n                elementTwo = this.createElement('div', 'editor', 'lorem ipsum');\n            elementTwo.setAttribute('data-medium-focused', true);\n\n            var editor = this.newMediumEditor('.editor'),\n                focused = editor.getFocusedElement();\n            expect(focused).not.toBe(elementOne);\n            expect(focused).toBe(elementTwo);\n        });\n\n        it('should return the element focused via call to selectElement', function () {\n            var elementOne = this.createElement('div', 'editor', 'lorem ipsum'),\n                elementTwo = this.createElement('div', 'editor', 'lorem ipsum');\n            elementTwo.setAttribute('data-medium-focused', true);\n\n            var editor = this.newMediumEditor('.editor');\n\n            editor.selectElement(elementOne.firstChild);\n            var focused = editor.getFocusedElement();\n            expect(focused).toBe(elementOne);\n        });\n    });\n\n    describe('setContent', function () {\n        it('should set the content of the editor\\'s element', function () {\n            var newHTML = 'Lorem ipsum dolor',\n                otherHTML = 'something different',\n                elementOne = this.createElement('div', 'editor', 'lorem ipsum'),\n                editor = this.newMediumEditor('.editor');\n\n            editor.setContent(newHTML);\n            expect(this.el.innerHTML).toEqual(newHTML);\n            expect(elementOne.innerHTML).not.toEqual(newHTML);\n\n            editor.setContent(otherHTML, 1);\n            expect(elementOne.innerHTML).toEqual(otherHTML);\n            expect(this.el.innerHTML).not.toEqual(otherHTML);\n        });\n    });\n\n    describe('getContent', function () {\n        it('should retrieve the content of the first element', function () {\n            var editor = this.newMediumEditor('.editor');\n            expect(editor.getContent()).toEqual('lore ipsum');\n        });\n\n        it('should retrieve the content of the element at the specified index', function () {\n            var otherHTML = 'something different';\n            this.createElement('div', 'editor', otherHTML);\n            var editor = this.newMediumEditor('.editor');\n            expect(editor.getContent(1)).toEqual(otherHTML);\n        });\n\n        it('should return null if no element exists', function () {\n            var editor = this.newMediumEditor('.no-valid-selector');\n            expect(editor.getContent()).toBeNull();\n        });\n    });\n\n    describe('resetContent', function () {\n        it('should reset the content of all editor elements to their initial values', function () {\n            var initialOne = this.el.innerHTML,\n                initialTwo = 'Lorem ipsum dolor',\n                elementTwo = this.createElement('div', 'editor', initialTwo),\n                editor = this.newMediumEditor('.editor');\n\n            editor.setContent('<p>changed content</p>');\n            expect(this.el.innerHTML).not.toEqual(initialOne);\n            editor.setContent('<p>changed content</p>', 1);\n            expect(elementTwo.innerHTML).not.toEqual(initialTwo);\n\n            editor.resetContent();\n\n            expect(this.el.innerHTML).toEqual(initialOne);\n            expect(elementTwo.innerHTML).toEqual(initialTwo);\n        });\n\n        it('should reset the content of a specific element when provided', function () {\n            var initialOne = this.el.innerHTML,\n                initialTwo = 'Lorem ipsum dolor',\n                elementTwo = this.createElement('div', 'editor', initialTwo),\n                editor = this.newMediumEditor('.editor');\n\n            editor.setContent('<p>changed content</p>');\n            expect(this.el.innerHTML).not.toEqual(initialOne);\n            editor.setContent('<p>changed content</p>', 1);\n            expect(elementTwo.innerHTML).not.toEqual(initialTwo);\n\n            editor.resetContent(elementTwo);\n\n            expect(this.el.innerHTML).not.toEqual(initialOne);\n            expect(elementTwo.innerHTML).toEqual(initialTwo);\n        });\n\n        it('should not reset anything if an invalid element is provided', function () {\n            var initialOne = this.el.innerHTML,\n                initialTwo = 'Lorem ipsum dolor',\n                elementTwo = this.createElement('div', 'editor', initialTwo),\n                dummyElement = this.createElement('div', 'not-editor', '<p>dummy element</p>'),\n                editor = this.newMediumEditor('.editor');\n\n            editor.setContent('<p>changed content</p>');\n            expect(this.el.innerHTML).not.toEqual(initialOne);\n            editor.setContent('<p>changed content</p>', 1);\n            expect(elementTwo.innerHTML).not.toEqual(initialTwo);\n\n            editor.resetContent(dummyElement);\n\n            expect(this.el.innerHTML).not.toEqual(initialOne);\n            expect(elementTwo.innerHTML).not.toEqual(initialTwo);\n        });\n    });\n\n    describe('saveSelection/restoreSelection', function () {\n        it('should be applicable if html changes but text does not', function () {\n            this.el.innerHTML = 'lorem <i>ipsum</i> dolor';\n\n            var editor = this.newMediumEditor('.editor', {\n                    toolbar: {\n                        buttons: ['italic', 'underline', 'strikethrough']\n                    }\n                }),\n                toolbar = editor.getExtensionByName('toolbar'),\n                button,\n                // Beacuse not all browsers use <strike> or <s>, check for both\n                sTagO = '<(s|strike)>',\n                sTagC = '</(s|strike)>',\n                // Edge breaks this into 3 separate <u> tags for some reason...\n                regex = new RegExp([\n                    '^<u>lorem ',\n                    '(<i>' + sTagO + '|' + sTagO + '<i>|</u><i><u>' + sTagO + ')',\n                    'ipsum',\n                    '(</i>' + sTagC + '|' + sTagC + '</i>|' + sTagC + '</u></i><u>)',\n                    ' dolor</u>$'\n                ].join(''));\n\n            // Save selection around <i> tag\n            selectElementContents(editor.elements[0].querySelector('i'));\n            editor.saveSelection();\n\n            // Underline entire element\n            selectElementContents(editor.elements[0]);\n            button = toolbar.getToolbarElement().querySelector('[data-action=\"underline\"]');\n            fireEvent(button, 'click');\n\n            // Restore selection back to <i> tag and add a <s> tag\n            editor.restoreSelection();\n            button = toolbar.getToolbarElement().querySelector('[data-action=\"strikethrough\"]');\n            fireEvent(button, 'click');\n            expect(editor.elements[0].innerHTML).toMatch(regex);\n        });\n    });\n\n    describe('exportSelection', function () {\n        it('should have an index in the exported selection when it is in the second contenteditable', function () {\n            this.createElement('div', 'editor', 'lorem <i>ipsum</i> dolor');\n            var editor = this.newMediumEditor('.editor', {\n                toolbar: {\n                    buttons: ['italic', 'underline', 'strikethrough']\n                }\n            });\n\n            selectElementContents(editor.elements[1].querySelector('i'));\n            var exportedSelection = editor.exportSelection();\n            expect(Object.keys(exportedSelection).sort()).toEqual(['editableElementIndex', 'end', 'start']);\n            expect(exportedSelection.editableElementIndex).toEqual(1);\n        });\n    });\n\n    describe('execAction', function () {\n        it('should pass opt directly to document.execCommand', function () {\n            // In order to safely spy on document.execCommand we need to disable functionality\n            // which overrides document.execCommand in IE & Edge\n            var origSupported = MediumEditor.Events.prototype.InputEventOnContenteditableSupported;\n            MediumEditor.Events.prototype.InputEventOnContenteditableSupported = true;\n\n            spyOn(document, 'execCommand').and.callThrough();\n            var editor = this.newMediumEditor('.editor');\n\n            editor.execAction('foreColor', { value: 'red' });\n            expect(document.execCommand).toHaveBeenCalledWith('foreColor', false, 'red');\n            MediumEditor.Events.prototype.InputEventOnContenteditableSupported = origSupported;\n        });\n\n        it('fontName support old style', function () {\n            // In order to safely spy on document.execCommand we need to disable functionality\n            // which overrides document.execCommand in IE & Edge\n            var origSupported = MediumEditor.Events.prototype.InputEventOnContenteditableSupported;\n            MediumEditor.Events.prototype.InputEventOnContenteditableSupported = true;\n\n            spyOn(document, 'execCommand').and.callThrough();\n            var editor = this.newMediumEditor('.editor');\n\n            editor.execAction('fontName', { name: 'Tahoma' });\n            expect(document.execCommand).toHaveBeenCalledWith('fontName', false, 'Tahoma');\n            MediumEditor.Events.prototype.InputEventOnContenteditableSupported = origSupported;\n        });\n\n        it('fontName support new stle', function () {\n            // In order to safely spy on document.execCommand we need to disable functionality\n            // which overrides document.execCommand in IE & Edge\n            var origSupported = MediumEditor.Events.prototype.InputEventOnContenteditableSupported;\n            MediumEditor.Events.prototype.InputEventOnContenteditableSupported = true;\n\n            spyOn(document, 'execCommand').and.callThrough();\n            var editor = this.newMediumEditor('.editor');\n\n            editor.execAction('fontName', { value: 'Tahoma' });\n            expect(document.execCommand).toHaveBeenCalledWith('fontName', false, 'Tahoma');\n            MediumEditor.Events.prototype.InputEventOnContenteditableSupported = origSupported;\n        });\n\n        it('fontSize support old style', function () {\n            // In order to safely spy on document.execCommand we need to disable functionality\n            // which overrides document.execCommand in IE & Edge\n            var origSupported = MediumEditor.Events.prototype.InputEventOnContenteditableSupported;\n            MediumEditor.Events.prototype.InputEventOnContenteditableSupported = true;\n\n            spyOn(document, 'execCommand').and.callThrough();\n            var editor = this.newMediumEditor('.editor');\n\n            editor.execAction('fontSize', { size: 14 });\n            expect(document.execCommand).toHaveBeenCalledWith('fontSize', false, 14);\n            MediumEditor.Events.prototype.InputEventOnContenteditableSupported = origSupported;\n        });\n\n        it('fontSize support new stle', function () {\n            // In order to safely spy on document.execCommand we need to disable functionality\n            // which overrides document.execCommand in IE & Edge\n            var origSupported = MediumEditor.Events.prototype.InputEventOnContenteditableSupported;\n            MediumEditor.Events.prototype.InputEventOnContenteditableSupported = true;\n\n            spyOn(document, 'execCommand').and.callThrough();\n            var editor = this.newMediumEditor('.editor');\n\n            editor.execAction('fontSize', { value: 14 });\n            expect(document.execCommand).toHaveBeenCalledWith('fontSize', false, 14);\n            MediumEditor.Events.prototype.InputEventOnContenteditableSupported = origSupported;\n        });\n\n        it('createLink support old style', function () {\n            // In order to safely spy on document.execCommand we need to disable functionality\n            // which overrides document.execCommand in IE & Edge\n            var origSupported = MediumEditor.Events.prototype.InputEventOnContenteditableSupported;\n            MediumEditor.Events.prototype.InputEventOnContenteditableSupported = true;\n\n            spyOn(document, 'execCommand').and.callThrough();\n            var editor = this.newMediumEditor('.editor');\n\n            selectElementContentsAndFire(editor.elements[0].firstChild);\n            jasmine.clock().tick(1);\n\n            editor.execAction('createLink', { url: 'http://www.test.com' });\n            expect(document.execCommand).toHaveBeenCalledWith('createLink', false, 'http://www.test.com');\n            MediumEditor.Events.prototype.InputEventOnContenteditableSupported = origSupported;\n        });\n\n        it('createLink support new style', function () {\n            // In order to safely spy on document.execCommand we need to disable functionality\n            // which overrides document.execCommand in IE & Edge\n            var origSupported = MediumEditor.Events.prototype.InputEventOnContenteditableSupported;\n            MediumEditor.Events.prototype.InputEventOnContenteditableSupported = true;\n\n            spyOn(document, 'execCommand').and.callThrough();\n            var editor = this.newMediumEditor('.editor');\n\n            selectElementContentsAndFire(editor.elements[0].firstChild);\n            jasmine.clock().tick(1);\n\n            editor.execAction('createLink', { value: 'http://www.test.com' });\n            expect(document.execCommand).toHaveBeenCalledWith('createLink', false, 'http://www.test.com');\n            MediumEditor.Events.prototype.InputEventOnContenteditableSupported = origSupported;\n        });\n    });\n\n    describe('checkContentChanged', function () {\n        it('should trigger editableInput when called after the html has changed', function () {\n            var editor = this.newMediumEditor('.editor', {\n                    toolbar: {\n                        buttons: ['italic', 'underline', 'strikethrough']\n                    }\n                }),\n                spy = jasmine.createSpy('handler');\n\n            editor.subscribe('editableInput', spy);\n            expect(spy).not.toHaveBeenCalled();\n\n            selectElementContentsAndFire(this.el.firstChild);\n            jasmine.clock().tick(1);\n\n            this.el.innerHTML = 'lorem ipsum';\n            expect(spy).not.toHaveBeenCalled();\n\n            var obj = { target: this.el, currentTarget: this.el };\n            editor.checkContentChanged();\n            expect(spy).toHaveBeenCalledWith(obj, this.el);\n        });\n\n        it('should not trigger editableInput when called after the html has not changed', function () {\n            var editor = this.newMediumEditor('.editor', {\n                    toolbar: {\n                        buttons: ['italic', 'underline', 'strikethrough']\n                    }\n                }),\n                spy = jasmine.createSpy('handler');\n\n            editor.subscribe('editableInput', spy);\n            expect(spy).not.toHaveBeenCalled();\n\n            selectElementContentsAndFire(this.el.firstChild);\n            jasmine.clock().tick(1);\n\n            this.el.innerHTML = 'lore ipsum';\n            expect(spy).not.toHaveBeenCalled();\n\n            editor.checkContentChanged();\n            expect(spy).not.toHaveBeenCalled();\n        });\n    });\n\n    describe('getEditorFromElement', function () {\n        it('should return the editor instance the element belongs to', function () {\n            var elTwo = this.createElement('div', 'editor-two', 'lore ipsum'),\n                editorOne = this.newMediumEditor('.editor'),\n                editorTwo = this.newMediumEditor('.editor-two');\n            expect(editorOne.elements[0]).toBe(this.el);\n            expect(editorTwo.elements[0]).toBe(elTwo);\n\n            expect(MediumEditor.getEditorFromElement(this.el)).toBe(editorOne);\n            expect(MediumEditor.getEditorFromElement(elTwo)).toBe(editorTwo);\n        });\n\n        it('should return null if the element is not within an editor', function () {\n            expect(MediumEditor.getEditorFromElement(this.el)).toBeNull();\n        });\n    });\n});\n"
  },
  {
    "path": "spec/delay.spec.js",
    "content": "describe('Delay TestCase', function () {\n    'use strict';\n\n    beforeEach(function () {\n        setupTestHelpers.call(this);\n        this.el = this.createElement('div', 'editor', 'lore ipsum');\n    });\n\n    afterEach(function () {\n        this.cleanupTest();\n    });\n\n    it('should call function after delay', function () {\n        var editor, spy;\n\n        editor = this.newMediumEditor('.editor', { delay: 100 });\n        spy = jasmine.createSpy('spy');\n        editor.delay(spy);\n        jasmine.clock().tick(50);\n        expect(spy).not.toHaveBeenCalled();\n        jasmine.clock().tick(150);\n        expect(spy).toHaveBeenCalled();\n    });\n    it('should not call function if editor not active', function () {\n        var editor, spy;\n\n        editor = this.newMediumEditor('.editor', { delay: 1 });\n        spy = jasmine.createSpy('spy');\n\n        editor.destroy();\n        editor.delay(spy);\n        jasmine.clock().tick(100);\n        expect(spy).not.toHaveBeenCalled();\n    });\n});\n"
  },
  {
    "path": "spec/drag-and-drop.spec.js",
    "content": "/*global fireEvent */\n\ndescribe('Drag and Drop TestCase', function () {\n    'use strict';\n\n    beforeEach(function () {\n        setupTestHelpers.call(this);\n        this.el = this.createElement('div', 'editor', 'lore ipsum');\n    });\n\n    afterEach(function () {\n        this.cleanupTest();\n    });\n\n    describe('drag', function () {\n        it('should add medium-editor-dragover class', function () {\n            var editor = this.newMediumEditor(this.el);\n            fireEvent(editor.elements[0], 'dragover');\n            expect(editor.elements[0].className).toContain('medium-editor-dragover');\n        });\n\n        it('should add medium-editor-dragover class even when data is invalid', function () {\n            var editor = this.newMediumEditor(this.el, {\n                imageDragging: false\n            });\n            fireEvent(editor.elements[0], 'dragover');\n            expect(editor.elements[0].className).toContain('medium-editor-dragover');\n        });\n\n        it('should remove medium-editor-dragover class on drag leave', function () {\n            var editor = this.newMediumEditor(this.el);\n            fireEvent(editor.elements[0], 'dragover');\n            expect(editor.elements[0].className).toContain('medium-editor-dragover');\n            fireEvent(editor.elements[0], 'dragleave');\n            expect(editor.elements[0].className).not.toContain('medium-editor-dragover');\n        });\n    });\n\n    describe('drop', function () {\n        var eventListener;\n\n        beforeEach(function () {\n            eventListener = jasmine.createSpy();\n\n            // File API just doesn't work in IE9, so only verify this functionality if it's not IE9\n            if (typeof FileReader === 'function') {\n                // Spy on the FileReader and use the spy for any added event listeners\n                spyOn(window, 'FileReader').and.returnValue({\n                    addEventListener: eventListener,\n                    readAsDataURL: function () {\n                    }\n                });\n            }\n            // Spy to ensure that image is inserted\n            spyOn(MediumEditor.util, 'insertHTMLCommand').and.callThrough();\n        });\n\n        it('should remove medium-editor-dragover class and add the image to the editor content', function () {\n            var editor = this.newMediumEditor(this.el),\n                editableInputListener = jasmine.createSpy();\n\n            editor.subscribe('editableInput', editableInputListener);\n            expect(editableInputListener).not.toHaveBeenCalled();\n\n            fireEvent(editor.elements[0], 'dragover');\n            expect(editor.elements[0].className).toContain('medium-editor-dragover');\n            fireEvent(editor.elements[0], 'drop');\n            expect(editor.elements[0].className).not.toContain('medium-editor-dragover');\n\n            // File API just doesn't work in IE9, so only verify this functionality if it's not IE9\n            if (typeof FileReader === 'function') {\n                // Ensure that the load event is bound to the FileReader\n                expect(eventListener.calls.mostRecent().args[0]).toEqual('load');\n                // Pass into the event handler our dummy image source\n                eventListener.calls.mostRecent().args[1]({\n                    target: {\n                        result: 'data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7'\n                    }\n                });\n\n                // Expect that the image is inserted\n                expect(MediumEditor.util.insertHTMLCommand).toHaveBeenCalled();\n                // Expect that the editableInput event is fired\n                expect(editableInputListener).toHaveBeenCalled();\n            }\n        });\n\n        it('should remove medium-editor-dragover class and NOT add the image to the editor content', function () {\n            var editor = this.newMediumEditor(this.el, { imageDragging: false }),\n                editableInputListener = jasmine.createSpy();\n\n            editor.subscribe('editableInput', editableInputListener);\n            fireEvent(editor.elements[0], 'dragover');\n            expect(editor.elements[0].className).toContain('medium-editor-dragover');\n            fireEvent(editor.elements[0], 'drop');\n            expect(editor.elements[0].className).not.toContain('medium-editor-dragover');\n\n            //The following ensures that MediumEditor.Extension.insertImageFile is not called:\n            // 1. Ensure that a load event is not bound to the FileReader\n            expect(eventListener.calls.mostRecent()).toEqual(undefined);\n            // 2. Expect that the image is not inserted\n            expect(MediumEditor.util.insertHTMLCommand).not.toHaveBeenCalled();\n            // 3. Expect that the editableInput event is not fired\n            expect(editableInputListener).not.toHaveBeenCalled();\n        });\n    });\n});\n"
  },
  {
    "path": "spec/dyn-elements.spec.js",
    "content": "/*global fireEvent */\n\ndescribe('MediumEditor.DynamicElements TestCase', function () {\n    'use strict';\n\n    beforeEach(function () {\n        setupTestHelpers.call(this);\n        this.el = this.createElement('div', 'editor', 'lore ipsum');\n        this.addOne = this.createElement('div', 'add-one', 'lore ipsum dolor');\n        this.addTwo = this.createElement('div', 'add-two', 'lore ipsum dollar');\n    });\n\n    afterEach(function () {\n        this.cleanupTest();\n    });\n\n    describe('addElements', function () {\n        it('should initialize dom element properly when adding dynamically', function () {\n            var editor = this.newMediumEditor('.editor'),\n            focusedEditable,\n            blurredEditable,\n            focusListener = function (event, editable) {\n                focusedEditable = editable;\n            },\n            blurListener = function (event, editable) {\n                blurredEditable = editable;\n            };\n            editor.subscribe('focus', focusListener);\n            editor.subscribe('blur', blurListener);\n\n            editor.addElements(this.addOne);\n            expect(this.addOne.getAttribute('data-medium-editor-element')).toBeDefined();\n            expect(editor.elements.length).toBe(2);\n\n            editor.addElements(this.addOne);\n            expect(editor.elements.length).toBe(2);\n\n            editor.selectElement(this.addOne.firstChild);\n            expect(focusedEditable).toBe(this.addOne);\n            expect(blurredEditable).toBeUndefined();\n\n            fireEvent(document.body, 'mousedown');\n            fireEvent(document.body, 'mouseup');\n            fireEvent(document.body, 'click');\n            expect(blurredEditable).toBe(this.addOne);\n        });\n\n        it('should attach all event handlers to an element when adding an element', function () {\n            var listenerWrapper = function () {\n                this.listenerInfo.push([arguments[0], arguments[1], arguments[2]]);\n                this._addEventListner.apply(this, arguments);\n            };\n\n            this.el._addEventListner = this.el.addEventListener;\n            this.el.listenerInfo = [];\n            this.el.addEventListener = listenerWrapper.bind(this.el);\n            this.addOne._addEventListner = this.addOne.addEventListener;\n            this.addOne.listenerInfo = [];\n            this.addOne.addEventListener = listenerWrapper.bind(this.addOne);\n\n            // Instatiating editor will trigger adding event handlers to each element\n            expect(this.el.listenerInfo.length).toBe(0);\n            var editor = this.newMediumEditor('.editor', { anchorPreview: false });\n            expect(this.el.listenerInfo.length).not.toBe(0);\n            var listenerCount = this.el.listenerInfo.length;\n            editor.subscribe('editableBlur', function blurHandler () { });\n            expect(this.el.listenerInfo.length).toBe(listenerCount + 1);\n\n            // When adding a new element, all handlers should also be added to that element\n            expect(this.addOne.listenerInfo.length).toBe(0);\n            editor.addElements(this.addOne);\n            expect(this.addOne.listenerInfo.length).toBe(this.el.listenerInfo.length);\n\n            // When attaching a new handler, the handler should be added to dynamically added elements too\n            editor.subscribe('editableMouseover', function mouseoverHandler () {});\n            expect(this.el.listenerInfo.length).toBe(listenerCount + 2);\n            expect(this.addOne.listenerInfo.length).toBe(listenerCount + 2);\n\n            // Check that the same handlers have been added to each element\n            this.el.listenerInfo.forEach(function (elListener) {\n                var found = this.addOne.listenerInfo.some(function (addOneListener) {\n                    return elListener[0] === addOneListener[0] && elListener[0].name === addOneListener[0].name;\n                });\n                expect(found).toBe(true);\n            }, this);\n        });\n\n        it('should accept a selector to specify elements to add', function () {\n            var editor = this.newMediumEditor('.editor');\n            expect(editor.elements.length).toBe(1);\n            editor.addElements('.add-one');\n            expect(editor.elements.length).toBe(2);\n        });\n\n        it('should accept a NodeList to specify elements to add', function () {\n            var editor = this.newMediumEditor('.editor');\n            expect(editor.elements.length).toBe(1);\n            editor.addElements(document.getElementsByClassName('add-one'));\n            expect(editor.elements.length).toBe(2);\n        });\n\n        it('should not add an element that is already an editor element', function () {\n            var editor = this.newMediumEditor('.editor');\n            expect(editor.elements.length).toBe(1);\n            editor.addElements('.editor');\n            expect(editor.elements.length).toBe(1);\n        });\n\n        it('should attach editableKeydownEnter to the editor when adding an element with a data-disable-return attribute', function () {\n            var editor = this.newMediumEditor('.editor');\n            expect(editor.events.customEvents['editableKeydownEnter'].length).toBe(1);\n\n            this.addOne.setAttribute('data-disable-return', true);\n            editor.addElements(this.addOne);\n            expect(editor.events.customEvents['editableKeydownEnter'].length).toBe(2, 'editableKeydownEnter should be subscribed to when adding a data-disbale-return element');\n        });\n\n        it('should trigger addElement custom event for each element', function () {\n            var editor = this.newMediumEditor('.editor'),\n                spy = jasmine.createSpy('handler');\n\n            editor.subscribe('addElement', spy);\n            editor.addElements('.add-one');\n            expect(spy).toHaveBeenCalledWith({ target: this.addOne, currentTarget: this.addOne }, this.addOne);\n\n            editor.addElements(document.getElementsByClassName('add-two'));\n            expect(spy).toHaveBeenCalledWith({ target: this.addTwo, currentTarget: this.addTwo }, this.addTwo);\n        });\n\n        function runAddTest(inputSupported) {\n            it('should re-attach element properly when removed from dom, cleaned up and injected to dom again', function () {\n                var originalInputSupport = MediumEditor.Events.prototype.InputEventOnContenteditableSupported;\n                MediumEditor.Events.prototype.InputEventOnContenteditableSupported = inputSupported;\n\n                var editor = this.newMediumEditor('.editor'),\n                focusedEditable,\n                firedTarget,\n                firedCounter,\n                handler = function (event, editable) {\n                    firedTarget = editable;\n                    firedCounter++;\n                },\n                focusListener = function (event, editable) {\n                    focusedEditable = editable;\n                };\n\n                firedCounter = 0;\n\n                editor.subscribe('focus', focusListener);\n                editor.subscribe('editableInput', handler);\n\n                editor.addElements(this.addOne);\n                expect(this.addOne.getAttribute('data-medium-editor-element')).toBeDefined();\n                expect(editor.elements.length).toBe(2);\n\n                // Detach + exec fn + reattach, asynchronous.\n                detach(this.addOne, true, function (reattach) {\n                    editor.removeElements(this.addOne);\n                    expect(editor.elements.length).toBe(1);\n\n                    reattach();\n\n                    editor.addElements(this.addTwo);\n                    expect(editor.elements.length).toBe(2);\n                    expect(this.addTwo.getAttribute('data-medium-editor-element')).toBeDefined();\n\n                    editor.selectElement(this.addTwo.firstChild);\n                    expect(focusedEditable).toBe(this.addTwo);\n\n                    editor.selectElement(this.addTwo.firstChild);\n                    this.addTwo.textContent = 'lore ipsum!';\n\n                    // trigger onInput\n                    fireEvent(this.addTwo, 'input');\n\n                    // trigger faked 'selectionchange' event\n                    fireEvent(document, 'selectionchange', { target: document, currentTarget: this.addTwo });\n\n                    jasmine.clock().tick(1);\n                    expect(firedTarget).toBe(this.addTwo);\n                    expect(firedCounter).toBe(1);\n\n                    MediumEditor.Events.prototype.InputEventOnContenteditableSupported = originalInputSupport;\n                }.bind(this));\n            });\n        }\n\n        runAddTest(true);\n        runAddTest(false);\n    });\n\n    describe('removeElements', function () {\n        it('should accept specific elements to remove', function () {\n            var editor = this.newMediumEditor('.editor, .add-one');\n            expect(editor.elements.indexOf(this.addOne)).not.toBe(-1);\n            expect(editor.elements.indexOf(this.el)).not.toBe(-1);\n            editor.removeElements(this.addOne);\n            expect(editor.elements.indexOf(this.addOne)).toBe(-1);\n            expect(editor.elements.indexOf(this.el)).not.toBe(-1);\n            editor.removeElements(this.el);\n            expect(editor.elements.indexOf(this.el)).toBe(-1);\n        });\n\n        it('should accept a selector to specify elements to remove', function () {\n            var editor = this.newMediumEditor('.editor, .add-one');\n            expect(editor.elements.length).toBe(2);\n            editor.removeElements('.editor');\n            expect(editor.elements.length).toBe(1);\n            editor.removeElements('.add-one');\n            expect(editor.elements.length).toBe(0);\n        });\n\n        it('should accept a NodeList to specify elements to remove', function () {\n            var editor = this.newMediumEditor('.editor, .add-one');\n            expect(editor.elements.length).toBe(2);\n            editor.removeElements(document.getElementsByClassName('editor'));\n            expect(editor.elements.length).toBe(1);\n            editor.removeElements(document.getElementsByClassName('add-one'));\n            expect(editor.elements.length).toBe(0);\n        });\n\n        it('should detach all event handlers from an element', function () {\n            var attached = [],\n                origAdd = this.el.addEventListener,\n                origRemove = this.el.removeEventListener;\n\n            this.el.removeEventListener = function () {\n                var args = arguments;\n                attached = attached.filter(function (props) {\n                    if (props[0] === args[0] && props[1] === args[1] && props[2] === args[2]) {\n                        return false;\n                    }\n                    return true;\n                });\n                origRemove.apply(this, arguments);\n            }.bind(this.el);\n            this.el.addEventListener = function () {\n                attached.push([arguments[0], arguments[1], arguments[2]]);\n                origAdd.apply(this, arguments);\n            }.bind(this.el);\n\n            // Instatiating editor will trigger adding event handlers to each element\n            var editor = this.newMediumEditor('.editor, .add-one');\n            expect(attached.length).not.toBe(0);\n\n            // Removing should make calls to remove each individual event handler\n            editor.removeElements(this.el);\n            expect(attached.length).toBe(0);\n        });\n\n        it('should trigger removeElement custom event for each element', function () {\n            var editor = this.newMediumEditor('.editor, .add-one, .add-two'),\n                spy = jasmine.createSpy('handler');\n\n            editor.subscribe('removeElement', spy);\n            editor.removeElements('.add-one');\n            expect(spy).toHaveBeenCalledWith({ target: this.addOne, currentTarget: this.addOne }, this.addOne);\n\n            editor.removeElements(document.getElementsByClassName('add-two'));\n            expect(spy).toHaveBeenCalledWith({ target: this.addTwo, currentTarget: this.addTwo }, this.addTwo);\n        });\n    });\n});\n\nfunction detach(node, async, fn) {\n    var parent = node.parentNode,\n        next = node.nextSibling;\n    // No parent node? Abort!\n    if (!parent) {\n        return;\n    }\n    // Detach node from DOM.\n    parent.removeChild(node);\n    // Handle case where optional `async` argument is omitted.\n    if (typeof async !== 'boolean') {\n        fn = async;\n        async = false;\n    }\n    // Note that if a function wasn't passed, the node won't be re-attached!\n    if (fn && async) {\n        // If async == true, reattach must be called manually.\n        fn.call(node, reattach);\n    } else if (fn) {\n        // If async != true, reattach will happen automatically.\n        fn.call(node);\n        reattach();\n    }\n    // Re-attach node to DOM.\n    function reattach() {\n        parent.insertBefore(node, next);\n    }\n}\n"
  },
  {
    "path": "spec/elements.spec.js",
    "content": "describe('Elements TestCase', function () {\n    'use strict';\n\n    beforeEach(function () {\n        setupTestHelpers.call(this);\n        this.el = this.createElement('div', 'editor', 'lore ipsum');\n    });\n\n    afterEach(function () {\n        this.cleanupTest();\n    });\n\n    describe('Initialization', function () {\n        it('should set element contenteditable attribute to true', function () {\n            var editor = this.newMediumEditor('.editor');\n            expect(editor.elements.length).toBe(1);\n            expect(this.el.getAttribute('contenteditable')).toEqual('true');\n        });\n\n        it('should not set element contenteditable when disableEditing is true', function () {\n            var editor = this.newMediumEditor('.editor', {\n                disableEditing: true\n            });\n            expect(editor.elements.length).toBe(1);\n            expect(this.el.getAttribute('contenteditable')).toBeFalsy();\n        });\n\n        it('should not set element contenteditable when data-disable-editing is true', function () {\n            this.el.setAttribute('data-disable-editing', true);\n            var editor = this.newMediumEditor('.editor');\n            expect(editor.elements.length).toBe(1);\n            expect(this.el.getAttribute('contenteditable')).toBeFalsy();\n        });\n\n        it('should set element data attr medium-editor-element to true and add medium-editor-element class', function () {\n            var editor = this.newMediumEditor('.editor');\n            expect(editor.elements.length).toBe(1);\n            expect(this.el.getAttribute('data-medium-editor-element')).toEqual('true');\n            expect(this.el.className).toBe('editor medium-editor-element');\n        });\n\n        it('should set element role attribute to textbox', function () {\n            var editor = this.newMediumEditor('.editor');\n            expect(editor.elements.length).toBe(1);\n            expect(this.el.getAttribute('role')).toEqual('textbox');\n        });\n\n        it('should set element aria multiline attribute to true', function () {\n            var editor = this.newMediumEditor('.editor');\n            expect(editor.elements.length).toBe(1);\n            expect(this.el.getAttribute('aria-multiline')).toEqual('true');\n        });\n\n        it('should set the data-medium-editor-editor-index attribute to be the id of the editor instance', function () {\n            var editor = this.newMediumEditor('.editor');\n            expect(editor.elements[0]).toBe(this.el);\n            expect(parseInt(this.el.getAttribute('data-medium-editor-editor-index'))).toBe(editor.id);\n        });\n    });\n\n    describe('Destroy', function () {\n        it('should remove the contenteditable attribute', function () {\n            var editor = this.newMediumEditor('.editor');\n            expect(this.el.getAttribute('contenteditable')).toEqual('true');\n            editor.destroy();\n            expect(this.el.hasAttribute('contenteditable')).toBe(false);\n        });\n\n        it('should remove the medium-editor-element attribute and class name', function () {\n            this.el.classList.add('temp-class');\n            expect(this.el.className).toBe('editor temp-class');\n            var editor = this.newMediumEditor('.editor');\n            expect(this.el.getAttribute('data-medium-editor-element')).toEqual('true');\n            expect(this.el.className).toBe('editor temp-class medium-editor-element');\n            editor.destroy();\n            expect(this.el.hasAttribute('data-medium-editor-element')).toBe(false);\n            expect(this.el.className).toBe('editor temp-class');\n        });\n\n        it('should remove the role attribute', function () {\n            var editor = this.newMediumEditor('.editor');\n            expect(this.el.getAttribute('role')).toEqual('textbox');\n            editor.destroy();\n            expect(this.el.hasAttribute('role')).toBe(false);\n        });\n\n        it('should remove the aria-multiline attribute', function () {\n            var editor = this.newMediumEditor('.editor');\n            expect(this.el.getAttribute('aria-multiline')).toEqual('true');\n            editor.destroy();\n            expect(this.el.hasAttribute('aria-multiline')).toBe(false);\n        });\n\n        it('should remove the data-medium-editor-editor-index attribute', function () {\n            var editor = this.newMediumEditor('.editor');\n            expect(parseInt(this.el.getAttribute('data-medium-editor-editor-index'))).toBe(editor.id);\n            editor.destroy();\n            expect(this.el.hasAttribute('data-medium-editor-editor-index')).toBe(false);\n        });\n    });\n});\n"
  },
  {
    "path": "spec/events.spec.js",
    "content": "/*global fireEvent, selectElementContents,\n         selectElementContentsAndFire */\n\ndescribe('MediumEditor.Events TestCase', function () {\n    'use strict';\n\n    beforeEach(function () {\n        setupTestHelpers.call(this);\n        this.el = this.createElement('div', 'editor', 'lore ipsum');\n    });\n\n    afterEach(function () {\n        this.cleanupTest();\n    });\n\n    describe('On', function () {\n        it('should bind listener', function () {\n            var el, editor, spy;\n            el = this.createElement('div');\n            spy = jasmine.createSpy('handler');\n            editor = this.newMediumEditor('.editor');\n            editor.on(el, 'click', spy);\n            fireEvent(el, 'click');\n            jasmine.clock().tick(1);\n            expect(spy).toHaveBeenCalled();\n        });\n\n        it('should bind listener even to list of elements', function () {\n            var el1, el2, elements, editor, spy;\n            el1 = this.createElement('div');\n            el1.classList.add('test-element');\n            el2 = this.createElement('div');\n            el2.classList.add('test-element');\n            elements = document.getElementsByClassName('test-element');\n            spy = jasmine.createSpy('handler');\n            editor = this.newMediumEditor('.editor');\n            editor.on(elements, 'click', spy);\n            fireEvent(el1, 'click');\n            jasmine.clock().tick(1);\n            expect(spy).toHaveBeenCalled();\n        });\n    });\n\n    describe('Off', function () {\n        it('should unbind listener', function () {\n            var el, editor, spy;\n            el = this.createElement('div');\n            spy = jasmine.createSpy('handler');\n            editor = this.newMediumEditor('.editor');\n            editor.on(el, 'click', spy);\n            editor.off(el, 'click', spy);\n            fireEvent(el, 'click');\n            jasmine.clock().tick(1);\n            expect(spy).not.toHaveBeenCalled();\n        });\n\n        it('should unbind listener even from list of elements', function () {\n            var el1, el2, elements, editor, spy;\n            el1 = this.createElement('div');\n            el1.classList.add('test-element');\n            el2 = this.createElement('div');\n            el2.classList.add('test-element');\n            elements = document.getElementsByClassName('test-element');\n            spy = jasmine.createSpy('handler');\n            editor = this.newMediumEditor('.editor');\n            editor.on(elements, 'click', spy);\n            editor.off(elements, 'click', spy);\n            fireEvent(el1, 'click');\n            jasmine.clock().tick(1);\n            expect(spy).not.toHaveBeenCalled();\n        });\n    });\n\n    describe('Custom Events', function () {\n        it('should be attachable and triggerable if they are not built-in events', function () {\n            var editor = this.newMediumEditor('.editor'),\n                spy = jasmine.createSpy('handler'),\n                tempData = { temp: 'data' };\n            editor.subscribe('myIncredibleEvent', spy);\n            expect(spy).not.toHaveBeenCalled();\n            editor.trigger('myIncredibleEvent', tempData, editor.elements[0]);\n            expect(spy).toHaveBeenCalledWith(tempData, editor.elements[0]);\n        });\n\n        it('can be disabled for a temporary period of time on a named basis', function () {\n            var editor = this.newMediumEditor('.editor'),\n                spy = jasmine.createSpy('handler'),\n                tempData = { temp: 'data' };\n            editor.subscribe('myIncredibleEvent', spy);\n            expect(spy).not.toHaveBeenCalled();\n            editor.events.disableCustomEvent('myIncredibleEvent');\n            editor.trigger('myIncredibleEvent', tempData, editor.elements[0]);\n            expect(spy).not.toHaveBeenCalled();\n            editor.events.enableCustomEvent('myIncredibleEvent');\n            editor.trigger('myIncredibleEvent', tempData, editor.elements[0]);\n            expect(spy).toHaveBeenCalledWith(tempData, editor.elements[0]);\n        });\n    });\n\n    describe('Custom Focus/Blur Listener', function () {\n        it('should be called and passed the editable element when the editable gets focus', function () {\n            var editor = this.newMediumEditor('.editor'),\n                focusedEditable,\n                blurredEditable,\n                focusListener = function (event, editable) {\n                    focusedEditable = editable;\n                },\n                blurListener = function (event, editable) {\n                    blurredEditable = editable;\n                };\n            editor.subscribe('focus', focusListener);\n            editor.subscribe('blur', blurListener);\n\n            editor.selectElement(this.el.firstChild);\n            expect(focusedEditable).toBe(this.el);\n            expect(blurredEditable).toBeUndefined();\n\n            fireEvent(document.body, 'mousedown');\n            fireEvent(document.body, 'mouseup');\n            fireEvent(document.body, 'click');\n            expect(blurredEditable).toBe(this.el);\n        });\n\n        it('should not trigger after detaching', function () {\n            var focusSpy = jasmine.createSpy('handler'),\n                blurSpy = jasmine.createSpy('handler'),\n                editor = this.newMediumEditor('.editor');\n            editor.subscribe('focus', focusSpy);\n            editor.subscribe('blur', blurSpy);\n\n            editor.selectElement(this.el.firstChild);\n            expect(focusSpy.calls.count()).toBe(1);\n            expect(blurSpy).not.toHaveBeenCalled();\n\n            fireEvent(document.body, 'click');\n            expect(blurSpy).toHaveBeenCalled();\n\n            editor.unsubscribe('focus', focusSpy);\n            editor.selectElement(this.el.firstChild);\n            expect(focusSpy.calls.count()).toBe(1);\n\n            editor.unsubscribe('blur', blurSpy);\n            fireEvent(document.body, 'click');\n            expect(blurSpy.calls.count()).toBe(1);\n        });\n\n        it('should not be called after destroying editor', function () {\n            var editor = this.newMediumEditor('.editor'),\n                focusSpy = jasmine.createSpy('handler'),\n                blurSpy = jasmine.createSpy('handler');\n            editor.subscribe('focus', focusSpy);\n            editor.subscribe('blur', blurSpy);\n\n            this.el.focus();\n            fireEvent(this.el, 'focus');\n            expect(focusSpy.calls.count()).toBe(1);\n            expect(blurSpy).not.toHaveBeenCalled();\n\n            fireEvent(document.body, 'click');\n            expect(blurSpy).toHaveBeenCalled();\n\n            editor.destroy();\n\n            this.el.focus();\n            fireEvent(this.el, 'focus');\n            expect(focusSpy.calls.count()).toBe(1);\n\n            fireEvent(document.body, 'click');\n            expect(blurSpy.calls.count()).toBe(1);\n        });\n    });\n\n    describe('ExecCommand Listener', function () {\n        it('should only wrap document.execCommand when required', function () {\n            var origExecCommand = document.execCommand;\n            this.newMediumEditor('.editor', {\n                placeholder: false\n            });\n            expect(document.execCommand).toBe(origExecCommand);\n        });\n\n        it('should wrap document.execCommand with a custom method', function () {\n            spyOn(document, 'execCommand').and.callThrough();\n            var origExecCommand = document.execCommand,\n                mockInstance = {\n                    options: { ownerDocument: document }\n                },\n                events = new MediumEditor.Events(mockInstance),\n                handler = spyOn(events, 'handleDocumentExecCommand');\n\n            events.attachToExecCommand();\n            expect(document.execCommand).not.toBe(origExecCommand);\n            expect(document.execCommand.listeners.length).toBe(1);\n\n            // Creating a real contenteditable to select to keep firefox happy during tests\n            var tempEl = this.createElement('div', '', 'firefox is lame');\n            tempEl.setAttribute('contenteditable', true);\n            selectElementContents(tempEl);\n            document.execCommand('bold', false, null);\n\n            expect(handler).toHaveBeenCalled();\n            expect(origExecCommand).toHaveBeenCalledWith('bold', false, null);\n        });\n\n        it('should notify all listeners when execCommand is called', function () {\n            spyOn(document, 'execCommand').and.callThrough();\n            var origExecCommand = document.execCommand,\n                mockInstance = {\n                    options: { ownerDocument: document }\n                },\n                eventsOne = new MediumEditor.Events(mockInstance),\n                eventsTwo = new MediumEditor.Events(mockInstance),\n                handlerOne = spyOn(eventsOne, 'handleDocumentExecCommand'),\n                handlerTwo = spyOn(eventsTwo, 'handleDocumentExecCommand'),\n                args = ['bold', false, 'something'];\n\n            eventsOne.attachToExecCommand();\n            eventsTwo.attachToExecCommand();\n            expect(document.execCommand).not.toBe(origExecCommand);\n            expect(document.execCommand.listeners.length).toBe(2);\n            expect(handlerOne).not.toHaveBeenCalled();\n            expect(handlerTwo).not.toHaveBeenCalled();\n\n            // Creating a real contenteditable to select to keep firefox happy during tests\n            var tempEl = this.createElement('div', '', 'firefox is lame');\n            tempEl.setAttribute('contenteditable', true);\n            selectElementContents(tempEl);\n            document.execCommand.apply(document, args);\n\n            var expectedObj = {\n                command: 'bold',\n                value: 'something',\n                args: args,\n                result: true\n            };\n\n            expect(handlerOne).toHaveBeenCalledWith(expectedObj);\n            expect(handlerTwo).toHaveBeenCalledWith(expectedObj);\n            expect(origExecCommand).toHaveBeenCalledWith('bold', false, 'something');\n\n            eventsOne.destroy();\n            eventsTwo.destroy();\n        });\n\n        it('should revert back to original execCommand when all listeners are removed', function () {\n            var origExecCommand = document.execCommand,\n                mockInstance = {\n                    options: { ownerDocument: document }\n                },\n                eventsOne = new MediumEditor.Events(mockInstance),\n                eventsTwo = new MediumEditor.Events(mockInstance);\n\n            expect(document.execCommand).toBe(origExecCommand);\n            eventsOne.attachToExecCommand();\n            eventsTwo.attachToExecCommand();\n            expect(document.execCommand).not.toBe(origExecCommand);\n\n            eventsOne.detachExecCommand();\n            expect(document.execCommand).not.toBe(origExecCommand);\n\n            eventsTwo.detachExecCommand();\n            expect(document.execCommand).toBe(origExecCommand);\n\n            eventsOne.destroy();\n            eventsTwo.destroy();\n        });\n\n        it('should wrap and unwrap execCommand when using MediumEditor methods', function () {\n            var originalInputSupport = MediumEditor.Events.prototype.InputEventOnContenteditableSupported;\n            MediumEditor.Events.prototype.InputEventOnContenteditableSupported = false;\n\n            spyOn(document, 'execCommand').and.callThrough();\n            var origExecCommand = document.execCommand,\n                editor = this.newMediumEditor('.editor');\n\n            editor.subscribe('editableInput', function () { });\n            expect(document.execCommand).not.toBe(origExecCommand);\n\n            editor.selectElement(editor.elements[0].firstChild);\n            document.execCommand('bold', null, false);\n            expect(origExecCommand).toHaveBeenCalledWith('bold', null, false);\n\n            editor.destroy();\n            expect(document.execCommand).toBe(origExecCommand);\n\n            MediumEditor.Events.prototype.InputEventOnContenteditableSupported = originalInputSupport;\n        });\n\n        it('should expose a method for calling all listeners manually', function () {\n            var originalInputSupport = MediumEditor.Events.prototype.InputEventOnContenteditableSupported;\n            MediumEditor.Events.prototype.InputEventOnContenteditableSupported = false;\n\n            spyOn(document, 'execCommand').and.callThrough();\n            var origExecCommand = document.execCommand,\n                mockInstance = {\n                    options: { ownerDocument: document }\n                },\n                eventsOne = new MediumEditor.Events(mockInstance),\n                eventsTwo = new MediumEditor.Events(mockInstance),\n                handlerOne = spyOn(eventsOne, 'handleDocumentExecCommand'),\n                handlerTwo = spyOn(eventsTwo, 'handleDocumentExecCommand'),\n                args = ['bold', false, 'something'];\n\n            eventsOne.attachToExecCommand();\n            eventsTwo.attachToExecCommand();\n            expect(document.execCommand).not.toBe(origExecCommand);\n            expect(document.execCommand.listeners.length).toBe(2);\n            expect(document.execCommand.callListeners).not.toBeUndefined();\n            expect(handlerOne).not.toHaveBeenCalled();\n            expect(handlerTwo).not.toHaveBeenCalled();\n\n            document.execCommand.callListeners(args, true);\n\n            var expectedObj = {\n                command: 'bold',\n                value: 'something',\n                args: args,\n                result: true\n            };\n\n            expect(handlerOne).toHaveBeenCalledWith(expectedObj);\n            expect(handlerTwo).toHaveBeenCalledWith(expectedObj);\n            expect(origExecCommand).not.toHaveBeenCalled();\n\n            eventsOne.destroy();\n            eventsTwo.destroy();\n\n            MediumEditor.Events.prototype.InputEventOnContenteditableSupported = originalInputSupport;\n        });\n    });\n\n    describe('Custom EditableInput Listener', function () {\n\n        function runEditableInputTests(inputSupported) {\n            var namePrefix = inputSupported ? 'when Input is supported' : 'when Input is NOT supported';\n\n            it(namePrefix + ' should trigger with the corresponding editor element passed as an argument', function () {\n                var originalInputSupport = MediumEditor.Events.prototype.InputEventOnContenteditableSupported;\n                MediumEditor.Events.prototype.InputEventOnContenteditableSupported = inputSupported;\n\n                var editableTwo = this.createElement('div', 'editor', 'lore ipsum'),\n                    firedTarget,\n                    editor = this.newMediumEditor('.editor'),\n                    handler = function (event, editable) {\n                        firedTarget = editable;\n                    };\n                expect(editor.elements.length).toBe(2);\n\n                editor.subscribe('editableInput', handler);\n                editor.selectElement(editableTwo.firstChild);\n\n                editableTwo.textContent = 'lore ipsum!';\n\n                // trigger onInput\n                fireEvent(editableTwo, 'input');\n\n                // trigger faked 'selectionchange' event\n                fireEvent(document, 'selectionchange', { target: document, currentTarget: editableTwo });\n\n                jasmine.clock().tick(1);\n                expect(firedTarget).toBe(editableTwo);\n\n                MediumEditor.Events.prototype.InputEventOnContenteditableSupported = originalInputSupport;\n            });\n\n            it(namePrefix + ' should only trigger when the content has actually changed', function () {\n                var originalInputSupport = MediumEditor.Events.prototype.InputEventOnContenteditableSupported;\n                MediumEditor.Events.prototype.InputEventOnContenteditableSupported = inputSupported;\n\n                var editableTwo = this.createElement('div', 'editor', 'lore ipsum'),\n                    firedTarget,\n                    editor = this.newMediumEditor('.editor'),\n                    handler = function (event, editable) {\n                        firedTarget = editable;\n                    };\n                expect(editor.elements.length).toBe(2);\n\n                editor.subscribe('editableInput', handler);\n\n                // If content hasn't changed, custom event won't fire\n                fireEvent(editableTwo, 'input');\n                fireEvent(editableTwo, 'keypress');\n                expect(firedTarget).toBeUndefined();\n\n                // Change the content, custom event should fire\n                editableTwo.textContent = 'lore ipsum!';\n                fireEvent(editableTwo, 'input');\n                fireEvent(editableTwo, 'keypress');\n                jasmine.clock().tick(1);\n                expect(firedTarget).toBe(editableTwo);\n\n                MediumEditor.Events.prototype.InputEventOnContenteditableSupported = originalInputSupport;\n            });\n\n            it(namePrefix + ',setContent should fire editableInput when content changes', function () {\n                var originalInputSupport = MediumEditor.Events.prototype.InputEventOnContenteditableSupported;\n                MediumEditor.Events.prototype.InputEventOnContenteditableSupported = inputSupported;\n\n                var newHTML = 'Lorem ipsum dolor',\n                    editor = this.newMediumEditor('.editor'),\n                    spy = jasmine.createSpy('handler');\n\n                editor.subscribe('editableInput', spy);\n                expect(spy).not.toHaveBeenCalled();\n\n                editor.setContent(newHTML, 0);\n                var obj = { target: this.el, currentTarget: this.el };\n                expect(spy).toHaveBeenCalledWith(obj, this.el);\n                MediumEditor.Events.prototype.InputEventOnContenteditableSupported = originalInputSupport;\n            });\n\n            it(namePrefix + ', setContent should not fire editableInput when content doesn\\'t change', function () {\n                var originalInputSupport = MediumEditor.Events.prototype.InputEventOnContenteditableSupported;\n                MediumEditor.Events.prototype.InputEventOnContenteditableSupported = inputSupported;\n\n                var sameHTML = 'lore ipsum',\n                    editor = this.newMediumEditor('.editor'),\n                    spy = jasmine.createSpy('handler');\n\n                editor.subscribe('editableInput', spy);\n                expect(spy).not.toHaveBeenCalled();\n\n                editor.setContent(sameHTML, 0);\n                expect(spy).not.toHaveBeenCalled();\n                MediumEditor.Events.prototype.InputEventOnContenteditableSupported = originalInputSupport;\n            });\n        }\n\n        runEditableInputTests(true);\n        runEditableInputTests(false);\n\n        it('should trigger when bolding text when input event is NOT supported', function () {\n            var originalInputSupport = MediumEditor.Events.prototype.InputEventOnContenteditableSupported;\n            MediumEditor.Events.prototype.InputEventOnContenteditableSupported = false;\n\n            var editableTwo = this.createElement('div', 'editor', 'lore ipsum'),\n                firedTarget,\n                editor = this.newMediumEditor('.editor'),\n                toolbar = editor.getExtensionByName('toolbar'),\n                button = toolbar.getToolbarElement().querySelector('[data-action=\"bold\"]'),\n                handler = function (event, editable) {\n                    firedTarget = editable;\n                };\n            expect(editor.elements.length).toBe(2);\n\n            editor.subscribe('editableInput', handler);\n\n            selectElementContentsAndFire(editableTwo.firstChild);\n            expect(firedTarget).toBeUndefined();\n            fireEvent(button, 'click');\n            jasmine.clock().tick(1);\n\n            expect(firedTarget).toBe(editableTwo);\n\n            MediumEditor.Events.prototype.InputEventOnContenteditableSupported = originalInputSupport;\n        });\n    });\n\n    describe('Setup some listeners', function () {\n        var links = [\n            'externalInteraction',\n            'blur',\n            'focus',\n            'editableInput',\n            'editableClick',\n            'editableBlur',\n            'editableKeypress',\n            'editableKeyup',\n            'editableKeydown',\n            'editableKeydownEnter',\n            'editableKeydownTab',\n            'editableKeydownDelete',\n            'editableMouseover',\n            'editableDrag',\n            'editableDrop',\n            'editablePaste'\n        ];\n\n        links.forEach(function (listener) {\n            it('should setup \"' + listener + '\" listener', function () {\n                var editor = this.newMediumEditor('.editor'),\n                    events = new MediumEditor.Events(editor);\n\n                events.setupListener(listener);\n\n                expect(events.listeners[listener]).toBe(true);\n            });\n        });\n    });\n});\n"
  },
  {
    "path": "spec/exploits.spec.js",
    "content": "describe('Exploits', function () {\n    'use strict';\n\n    beforeEach(function () {\n        setupTestHelpers.call(this);\n        this.el = this.createElement('div', 'editor', 'hhh');\n        this.el.id = 'paste-editor';\n    });\n\n    afterEach(function () {\n        this.cleanupTest();\n    });\n\n    it('Should not execute javascript with disableReturn false', function () {\n        var evt, range,\n            editorEl = this.el,\n            sel = window.getSelection(),\n            editor = this.newMediumEditor('.editor', {\n                delay: 200,\n                disableReturn: false\n            }),\n            pasteHandler = editor.getExtensionByName('paste'),\n            test = {\n                source: 'img onerror handler',\n                paste: '><img src=\"x\" onerror=\"alert(\\'xss\\')\">',\n                output: '<span id=\"editor-inner\">&gt;&lt;img src=\"x\" onerror=\"alert(\\'xss\\')\"&gt;</span>'\n            };\n\n        // mock event with clipboardData API\n        // test requires creating a function, so can't loop or jslint balks\n        evt = {\n            preventDefault: function () {\n                return;\n            },\n            clipboardData: {\n                getData: function () {\n                    // do we need to return different results for the different types? text/plain, text/html\n                    return test.paste;\n                }\n            }\n        };\n\n        editorEl.innerHTML = '<span id=\"editor-inner\">&nbsp</span>';\n\n        range = document.createRange();\n        range.selectNodeContents(editorEl.firstChild);\n        sel.removeAllRanges();\n        sel.addRange(range);\n\n        pasteHandler.handlePaste(evt, editorEl);\n        jasmine.clock().tick(100);\n        expect(editorEl.innerHTML).toEqual(test.output);\n    });\n\n    it('Should not execute javascript with disableReturn true', function () {\n        var evt, range,\n            editorEl = this.el,\n            sel = window.getSelection(),\n            editor = this.newMediumEditor('.editor', {\n                delay: 200,\n                disableReturn: true\n            }),\n            pasteHandler = editor.getExtensionByName('paste'),\n            test = {\n                source: 'img onerror handler',\n                paste: '><img src=\"x\" onerror=\"alert(\\'xss\\')\">',\n                output: '<span id=\"editor-inner\">&gt;&lt;img src=\"x\" onerror=\"alert(\\'xss\\')\"&gt;</span>'\n            };\n\n        // mock event with clipboardData API\n        // test requires creating a function, so can't loop or jslint balks\n        evt = {\n            preventDefault: function () {\n                return;\n            },\n            clipboardData: {\n                getData: function () {\n                    // do we need to return different results for the different types? text/plain, text/html\n                    return test.paste;\n                }\n            }\n        };\n\n        editorEl.innerHTML = '<span id=\"editor-inner\">&nbsp</span>';\n\n        range = document.createRange();\n        range.selectNodeContents(document.getElementById('editor-inner'));\n        sel.removeAllRanges();\n        sel.addRange(range);\n\n        pasteHandler.handlePaste(evt, editorEl);\n        jasmine.clock().tick(100);\n        expect(editorEl.innerHTML).toEqual(test.output);\n    });\n});\n"
  },
  {
    "path": "spec/extension.spec.js",
    "content": "/*global selectElementContentsAndFire, fireEvent */\n\ndescribe('Extensions TestCase', function () {\n    'use strict';\n\n    beforeEach(function () {\n        setupTestHelpers.call(this);\n        this.el = this.createElement('div', 'editor', 'lore ipsum');\n    });\n\n    afterEach(function () {\n        this.cleanupTest();\n    });\n\n    describe('Editor', function () {\n        it('should accept a number of extensions as parameter', function () {\n            var extensions = {\n                    'extension1': {},\n                    'extension2': {}\n                },\n                editor = this.newMediumEditor('.editor', {\n                    extensions: extensions\n                });\n            expect(editor.options.extensions).toBe(extensions);\n        });\n\n        it('should set the base property to an instance of MediumEditor', function () {\n            var extOne = new MediumEditor.Extension(),\n                editor = this.newMediumEditor('.editor', {\n                    extensions: {\n                        'one': extOne\n                    }\n                });\n\n            expect(editor instanceof MediumEditor).toBeTruthy();\n            expect(extOne.base instanceof MediumEditor).toBeTruthy();\n        });\n\n        it('should not override the base or name properties of an extension if overriden', function () {\n            var TempExtension = MediumEditor.Extension.extend({\n                    name: 'tempExtension',\n                    base: 'something'\n                }),\n                editor = this.newMediumEditor('.editor', {\n                    extensions: {\n                        'one': new TempExtension()\n                    }\n                });\n\n            expect(editor.getExtensionByName('one')).toBeUndefined();\n            expect(editor.getExtensionByName('tempExtension').base).toBe('something');\n        });\n\n        it('should set the name of property of extensions', function () {\n            var ExtensionOne = function () {},\n                ExtensionTwo = function () {},\n                extOne = new ExtensionOne(),\n                extTwo = new ExtensionTwo(),\n                editor = this.newMediumEditor('.editor', {\n                    extensions: {\n                        'one': extOne,\n                        'two': extTwo\n                    }\n                });\n\n            expect(extOne.name).toBe('one');\n            expect(extTwo.name).toBe('two');\n\n            expect(editor.getExtensionByName('one')).toBe(extOne);\n            expect(editor.getExtensionByName('two')).toBe(extTwo);\n        });\n\n        it('should set window and document properties on each extension', function () {\n            var TempExtension = MediumEditor.Extension.extend({}),\n                extInstance = new TempExtension(),\n                fakeDocument = {\n                    body: document.body,\n                    documentElement: document.documentElement,\n                    querySelectorAll: function () {\n                        return document.querySelectorAll.apply(document, arguments);\n                    },\n                    createElement: function () {\n                        return document.createElement.apply(document, arguments);\n                    },\n                    execCommand: function () {\n                        return document.execCommand.apply(document, arguments);\n                    },\n                    getElementById: function () {\n                        return document.getElementById.apply(document, arguments);\n                    }\n                },\n                fakeWindow = {\n                    addEventListener: function () {\n                        return window.addEventListener.apply(window, arguments);\n                    },\n                    removeEventListener: function () {\n                        return window.removeEventListener.apply(window, arguments);\n                    }\n                };\n            this.newMediumEditor('.editor', {\n                ownerDocument: fakeDocument,\n                contentWindow: fakeWindow,\n                extensions: {\n                    'temp-extension': extInstance\n                }\n            });\n\n            expect(extInstance.window).toBe(fakeWindow);\n            expect(extInstance.document).toBe(fakeDocument);\n        });\n\n        it('should call destroy on extensions when being destroyed', function () {\n            var TempExtension = MediumEditor.Extension.extend({\n                    destroy: function () {}\n                }),\n                extInstance = new TempExtension();\n            spyOn(extInstance, 'destroy');\n            var editor = this.newMediumEditor('.editor', {\n                    extensions: {\n                        'temp-extension': extInstance\n                    }\n                });\n            editor.destroy();\n            expect(extInstance.destroy).toHaveBeenCalled();\n        });\n    });\n\n    describe('Core Extension', function () {\n        it('exists', function () {\n            expect(MediumEditor.Extension).toBeTruthy();\n        });\n\n        it('provides an .extend method', function () {\n            expect(MediumEditor.Extension.extend).toBeTruthy();\n            var Extended = MediumEditor.Extension.extend({\n                foo: 'bar'\n            });\n\n            expect(Extended.prototype.foo).toBe('bar');\n            expect(Extended.extend).toBe(MediumEditor.Extension.extend);\n        });\n\n        it('can be passed as an extension', function () {\n            var Sub, editor, e1, e2;\n\n            Sub = MediumEditor.Extension.extend({\n                y: 10\n            });\n\n            e1 = new Sub();\n            e2 = new Sub({ y: 20 });\n\n            spyOn(e1, 'init');\n\n            editor = this.newMediumEditor('.editor', {\n                extensions: {\n                    'foo': e1,\n                    'bar': e2\n                }\n            });\n\n            expect(e1.y).toBe(10);\n            expect(e2.y).toBe(20);\n\n            expect(e1.init).toHaveBeenCalledWith();\n            expect(e1.base).toBe(editor);\n        });\n\n        it('should not add default extensions when overriden', function () {\n            var editor,\n                Preview, Placeholder, AutoLink, FileDragging,\n                extPreview, extPlaceholder, extAutoLink, extFileDragging;\n\n            Preview = MediumEditor.Extension.extend({ name: 'anchor-preview' });\n            Placeholder = MediumEditor.Extension.extend({ name: 'placeholder' });\n            AutoLink = MediumEditor.Extension.extend({ name: 'auto-link' });\n            FileDragging = MediumEditor.Extension.extend({ name: 'fileDragging' });\n\n            extPreview = new Preview();\n            extPlaceholder = new Placeholder();\n            extAutoLink = new AutoLink();\n            extFileDragging = new FileDragging();\n\n            editor = this.newMediumEditor('.editor', {\n                extensions: {\n                    'anchor-preview': extPreview,\n                    'placeholder': extPlaceholder,\n                    'auto-link': extAutoLink,\n                    'fileDragging': extFileDragging\n                }\n            });\n\n            expect(editor.getExtensionByName('anchor-preview')).toBe(extPreview);\n            expect(editor.getExtensionByName('placeholder')).toBe(extPlaceholder);\n            expect(editor.getExtensionByName('auto-link')).toBe(extAutoLink);\n            expect(editor.getExtensionByName('fileDragging')).toBe(extFileDragging);\n        });\n\n        it('should call constructor function if it exists', function () {\n            var editor, Sub, SubExtend, e1;\n\n            SubExtend = {\n                name: 'Sub',\n                constructor: function () {\n                    // dummy constructor\n                    return this;\n                }\n            };\n\n            spyOn(SubExtend, 'constructor');\n\n            Sub = MediumEditor.Extension.extend(SubExtend);\n\n            e1 = new Sub();\n\n            editor = this.newMediumEditor('.editor', {\n                extensions: {\n                    'sub': e1\n                }\n            });\n\n            expect(SubExtend.constructor).toHaveBeenCalled();\n        });\n    });\n\n    describe('All extensions', function () {\n        it('should get helper methods to call into base instance methods', function () {\n            var noop = function () {},\n                helpers = {\n                    'on': [document, 'click', noop, false],\n                    'off': [document, 'click', noop, false],\n                    'subscribe': ['editableClick', noop],\n                    'execAction': ['bold'],\n                    'trigger': ['someEvent', noop, this.el]\n                },\n                tempExtension = new MediumEditor.Extension(),\n                editor = this.newMediumEditor('.editor', {\n                    extensions: {\n                        'temp-extension': tempExtension\n                    }\n                });\n\n            Object.keys(helpers).forEach(function (helper) {\n                spyOn(editor, helper);\n            });\n\n            Object.keys(helpers).forEach(function (helper) {\n                tempExtension[helper].apply(tempExtension, helpers[helper]);\n                expect((editor[helper]).calls.count()).toBe(1);\n            });\n        });\n\n        it('should be able to access the editor id via getEditorId()', function () {\n            var tempExtension = new MediumEditor.Extension(),\n                editor = this.newMediumEditor('.editor', {\n                    extensions: {\n                        'temp-extension': tempExtension\n                    }\n                });\n\n            expect(tempExtension.getEditorId()).toBe(editor.id);\n        });\n\n        it('should be able to access elements in this editor via getEditorElements()', function () {\n            var tempExtension = new MediumEditor.Extension(),\n                editor = this.newMediumEditor('.editor', {\n                    extensions: {\n                        'temp-extension': tempExtension\n                    }\n                });\n\n            expect(tempExtension.getEditorElements()).toBe(editor.elements);\n        });\n\n        it('should be able to access editor options via getEditorOption()', function () {\n            var tempExtension = new MediumEditor.Extension(),\n                editor = this.newMediumEditor('.editor', {\n                    disableReturn: true,\n                    extensions: {\n                        'temp-extension': tempExtension\n                    }\n                });\n\n            expect(tempExtension.getEditorOption('disableReturn')).toBe(true);\n            expect(tempExtension.getEditorOption('spellcheck')).toBe(editor.options.spellcheck);\n        });\n\n        it('should be able to prevent blur on the editor when user iteracts with extension elements', function () {\n            var sampleElOne = this.createElement('button', null, 'Test Button'),\n                sampleElTwo = this.createElement('textarea', null, 'Test Div'),\n                externalEl = this.createElement('div', 'external-element', 'External Element'),\n                TempExtension = MediumEditor.Extension.extend({\n                    getInteractionElements: function () {\n                        return [sampleElOne, sampleElTwo];\n                    }\n                }),\n                editor = this.newMediumEditor('.editor', {\n                    extensions: {\n                        'temp-extension': new TempExtension()\n                    }\n                }),\n                spy = jasmine.createSpy('handler');\n\n            selectElementContentsAndFire(editor.elements[0]);\n            jasmine.clock().tick(1);\n            expect(editor.getExtensionByName('toolbar').isDisplayed()).toBe(true);\n\n            editor.subscribe('blur', spy);\n\n            fireEvent(sampleElTwo, 'mousedown');\n            fireEvent(sampleElTwo, 'mouseup');\n            fireEvent(sampleElTwo, 'click');\n            jasmine.clock().tick(51);\n            expect(spy).not.toHaveBeenCalled();\n\n            fireEvent(externalEl, 'mousedown');\n            fireEvent(document.body, 'mouseup');\n            fireEvent(document.body, 'click');\n            expect(spy).toHaveBeenCalled();\n        });\n    });\n\n    describe('Button integration', function () {\n        var ExtensionWithElement = {\n                getButton: function () {\n                    var button = document.createElement('button');\n                    button.className = 'extension-button';\n                    button.innerText = 'XXX';\n                    return button;\n                },\n                checkState: function () {}\n            },\n            ExtensionWithString = {\n                getButton: function () {\n                    return '<button class=\"extension-button\">XXX</button>';\n                }\n            },\n            ExtensionWithNoButton = function () {\n                this.init = function () {};\n            };\n\n        it('should include extensions button into toolbar', function () {\n            var editor = this.newMediumEditor('.editor', {\n                    toolbar: {\n                        buttons: ['dummy']\n                    },\n                    extensions: {\n                        'dummy': ExtensionWithElement\n                    }\n                }),\n                toolbar = editor.getExtensionByName('toolbar');\n            expect(toolbar.getToolbarElement().querySelectorAll('.extension-button').length).toBe(1);\n        });\n\n        it('should call checkState on extensions when toolbar selection updates', function () {\n            var editor = this.newMediumEditor('.editor', {\n                    toolbar: {\n                        buttons: ['dummy']\n                    },\n                    extensions: {\n                        'dummy': ExtensionWithElement\n                    }\n                });\n            selectElementContentsAndFire(editor.elements[0].firstChild, { eventToFire: 'focus' });\n            spyOn(ExtensionWithElement, 'checkState').and.callThrough();\n            editor.checkSelection();\n            jasmine.clock().tick(51);\n            expect(ExtensionWithElement.checkState.calls.count()).toEqual(1);\n            jasmine.clock().uninstall();\n        });\n\n        it('should include extensions button by string into the toolbar', function () {\n            var editor = this.newMediumEditor('.editor', {\n                    toolbar: {\n                        buttons: ['dummy']\n                    },\n                    extensions: {\n                        'dummy': ExtensionWithString\n                    }\n                }),\n                toolbar = editor.getExtensionByName('toolbar');\n            expect(toolbar.getToolbarElement().querySelectorAll('.extension-button').length).toBe(1);\n        });\n\n        it('should not include extensions button into toolbar that are not in \"buttons\"', function () {\n            var editor = this.newMediumEditor('.editor', {\n                    toolbar: {\n                        buttons: ['bold']\n                    },\n                    extensions: {\n                        'dummy': ExtensionWithElement\n                    }\n                }),\n                toolbar = editor.getExtensionByName('toolbar');\n            expect(toolbar.getToolbarElement().querySelectorAll('.extension-button').length).toBe(0);\n        });\n\n        it('should not include buttons into the toolbar when an overriding extension is present', function () {\n            var ext = new ExtensionWithNoButton(),\n                editor;\n\n            spyOn(ext, 'init');\n            editor = this.newMediumEditor('.editor', {\n                toolbar: {\n                    buttons: ['bold', 'italic']\n                },\n                extensions: {\n                    'bold': ext\n                }\n            });\n            var toolbar = editor.getExtensionByName('toolbar');\n\n            expect(toolbar.getToolbarElement().querySelectorAll('button').length).toBe(1);\n            expect(toolbar.getToolbarElement().querySelectorAll('button[data-action=\"italic\"]').length).toBe(1);\n            expect(toolbar.getToolbarElement().querySelectorAll('button[data-action=\"bold\"]').length).toBe(0);\n            expect(ext.init).toHaveBeenCalled();\n        });\n    });\n});\n"
  },
  {
    "path": "spec/fontname.spec.js",
    "content": "/*global fireEvent, selectElementContents,\n         selectElementContentsAndFire */\n\ndescribe('Font Name Button TestCase', function () {\n    'use strict';\n\n    function testFontNameContents(el, name) {\n        expect(el.childNodes.length).toBe(1);\n        var child = el.childNodes[0];\n        expect(child.nodeName.toLowerCase()).toBe('font');\n        expect(child.getAttribute('face')).toBe(name);\n        expect(child.innerHTML).toBe('lorem ipsum');\n    }\n\n    beforeEach(function () {\n        setupTestHelpers.call(this);\n        this.el = this.createElement('div', 'editor', 'lorem ipsum');\n        this.mediumOpts = {\n            toolbar: {\n                buttons: ['fontname']\n            }\n        };\n    });\n\n    afterEach(function () {\n        this.cleanupTest();\n        delete this.mediumOpts;\n    });\n\n    describe('Click', function () {\n        it('should display the font name form when toolbar is visible', function () {\n            spyOn(MediumEditor.extensions.fontName.prototype, 'showForm').and.callThrough();\n            var button,\n                editor = this.newMediumEditor('.editor', this.mediumOpts),\n                fontNameExtension = editor.getExtensionByName('fontname'),\n                toolbar = editor.getExtensionByName('toolbar');\n\n            selectElementContentsAndFire(editor.elements[0]);\n            button = toolbar.getToolbarElement().querySelector('[data-action=\"fontName\"]');\n            fireEvent(button, 'click');\n            expect(toolbar.getToolbarActionsElement().style.display).toBe('none');\n            expect(fontNameExtension.isDisplayed()).toBe(true);\n            expect(fontNameExtension.showForm).toHaveBeenCalled();\n        });\n    });\n\n    describe('Font Name', function () {\n        it('should change font name when select is changed', function () {\n            spyOn(document, 'execCommand').and.callThrough();\n            var editor = this.newMediumEditor('.editor', {\n                    toolbar: {\n                        buttons: ['fontname']\n                    },\n                    buttonLabels: 'fontawesome'\n                }),\n                fontNameExtension = editor.getExtensionByName('fontname'),\n                toolbar = editor.getExtensionByName('toolbar'),\n                button,\n                select;\n\n            selectElementContentsAndFire(editor.elements[0]);\n            button = toolbar.getToolbarElement().querySelector('[data-action=\"fontName\"]');\n            fireEvent(button, 'click');\n\n            select = fontNameExtension.getSelect();\n            select.value = 'Arial';\n            selectElementContents(this.el);\n            fireEvent(select, 'change');\n\n            expect(document.execCommand).toHaveBeenCalledWith('fontName', false, 'Arial');\n\n            fireEvent(fontNameExtension.getForm().querySelector('a.medium-editor-toobar-save'), 'click');\n            testFontNameContents(this.el, 'Arial');\n        });\n\n        it('should display current font name when displayed', function () {\n            spyOn(MediumEditor.extensions.fontName.prototype, 'showForm').and.callThrough();\n            var editor = this.newMediumEditor('.editor', this.mediumOpts),\n                fontNameExtension = editor.getExtensionByName('fontname'),\n                toolbar = editor.getExtensionByName('toolbar');\n            this.el.innerHTML = '<font face=\"Arial\">lorem ipsum dolor</font>';\n            selectElementContentsAndFire(editor.elements[0].firstChild);\n            fireEvent(toolbar.getToolbarElement().querySelector('[data-action=\"fontName\"]'), 'click');\n            expect(fontNameExtension.showForm).toHaveBeenCalledWith('Arial');\n        });\n\n        it('should revert font name when select value is set to empty', function () {\n            spyOn(document, 'execCommand').and.callThrough();\n            spyOn(MediumEditor.extensions.fontName.prototype, 'clearFontName').and.callThrough();\n            var editor = this.newMediumEditor('.editor', this.mediumOpts),\n                fontNameExtension = editor.getExtensionByName('fontname'),\n                toolbar = editor.getExtensionByName('toolbar'),\n                button,\n                select;\n\n            selectElementContentsAndFire(editor.elements[0]);\n            button = toolbar.getToolbarElement().querySelector('[data-action=\"fontName\"]');\n            fireEvent(button, 'click');\n\n            select = fontNameExtension.getSelect();\n            select.value = 'Arial';\n            selectElementContents(editor.elements[0]);\n            fireEvent(select, 'change');\n            expect(document.execCommand).toHaveBeenCalledWith('fontName', false, 'Arial');\n            expect(fontNameExtension.clearFontName).not.toHaveBeenCalled();\n            testFontNameContents(this.el, 'Arial');\n\n            select.value = '';\n            selectElementContents(editor.elements[0]);\n            fireEvent(select, 'change');\n\n            fireEvent(fontNameExtension.getForm().querySelector('a.medium-editor-toobar-save'), 'click');\n            testFontNameContents(this.el, null); // TODO: remove the <font> element entirely instead of just the `size` attribute\n            expect(fontNameExtension.clearFontName).toHaveBeenCalled();\n        });\n    });\n\n    describe('Cancel', function () {\n        it('should close the font name form when user clicks on cancel', function () {\n            spyOn(MediumEditor.extensions.toolbar.prototype, 'showAndUpdateToolbar').and.callThrough();\n            var editor = this.newMediumEditor('.editor', this.mediumOpts),\n                fontNameExtension = editor.getExtensionByName('fontname'),\n                toolbar = editor.getExtensionByName('toolbar'),\n                button,\n                select,\n                cancel;\n\n            selectElementContentsAndFire(editor.elements[0]);\n            button = toolbar.getToolbarElement().querySelector('[data-action=\"fontName\"]');\n            cancel = fontNameExtension.getForm().querySelector('a.medium-editor-toobar-close');\n\n            fireEvent(button, 'click');\n            expect(fontNameExtension.isDisplayed()).toBe(true);\n            select = editor.getExtensionByName('fontname').getSelect();\n            select.value = 'Arial';\n            fireEvent(select, 'change');\n            fireEvent(cancel, 'click');\n            expect(this.el.innerHTML).toMatch(/^(<font>)?lorem ipsum(<\\/font>)?$/i);\n            expect(select.value).toBe('');\n            expect(toolbar.showAndUpdateToolbar).toHaveBeenCalled();\n            expect(fontNameExtension.isDisplayed()).toBe(false);\n        });\n    });\n\n    describe('Destroying MediumEditor', function () {\n        it('should destroy the font name extension and remove the form', function () {\n            spyOn(MediumEditor.extensions.fontName.prototype, 'destroy').and.callThrough();\n            var editor = this.newMediumEditor('.editor', this.mediumOpts),\n                fontNameExtension = editor.getExtensionByName('fontname'),\n                form = fontNameExtension.getForm();\n\n            expect(MediumEditor.util.isDescendant(document.body, form)).toBe(true);\n            editor.destroy();\n\n            expect(fontNameExtension.destroy).toHaveBeenCalled();\n            expect(MediumEditor.util.isDescendant(document.body, form)).toBe(false);\n        });\n    });\n});\n"
  },
  {
    "path": "spec/fontsize.spec.js",
    "content": "/*global fireEvent, selectElementContents,\n         selectElementContentsAndFire, isIE9 */\n\ndescribe('Font Size Button TestCase', function () {\n    'use strict';\n\n    function testFontSizeContents(el, size) {\n        expect(el.childNodes.length).toBe(1);\n        var child = el.childNodes[0];\n        expect(child.nodeName.toLowerCase()).toBe('font');\n        expect(child.getAttribute('size')).toBe(size);\n        expect(child.innerHTML).toBe('lorem ipsum');\n    }\n\n    beforeEach(function () {\n        setupTestHelpers.call(this);\n        this.el = this.createElement('div', 'editor', 'lorem ipsum');\n        this.mediumOpts = {\n            toolbar: {\n                buttons: ['fontsize']\n            }\n        };\n    });\n\n    afterEach(function () {\n        this.cleanupTest();\n        delete this.mediumOpts;\n    });\n\n    describe('Click', function () {\n        it('should display the font size form when toolbar is visible', function () {\n            spyOn(MediumEditor.extensions.fontSize.prototype, 'showForm').and.callThrough();\n            var button,\n                editor = this.newMediumEditor('.editor', this.mediumOpts),\n                fontSizeExtension = editor.getExtensionByName('fontsize'),\n                toolbar = editor.getExtensionByName('toolbar');\n\n            selectElementContentsAndFire(editor.elements[0]);\n            button = toolbar.getToolbarElement().querySelector('[data-action=\"fontSize\"]');\n            fireEvent(button, 'click');\n            expect(toolbar.getToolbarActionsElement().style.display).toBe('none');\n            expect(fontSizeExtension.isDisplayed()).toBe(true);\n            expect(fontSizeExtension.showForm).toHaveBeenCalled();\n        });\n    });\n\n    describe('Font Size', function () {\n        it('should change font size when slider is moved', function () {\n            spyOn(document, 'execCommand').and.callThrough();\n            var editor = this.newMediumEditor('.editor', {\n                    toolbar: {\n                        buttons: ['fontsize']\n                    },\n                    buttonLabels: 'fontawesome'\n                }),\n                fontSizeExtension = editor.getExtensionByName('fontsize'),\n                toolbar = editor.getExtensionByName('toolbar'),\n                button,\n                input;\n\n            selectElementContentsAndFire(editor.elements[0]);\n            button = toolbar.getToolbarElement().querySelector('[data-action=\"fontSize\"]');\n            fireEvent(button, 'click');\n\n            input = fontSizeExtension.getInput();\n            input.value = '7';\n            selectElementContents(this.el);\n            fireEvent(input, 'change');\n\n            expect(document.execCommand).toHaveBeenCalledWith('fontSize', false, '7');\n\n            fireEvent(fontSizeExtension.getForm().querySelector('a.medium-editor-toobar-save'), 'click');\n            testFontSizeContents(this.el, '7');\n        });\n\n        it('should display current font size when displayed', function () {\n            spyOn(MediumEditor.extensions.fontSize.prototype, 'showForm').and.callThrough();\n            var editor = this.newMediumEditor('.editor', this.mediumOpts),\n                fontSizeExtension = editor.getExtensionByName('fontsize'),\n                toolbar = editor.getExtensionByName('toolbar');\n            this.el.innerHTML = '<font size=\"7\">lorem ipsum dolor</font>';\n            selectElementContentsAndFire(editor.elements[0].firstChild);\n            fireEvent(toolbar.getToolbarElement().querySelector('[data-action=\"fontSize\"]'), 'click');\n            expect(fontSizeExtension.showForm).toHaveBeenCalledWith('7');\n        });\n\n        it('should revert font size when slider value is set to 4', function () {\n            spyOn(document, 'execCommand').and.callThrough();\n            spyOn(MediumEditor.extensions.fontSize.prototype, 'clearFontSize').and.callThrough();\n            var editor = this.newMediumEditor('.editor', this.mediumOpts),\n                fontSizeExtension = editor.getExtensionByName('fontsize'),\n                toolbar = editor.getExtensionByName('toolbar'),\n                button,\n                input;\n\n            selectElementContentsAndFire(editor.elements[0]);\n            button = toolbar.getToolbarElement().querySelector('[data-action=\"fontSize\"]');\n            fireEvent(button, 'click');\n\n            input = fontSizeExtension.getInput();\n            input.value = '1';\n            selectElementContents(editor.elements[0]);\n            fireEvent(input, 'change');\n            expect(document.execCommand).toHaveBeenCalledWith('fontSize', false, '1');\n            expect(fontSizeExtension.clearFontSize).not.toHaveBeenCalled();\n            testFontSizeContents(this.el, '1');\n\n            input.value = '4';\n            selectElementContents(editor.elements[0]);\n            fireEvent(input, 'change');\n\n            fireEvent(fontSizeExtension.getForm().querySelector('a.medium-editor-toobar-save'), 'click');\n            testFontSizeContents(this.el, null); // TODO: remove the <font> element entirely instead of just the `size` attribute\n            expect(fontSizeExtension.clearFontSize).toHaveBeenCalled();\n        });\n    });\n\n    describe('Cancel', function () {\n        it('should close the font size form when user clicks on cancel', function () {\n            spyOn(MediumEditor.extensions.toolbar.prototype, 'showAndUpdateToolbar').and.callThrough();\n            var editor = this.newMediumEditor('.editor', this.mediumOpts),\n                fontSizeExtension = editor.getExtensionByName('fontsize'),\n                toolbar = editor.getExtensionByName('toolbar'),\n                button,\n                input,\n                cancel;\n\n            selectElementContentsAndFire(editor.elements[0]);\n            button = toolbar.getToolbarElement().querySelector('[data-action=\"fontSize\"]');\n            cancel = fontSizeExtension.getForm().querySelector('a.medium-editor-toobar-close');\n\n            fireEvent(button, 'click');\n            expect(fontSizeExtension.isDisplayed()).toBe(true);\n            input = editor.getExtensionByName('fontsize').getInput();\n            input.value = '7';\n            fireEvent(input, 'change');\n            fireEvent(cancel, 'click');\n            expect(this.el.innerHTML).toMatch(/^(<font>)?lorem ipsum(<\\/font>)?$/i);\n            // IE9 doesn't support sliders\n            if (isIE9()) {\n                expect(input.value).toBe('');\n            } else {\n                expect(input.value).toBe('4');\n            }\n            expect(toolbar.showAndUpdateToolbar).toHaveBeenCalled();\n            expect(fontSizeExtension.isDisplayed()).toBe(false);\n        });\n    });\n\n    describe('Destroying MediumEditor', function () {\n        it('should destroy the font size extension and remove the form', function () {\n            spyOn(MediumEditor.extensions.fontSize.prototype, 'destroy').and.callThrough();\n            var editor = this.newMediumEditor('.editor', this.mediumOpts),\n                fontSizeExtension = editor.getExtensionByName('fontsize'),\n                form = fontSizeExtension.getForm();\n\n            expect(MediumEditor.util.isDescendant(document.body, form)).toBe(true);\n            editor.destroy();\n\n            expect(fontSizeExtension.destroy).toHaveBeenCalled();\n            expect(MediumEditor.util.isDescendant(document.body, form)).toBe(false);\n        });\n    });\n});\n"
  },
  {
    "path": "spec/full-content.spec.js",
    "content": "/*global selectElementContentsAndFire */\n\ndescribe('Full Content Action TestCase', function () {\n    'use strict';\n\n    beforeEach(function () {\n        setupTestHelpers.call(this);\n        this.el = this.createElement('div', 'editor', 'lorem ipsum');\n    });\n\n    afterEach(function () {\n        this.cleanupTest();\n    });\n\n    describe('All editable contents', function () {\n        it('should be bolded and unbolded when using a full-bold command', function () {\n            /*jslint regexp: true*/\n            var resultRegEx = /^<(b|strong)>lorem ipsum<\\/(b|strong)>$/gi;\n            /*jslint regexp: false*/\n\n            this.el.innerHTML = '<b>lorem ipsum</b>';\n            var editor = this.newMediumEditor('.editor');\n            selectElementContentsAndFire(editor.elements[0]);\n\n            editor.execAction('full-bold');\n            expect(this.el.innerHTML).toBe('lorem ipsum');\n\n            editor.execAction('full-bold');\n            expect(resultRegEx.test(this.el.innerHTML)).toBe(true);\n        });\n    });\n\n    describe('Selection', function () {\n        it('should preserve selection after multiple full-content commands', function () {\n            this.el.innerHTML = '<p>lorem <u>ipsum</u> dolor</p>';\n\n            var editor = this.newMediumEditor('.editor'),\n                // Beacuse not all browsers use <strike> or <s>, check for both\n                sTagO = '<(s|strike)>',\n                sTagC = '</(s|strike)>',\n                regex = new RegExp('^<p><u>lorem ' + sTagO + 'ipsum' + sTagC + ' dolor</u></p>$');\n\n            selectElementContentsAndFire(editor.elements[0].querySelector('u'));\n\n            editor.execAction('full-underline');\n            expect(this.el.innerHTML).toBe('<p>lorem ipsum dolor</p>');\n\n            editor.execAction('full-underline');\n            expect(this.el.innerHTML).toBe('<p><u>lorem ipsum dolor</u></p>');\n\n            // Ensure the selection is still maintained\n            editor.execAction('strikethrough');\n            expect(this.el.innerHTML).toMatch(regex);\n        });\n\n        it('should justify all contents including multiple block elements', function () {\n            this.el.innerHTML = '<p align=\"center\">lorem ipsum dolor</p><p align=\"left\">lorem ipsum dolor</p>';\n            var editor = this.newMediumEditor('.editor');\n            selectElementContentsAndFire(editor.elements[0].firstChild);\n            expect(window.getComputedStyle(editor.elements[0].childNodes[0]).getPropertyValue('text-align').indexOf('center')).not.toBe(-1);\n            expect(window.getComputedStyle(editor.elements[0].childNodes[1]).getPropertyValue('text-align').indexOf('left')).not.toBe(-1);\n\n            editor.execAction('full-justifyRight');\n            expect(window.getComputedStyle(editor.elements[0].childNodes[0]).getPropertyValue('text-align').indexOf('right')).not.toBe(-1);\n            expect(window.getComputedStyle(editor.elements[0].childNodes[1]).getPropertyValue('text-align').indexOf('right')).not.toBe(-1);\n\n            // Ensure only original selected <p> is affected\n            editor.execAction('justifyFull');\n            expect(window.getComputedStyle(editor.elements[0].childNodes[0]).getPropertyValue('text-align')).toBe('justify');\n            expect(window.getComputedStyle(editor.elements[0].childNodes[1]).getPropertyValue('text-align').indexOf('right')).not.toBe(-1);\n        });\n    });\n});\n"
  },
  {
    "path": "spec/header-tags.spec.js",
    "content": "/*global fireEvent */\n\ndescribe('Protect Header Tags TestCase', function () {\n    'use strict';\n\n    beforeEach(function () {\n        setupTestHelpers.call(this);\n        this.el = this.createElement('div', 'editor', '<p id=\"first-p\">lorem ipsum</p><p></p><h2 id=\"header\">Cats</h2>');\n        this.el.id = 'editor';\n    });\n\n    afterEach(function () {\n        this.cleanupTest();\n    });\n\n    describe('ProtectHeaderTags', function () {\n        it('header intact after leading return', function () {\n            // place cursor at begining of header\n            var editor = this.newMediumEditor('.editor'),\n                el = document.getElementById('header'),\n                range = document.createRange(),\n                sel = window.getSelection();\n\n            range.setStart(el, 0);\n            range.collapse(true);\n            sel.removeAllRanges();\n            sel.addRange(range);\n\n            // hit return\n            fireEvent(editor.elements[0], 'keypress', {\n                keyCode: MediumEditor.util.keyCode.ENTER\n            });\n\n            el = document.getElementById('header');\n            expect(el).toBeDefined();\n            expect(el.nodeName.toLowerCase()).toBe('h2');\n        });\n\n        it('header leading return inserts paragraph, not additional header', function () {\n            // place cursor at begining of header\n            var editor = this.newMediumEditor('.editor'),\n                el = document.getElementById('header'),\n                range = document.createRange(),\n                sel = window.getSelection();\n            range.setStart(el, 0);\n            range.collapse(true);\n            sel.removeAllRanges();\n            sel.addRange(range);\n\n            // hit return\n            fireEvent(editor.elements[0], 'keypress', {\n                keyCode: MediumEditor.util.keyCode.ENTER\n            });\n\n            el = document.getElementById('header');\n            expect(el.previousElementSibling.nodeName.toLowerCase()).toBe('p');\n\n        });\n\n        it('header leading backspace into empty p preserves header', function () {\n            // place cursor at begining of header\n            var editor = this.newMediumEditor('.editor'),\n                originalHTML = document.getElementById('editor').innerHTML,\n                el = document.getElementById('header'),\n                range = document.createRange(),\n                sel = window.getSelection();\n            range.setStart(el, 0);\n            range.collapse(true);\n            sel.removeAllRanges();\n            sel.addRange(range);\n\n            // hit backspace\n            fireEvent(editor.elements[0].querySelector(el.nodeName.toLowerCase()), 'keydown', {\n                keyCode: MediumEditor.util.keyCode.BACKSPACE\n            });\n\n            el = document.getElementById('header');\n            expect(el).toBeDefined();\n            expect(el.nodeName.toLowerCase()).toBe('h2');\n\n            el = document.getElementById('editor');\n            expect(el.innerHTML).not.toBe(originalHTML);\n\n        });\n\n    });\n\n});\n"
  },
  {
    "path": "spec/helpers/util.js",
    "content": "/*global atob, unescape, Uint8Array, Blob*/\n\nfunction setupTestHelpers() {\n    jasmine.clock().install();\n    this.elements = [];\n    this.editors = [];\n\n    this.createElement = function (tag, className, html, dontAppend) {\n        var el = document.createElement(tag);\n        el.innerHTML = html || '';\n        if (className) {\n            el.className = className;\n        }\n        this.elements.push(el);\n        if (!dontAppend) {\n            document.body.appendChild(el);\n        }\n        return el;\n    };\n\n    this.newMediumEditor = function (selector, options) {\n        var editor = new MediumEditor(selector, options);\n        this.editors.push(editor);\n        return editor;\n    };\n\n    this.cleanupTest = function () {\n        this.editors.forEach(function (editor) {\n            editor.destroy();\n        });\n        this.elements.forEach(function (element) {\n            if (element.parentNode) {\n                element.parentNode.removeChild(element);\n            }\n        });\n\n        jasmine.clock().uninstall();\n\n        delete this.createElement;\n        delete this.createMedium;\n        delete this.elements;\n        delete this.editors;\n        delete this.cleanupTest;\n    }\n}\n\nfunction isIE9() {\n    return navigator.appName.indexOf('Internet Explorer') !== -1 && navigator.appVersion.indexOf(\"MSIE 9\") !== -1;\n}\n\nfunction isIE10() {\n    return navigator.appName.indexOf('Internet Explorer') !== -1 && navigator.appVersion.indexOf(\"MSIE 10\") !== -1;\n}\n\nfunction isOldIE() {\n    return isIE9() || isIE10();\n}\n\nfunction isIE() {\n    return ((navigator.appName === 'Microsoft Internet Explorer') || ((navigator.appName === 'Netscape') && (new RegExp('Trident/.*rv:([0-9]{1,}[.0-9]{0,})').exec(navigator.userAgent) !== null)));\n}\n\n// If the browser is Edge, returns the version number as a float, otherwise returns 0\nfunction getEdgeVersion() {\n    var match = /Edge\\/(\\d+[,.]\\d+)/.exec(navigator.userAgent);\n    if (match !== null) {\n        return +match[1];\n    }\n    return 0;\n}\n\nfunction isFirefox() {\n    return navigator.userAgent.toLowerCase().indexOf('firefox') !== -1;\n}\n\nfunction isSafari() {\n    return navigator.userAgent.toLowerCase().indexOf('safari') !== -1;\n}\n\nfunction dataURItoBlob(dataURI) {\n    // convert base64/URLEncoded data component to raw binary data held in a string\n    var byteString,\n        mimeString,\n        ia,\n        i;\n\n    if (dataURI.split(',')[0].indexOf('base64') >= 0) {\n        byteString = atob(dataURI.split(',')[1]);\n    } else {\n        byteString = unescape(dataURI.split(',')[1]);\n    }\n\n    // separate out the mime component\n    mimeString = dataURI.split(',')[0].split(':')[1].split(';')[0];\n\n    // write the bytes of the string to a typed array\n    ia = new Uint8Array(byteString.length);\n    for (i = 0; i < byteString.length; i += 1) {\n        ia[i] = byteString.charCodeAt(i);\n    }\n\n    return new Blob([ia], {type: mimeString});\n}\n\n// keyCode, ctrlKey, target, relatedTarget, shiftKey, altKey\nfunction fireEvent(element, eventName, options) {\n    var evt = prepareEvent(\n        element,\n        eventName,\n        options\n    );\n\n    return firePreparedEvent(evt, element, eventName);\n}\n\n/**\n * prepareEvent works with firePreparedEvent.\n *\n * It allows test to:\n *     - create the event\n *     - spy a method on this event\n *     - fire the event\n *\n * Example:\n *     var p = document.querySelector('p');\n *     var evt = prepareEvent(p, 'keydown', { keyCode: MediumEditor.util.keyCode.ENTER });\n *     spyOn(evt, 'preventDefault').and.callThrough();\n *     firePreparedEvent(evt, p, 'keydown');\n *     expect(evt.preventDefault).toHaveBeenCalled();\n *\n * You can see a live example for tests related to `disableDoubleReturn`\n */\nfunction prepareEvent (element, eventName, options) {\n    var evt;\n\n    options = options || {};\n\n    if (document.createEvent) {\n        // dispatch for firefox + others\n        evt = document.createEvent('HTMLEvents');\n        evt.initEvent(eventName, true, true); // event type,bubbling,cancelable\n\n        evt.currentTarget = options.currentTarget ? options.currentTarget : element;\n\n        if (options.keyCode) {\n            evt.keyCode = options.keyCode;\n            evt.which = options.keyCode;\n        }\n\n        if (options.ctrlKey) {\n            evt.ctrlKey = true;\n        }\n\n        if (options.metaKey) {\n            evt.metaKey = true;\n        }\n\n        if (options.target) {\n            evt.target = options.target;\n        }\n\n        if (options.relatedTarget) {\n            evt.relatedTarget = options.relatedTarget;\n        }\n\n        if (options.shiftKey) {\n            evt.shiftKey = true;\n        }\n\n        if (options.altKey) {\n          evt.altKey = true;\n        }\n\n        if (eventName.indexOf('drag') !== -1 || eventName === 'drop') {\n            evt.dataTransfer = {\n                dropEffect: ''\n            };\n            // File API only allows access to 'files' on drop, not on any other event\n            if (!isIE9() && eventName === 'drop') {\n                var file = dataURItoBlob('data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7');\n                if (!file.type) {\n                    file.type = 'image/gif';\n                }\n                evt.dataTransfer.files = [file];\n            }\n        }\n    } else {\n        // dispatch for IE\n        evt = document.createEventObject();\n    }\n\n    return evt;\n}\n\n/**\n * @see prepareEvent\n */\nfunction firePreparedEvent (event, element, eventName) {\n    if (document.createEvent) {\n        return !element.dispatchEvent(event);\n    }\n\n    return element.fireEvent('on' + eventName, event);\n}\n\nfunction placeCursorInsideElement(el, index) {\n    var selection = window.getSelection(),\n        newRange = document.createRange();\n    selection.removeAllRanges();\n    newRange.setStart(el, index);\n    selection.addRange(newRange);\n}\n\nfunction selectElementContents(el, options) {\n    options = options || {};\n\n    var range = document.createRange(),\n        sel = window.getSelection();\n    range.selectNodeContents(el);\n\n    if (options.collapse) {\n        range.collapse(options.collapse === true);\n    }\n\n    sel.removeAllRanges();\n    sel.addRange(range);\n}\n\nfunction selectElementContentsAndFire(el, options) {\n    options = options || {};\n    selectElementContents(el, options);\n    fireEvent(el, options.eventToFire || 'click');\n    if (options.testDelay !== -1) {\n        if (!options.testDelay) {\n            jasmine.clock().tick(1);\n        } else {\n            jasmine.clock().tick(options.testDelay);\n        }\n    }\n}\n\nvar WORD_PASTE_EXAMPLE = ['<html xmlns:o=\"urn:schemas-microsoft-com:office:office\"',\n'xmlns:w=\"urn:schemas-microsoft-com:office:word\"',\n'xmlns:m=\"http://schemas.microsoft.com/office/2004/12/omml\"',\n'xmlns=\"http://www.w3.org/TR/REC-html40\">',\n'',\n'<head>',\n'<meta name=Title content=\"\">',\n'<meta name=Keywords content=\"\">',\n'<meta http-equiv=Content-Type content=\"text/html; charset=utf-8\">',\n'<meta name=ProgId content=Word.Document>',\n'<meta name=Generator content=\"Microsoft Word 15\">',\n'<meta name=Originator content=\"Microsoft Word 15\">',\n'<link rel=File-List',\n'href=\"file://localhost/Users/nate/Library/Group%20Containers/UBF8T346G9.Office/msoclip1/01/clip_filelist.xml\">',\n'<!--[if gte mso 9]><xml>',\n' <o:OfficeDocumentSettings>',\n'  <o:AllowPNG/>',\n' </o:OfficeDocumentSettings>',\n'</xml><![endif]-->',\n'<link rel=themeData',\n'href=\"file://localhost/Users/nate/Library/Group%20Containers/UBF8T346G9.Office/msoclip1/01/clip_themedata.thmx\">',\n'<!--[if gte mso 9]><xml>',\n' <w:WordDocument>',\n'  <w:View>Normal</w:View>',\n'  <w:Zoom>0</w:Zoom>',\n'  <w:TrackMoves/>',\n'  <w:TrackFormatting/>',\n'  <w:PunctuationKerning/>',\n'  <w:ValidateAgainstSchemas/>',\n'  <w:SaveIfXMLInvalid>false</w:SaveIfXMLInvalid>',\n'  <w:IgnoreMixedContent>false</w:IgnoreMixedContent>',\n'  <w:AlwaysShowPlaceholderText>false</w:AlwaysShowPlaceholderText>',\n'  <w:DoNotPromoteQF/>',\n'  <w:LidThemeOther>EN-US</w:LidThemeOther>',\n'  <w:LidThemeAsian>JA</w:LidThemeAsian>',\n'  <w:LidThemeComplexScript>X-NONE</w:LidThemeComplexScript>',\n'  <w:Compatibility>',\n'   <w:BreakWrappedTables/>',\n'   <w:SnapToGridInCell/>',\n'   <w:WrapTextWithPunct/>',\n'   <w:UseAsianBreakRules/>',\n'   <w:DontGrowAutofit/>',\n'   <w:SplitPgBreakAndParaMark/>',\n'   <w:EnableOpenTypeKerning/>',\n'   <w:DontFlipMirrorIndents/>',\n'   <w:OverrideTableStyleHps/>',\n'   <w:UseFELayout/>',\n'  </w:Compatibility>',\n'  <m:mathPr>',\n'   <m:mathFont m:val=\"Cambria Math\"/>',\n'   <m:brkBin m:val=\"before\"/>',\n'   <m:brkBinSub m:val=\"&#45;-\"/>',\n'   <m:smallFrac m:val=\"off\"/>',\n'   <m:dispDef/>',\n'   <m:lMargin m:val=\"0\"/>',\n'   <m:rMargin m:val=\"0\"/>',\n'   <m:defJc m:val=\"centerGroup\"/>',\n'   <m:wrapIndent m:val=\"1440\"/>',\n'   <m:intLim m:val=\"subSup\"/>',\n'   <m:naryLim m:val=\"undOvr\"/>',\n'  </m:mathPr></w:WordDocument>',\n'</xml><![endif]--><!--[if gte mso 9]><xml>',\n' <w:LatentStyles DefLockedState=\"false\" DefUnhideWhenUsed=\"false\"',\n'  DefSemiHidden=\"false\" DefQFormat=\"false\" DefPriority=\"99\"',\n'  LatentStyleCount=\"380\">',\n'  <w:LsdException Locked=\"false\" Priority=\"0\" QFormat=\"true\" Name=\"Normal\"/>',\n'  <w:LsdException Locked=\"false\" Priority=\"9\" QFormat=\"true\" Name=\"heading 1\"/>',\n'  <w:LsdException Locked=\"false\" Priority=\"9\" SemiHidden=\"true\"',\n'   UnhideWhenUsed=\"true\" QFormat=\"true\" Name=\"heading 2\"/>',\n'  <w:LsdException Locked=\"false\" Priority=\"9\" SemiHidden=\"true\"',\n'   UnhideWhenUsed=\"true\" QFormat=\"true\" Name=\"heading 3\"/>',\n'  <w:LsdException Locked=\"false\" Priority=\"9\" SemiHidden=\"true\"',\n'   UnhideWhenUsed=\"true\" QFormat=\"true\" Name=\"heading 4\"/>',\n'  <w:LsdException Locked=\"false\" Priority=\"9\" SemiHidden=\"true\"',\n'   UnhideWhenUsed=\"true\" QFormat=\"true\" Name=\"heading 5\"/>',\n'  <w:LsdException Locked=\"false\" Priority=\"9\" SemiHidden=\"true\"',\n'   UnhideWhenUsed=\"true\" QFormat=\"true\" Name=\"heading 6\"/>',\n'  <w:LsdException Locked=\"false\" Priority=\"9\" SemiHidden=\"true\"',\n'   UnhideWhenUsed=\"true\" QFormat=\"true\" Name=\"heading 7\"/>',\n'  <w:LsdException Locked=\"false\" Priority=\"9\" SemiHidden=\"true\"',\n'   UnhideWhenUsed=\"true\" QFormat=\"true\" Name=\"heading 8\"/>',\n'  <w:LsdException Locked=\"false\" Priority=\"9\" SemiHidden=\"true\"',\n'   UnhideWhenUsed=\"true\" QFormat=\"true\" Name=\"heading 9\"/>',\n'  <w:LsdException Locked=\"false\" SemiHidden=\"true\" UnhideWhenUsed=\"true\"',\n'   Name=\"index 1\"/>',\n'  <w:LsdException Locked=\"false\" SemiHidden=\"true\" UnhideWhenUsed=\"true\"',\n'   Name=\"index 2\"/>',\n'  <w:LsdException Locked=\"false\" SemiHidden=\"true\" UnhideWhenUsed=\"true\"',\n'   Name=\"index 3\"/>',\n'  <w:LsdException Locked=\"false\" SemiHidden=\"true\" UnhideWhenUsed=\"true\"',\n'   Name=\"index 4\"/>',\n'  <w:LsdException Locked=\"false\" SemiHidden=\"true\" UnhideWhenUsed=\"true\"',\n'   Name=\"index 5\"/>',\n'  <w:LsdException Locked=\"false\" SemiHidden=\"true\" UnhideWhenUsed=\"true\"',\n'   Name=\"index 6\"/>',\n'  <w:LsdException Locked=\"false\" SemiHidden=\"true\" UnhideWhenUsed=\"true\"',\n'   Name=\"index 7\"/>',\n'  <w:LsdException Locked=\"false\" SemiHidden=\"true\" UnhideWhenUsed=\"true\"',\n'   Name=\"index 8\"/>',\n'  <w:LsdException Locked=\"false\" SemiHidden=\"true\" UnhideWhenUsed=\"true\"',\n'   Name=\"index 9\"/>',\n'  <w:LsdException Locked=\"false\" Priority=\"39\" SemiHidden=\"true\"',\n'   UnhideWhenUsed=\"true\" Name=\"toc 1\"/>',\n'  <w:LsdException Locked=\"false\" Priority=\"39\" SemiHidden=\"true\"',\n'   UnhideWhenUsed=\"true\" Name=\"toc 2\"/>',\n'  <w:LsdException Locked=\"false\" Priority=\"39\" SemiHidden=\"true\"',\n'   UnhideWhenUsed=\"true\" Name=\"toc 3\"/>',\n'  <w:LsdException Locked=\"false\" Priority=\"39\" SemiHidden=\"true\"',\n'   UnhideWhenUsed=\"true\" Name=\"toc 4\"/>',\n'  <w:LsdException Locked=\"false\" Priority=\"39\" SemiHidden=\"true\"',\n'   UnhideWhenUsed=\"true\" Name=\"toc 5\"/>',\n'  <w:LsdException Locked=\"false\" Priority=\"39\" SemiHidden=\"true\"',\n'   UnhideWhenUsed=\"true\" Name=\"toc 6\"/>',\n'  <w:LsdException Locked=\"false\" Priority=\"39\" SemiHidden=\"true\"',\n'   UnhideWhenUsed=\"true\" Name=\"toc 7\"/>',\n'  <w:LsdException Locked=\"false\" Priority=\"39\" SemiHidden=\"true\"',\n'   UnhideWhenUsed=\"true\" Name=\"toc 8\"/>',\n'  <w:LsdException Locked=\"false\" Priority=\"39\" SemiHidden=\"true\"',\n'   UnhideWhenUsed=\"true\" Name=\"toc 9\"/>',\n'  <w:LsdException Locked=\"false\" SemiHidden=\"true\" UnhideWhenUsed=\"true\"',\n'   Name=\"Normal Indent\"/>',\n'  <w:LsdException Locked=\"false\" SemiHidden=\"true\" UnhideWhenUsed=\"true\"',\n'   Name=\"footnote text\"/>',\n'  <w:LsdException Locked=\"false\" SemiHidden=\"true\" UnhideWhenUsed=\"true\"',\n'   Name=\"annotation text\"/>',\n'  <w:LsdException Locked=\"false\" SemiHidden=\"true\" UnhideWhenUsed=\"true\"',\n'   Name=\"header\"/>',\n'  <w:LsdException Locked=\"false\" SemiHidden=\"true\" UnhideWhenUsed=\"true\"',\n'   Name=\"footer\"/>',\n'  <w:LsdException Locked=\"false\" SemiHidden=\"true\" UnhideWhenUsed=\"true\"',\n'   Name=\"index heading\"/>',\n'  <w:LsdException Locked=\"false\" Priority=\"35\" SemiHidden=\"true\"',\n'   UnhideWhenUsed=\"true\" QFormat=\"true\" Name=\"caption\"/>',\n'  <w:LsdException Locked=\"false\" SemiHidden=\"true\" UnhideWhenUsed=\"true\"',\n'   Name=\"table of figures\"/>',\n'  <w:LsdException Locked=\"false\" SemiHidden=\"true\" UnhideWhenUsed=\"true\"',\n'   Name=\"envelope address\"/>',\n'  <w:LsdException Locked=\"false\" SemiHidden=\"true\" UnhideWhenUsed=\"true\"',\n'   Name=\"envelope return\"/>',\n'  <w:LsdException Locked=\"false\" SemiHidden=\"true\" UnhideWhenUsed=\"true\"',\n'   Name=\"footnote reference\"/>',\n'  <w:LsdException Locked=\"false\" SemiHidden=\"true\" UnhideWhenUsed=\"true\"',\n'   Name=\"annotation reference\"/>',\n'  <w:LsdException Locked=\"false\" SemiHidden=\"true\" UnhideWhenUsed=\"true\"',\n'   Name=\"line number\"/>',\n'  <w:LsdException Locked=\"false\" SemiHidden=\"true\" UnhideWhenUsed=\"true\"',\n'   Name=\"page number\"/>',\n'  <w:LsdException Locked=\"false\" SemiHidden=\"true\" UnhideWhenUsed=\"true\"',\n'   Name=\"endnote reference\"/>',\n'  <w:LsdException Locked=\"false\" SemiHidden=\"true\" UnhideWhenUsed=\"true\"',\n'   Name=\"endnote text\"/>',\n'  <w:LsdException Locked=\"false\" SemiHidden=\"true\" UnhideWhenUsed=\"true\"',\n'   Name=\"table of authorities\"/>',\n'  <w:LsdException Locked=\"false\" SemiHidden=\"true\" UnhideWhenUsed=\"true\"',\n'   Name=\"macro\"/>',\n'  <w:LsdException Locked=\"false\" SemiHidden=\"true\" UnhideWhenUsed=\"true\"',\n'   Name=\"toa heading\"/>',\n'  <w:LsdException Locked=\"false\" SemiHidden=\"true\" UnhideWhenUsed=\"true\"',\n'   Name=\"List\"/>',\n'  <w:LsdException Locked=\"false\" SemiHidden=\"true\" UnhideWhenUsed=\"true\"',\n'   Name=\"List Bullet\"/>',\n'  <w:LsdException Locked=\"false\" SemiHidden=\"true\" UnhideWhenUsed=\"true\"',\n'   Name=\"List Number\"/>',\n'  <w:LsdException Locked=\"false\" SemiHidden=\"true\" UnhideWhenUsed=\"true\"',\n'   Name=\"List 2\"/>',\n'  <w:LsdException Locked=\"false\" SemiHidden=\"true\" UnhideWhenUsed=\"true\"',\n'   Name=\"List 3\"/>',\n'  <w:LsdException Locked=\"false\" SemiHidden=\"true\" UnhideWhenUsed=\"true\"',\n'   Name=\"List 4\"/>',\n'  <w:LsdException Locked=\"false\" SemiHidden=\"true\" UnhideWhenUsed=\"true\"',\n'   Name=\"List 5\"/>',\n'  <w:LsdException Locked=\"false\" SemiHidden=\"true\" UnhideWhenUsed=\"true\"',\n'   Name=\"List Bullet 2\"/>',\n'  <w:LsdException Locked=\"false\" SemiHidden=\"true\" UnhideWhenUsed=\"true\"',\n'   Name=\"List Bullet 3\"/>',\n'  <w:LsdException Locked=\"false\" SemiHidden=\"true\" UnhideWhenUsed=\"true\"',\n'   Name=\"List Bullet 4\"/>',\n'  <w:LsdException Locked=\"false\" SemiHidden=\"true\" UnhideWhenUsed=\"true\"',\n'   Name=\"List Bullet 5\"/>',\n'  <w:LsdException Locked=\"false\" SemiHidden=\"true\" UnhideWhenUsed=\"true\"',\n'   Name=\"List Number 2\"/>',\n'  <w:LsdException Locked=\"false\" SemiHidden=\"true\" UnhideWhenUsed=\"true\"',\n'   Name=\"List Number 3\"/>',\n'  <w:LsdException Locked=\"false\" SemiHidden=\"true\" UnhideWhenUsed=\"true\"',\n'   Name=\"List Number 4\"/>',\n'  <w:LsdException Locked=\"false\" SemiHidden=\"true\" UnhideWhenUsed=\"true\"',\n'   Name=\"List Number 5\"/>',\n'  <w:LsdException Locked=\"false\" Priority=\"10\" QFormat=\"true\" Name=\"Title\"/>',\n'  <w:LsdException Locked=\"false\" SemiHidden=\"true\" UnhideWhenUsed=\"true\"',\n'   Name=\"Closing\"/>',\n'  <w:LsdException Locked=\"false\" SemiHidden=\"true\" UnhideWhenUsed=\"true\"',\n'   Name=\"Signature\"/>',\n'  <w:LsdException Locked=\"false\" Priority=\"1\" SemiHidden=\"true\"',\n'   UnhideWhenUsed=\"true\" Name=\"Default Paragraph Font\"/>',\n'  <w:LsdException Locked=\"false\" SemiHidden=\"true\" UnhideWhenUsed=\"true\"',\n'   Name=\"Body Text\"/>',\n'  <w:LsdException Locked=\"false\" SemiHidden=\"true\" UnhideWhenUsed=\"true\"',\n'   Name=\"Body Text Indent\"/>',\n'  <w:LsdException Locked=\"false\" SemiHidden=\"true\" UnhideWhenUsed=\"true\"',\n'   Name=\"List Continue\"/>',\n'  <w:LsdException Locked=\"false\" SemiHidden=\"true\" UnhideWhenUsed=\"true\"',\n'   Name=\"List Continue 2\"/>',\n'  <w:LsdException Locked=\"false\" SemiHidden=\"true\" UnhideWhenUsed=\"true\"',\n'   Name=\"List Continue 3\"/>',\n'  <w:LsdException Locked=\"false\" SemiHidden=\"true\" UnhideWhenUsed=\"true\"',\n'   Name=\"List Continue 4\"/>',\n'  <w:LsdException Locked=\"false\" SemiHidden=\"true\" UnhideWhenUsed=\"true\"',\n'   Name=\"List Continue 5\"/>',\n'  <w:LsdException Locked=\"false\" SemiHidden=\"true\" UnhideWhenUsed=\"true\"',\n'   Name=\"Message Header\"/>',\n'  <w:LsdException Locked=\"false\" Priority=\"11\" QFormat=\"true\" Name=\"Subtitle\"/>',\n'  <w:LsdException Locked=\"false\" SemiHidden=\"true\" UnhideWhenUsed=\"true\"',\n'   Name=\"Salutation\"/>',\n'  <w:LsdException Locked=\"false\" SemiHidden=\"true\" UnhideWhenUsed=\"true\"',\n'   Name=\"Date\"/>',\n'  <w:LsdException Locked=\"false\" SemiHidden=\"true\" UnhideWhenUsed=\"true\"',\n'   Name=\"Body Text First Indent\"/>',\n'  <w:LsdException Locked=\"false\" SemiHidden=\"true\" UnhideWhenUsed=\"true\"',\n'   Name=\"Body Text First Indent 2\"/>',\n'  <w:LsdException Locked=\"false\" SemiHidden=\"true\" UnhideWhenUsed=\"true\"',\n'   Name=\"Note Heading\"/>',\n'  <w:LsdException Locked=\"false\" SemiHidden=\"true\" UnhideWhenUsed=\"true\"',\n'   Name=\"Body Text 2\"/>',\n'  <w:LsdException Locked=\"false\" SemiHidden=\"true\" UnhideWhenUsed=\"true\"',\n'   Name=\"Body Text 3\"/>',\n'  <w:LsdException Locked=\"false\" SemiHidden=\"true\" UnhideWhenUsed=\"true\"',\n'   Name=\"Body Text Indent 2\"/>',\n'  <w:LsdException Locked=\"false\" SemiHidden=\"true\" UnhideWhenUsed=\"true\"',\n'   Name=\"Body Text Indent 3\"/>',\n'  <w:LsdException Locked=\"false\" SemiHidden=\"true\" UnhideWhenUsed=\"true\"',\n'   Name=\"Block Text\"/>',\n'  <w:LsdException Locked=\"false\" SemiHidden=\"true\" UnhideWhenUsed=\"true\"',\n'   Name=\"Hyperlink\"/>',\n'  <w:LsdException Locked=\"false\" SemiHidden=\"true\" UnhideWhenUsed=\"true\"',\n'   Name=\"FollowedHyperlink\"/>',\n'  <w:LsdException Locked=\"false\" Priority=\"22\" QFormat=\"true\" Name=\"Strong\"/>',\n'  <w:LsdException Locked=\"false\" Priority=\"20\" QFormat=\"true\" Name=\"Emphasis\"/>',\n'  <w:LsdException Locked=\"false\" SemiHidden=\"true\" UnhideWhenUsed=\"true\"',\n'   Name=\"Document Map\"/>',\n'  <w:LsdException Locked=\"false\" SemiHidden=\"true\" UnhideWhenUsed=\"true\"',\n'   Name=\"Plain Text\"/>',\n'  <w:LsdException Locked=\"false\" SemiHidden=\"true\" UnhideWhenUsed=\"true\"',\n'   Name=\"E-mail Signature\"/>',\n'  <w:LsdException Locked=\"false\" SemiHidden=\"true\" UnhideWhenUsed=\"true\"',\n'   Name=\"HTML Top of Form\"/>',\n'  <w:LsdException Locked=\"false\" SemiHidden=\"true\" UnhideWhenUsed=\"true\"',\n'   Name=\"HTML Bottom of Form\"/>',\n'  <w:LsdException Locked=\"false\" SemiHidden=\"true\" UnhideWhenUsed=\"true\"',\n'   Name=\"Normal (Web)\"/>',\n'  <w:LsdException Locked=\"false\" SemiHidden=\"true\" UnhideWhenUsed=\"true\"',\n'   Name=\"HTML Acronym\"/>',\n'  <w:LsdException Locked=\"false\" SemiHidden=\"true\" UnhideWhenUsed=\"true\"',\n'   Name=\"HTML Address\"/>',\n'  <w:LsdException Locked=\"false\" SemiHidden=\"true\" UnhideWhenUsed=\"true\"',\n'   Name=\"HTML Cite\"/>',\n'  <w:LsdException Locked=\"false\" SemiHidden=\"true\" UnhideWhenUsed=\"true\"',\n'   Name=\"HTML Code\"/>',\n'  <w:LsdException Locked=\"false\" SemiHidden=\"true\" UnhideWhenUsed=\"true\"',\n'   Name=\"HTML Definition\"/>',\n'  <w:LsdException Locked=\"false\" SemiHidden=\"true\" UnhideWhenUsed=\"true\"',\n'   Name=\"HTML Keyboard\"/>',\n'  <w:LsdException Locked=\"false\" SemiHidden=\"true\" UnhideWhenUsed=\"true\"',\n'   Name=\"HTML Preformatted\"/>',\n'  <w:LsdException Locked=\"false\" SemiHidden=\"true\" UnhideWhenUsed=\"true\"',\n'   Name=\"HTML Sample\"/>',\n'  <w:LsdException Locked=\"false\" SemiHidden=\"true\" UnhideWhenUsed=\"true\"',\n'   Name=\"HTML Typewriter\"/>',\n'  <w:LsdException Locked=\"false\" SemiHidden=\"true\" UnhideWhenUsed=\"true\"',\n'   Name=\"HTML Variable\"/>',\n'  <w:LsdException Locked=\"false\" SemiHidden=\"true\" UnhideWhenUsed=\"true\"',\n'   Name=\"Normal Table\"/>',\n'  <w:LsdException Locked=\"false\" SemiHidden=\"true\" UnhideWhenUsed=\"true\"',\n'   Name=\"annotation subject\"/>',\n'  <w:LsdException Locked=\"false\" SemiHidden=\"true\" UnhideWhenUsed=\"true\"',\n'   Name=\"No List\"/>',\n'  <w:LsdException Locked=\"false\" SemiHidden=\"true\" UnhideWhenUsed=\"true\"',\n'   Name=\"Outline List 1\"/>',\n'  <w:LsdException Locked=\"false\" SemiHidden=\"true\" UnhideWhenUsed=\"true\"',\n'   Name=\"Outline List 2\"/>',\n'  <w:LsdException Locked=\"false\" SemiHidden=\"true\" UnhideWhenUsed=\"true\"',\n'   Name=\"Outline List 3\"/>',\n'  <w:LsdException Locked=\"false\" SemiHidden=\"true\" UnhideWhenUsed=\"true\"',\n'   Name=\"Table Simple 1\"/>',\n'  <w:LsdException Locked=\"false\" SemiHidden=\"true\" UnhideWhenUsed=\"true\"',\n'   Name=\"Table Simple 2\"/>',\n'  <w:LsdException Locked=\"false\" SemiHidden=\"true\" UnhideWhenUsed=\"true\"',\n'   Name=\"Table Simple 3\"/>',\n'  <w:LsdException Locked=\"false\" SemiHidden=\"true\" UnhideWhenUsed=\"true\"',\n'   Name=\"Table Classic 1\"/>',\n'  <w:LsdException Locked=\"false\" SemiHidden=\"true\" UnhideWhenUsed=\"true\"',\n'   Name=\"Table Classic 2\"/>',\n'  <w:LsdException Locked=\"false\" SemiHidden=\"true\" UnhideWhenUsed=\"true\"',\n'   Name=\"Table Classic 3\"/>',\n'  <w:LsdException Locked=\"false\" SemiHidden=\"true\" UnhideWhenUsed=\"true\"',\n'   Name=\"Table Classic 4\"/>',\n'  <w:LsdException Locked=\"false\" SemiHidden=\"true\" UnhideWhenUsed=\"true\"',\n'   Name=\"Table Colorful 1\"/>',\n'  <w:LsdException Locked=\"false\" SemiHidden=\"true\" UnhideWhenUsed=\"true\"',\n'   Name=\"Table Colorful 2\"/>',\n'  <w:LsdException Locked=\"false\" SemiHidden=\"true\" UnhideWhenUsed=\"true\"',\n'   Name=\"Table Colorful 3\"/>',\n'  <w:LsdException Locked=\"false\" SemiHidden=\"true\" UnhideWhenUsed=\"true\"',\n'   Name=\"Table Columns 1\"/>',\n'  <w:LsdException Locked=\"false\" SemiHidden=\"true\" UnhideWhenUsed=\"true\"',\n'   Name=\"Table Columns 2\"/>',\n'  <w:LsdException Locked=\"false\" SemiHidden=\"true\" UnhideWhenUsed=\"true\"',\n'   Name=\"Table Columns 3\"/>',\n'  <w:LsdException Locked=\"false\" SemiHidden=\"true\" UnhideWhenUsed=\"true\"',\n'   Name=\"Table Columns 4\"/>',\n'  <w:LsdException Locked=\"false\" SemiHidden=\"true\" UnhideWhenUsed=\"true\"',\n'   Name=\"Table Columns 5\"/>',\n'  <w:LsdException Locked=\"false\" SemiHidden=\"true\" UnhideWhenUsed=\"true\"',\n'   Name=\"Table Grid 1\"/>',\n'  <w:LsdException Locked=\"false\" SemiHidden=\"true\" UnhideWhenUsed=\"true\"',\n'   Name=\"Table Grid 2\"/>',\n'  <w:LsdException Locked=\"false\" SemiHidden=\"true\" UnhideWhenUsed=\"true\"',\n'   Name=\"Table Grid 3\"/>',\n'  <w:LsdException Locked=\"false\" SemiHidden=\"true\" UnhideWhenUsed=\"true\"',\n'   Name=\"Table Grid 4\"/>',\n'  <w:LsdException Locked=\"false\" SemiHidden=\"true\" UnhideWhenUsed=\"true\"',\n'   Name=\"Table Grid 5\"/>',\n'  <w:LsdException Locked=\"false\" SemiHidden=\"true\" UnhideWhenUsed=\"true\"',\n'   Name=\"Table Grid 6\"/>',\n'  <w:LsdException Locked=\"false\" SemiHidden=\"true\" UnhideWhenUsed=\"true\"',\n'   Name=\"Table Grid 7\"/>',\n'  <w:LsdException Locked=\"false\" SemiHidden=\"true\" UnhideWhenUsed=\"true\"',\n'   Name=\"Table Grid 8\"/>',\n'  <w:LsdException Locked=\"false\" SemiHidden=\"true\" UnhideWhenUsed=\"true\"',\n'   Name=\"Table List 1\"/>',\n'  <w:LsdException Locked=\"false\" SemiHidden=\"true\" UnhideWhenUsed=\"true\"',\n'   Name=\"Table List 2\"/>',\n'  <w:LsdException Locked=\"false\" SemiHidden=\"true\" UnhideWhenUsed=\"true\"',\n'   Name=\"Table List 3\"/>',\n'  <w:LsdException Locked=\"false\" SemiHidden=\"true\" UnhideWhenUsed=\"true\"',\n'   Name=\"Table List 4\"/>',\n'  <w:LsdException Locked=\"false\" SemiHidden=\"true\" UnhideWhenUsed=\"true\"',\n'   Name=\"Table List 5\"/>',\n'  <w:LsdException Locked=\"false\" SemiHidden=\"true\" UnhideWhenUsed=\"true\"',\n'   Name=\"Table List 6\"/>',\n'  <w:LsdException Locked=\"false\" SemiHidden=\"true\" UnhideWhenUsed=\"true\"',\n'   Name=\"Table List 7\"/>',\n'  <w:LsdException Locked=\"false\" SemiHidden=\"true\" UnhideWhenUsed=\"true\"',\n'   Name=\"Table List 8\"/>',\n'  <w:LsdException Locked=\"false\" SemiHidden=\"true\" UnhideWhenUsed=\"true\"',\n'   Name=\"Table 3D effects 1\"/>',\n'  <w:LsdException Locked=\"false\" SemiHidden=\"true\" UnhideWhenUsed=\"true\"',\n'   Name=\"Table 3D effects 2\"/>',\n'  <w:LsdException Locked=\"false\" SemiHidden=\"true\" UnhideWhenUsed=\"true\"',\n'   Name=\"Table 3D effects 3\"/>',\n'  <w:LsdException Locked=\"false\" SemiHidden=\"true\" UnhideWhenUsed=\"true\"',\n'   Name=\"Table Contemporary\"/>',\n'  <w:LsdException Locked=\"false\" SemiHidden=\"true\" UnhideWhenUsed=\"true\"',\n'   Name=\"Table Elegant\"/>',\n'  <w:LsdException Locked=\"false\" SemiHidden=\"true\" UnhideWhenUsed=\"true\"',\n'   Name=\"Table Professional\"/>',\n'  <w:LsdException Locked=\"false\" SemiHidden=\"true\" UnhideWhenUsed=\"true\"',\n'   Name=\"Table Subtle 1\"/>',\n'  <w:LsdException Locked=\"false\" SemiHidden=\"true\" UnhideWhenUsed=\"true\"',\n'   Name=\"Table Subtle 2\"/>',\n'  <w:LsdException Locked=\"false\" SemiHidden=\"true\" UnhideWhenUsed=\"true\"',\n'   Name=\"Table Web 1\"/>',\n'  <w:LsdException Locked=\"false\" SemiHidden=\"true\" UnhideWhenUsed=\"true\"',\n'   Name=\"Table Web 2\"/>',\n'  <w:LsdException Locked=\"false\" SemiHidden=\"true\" UnhideWhenUsed=\"true\"',\n'   Name=\"Table Web 3\"/>',\n'  <w:LsdException Locked=\"false\" SemiHidden=\"true\" UnhideWhenUsed=\"true\"',\n'   Name=\"Balloon Text\"/>',\n'  <w:LsdException Locked=\"false\" Priority=\"39\" Name=\"Table Grid\"/>',\n'  <w:LsdException Locked=\"false\" SemiHidden=\"true\" UnhideWhenUsed=\"true\"',\n'   Name=\"Table Theme\"/>',\n'  <w:LsdException Locked=\"false\" SemiHidden=\"true\" UnhideWhenUsed=\"true\"',\n'   Name=\"Note Level 1\"/>',\n'  <w:LsdException Locked=\"false\" SemiHidden=\"true\" UnhideWhenUsed=\"true\"',\n'   Name=\"Note Level 2\"/>',\n'  <w:LsdException Locked=\"false\" SemiHidden=\"true\" UnhideWhenUsed=\"true\"',\n'   Name=\"Note Level 3\"/>',\n'  <w:LsdException Locked=\"false\" SemiHidden=\"true\" UnhideWhenUsed=\"true\"',\n'   Name=\"Note Level 4\"/>',\n'  <w:LsdException Locked=\"false\" SemiHidden=\"true\" UnhideWhenUsed=\"true\"',\n'   Name=\"Note Level 5\"/>',\n'  <w:LsdException Locked=\"false\" SemiHidden=\"true\" UnhideWhenUsed=\"true\"',\n'   Name=\"Note Level 6\"/>',\n'  <w:LsdException Locked=\"false\" SemiHidden=\"true\" UnhideWhenUsed=\"true\"',\n'   Name=\"Note Level 7\"/>',\n'  <w:LsdException Locked=\"false\" SemiHidden=\"true\" UnhideWhenUsed=\"true\"',\n'   Name=\"Note Level 8\"/>',\n'  <w:LsdException Locked=\"false\" SemiHidden=\"true\" UnhideWhenUsed=\"true\"',\n'   Name=\"Note Level 9\"/>',\n'  <w:LsdException Locked=\"false\" SemiHidden=\"true\" Name=\"Placeholder Text\"/>',\n'  <w:LsdException Locked=\"false\" Priority=\"1\" QFormat=\"true\" Name=\"No Spacing\"/>',\n'  <w:LsdException Locked=\"false\" Priority=\"60\" Name=\"Light Shading\"/>',\n'  <w:LsdException Locked=\"false\" Priority=\"61\" Name=\"Light List\"/>',\n'  <w:LsdException Locked=\"false\" Priority=\"62\" Name=\"Light Grid\"/>',\n'  <w:LsdException Locked=\"false\" Priority=\"63\" Name=\"Medium Shading 1\"/>',\n'  <w:LsdException Locked=\"false\" Priority=\"64\" Name=\"Medium Shading 2\"/>',\n'  <w:LsdException Locked=\"false\" Priority=\"65\" Name=\"Medium List 1\"/>',\n'  <w:LsdException Locked=\"false\" Priority=\"66\" Name=\"Medium List 2\"/>',\n'  <w:LsdException Locked=\"false\" Priority=\"67\" Name=\"Medium Grid 1\"/>',\n'  <w:LsdException Locked=\"false\" Priority=\"68\" Name=\"Medium Grid 2\"/>',\n'  <w:LsdException Locked=\"false\" Priority=\"69\" Name=\"Medium Grid 3\"/>',\n'  <w:LsdException Locked=\"false\" Priority=\"70\" Name=\"Dark List\"/>',\n'  <w:LsdException Locked=\"false\" Priority=\"71\" Name=\"Colorful Shading\"/>',\n'  <w:LsdException Locked=\"false\" Priority=\"72\" Name=\"Colorful List\"/>',\n'  <w:LsdException Locked=\"false\" Priority=\"73\" Name=\"Colorful Grid\"/>',\n'  <w:LsdException Locked=\"false\" Priority=\"60\" Name=\"Light Shading Accent 1\"/>',\n'  <w:LsdException Locked=\"false\" Priority=\"61\" Name=\"Light List Accent 1\"/>',\n'  <w:LsdException Locked=\"false\" Priority=\"62\" Name=\"Light Grid Accent 1\"/>',\n'  <w:LsdException Locked=\"false\" Priority=\"63\" Name=\"Medium Shading 1 Accent 1\"/>',\n'  <w:LsdException Locked=\"false\" Priority=\"64\" Name=\"Medium Shading 2 Accent 1\"/>',\n'  <w:LsdException Locked=\"false\" Priority=\"65\" Name=\"Medium List 1 Accent 1\"/>',\n'  <w:LsdException Locked=\"false\" SemiHidden=\"true\" Name=\"Revision\"/>',\n'  <w:LsdException Locked=\"false\" Priority=\"34\" QFormat=\"true\"',\n'   Name=\"List Paragraph\"/>',\n'  <w:LsdException Locked=\"false\" Priority=\"29\" QFormat=\"true\" Name=\"Quote\"/>',\n'  <w:LsdException Locked=\"false\" Priority=\"30\" QFormat=\"true\"',\n'   Name=\"Intense Quote\"/>',\n'  <w:LsdException Locked=\"false\" Priority=\"66\" Name=\"Medium List 2 Accent 1\"/>',\n'  <w:LsdException Locked=\"false\" Priority=\"67\" Name=\"Medium Grid 1 Accent 1\"/>',\n'  <w:LsdException Locked=\"false\" Priority=\"68\" Name=\"Medium Grid 2 Accent 1\"/>',\n'  <w:LsdException Locked=\"false\" Priority=\"69\" Name=\"Medium Grid 3 Accent 1\"/>',\n'  <w:LsdException Locked=\"false\" Priority=\"70\" Name=\"Dark List Accent 1\"/>',\n'  <w:LsdException Locked=\"false\" Priority=\"71\" Name=\"Colorful Shading Accent 1\"/>',\n'  <w:LsdException Locked=\"false\" Priority=\"72\" Name=\"Colorful List Accent 1\"/>',\n'  <w:LsdException Locked=\"false\" Priority=\"73\" Name=\"Colorful Grid Accent 1\"/>',\n'  <w:LsdException Locked=\"false\" Priority=\"60\" Name=\"Light Shading Accent 2\"/>',\n'  <w:LsdException Locked=\"false\" Priority=\"61\" Name=\"Light List Accent 2\"/>',\n'  <w:LsdException Locked=\"false\" Priority=\"62\" Name=\"Light Grid Accent 2\"/>',\n'  <w:LsdException Locked=\"false\" Priority=\"63\" Name=\"Medium Shading 1 Accent 2\"/>',\n'  <w:LsdException Locked=\"false\" Priority=\"64\" Name=\"Medium Shading 2 Accent 2\"/>',\n'  <w:LsdException Locked=\"false\" Priority=\"65\" Name=\"Medium List 1 Accent 2\"/>',\n'  <w:LsdException Locked=\"false\" Priority=\"66\" Name=\"Medium List 2 Accent 2\"/>',\n'  <w:LsdException Locked=\"false\" Priority=\"67\" Name=\"Medium Grid 1 Accent 2\"/>',\n'  <w:LsdException Locked=\"false\" Priority=\"68\" Name=\"Medium Grid 2 Accent 2\"/>',\n'  <w:LsdException Locked=\"false\" Priority=\"69\" Name=\"Medium Grid 3 Accent 2\"/>',\n'  <w:LsdException Locked=\"false\" Priority=\"70\" Name=\"Dark List Accent 2\"/>',\n'  <w:LsdException Locked=\"false\" Priority=\"71\" Name=\"Colorful Shading Accent 2\"/>',\n'  <w:LsdException Locked=\"false\" Priority=\"72\" Name=\"Colorful List Accent 2\"/>',\n'  <w:LsdException Locked=\"false\" Priority=\"73\" Name=\"Colorful Grid Accent 2\"/>',\n'  <w:LsdException Locked=\"false\" Priority=\"60\" Name=\"Light Shading Accent 3\"/>',\n'  <w:LsdException Locked=\"false\" Priority=\"61\" Name=\"Light List Accent 3\"/>',\n'  <w:LsdException Locked=\"false\" Priority=\"62\" Name=\"Light Grid Accent 3\"/>',\n'  <w:LsdException Locked=\"false\" Priority=\"63\" Name=\"Medium Shading 1 Accent 3\"/>',\n'  <w:LsdException Locked=\"false\" Priority=\"64\" Name=\"Medium Shading 2 Accent 3\"/>',\n'  <w:LsdException Locked=\"false\" Priority=\"65\" Name=\"Medium List 1 Accent 3\"/>',\n'  <w:LsdException Locked=\"false\" Priority=\"66\" Name=\"Medium List 2 Accent 3\"/>',\n'  <w:LsdException Locked=\"false\" Priority=\"67\" Name=\"Medium Grid 1 Accent 3\"/>',\n'  <w:LsdException Locked=\"false\" Priority=\"68\" Name=\"Medium Grid 2 Accent 3\"/>',\n'  <w:LsdException Locked=\"false\" Priority=\"69\" Name=\"Medium Grid 3 Accent 3\"/>',\n'  <w:LsdException Locked=\"false\" Priority=\"70\" Name=\"Dark List Accent 3\"/>',\n'  <w:LsdException Locked=\"false\" Priority=\"71\" Name=\"Colorful Shading Accent 3\"/>',\n'  <w:LsdException Locked=\"false\" Priority=\"72\" Name=\"Colorful List Accent 3\"/>',\n'  <w:LsdException Locked=\"false\" Priority=\"73\" Name=\"Colorful Grid Accent 3\"/>',\n'  <w:LsdException Locked=\"false\" Priority=\"60\" Name=\"Light Shading Accent 4\"/>',\n'  <w:LsdException Locked=\"false\" Priority=\"61\" Name=\"Light List Accent 4\"/>',\n'  <w:LsdException Locked=\"false\" Priority=\"62\" Name=\"Light Grid Accent 4\"/>',\n'  <w:LsdException Locked=\"false\" Priority=\"63\" Name=\"Medium Shading 1 Accent 4\"/>',\n'  <w:LsdException Locked=\"false\" Priority=\"64\" Name=\"Medium Shading 2 Accent 4\"/>',\n'  <w:LsdException Locked=\"false\" Priority=\"65\" Name=\"Medium List 1 Accent 4\"/>',\n'  <w:LsdException Locked=\"false\" Priority=\"66\" Name=\"Medium List 2 Accent 4\"/>',\n'  <w:LsdException Locked=\"false\" Priority=\"67\" Name=\"Medium Grid 1 Accent 4\"/>',\n'  <w:LsdException Locked=\"false\" Priority=\"68\" Name=\"Medium Grid 2 Accent 4\"/>',\n'  <w:LsdException Locked=\"false\" Priority=\"69\" Name=\"Medium Grid 3 Accent 4\"/>',\n'  <w:LsdException Locked=\"false\" Priority=\"70\" Name=\"Dark List Accent 4\"/>',\n'  <w:LsdException Locked=\"false\" Priority=\"71\" Name=\"Colorful Shading Accent 4\"/>',\n'  <w:LsdException Locked=\"false\" Priority=\"72\" Name=\"Colorful List Accent 4\"/>',\n'  <w:LsdException Locked=\"false\" Priority=\"73\" Name=\"Colorful Grid Accent 4\"/>',\n'  <w:LsdException Locked=\"false\" Priority=\"60\" Name=\"Light Shading Accent 5\"/>',\n'  <w:LsdException Locked=\"false\" Priority=\"61\" Name=\"Light List Accent 5\"/>',\n'  <w:LsdException Locked=\"false\" Priority=\"62\" Name=\"Light Grid Accent 5\"/>',\n'  <w:LsdException Locked=\"false\" Priority=\"63\" Name=\"Medium Shading 1 Accent 5\"/>',\n'  <w:LsdException Locked=\"false\" Priority=\"64\" Name=\"Medium Shading 2 Accent 5\"/>',\n'  <w:LsdException Locked=\"false\" Priority=\"65\" Name=\"Medium List 1 Accent 5\"/>',\n'  <w:LsdException Locked=\"false\" Priority=\"66\" Name=\"Medium List 2 Accent 5\"/>',\n'  <w:LsdException Locked=\"false\" Priority=\"67\" Name=\"Medium Grid 1 Accent 5\"/>',\n'  <w:LsdException Locked=\"false\" Priority=\"68\" Name=\"Medium Grid 2 Accent 5\"/>',\n'  <w:LsdException Locked=\"false\" Priority=\"69\" Name=\"Medium Grid 3 Accent 5\"/>',\n'  <w:LsdException Locked=\"false\" Priority=\"70\" Name=\"Dark List Accent 5\"/>',\n'  <w:LsdException Locked=\"false\" Priority=\"71\" Name=\"Colorful Shading Accent 5\"/>',\n'  <w:LsdException Locked=\"false\" Priority=\"72\" Name=\"Colorful List Accent 5\"/>',\n'  <w:LsdException Locked=\"false\" Priority=\"73\" Name=\"Colorful Grid Accent 5\"/>',\n'  <w:LsdException Locked=\"false\" Priority=\"60\" Name=\"Light Shading Accent 6\"/>',\n'  <w:LsdException Locked=\"false\" Priority=\"61\" Name=\"Light List Accent 6\"/>',\n'  <w:LsdException Locked=\"false\" Priority=\"62\" Name=\"Light Grid Accent 6\"/>',\n'  <w:LsdException Locked=\"false\" Priority=\"63\" Name=\"Medium Shading 1 Accent 6\"/>',\n'  <w:LsdException Locked=\"false\" Priority=\"64\" Name=\"Medium Shading 2 Accent 6\"/>',\n'  <w:LsdException Locked=\"false\" Priority=\"65\" Name=\"Medium List 1 Accent 6\"/>',\n'  <w:LsdException Locked=\"false\" Priority=\"66\" Name=\"Medium List 2 Accent 6\"/>',\n'  <w:LsdException Locked=\"false\" Priority=\"67\" Name=\"Medium Grid 1 Accent 6\"/>',\n'  <w:LsdException Locked=\"false\" Priority=\"68\" Name=\"Medium Grid 2 Accent 6\"/>',\n'  <w:LsdException Locked=\"false\" Priority=\"69\" Name=\"Medium Grid 3 Accent 6\"/>',\n'  <w:LsdException Locked=\"false\" Priority=\"70\" Name=\"Dark List Accent 6\"/>',\n'  <w:LsdException Locked=\"false\" Priority=\"71\" Name=\"Colorful Shading Accent 6\"/>',\n'  <w:LsdException Locked=\"false\" Priority=\"72\" Name=\"Colorful List Accent 6\"/>',\n'  <w:LsdException Locked=\"false\" Priority=\"73\" Name=\"Colorful Grid Accent 6\"/>',\n'  <w:LsdException Locked=\"false\" Priority=\"19\" QFormat=\"true\"',\n'   Name=\"Subtle Emphasis\"/>',\n'  <w:LsdException Locked=\"false\" Priority=\"21\" QFormat=\"true\"',\n'   Name=\"Intense Emphasis\"/>',\n'  <w:LsdException Locked=\"false\" Priority=\"31\" QFormat=\"true\"',\n'   Name=\"Subtle Reference\"/>',\n'  <w:LsdException Locked=\"false\" Priority=\"32\" QFormat=\"true\"',\n'   Name=\"Intense Reference\"/>',\n'  <w:LsdException Locked=\"false\" Priority=\"33\" QFormat=\"true\" Name=\"Book Title\"/>',\n'  <w:LsdException Locked=\"false\" Priority=\"37\" SemiHidden=\"true\"',\n'   UnhideWhenUsed=\"true\" Name=\"Bibliography\"/>',\n'  <w:LsdException Locked=\"false\" Priority=\"39\" SemiHidden=\"true\"',\n'   UnhideWhenUsed=\"true\" QFormat=\"true\" Name=\"TOC Heading\"/>',\n'  <w:LsdException Locked=\"false\" Priority=\"41\" Name=\"Plain Table 1\"/>',\n'  <w:LsdException Locked=\"false\" Priority=\"42\" Name=\"Plain Table 2\"/>',\n'  <w:LsdException Locked=\"false\" Priority=\"43\" Name=\"Plain Table 3\"/>',\n'  <w:LsdException Locked=\"false\" Priority=\"44\" Name=\"Plain Table 4\"/>',\n'  <w:LsdException Locked=\"false\" Priority=\"45\" Name=\"Plain Table 5\"/>',\n'  <w:LsdException Locked=\"false\" Priority=\"40\" Name=\"Grid Table Light\"/>',\n'  <w:LsdException Locked=\"false\" Priority=\"46\" Name=\"Grid Table 1 Light\"/>',\n'  <w:LsdException Locked=\"false\" Priority=\"47\" Name=\"Grid Table 2\"/>',\n'  <w:LsdException Locked=\"false\" Priority=\"48\" Name=\"Grid Table 3\"/>',\n'  <w:LsdException Locked=\"false\" Priority=\"49\" Name=\"Grid Table 4\"/>',\n'  <w:LsdException Locked=\"false\" Priority=\"50\" Name=\"Grid Table 5 Dark\"/>',\n'  <w:LsdException Locked=\"false\" Priority=\"51\" Name=\"Grid Table 6 Colorful\"/>',\n'  <w:LsdException Locked=\"false\" Priority=\"52\" Name=\"Grid Table 7 Colorful\"/>',\n'  <w:LsdException Locked=\"false\" Priority=\"46\"',\n'   Name=\"Grid Table 1 Light Accent 1\"/>',\n'  <w:LsdException Locked=\"false\" Priority=\"47\" Name=\"Grid Table 2 Accent 1\"/>',\n'  <w:LsdException Locked=\"false\" Priority=\"48\" Name=\"Grid Table 3 Accent 1\"/>',\n'  <w:LsdException Locked=\"false\" Priority=\"49\" Name=\"Grid Table 4 Accent 1\"/>',\n'  <w:LsdException Locked=\"false\" Priority=\"50\" Name=\"Grid Table 5 Dark Accent 1\"/>',\n'  <w:LsdException Locked=\"false\" Priority=\"51\"',\n'   Name=\"Grid Table 6 Colorful Accent 1\"/>',\n'  <w:LsdException Locked=\"false\" Priority=\"52\"',\n'   Name=\"Grid Table 7 Colorful Accent 1\"/>',\n'  <w:LsdException Locked=\"false\" Priority=\"46\"',\n'   Name=\"Grid Table 1 Light Accent 2\"/>',\n'  <w:LsdException Locked=\"false\" Priority=\"47\" Name=\"Grid Table 2 Accent 2\"/>',\n'  <w:LsdException Locked=\"false\" Priority=\"48\" Name=\"Grid Table 3 Accent 2\"/>',\n'  <w:LsdException Locked=\"false\" Priority=\"49\" Name=\"Grid Table 4 Accent 2\"/>',\n'  <w:LsdException Locked=\"false\" Priority=\"50\" Name=\"Grid Table 5 Dark Accent 2\"/>',\n'  <w:LsdException Locked=\"false\" Priority=\"51\"',\n'   Name=\"Grid Table 6 Colorful Accent 2\"/>',\n'  <w:LsdException Locked=\"false\" Priority=\"52\"',\n'   Name=\"Grid Table 7 Colorful Accent 2\"/>',\n'  <w:LsdException Locked=\"false\" Priority=\"46\"',\n'   Name=\"Grid Table 1 Light Accent 3\"/>',\n'  <w:LsdException Locked=\"false\" Priority=\"47\" Name=\"Grid Table 2 Accent 3\"/>',\n'  <w:LsdException Locked=\"false\" Priority=\"48\" Name=\"Grid Table 3 Accent 3\"/>',\n'  <w:LsdException Locked=\"false\" Priority=\"49\" Name=\"Grid Table 4 Accent 3\"/>',\n'  <w:LsdException Locked=\"false\" Priority=\"50\" Name=\"Grid Table 5 Dark Accent 3\"/>',\n'  <w:LsdException Locked=\"false\" Priority=\"51\"',\n'   Name=\"Grid Table 6 Colorful Accent 3\"/>',\n'  <w:LsdException Locked=\"false\" Priority=\"52\"',\n'   Name=\"Grid Table 7 Colorful Accent 3\"/>',\n'  <w:LsdException Locked=\"false\" Priority=\"46\"',\n'   Name=\"Grid Table 1 Light Accent 4\"/>',\n'  <w:LsdException Locked=\"false\" Priority=\"47\" Name=\"Grid Table 2 Accent 4\"/>',\n'  <w:LsdException Locked=\"false\" Priority=\"48\" Name=\"Grid Table 3 Accent 4\"/>',\n'  <w:LsdException Locked=\"false\" Priority=\"49\" Name=\"Grid Table 4 Accent 4\"/>',\n'  <w:LsdException Locked=\"false\" Priority=\"50\" Name=\"Grid Table 5 Dark Accent 4\"/>',\n'  <w:LsdException Locked=\"false\" Priority=\"51\"',\n'   Name=\"Grid Table 6 Colorful Accent 4\"/>',\n'  <w:LsdException Locked=\"false\" Priority=\"52\"',\n'   Name=\"Grid Table 7 Colorful Accent 4\"/>',\n'  <w:LsdException Locked=\"false\" Priority=\"46\"',\n'   Name=\"Grid Table 1 Light Accent 5\"/>',\n'  <w:LsdException Locked=\"false\" Priority=\"47\" Name=\"Grid Table 2 Accent 5\"/>',\n'  <w:LsdException Locked=\"false\" Priority=\"48\" Name=\"Grid Table 3 Accent 5\"/>',\n'  <w:LsdException Locked=\"false\" Priority=\"49\" Name=\"Grid Table 4 Accent 5\"/>',\n'  <w:LsdException Locked=\"false\" Priority=\"50\" Name=\"Grid Table 5 Dark Accent 5\"/>',\n'  <w:LsdException Locked=\"false\" Priority=\"51\"',\n'   Name=\"Grid Table 6 Colorful Accent 5\"/>',\n'  <w:LsdException Locked=\"false\" Priority=\"52\"',\n'   Name=\"Grid Table 7 Colorful Accent 5\"/>',\n'  <w:LsdException Locked=\"false\" Priority=\"46\"',\n'   Name=\"Grid Table 1 Light Accent 6\"/>',\n'  <w:LsdException Locked=\"false\" Priority=\"47\" Name=\"Grid Table 2 Accent 6\"/>',\n'  <w:LsdException Locked=\"false\" Priority=\"48\" Name=\"Grid Table 3 Accent 6\"/>',\n'  <w:LsdException Locked=\"false\" Priority=\"49\" Name=\"Grid Table 4 Accent 6\"/>',\n'  <w:LsdException Locked=\"false\" Priority=\"50\" Name=\"Grid Table 5 Dark Accent 6\"/>',\n'  <w:LsdException Locked=\"false\" Priority=\"51\"',\n'   Name=\"Grid Table 6 Colorful Accent 6\"/>',\n'  <w:LsdException Locked=\"false\" Priority=\"52\"',\n'   Name=\"Grid Table 7 Colorful Accent 6\"/>',\n'  <w:LsdException Locked=\"false\" Priority=\"46\" Name=\"List Table 1 Light\"/>',\n'  <w:LsdException Locked=\"false\" Priority=\"47\" Name=\"List Table 2\"/>',\n'  <w:LsdException Locked=\"false\" Priority=\"48\" Name=\"List Table 3\"/>',\n'  <w:LsdException Locked=\"false\" Priority=\"49\" Name=\"List Table 4\"/>',\n'  <w:LsdException Locked=\"false\" Priority=\"50\" Name=\"List Table 5 Dark\"/>',\n'  <w:LsdException Locked=\"false\" Priority=\"51\" Name=\"List Table 6 Colorful\"/>',\n'  <w:LsdException Locked=\"false\" Priority=\"52\" Name=\"List Table 7 Colorful\"/>',\n'  <w:LsdException Locked=\"false\" Priority=\"46\"',\n'   Name=\"List Table 1 Light Accent 1\"/>',\n'  <w:LsdException Locked=\"false\" Priority=\"47\" Name=\"List Table 2 Accent 1\"/>',\n'  <w:LsdException Locked=\"false\" Priority=\"48\" Name=\"List Table 3 Accent 1\"/>',\n'  <w:LsdException Locked=\"false\" Priority=\"49\" Name=\"List Table 4 Accent 1\"/>',\n'  <w:LsdException Locked=\"false\" Priority=\"50\" Name=\"List Table 5 Dark Accent 1\"/>',\n'  <w:LsdException Locked=\"false\" Priority=\"51\"',\n'   Name=\"List Table 6 Colorful Accent 1\"/>',\n'  <w:LsdException Locked=\"false\" Priority=\"52\"',\n'   Name=\"List Table 7 Colorful Accent 1\"/>',\n'  <w:LsdException Locked=\"false\" Priority=\"46\"',\n'   Name=\"List Table 1 Light Accent 2\"/>',\n'  <w:LsdException Locked=\"false\" Priority=\"47\" Name=\"List Table 2 Accent 2\"/>',\n'  <w:LsdException Locked=\"false\" Priority=\"48\" Name=\"List Table 3 Accent 2\"/>',\n'  <w:LsdException Locked=\"false\" Priority=\"49\" Name=\"List Table 4 Accent 2\"/>',\n'  <w:LsdException Locked=\"false\" Priority=\"50\" Name=\"List Table 5 Dark Accent 2\"/>',\n'  <w:LsdException Locked=\"false\" Priority=\"51\"',\n'   Name=\"List Table 6 Colorful Accent 2\"/>',\n'  <w:LsdException Locked=\"false\" Priority=\"52\"',\n'   Name=\"List Table 7 Colorful Accent 2\"/>',\n'  <w:LsdException Locked=\"false\" Priority=\"46\"',\n'   Name=\"List Table 1 Light Accent 3\"/>',\n'  <w:LsdException Locked=\"false\" Priority=\"47\" Name=\"List Table 2 Accent 3\"/>',\n'  <w:LsdException Locked=\"false\" Priority=\"48\" Name=\"List Table 3 Accent 3\"/>',\n'  <w:LsdException Locked=\"false\" Priority=\"49\" Name=\"List Table 4 Accent 3\"/>',\n'  <w:LsdException Locked=\"false\" Priority=\"50\" Name=\"List Table 5 Dark Accent 3\"/>',\n'  <w:LsdException Locked=\"false\" Priority=\"51\"',\n'   Name=\"List Table 6 Colorful Accent 3\"/>',\n'  <w:LsdException Locked=\"false\" Priority=\"52\"',\n'   Name=\"List Table 7 Colorful Accent 3\"/>',\n'  <w:LsdException Locked=\"false\" Priority=\"46\"',\n'   Name=\"List Table 1 Light Accent 4\"/>',\n'  <w:LsdException Locked=\"false\" Priority=\"47\" Name=\"List Table 2 Accent 4\"/>',\n'  <w:LsdException Locked=\"false\" Priority=\"48\" Name=\"List Table 3 Accent 4\"/>',\n'  <w:LsdException Locked=\"false\" Priority=\"49\" Name=\"List Table 4 Accent 4\"/>',\n'  <w:LsdException Locked=\"false\" Priority=\"50\" Name=\"List Table 5 Dark Accent 4\"/>',\n'  <w:LsdException Locked=\"false\" Priority=\"51\"',\n'   Name=\"List Table 6 Colorful Accent 4\"/>',\n'  <w:LsdException Locked=\"false\" Priority=\"52\"',\n'   Name=\"List Table 7 Colorful Accent 4\"/>',\n'  <w:LsdException Locked=\"false\" Priority=\"46\"',\n'   Name=\"List Table 1 Light Accent 5\"/>',\n'  <w:LsdException Locked=\"false\" Priority=\"47\" Name=\"List Table 2 Accent 5\"/>',\n'  <w:LsdException Locked=\"false\" Priority=\"48\" Name=\"List Table 3 Accent 5\"/>',\n'  <w:LsdException Locked=\"false\" Priority=\"49\" Name=\"List Table 4 Accent 5\"/>',\n'  <w:LsdException Locked=\"false\" Priority=\"50\" Name=\"List Table 5 Dark Accent 5\"/>',\n'  <w:LsdException Locked=\"false\" Priority=\"51\"',\n'   Name=\"List Table 6 Colorful Accent 5\"/>',\n'  <w:LsdException Locked=\"false\" Priority=\"52\"',\n'   Name=\"List Table 7 Colorful Accent 5\"/>',\n'  <w:LsdException Locked=\"false\" Priority=\"46\"',\n'   Name=\"List Table 1 Light Accent 6\"/>',\n'  <w:LsdException Locked=\"false\" Priority=\"47\" Name=\"List Table 2 Accent 6\"/>',\n'  <w:LsdException Locked=\"false\" Priority=\"48\" Name=\"List Table 3 Accent 6\"/>',\n'  <w:LsdException Locked=\"false\" Priority=\"49\" Name=\"List Table 4 Accent 6\"/>',\n'  <w:LsdException Locked=\"false\" Priority=\"50\" Name=\"List Table 5 Dark Accent 6\"/>',\n'  <w:LsdException Locked=\"false\" Priority=\"51\"',\n'   Name=\"List Table 6 Colorful Accent 6\"/>',\n'  <w:LsdException Locked=\"false\" Priority=\"52\"',\n'   Name=\"List Table 7 Colorful Accent 6\"/>',\n' </w:LatentStyles>',\n'</xml><![endif]-->',\n'<style>',\n'<!--',\n' /* Font Definitions */',\n'@font-face',\n'\t{font-family:\"Courier New\";',\n'\tpanose-1:2 7 3 9 2 2 5 2 4 4;',\n'\tmso-font-charset:0;',\n'\tmso-generic-font-family:auto;',\n'\tmso-font-pitch:variable;',\n'\tmso-font-signature:-536859905 -1073711037 9 0 511 0;}',\n'@font-face',\n'\t{font-family:Wingdings;',\n'\tpanose-1:5 0 0 0 0 0 0 0 0 0;',\n'\tmso-font-charset:2;',\n'\tmso-generic-font-family:auto;',\n'\tmso-font-pitch:variable;',\n'\tmso-font-signature:0 268435456 0 0 -2147483648 0;}',\n'@font-face',\n'\t{font-family:\"ＭＳ 明朝\";',\n'\tmso-font-charset:128;',\n'\tmso-generic-font-family:auto;',\n'\tmso-font-pitch:variable;',\n'\tmso-font-signature:-536870145 1791491579 134217746 0 131231 0;}',\n'@font-face',\n'\t{font-family:\"Cambria Math\";',\n'\tpanose-1:2 4 5 3 5 4 6 3 2 4;',\n'\tmso-font-charset:0;',\n'\tmso-generic-font-family:auto;',\n'\tmso-font-pitch:variable;',\n'\tmso-font-signature:-536870145 1107305727 0 0 415 0;}',\n'@font-face',\n'\t{font-family:Cambria;',\n'\tpanose-1:2 4 5 3 5 4 6 3 2 4;',\n'\tmso-font-charset:0;',\n'\tmso-generic-font-family:auto;',\n'\tmso-font-pitch:variable;',\n'\tmso-font-signature:-536870145 1073743103 0 0 415 0;}',\n'@font-face',\n'\t{font-family:\"Comic Sans MS\";',\n'\tpanose-1:3 15 7 2 3 3 2 2 2 4;',\n'\tmso-font-charset:0;',\n'\tmso-generic-font-family:auto;',\n'\tmso-font-pitch:variable;',\n'\tmso-font-signature:647 0 0 0 159 0;}',\n' /* Style Definitions */',\n'p.MsoNormal, li.MsoNormal, div.MsoNormal',\n'\t{mso-style-unhide:no;',\n'\tmso-style-qformat:yes;',\n'\tmso-style-parent:\"\";',\n'\tmargin:0in;',\n'\tmargin-bottom:.0001pt;',\n'\tmso-pagination:widow-orphan;',\n'\tfont-size:12.0pt;',\n'\tfont-family:Cambria;',\n'\tmso-ascii-font-family:Cambria;',\n'\tmso-ascii-theme-font:minor-latin;',\n'\tmso-fareast-font-family:\"ＭＳ 明朝\";',\n'\tmso-fareast-theme-font:minor-fareast;',\n'\tmso-hansi-font-family:Cambria;',\n'\tmso-hansi-theme-font:minor-latin;',\n'\tmso-bidi-font-family:\"Times New Roman\";',\n'\tmso-bidi-theme-font:minor-bidi;}',\n'p.MsoListParagraph, li.MsoListParagraph, div.MsoListParagraph',\n'\t{mso-style-priority:34;',\n'\tmso-style-unhide:no;',\n'\tmso-style-qformat:yes;',\n'\tmargin-top:0in;',\n'\tmargin-right:0in;',\n'\tmargin-bottom:0in;',\n'\tmargin-left:.5in;',\n'\tmargin-bottom:.0001pt;',\n'\tmso-add-space:auto;',\n'\tmso-pagination:widow-orphan;',\n'\tfont-size:12.0pt;',\n'\tfont-family:Cambria;',\n'\tmso-ascii-font-family:Cambria;',\n'\tmso-ascii-theme-font:minor-latin;',\n'\tmso-fareast-font-family:\"ＭＳ 明朝\";',\n'\tmso-fareast-theme-font:minor-fareast;',\n'\tmso-hansi-font-family:Cambria;',\n'\tmso-hansi-theme-font:minor-latin;',\n'\tmso-bidi-font-family:\"Times New Roman\";',\n'\tmso-bidi-theme-font:minor-bidi;}',\n'p.MsoListParagraphCxSpFirst, li.MsoListParagraphCxSpFirst, div.MsoListParagraphCxSpFirst',\n'\t{mso-style-priority:34;',\n'\tmso-style-unhide:no;',\n'\tmso-style-qformat:yes;',\n'\tmso-style-type:export-only;',\n'\tmargin-top:0in;',\n'\tmargin-right:0in;',\n'\tmargin-bottom:0in;',\n'\tmargin-left:.5in;',\n'\tmargin-bottom:.0001pt;',\n'\tmso-add-space:auto;',\n'\tmso-pagination:widow-orphan;',\n'\tfont-size:12.0pt;',\n'\tfont-family:Cambria;',\n'\tmso-ascii-font-family:Cambria;',\n'\tmso-ascii-theme-font:minor-latin;',\n'\tmso-fareast-font-family:\"ＭＳ 明朝\";',\n'\tmso-fareast-theme-font:minor-fareast;',\n'\tmso-hansi-font-family:Cambria;',\n'\tmso-hansi-theme-font:minor-latin;',\n'\tmso-bidi-font-family:\"Times New Roman\";',\n'\tmso-bidi-theme-font:minor-bidi;}',\n'p.MsoListParagraphCxSpMiddle, li.MsoListParagraphCxSpMiddle, div.MsoListParagraphCxSpMiddle',\n'\t{mso-style-priority:34;',\n'\tmso-style-unhide:no;',\n'\tmso-style-qformat:yes;',\n'\tmso-style-type:export-only;',\n'\tmargin-top:0in;',\n'\tmargin-right:0in;',\n'\tmargin-bottom:0in;',\n'\tmargin-left:.5in;',\n'\tmargin-bottom:.0001pt;',\n'\tmso-add-space:auto;',\n'\tmso-pagination:widow-orphan;',\n'\tfont-size:12.0pt;',\n'\tfont-family:Cambria;',\n'\tmso-ascii-font-family:Cambria;',\n'\tmso-ascii-theme-font:minor-latin;',\n'\tmso-fareast-font-family:\"ＭＳ 明朝\";',\n'\tmso-fareast-theme-font:minor-fareast;',\n'\tmso-hansi-font-family:Cambria;',\n'\tmso-hansi-theme-font:minor-latin;',\n'\tmso-bidi-font-family:\"Times New Roman\";',\n'\tmso-bidi-theme-font:minor-bidi;}',\n'p.MsoListParagraphCxSpLast, li.MsoListParagraphCxSpLast, div.MsoListParagraphCxSpLast',\n'\t{mso-style-priority:34;',\n'\tmso-style-unhide:no;',\n'\tmso-style-qformat:yes;',\n'\tmso-style-type:export-only;',\n'\tmargin-top:0in;',\n'\tmargin-right:0in;',\n'\tmargin-bottom:0in;',\n'\tmargin-left:.5in;',\n'\tmargin-bottom:.0001pt;',\n'\tmso-add-space:auto;',\n'\tmso-pagination:widow-orphan;',\n'\tfont-size:12.0pt;',\n'\tfont-family:Cambria;',\n'\tmso-ascii-font-family:Cambria;',\n'\tmso-ascii-theme-font:minor-latin;',\n'\tmso-fareast-font-family:\"ＭＳ 明朝\";',\n'\tmso-fareast-theme-font:minor-fareast;',\n'\tmso-hansi-font-family:Cambria;',\n'\tmso-hansi-theme-font:minor-latin;',\n'\tmso-bidi-font-family:\"Times New Roman\";',\n'\tmso-bidi-theme-font:minor-bidi;}',\n'.MsoChpDefault',\n'\t{mso-style-type:export-only;',\n'\tmso-default-props:yes;',\n'\tfont-family:Cambria;',\n'\tmso-ascii-font-family:Cambria;',\n'\tmso-ascii-theme-font:minor-latin;',\n'\tmso-fareast-font-family:\"ＭＳ 明朝\";',\n'\tmso-fareast-theme-font:minor-fareast;',\n'\tmso-hansi-font-family:Cambria;',\n'\tmso-hansi-theme-font:minor-latin;',\n'\tmso-bidi-font-family:\"Times New Roman\";',\n'\tmso-bidi-theme-font:minor-bidi;}',\n'@page WordSection1',\n'\t{size:8.5in 11.0in;',\n'\tmargin:1.0in 1.0in 1.0in 1.0in;',\n'\tmso-header-margin:.5in;',\n'\tmso-footer-margin:.5in;',\n'\tmso-paper-source:0;}',\n'div.WordSection1',\n'\t{page:WordSection1;}',\n' /* List Definitions */',\n'@list l0',\n'\t{mso-list-id:1751544061;',\n'\tmso-list-type:hybrid;',\n'\tmso-list-template-ids:-622833810 67698689 67698691 67698693 67698689 67698691 67698693 67698689 67698691 67698693;}',\n'@list l0:level1',\n'\t{mso-level-number-format:bullet;',\n'\tmso-level-text:;',\n'\tmso-level-tab-stop:none;',\n'\tmso-level-number-position:left;',\n'\ttext-indent:-.25in;',\n'\tfont-family:Symbol;}',\n'@list l0:level2',\n'\t{mso-level-number-format:bullet;',\n'\tmso-level-text:o;',\n'\tmso-level-tab-stop:none;',\n'\tmso-level-number-position:left;',\n'\ttext-indent:-.25in;',\n'\tfont-family:\"Courier New\";',\n'\tmso-bidi-font-family:\"Times New Roman\";}',\n'@list l0:level3',\n'\t{mso-level-number-format:bullet;',\n'\tmso-level-text:;',\n'\tmso-level-tab-stop:none;',\n'\tmso-level-number-position:left;',\n'\ttext-indent:-.25in;',\n'\tfont-family:Wingdings;}',\n'@list l0:level4',\n'\t{mso-level-number-format:bullet;',\n'\tmso-level-text:;',\n'\tmso-level-tab-stop:none;',\n'\tmso-level-number-position:left;',\n'\ttext-indent:-.25in;',\n'\tfont-family:Symbol;}',\n'@list l0:level5',\n'\t{mso-level-number-format:bullet;',\n'\tmso-level-text:o;',\n'\tmso-level-tab-stop:none;',\n'\tmso-level-number-position:left;',\n'\ttext-indent:-.25in;',\n'\tfont-family:\"Courier New\";',\n'\tmso-bidi-font-family:\"Times New Roman\";}',\n'@list l0:level6',\n'\t{mso-level-number-format:bullet;',\n'\tmso-level-text:;',\n'\tmso-level-tab-stop:none;',\n'\tmso-level-number-position:left;',\n'\ttext-indent:-.25in;',\n'\tfont-family:Wingdings;}',\n'@list l0:level7',\n'\t{mso-level-number-format:bullet;',\n'\tmso-level-text:;',\n'\tmso-level-tab-stop:none;',\n'\tmso-level-number-position:left;',\n'\ttext-indent:-.25in;',\n'\tfont-family:Symbol;}',\n'@list l0:level8',\n'\t{mso-level-number-format:bullet;',\n'\tmso-level-text:o;',\n'\tmso-level-tab-stop:none;',\n'\tmso-level-number-position:left;',\n'\ttext-indent:-.25in;',\n'\tfont-family:\"Courier New\";',\n'\tmso-bidi-font-family:\"Times New Roman\";}',\n'@list l0:level9',\n'\t{mso-level-number-format:bullet;',\n'\tmso-level-text:;',\n'\tmso-level-tab-stop:none;',\n'\tmso-level-number-position:left;',\n'\ttext-indent:-.25in;',\n'\tfont-family:Wingdings;}',\n'@list l1',\n'\t{mso-list-id:1845972260;',\n'\tmso-list-type:hybrid;',\n'\tmso-list-template-ids:1079810306 67698703 67698713 67698715 67698703 67698713 67698715 67698703 67698713 67698715;}',\n'@list l1:level1',\n'\t{mso-level-tab-stop:none;',\n'\tmso-level-number-position:left;',\n'\ttext-indent:-.25in;}',\n'@list l1:level2',\n'\t{mso-level-number-format:alpha-lower;',\n'\tmso-level-tab-stop:none;',\n'\tmso-level-number-position:left;',\n'\ttext-indent:-.25in;}',\n'@list l1:level3',\n'\t{mso-level-number-format:roman-lower;',\n'\tmso-level-tab-stop:none;',\n'\tmso-level-number-position:right;',\n'\ttext-indent:-9.0pt;}',\n'@list l1:level4',\n'\t{mso-level-tab-stop:none;',\n'\tmso-level-number-position:left;',\n'\ttext-indent:-.25in;}',\n'@list l1:level5',\n'\t{mso-level-number-format:alpha-lower;',\n'\tmso-level-tab-stop:none;',\n'\tmso-level-number-position:left;',\n'\ttext-indent:-.25in;}',\n'@list l1:level6',\n'\t{mso-level-number-format:roman-lower;',\n'\tmso-level-tab-stop:none;',\n'\tmso-level-number-position:right;',\n'\ttext-indent:-9.0pt;}',\n'@list l1:level7',\n'\t{mso-level-tab-stop:none;',\n'\tmso-level-number-position:left;',\n'\ttext-indent:-.25in;}',\n'@list l1:level8',\n'\t{mso-level-number-format:alpha-lower;',\n'\tmso-level-tab-stop:none;',\n'\tmso-level-number-position:left;',\n'\ttext-indent:-.25in;}',\n'@list l1:level9',\n'\t{mso-level-number-format:roman-lower;',\n'\tmso-level-tab-stop:none;',\n'\tmso-level-number-position:right;',\n'\ttext-indent:-9.0pt;}',\n'ol',\n'\t{margin-bottom:0in;}',\n'ul',\n'\t{margin-bottom:0in;}',\n'-->',\n'</style>',\n'<!--[if gte mso 10]>',\n'<style>',\n' /* Style Definitions */',\n'table.MsoNormalTable',\n'\t{mso-style-name:\"Table Normal\";',\n'\tmso-tstyle-rowband-size:0;',\n'\tmso-tstyle-colband-size:0;',\n'\tmso-style-noshow:yes;',\n'\tmso-style-priority:99;',\n'\tmso-style-parent:\"\";',\n'\tmso-padding-alt:0in 5.4pt 0in 5.4pt;',\n'\tmso-para-margin:0in;',\n'\tmso-para-margin-bottom:.0001pt;',\n'\tmso-pagination:widow-orphan;',\n'\tfont-size:12.0pt;',\n'\tfont-family:Cambria;',\n'\tmso-ascii-font-family:Cambria;',\n'\tmso-ascii-theme-font:minor-latin;',\n'\tmso-hansi-font-family:Cambria;',\n'\tmso-hansi-theme-font:minor-latin;}',\n'</style>',\n'<![endif]-->',\n'</head>',\n'',\n'<body bgcolor=white lang=EN-US style=\\'tab-interval:.5in\\'>',\n'<!--StartFragment-->',\n'',\n'<p class=MsoNormal><span style=\\'font-size:14.0pt;font-family:\"Comic Sans MS\"\\'>My',\n'complicated <b style=\\'mso-bidi-font-weight:normal\\'>word document renders</b> <i ',\n'style=\\'mso-bidi-font-style:normal\\'>like this in the card</i> generator.<o:p></o:p></span></p>',\n'',\n'<p class=MsoNormal><span style=\\'font-size:14.0pt;font-family:\"Comic Sans MS\"\\'><o:p>&nbsp;</o:p></span></p>',\n'',\n'<p class=MsoNormal><span style=\\'font-size:24.0pt;font-family:\"Comic Sans MS\"\\'>Test',\n'big <span style=\\'color:red\\'>text</span><o:p></o:p></span></p>',\n'',\n'<p class=MsoNormal><span style=\\'font-size:24.0pt;font-family:\"Comic Sans MS\"\\'><o:p>&nbsp;</o:p></span></p>',\n'',\n'<p class=MsoNormal><span style=\\'font-size:24.0pt;font-family:\"Comic Sans MS\"\\'><o:p>&nbsp;</o:p></span></p>',\n'',\n'<p class=MsoNormal><span style=\\'font-size:18.0pt;font-family:\"Comic Sans MS\"\\'>Testing',\n'smaller text and <s>crossed out text.<o:p></o:p></s></span></p>',\n'',\n'<p class=MsoNormal><s><span style=\\'font-size:18.0pt;font-family:\"Comic Sans MS\"\\'><o:p><span ',\n' style=\\'text-decoration:none\\'>&nbsp;</span></o:p></span></s></p>',\n'',\n'<p class=MsoListParagraphCxSpFirst style=\\'text-indent:-.25in;mso-list:l0 level1 lfo1\\'><![if !supportLists]><span ',\n'style=\\'font-size:14.0pt;font-family:Symbol;mso-fareast-font-family:Symbol;',\n'mso-bidi-font-family:Symbol\\'><span style=\\'mso-list:Ignore\\'>·<span ',\n'style=\\'font:7.0pt \"Times New Roman\"\\'>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; </span></span></span><![endif]><span ',\n'style=\\'font-size:14.0pt;font-family:\"Comic Sans MS\"\\'>Test list<o:p></o:p></span></p>',\n'',\n'<p class=MsoListParagraphCxSpMiddle style=\\'text-indent:-.25in;mso-list:l0 level1 lfo1\\'><![if !supportLists]><span ',\n'style=\\'font-size:14.0pt;font-family:Symbol;mso-fareast-font-family:Symbol;',\n'mso-bidi-font-family:Symbol\\'><span style=\\'mso-list:Ignore\\'>·<span ',\n'style=\\'font:7.0pt \"Times New Roman\"\\'>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; </span></span></span><![endif]><span ',\n'style=\\'font-size:14.0pt;font-family:\"Comic Sans MS\"\\'>Test<o:p></o:p></span></p>',\n'',\n'<p class=MsoListParagraphCxSpMiddle style=\\'margin-left:1.0in;mso-add-space:',\n'auto;text-indent:-.25in;mso-list:l0 level2 lfo1\\'><![if !supportLists]><span ',\n'style=\\'font-size:14.0pt;font-family:\"Courier New\";mso-fareast-font-family:\"Courier New\"\\'><span ',\n'style=\\'mso-list:Ignore\\'>o<span style=\\'font:7.0pt \"Times New Roman\"\\'>&nbsp;&nbsp;',\n'</span></span></span><![endif]><span style=\\'font-size:14.0pt;font-family:\"Comic Sans MS\"\\'>Test',\n'indented<o:p></o:p></span></p>',\n'',\n'<p class=MsoListParagraphCxSpMiddle style=\\'text-indent:-.25in;mso-list:l0 level1 lfo1\\'><![if !supportLists]><span ',\n'style=\\'font-size:14.0pt;font-family:Symbol;mso-fareast-font-family:Symbol;',\n'mso-bidi-font-family:Symbol\\'><span style=\\'mso-list:Ignore\\'>·<span ',\n'style=\\'font:7.0pt \"Times New Roman\"\\'>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; </span></span></span><![endif]><span ',\n'style=\\'font-size:14.0pt;font-family:\"Comic Sans MS\"\\'>Tes6t<o:p></o:p></span></p>',\n'',\n'<p class=MsoListParagraphCxSpMiddle><span style=\\'font-size:14.0pt;font-family:',\n'\"Comic Sans MS\"\\'><o:p>&nbsp;</o:p></span></p>',\n'',\n'<p class=MsoListParagraphCxSpMiddle style=\\'text-indent:-.25in;mso-list:l1 level1 lfo2\\'><![if !supportLists]><span ',\n'style=\\'font-size:14.0pt;font-family:\"Comic Sans MS\";mso-fareast-font-family:',\n'\"Comic Sans MS\";mso-bidi-font-family:\"Comic Sans MS\"\\'><span style=\\'mso-list:',\n'Ignore\\'>1.<span style=\\'font:7.0pt \"Times New Roman\"\\'>&nbsp;&nbsp;&nbsp;&nbsp; </span></span></span><![endif]><span ',\n'style=\\'font-size:14.0pt;font-family:\"Comic Sans MS\"\\'>tes t test test<o:p></o:p></span></p>',\n'',\n'<p class=MsoListParagraphCxSpLast style=\\'margin-left:1.0in;mso-add-space:auto;',\n'text-indent:-.25in;mso-list:l1 level2 lfo2\\'><![if !supportLists]><span ',\n'style=\\'font-size:14.0pt;font-family:\"Comic Sans MS\";mso-fareast-font-family:',\n'\"Comic Sans MS\";mso-bidi-font-family:\"Comic Sans MS\"\\'><span style=\\'mso-list:',\n'Ignore\\'>a.<span style=\\'font:7.0pt \"Times New Roman\"\\'>&nbsp;&nbsp;&nbsp; </span></span></span><![endif]><span ',\n'style=\\'font-size:14.0pt;font-family:\"Comic Sans MS\"\\'>tes t indented<o:p></o:p></span></p>',\n'',\n'<p class=MsoNormal><o:p>&nbsp;</o:p></p>',\n'',\n'<!--EndFragment-->',\n'</body>',\n'',\n'</html>'].join('');\n"
  },
  {
    "path": "spec/init.spec.js",
    "content": "/*global _ */\n\ndescribe('Initialization TestCase', function () {\n    'use strict';\n\n    beforeEach(function () {\n        setupTestHelpers.call(this);\n        this.el = this.createElement('div', 'editor', 'lorem ipsum');\n    });\n\n    afterEach(function () {\n        this.cleanupTest();\n    });\n\n    describe('Objects', function () {\n        it('should call init when instantiated', function () {\n            spyOn(MediumEditor.prototype, 'init');\n            var editor = this.newMediumEditor('.test');\n            expect(editor.init).toHaveBeenCalled();\n        });\n\n        it('should accept multiple instances', function () {\n            spyOn(MediumEditor.prototype, 'init');\n            var editor1 = this.newMediumEditor('.test'),\n                editor2 = this.newMediumEditor('.test');\n            expect(editor1 === editor2).toBe(false);\n            expect(MediumEditor.prototype.init).toHaveBeenCalled();\n            expect(MediumEditor.prototype.init.calls.count()).toBe(2);\n        });\n\n        it('should do nothing when selector does not return any elements', function () {\n            spyOn(MediumEditor.prototype, 'setup');\n            var editor = this.newMediumEditor('.test');\n            expect(editor.isActive).toBeFalsy();\n            expect(editor.events).toBeUndefined();\n            expect(editor.getExtensionByName('toolbar')).toBeUndefined();\n            expect(editor.getExtensionByName('anchor')).toBeUndefined();\n            expect(editor.getExtensionByName('anchor-preview')).toBeUndefined();\n        });\n    });\n\n    describe('Elements', function () {\n        it('should allow a string as parameter', function () {\n            spyOn(document, 'querySelectorAll').and.callThrough();\n            this.newMediumEditor('.test');\n            expect(document.querySelectorAll).toHaveBeenCalled();\n        });\n\n        it('should allow a list of html elements as parameters', function () {\n            var elements = document.querySelectorAll('span'),\n                editor = this.newMediumEditor(elements);\n            expect(editor.elements.length).toEqual(elements.length);\n        });\n\n        it('should allow a single element as parameter', function () {\n            var element = document.querySelector('span'),\n                editor = this.newMediumEditor(element);\n            expect(editor.elements).toEqual([element]);\n        });\n\n        it('should always initalize elements as an Array', function () {\n            var nodeList = document.querySelectorAll('span'),\n                node = document.querySelector('span'),\n                editor = this.newMediumEditor(nodeList);\n\n            // nodeList is a NodeList, similar to an array but not of the same type\n            expect(editor.elements.length).toEqual(nodeList.length);\n            expect(Array.isArray(nodeList)).toBe(false);\n            expect(typeof editor.elements.forEach).toBe('function');\n            editor.destroy();\n\n            editor = this.newMediumEditor('span');\n            expect(editor.elements.length).toEqual(nodeList.length);\n            editor.destroy();\n\n            editor = this.newMediumEditor(node);\n            expect(editor.elements.length).toEqual(1);\n            expect(editor.elements[0]).toBe(node);\n            editor.destroy();\n\n            editor = this.newMediumEditor();\n            expect(editor.elements).not.toBe(null);\n            expect(editor.elements.length).toBe(0);\n            editor.destroy();\n        });\n\n        it('should be available after destroying and calling setup again', function () {\n            var editor = this.newMediumEditor('.editor');\n            expect(editor.elements.length).toBe(1);\n            editor.destroy();\n            expect(editor.elements.length).toBe(0);\n            editor.setup();\n            expect(editor.elements.length).toBe(1);\n        });\n    });\n\n    describe('With a valid element', function () {\n        it('should have a default set of options', function () {\n            var defaultOptions = {\n                delay: 0,\n                disableReturn: false,\n                disableDoubleReturn: false,\n                disableExtraSpaces: false,\n                disableEditing: false,\n                autoLink: false,\n                elementsContainer: document.body,\n                contentWindow: window,\n                ownerDocument: document,\n                buttonLabels: false,\n                targetBlank: false,\n                extensions: {},\n                activeButtonClass: 'medium-editor-button-active',\n                spellcheck: true\n            },\n                editor = this.newMediumEditor('.editor');\n            expect(Object.keys(editor.options).length).toBe(Object.keys(defaultOptions).length);\n            expect(_.isEqual(editor.options, defaultOptions)).toBe(true);\n        });\n\n        it('should accept custom options values', function () {\n            var options = {\n                delay: 300,\n                toolbar: {\n                    diffLeft: 10,\n                    diffTop: 5\n                },\n                anchor: {\n                    placeholderText: 'test',\n                    targetCheckboxText: 'new window?'\n                },\n                paste: {\n                    forcePlainText: false,\n                    cleanPastedHTML: true\n                }\n            },\n                editor = this.newMediumEditor('.editor', options);\n            Object.keys(options).forEach(function (customOption) {\n                expect(editor.options[customOption]).toBe(options[customOption]);\n            });\n        });\n\n        it('should call the default initialization methods', function () {\n            spyOn(MediumEditor.prototype, 'setup').and.callThrough();\n            spyOn(MediumEditor.extensions.toolbar.prototype, 'createToolbar').and.callThrough();\n            spyOn(MediumEditor.extensions.anchor.prototype, 'createForm').and.callThrough();\n            spyOn(MediumEditor.extensions.anchorPreview.prototype, 'createPreview').and.callThrough();\n            var editor = this.newMediumEditor('.editor'),\n                anchorExtension = editor.getExtensionByName('anchor'),\n                anchorPreview = editor.getExtensionByName('anchor-preview'),\n                toolbar = editor.getExtensionByName('toolbar');\n            expect(editor.setup).toHaveBeenCalled();\n            expect(toolbar).not.toBeUndefined();\n            expect(toolbar.createToolbar).toHaveBeenCalled();\n            expect(anchorExtension).not.toBeUndefined();\n            expect(anchorExtension.createForm).toHaveBeenCalled();\n            expect(anchorPreview).not.toBeUndefined();\n            expect(anchorPreview.createPreview).toHaveBeenCalled();\n        });\n\n        it('should set the ID according to the numbers of editors instantiated', function () {\n            var editor1 = this.newMediumEditor('.editor'),\n                firstId = editor1.id,\n                editor2 = this.newMediumEditor(this.createElement('div')),\n                editor3 = this.newMediumEditor(this.createElement('div'));\n\n            expect(editor2.id).toBe(firstId + 1);\n            expect(editor3.id).toBe(firstId + 2);\n        });\n\n        it('should not reset id when destroyed and then re-initialized', function () {\n            var editor1 = this.newMediumEditor('.editor'),\n                origId = editor1.id,\n                editor2;\n\n            this.createElement('div', 'editor-two');\n            editor2 = this.newMediumEditor('.editor-two');\n            editor1.destroy();\n            editor1.init('.editor');\n\n            expect(editor1.id).not.toEqual(editor2.id);\n            expect(editor1.id).toBe(origId);\n        });\n\n        it('should use document.body as element container when no container element is specified', function () {\n            spyOn(document.body, 'appendChild').and.callThrough();\n            this.newMediumEditor('.editor');\n            expect(document.body.appendChild).toHaveBeenCalled();\n        });\n\n        it('should accept a custom element container for MediumEditor elements', function () {\n            var div = this.createElement('div');\n            spyOn(div, 'appendChild').and.callThrough();\n            this.newMediumEditor('.editor', {\n                elementsContainer: div\n            });\n            expect(div.appendChild).toHaveBeenCalled();\n        });\n    });\n});\n"
  },
  {
    "path": "spec/keyboard-commands.spec.js",
    "content": "/*global fireEvent, selectElementContentsAndFire */\n\ndescribe('KeyboardCommands TestCase', function () {\n    'use strict';\n\n    beforeEach(function () {\n        setupTestHelpers.call(this);\n        this.el = this.createElement('div', 'editor', 'lorem ipsum');\n    });\n\n    afterEach(function () {\n        this.cleanupTest();\n    });\n\n    describe('execAction', function () {\n        it('should be executed when the keys are pressed', function () {\n            spyOn(MediumEditor.prototype, 'execAction');\n            var editor = this.newMediumEditor('.editor');\n\n            selectElementContentsAndFire(editor.elements[0]);\n            // bold\n            fireEvent(editor.elements[0], 'keydown', {\n                keyCode: 'B'.charCodeAt(0),\n                ctrlKey: true,\n                metaKey: true\n            });\n            expect(editor.execAction).toHaveBeenCalledWith('bold');\n\n            // italics\n            fireEvent(editor.elements[0], 'keydown', {\n                keyCode: 'I'.charCodeAt(0),\n                ctrlKey: true,\n                metaKey: true\n            });\n            expect(editor.execAction).toHaveBeenCalledWith('italic');\n\n            // underline\n            fireEvent(editor.elements[0], 'keydown', {\n                keyCode: 'U'.charCodeAt(0),\n                ctrlKey: true,\n                metaKey: true\n            });\n            expect(editor.execAction).toHaveBeenCalledWith('underline');\n        });\n\n        it('should be executed for custom commands', function () {\n            spyOn(MediumEditor.prototype, 'execAction');\n            var editor = this.newMediumEditor('.editor', {\n                keyboardCommands: {\n                    commands: [\n                        {\n                            command: 'superscript',\n                            key: 'p',\n                            meta: true,\n                            shift: false,\n                            alt: false\n                        },\n                        {\n                            command: 'subscript',\n                            key: 'p',\n                            meta: true,\n                            shift: true,\n                            alt: false\n                        }\n                    ]\n                }\n            });\n\n            selectElementContentsAndFire(editor.elements[0]);\n            fireEvent(editor.elements[0], 'keydown', {\n                keyCode: 'p'.charCodeAt(0),\n                ctrlKey: true,\n                metaKey: true,\n                shiftKey: true\n            });\n            expect(editor.execAction).toHaveBeenCalledWith('subscript');\n            fireEvent(editor.elements[0], 'keydown', {\n                keyCode: 'p'.charCodeAt(0),\n                ctrlKey: true,\n                metaKey: true\n            });\n            expect(editor.execAction).toHaveBeenCalledWith('superscript');\n        });\n\n        it('should execute custom command functions', function () {\n            var foo, editor;\n\n            foo = {\n                bar: function () {\n                    return true;\n                }\n            };\n\n            spyOn(foo, 'bar');\n\n            editor = this.newMediumEditor('.editor', {\n                keyboardCommands: {\n                    commands: [\n                        {\n                            command: foo.bar,\n                            key: 'f',\n                            meta: true,\n                            shift: false,\n                            alt: false\n                        }\n                    ]\n                }\n            });\n\n            selectElementContentsAndFire(editor.elements[0]);\n            fireEvent(editor.elements[0], 'keydown', {\n                keyCode: 'f'.charCodeAt(0),\n                ctrlKey: true,\n                metaKey: true,\n                shiftKey: false\n            });\n            expect(foo.bar).toHaveBeenCalled();\n        });\n\n        // TODO: remove this test when jumping in 6.0.0\n        it('should be executed for custom command without alt defined', function () {\n            spyOn(MediumEditor.prototype, 'execAction');\n            var editor = this.newMediumEditor('.editor', {\n                keyboardCommands: {\n                    commands: [\n                        {\n                            command: 'superscript',\n                            key: 'p',\n                            meta: true,\n                            shift: false\n                        },\n                        {\n                            command: 'subscript',\n                            key: 'r',\n                            meta: true,\n                            shift: false\n                        }\n                    ]\n                }\n            });\n\n            selectElementContentsAndFire(editor.elements[0]);\n            // If alt-key is not pressed, command should still be executed\n            fireEvent(editor.elements[0], 'keydown', {\n                keyCode: 'p'.charCodeAt(0),\n                ctrlKey: true,\n                metaKey: true,\n                altKey: false\n            });\n            expect(editor.execAction).toHaveBeenCalledWith('superscript');\n\n            // If alt-key is pressed, command should stil be executed\n            fireEvent(editor.elements[0], 'keydown', {\n                keyCode: 'r'.charCodeAt(0),\n                ctrlKey: true,\n                metaKey: true,\n                altKey: true\n            });\n            expect(editor.execAction).toHaveBeenCalledWith('subscript');\n        });\n\n        it('should support the use of Alt key', function () {\n            spyOn(MediumEditor.prototype, 'execAction');\n            var editor = this.newMediumEditor('.editor', {\n                keyboardCommands: {\n                    commands: [\n                        {\n                            command: 'append-h1',\n                            key: '1',\n                            meta: true,\n                            shift: false,\n                            alt: true\n                        },\n                        {\n                            command: 'append-h2',\n                            key: '2',\n                            meta: true,\n                            shift: false,\n                            alt: true\n                        },\n                        {\n                            command: 'append-h3',\n                            key: '3',\n                            meta: true,\n                            shift: false,\n                            alt: true\n                        },\n                        {\n                            command: 'append-h4',\n                            key: '4',\n                            meta: true,\n                            shift: false,\n                            alt: true\n                        },\n                        {\n                            command: 'append-h5',\n                            key: '5',\n                            meta: true,\n                            shift: false,\n                            alt: true\n                        },\n                        {\n                            command: 'append-h6',\n                            key: '6',\n                            meta: true,\n                            shift: false,\n                            alt: true\n                        }\n                    ]\n                }\n            });\n\n            selectElementContentsAndFire(editor.elements[0]);\n            ['1', '2', '3', '4', '5', '6'].forEach(function (heading) {\n                fireEvent(editor.elements[0], 'keydown', {\n                    keyCode: (heading).toString().charCodeAt(0),\n                    ctrlKey: true,\n                    metaKey: true,\n                    altKey: true\n                });\n                expect(editor.execAction).toHaveBeenCalledWith('append-h' + heading);\n            });\n        });\n\n        it('should not execute the button action when shift key is pressed', function () {\n            spyOn(MediumEditor.prototype, 'execAction');\n            var editor = this.newMediumEditor('.editor');\n\n            selectElementContentsAndFire(editor.elements[0]);\n            fireEvent(editor.elements[0], 'keydown', {\n                keyCode: 'B'.charCodeAt(0),\n                ctrlKey: true,\n                metaKey: true,\n                shiftKey: true\n            });\n            expect(editor.execAction).not.toHaveBeenCalled();\n        });\n\n        it('should not execute when keyboard-commands are disabled', function () {\n            spyOn(MediumEditor.prototype, 'execAction');\n            var editor = this.newMediumEditor('.editor', {\n                keyboardCommands: false\n            });\n\n            selectElementContentsAndFire(editor.elements[0]);\n            fireEvent(editor.elements[0], 'keydown', {\n                keyCode: 'B'.charCodeAt(0),\n                ctrlKey: true,\n                metaKey: true\n            });\n            expect(editor.execAction).not.toHaveBeenCalled();\n        });\n\n        it('should not execute when the command key are false', function () {\n            spyOn(MediumEditor.prototype, 'execAction');\n            var result,\n                editor = this.newMediumEditor('.editor', {\n                keyboardCommands: {\n                    commands: [\n                        {\n                            command: false,\n                            key: 'J',\n                            meta: true,\n                            shift: false\n                        }\n                    ]\n                }\n            });\n\n            selectElementContentsAndFire(editor.elements[0]);\n            fireEvent(editor.elements[0], 'keydown', {\n                keyCode: 'J'.charCodeAt(0),\n                ctrlKey: true,\n                metaKey: true,\n                shiftKey: false\n            });\n            expect(editor.execAction).not.toHaveBeenCalled();\n\n            result = fireEvent(editor.elements[0], 'keydown', {\n                keyCode: 'J'.charCodeAt(0),\n                ctrlKey: true,\n                metaKey: true,\n                shiftKey: true\n            });\n            expect(result).toBe(false, 'The command was not blocked because shift key was pressed');\n        });\n    });\n});\n"
  },
  {
    "path": "spec/paste.spec.js",
    "content": "/*global selectElementContents,\n         selectElementContentsAndFire,\n         fireEvent, prepareEvent,\n         firePreparedEvent, WORD_PASTE_EXAMPLE */\n\ndescribe('Pasting content', function () {\n    'use strict';\n\n    var multiLineTests = [\n            {\n                source: 'Google docs',\n                paste: '<meta charset=\\'utf-8\\'><meta charset=\"utf-8\"><b style=\"font-weight:normal;\" id=\"docs-internal-guid-b1bb8bfe-f54c-2e1f-72e2-4c7608d2be70\"><div dir=\"ltr\" style=\"line-height:1.15;margin-top:0pt;margin-bottom:0pt;\"><span style=\"font-size:15px;font-family:Arial;color:#000000;background-color:transparent;font-weight:bold;font-style:normal;font-variant:normal;text-decoration:none;vertical-align:baseline;white-space:pre-wrap;\">Bold</span></div><br><span style=\"font-size:15px;font-family:Arial;color:#000000;background-color:transparent;font-weight:normal;font-style:normal;font-variant:normal;text-decoration:none;vertical-align:baseline;white-space:pre-wrap;\"></span><p dir=\"ltr\" style=\"line-height:1.15;margin-top:0pt;margin-bottom:0pt;\"><span style=\"font-size:15px;font-family:Arial;color:#000000;background-color:transparent;font-weight:normal;font-style:italic;font-variant:normal;text-decoration:none;vertical-align:baseline;white-space:pre-wrap;\">Italic</span></p><br><span style=\"font-size:15px;font-family:Arial;color:#000000;background-color:transparent;font-weight:normal;font-style:normal;font-variant:normal;text-decoration:none;vertical-align:baseline;white-space:pre-wrap;\"></span><p dir=\"ltr\" style=\"line-height:1.15;margin-top:0pt;margin-bottom:0pt;\"><span style=\"font-size:15px;font-family:Arial;color:#000000;background-color:transparent;font-weight:bold;font-style:italic;font-variant:normal;text-decoration:none;vertical-align:baseline;white-space:pre-wrap;\">Bold and Italic</span></p><br><span style=\"font-size:15px;font-family:Arial;color:#000000;background-color:transparent;font-weight:normal;font-style:normal;font-variant:normal;text-decoration:none;vertical-align:baseline;white-space:pre-wrap;\"></span><p dir=\"ltr\" style=\"line-height:1.15;margin-top:0pt;margin-bottom:0pt;\"><span style=\"font-size:15px;font-family:Arial;color:#000000;background-color:transparent;font-weight:normal;font-style:normal;font-variant:normal;text-decoration:none;vertical-align:baseline;white-space:pre-wrap;\">A </span><a href=\"http://en.wikipedia.org/wiki/Link_(The_Legend_of_Zelda)\" style=\"text-decoration:none;\"><span style=\"font-size:15px;font-family:Arial;color:#1155cc;background-color:transparent;font-weight:normal;font-style:normal;font-variant:normal;text-decoration:underline;vertical-align:baseline;white-space:pre-wrap;\">link</span></a><span style=\"font-size:15px;font-family:Arial;color:#000000;background-color:transparent;font-weight:normal;font-style:normal;font-variant:normal;text-decoration:none;vertical-align:baseline;white-space:pre-wrap;\">.</span></p></b><br class=\"Apple-interchange-newline\">',\n                output: '<div><b>Bold</b></div><p><i>Italic</i></p><p><b><i>Bold and Italic</i></b></p><p>A <a href=\"http://en.wikipedia.org/wiki/Link_(The_Legend_of_Zelda)\">link</a>.</p>'\n            },\n            {\n                source: 'Google docs w/ numbered font weight',\n                paste: '<meta charset=\\'utf-8\\'><meta charset=\"utf-8\"><b style=\"font-weight:normal;\" id=\"docs-internal-guid-b1bb8bfe-f54c-2e1f-72e2-4c7608d2be70\"><div dir=\"ltr\" style=\"line-height:1.15;margin-top:0pt;margin-bottom:0pt;\"><span style=\"font-size:15px;font-family:Arial;color:#000000;background-color:transparent;font-weight:700;font-style:normal;font-variant:normal;text-decoration:none;vertical-align:baseline;white-space:pre-wrap;\">Bold</span></div><br><span style=\"font-size:15px;font-family:Arial;color:#000000;background-color:transparent;font-weight:normal;font-style:normal;font-variant:normal;text-decoration:none;vertical-align:baseline;white-space:pre-wrap;\"></span><p dir=\"ltr\" style=\"line-height:1.15;margin-top:0pt;margin-bottom:0pt;\"><span style=\"font-size:15px;font-family:Arial;color:#000000;background-color:transparent;font-weight:normal;font-style:italic;font-variant:normal;text-decoration:none;vertical-align:baseline;white-space:pre-wrap;\">Italic</span></p><br><span style=\"font-size:15px;font-family:Arial;color:#000000;background-color:transparent;font-weight:normal;font-style:normal;font-variant:normal;text-decoration:none;vertical-align:baseline;white-space:pre-wrap;\"></span><p dir=\"ltr\" style=\"line-height:1.15;margin-top:0pt;margin-bottom:0pt;\"><span style=\"font-size:15px;font-family:Arial;color:#000000;background-color:transparent;font-weight:700;font-style:italic;font-variant:normal;text-decoration:none;vertical-align:baseline;white-space:pre-wrap;\">Bold and Italic</span></p><br><span style=\"font-size:15px;font-family:Arial;color:#000000;background-color:transparent;font-weight:normal;font-style:normal;font-variant:normal;text-decoration:none;vertical-align:baseline;white-space:pre-wrap;\"></span><p dir=\"ltr\" style=\"line-height:1.15;margin-top:0pt;margin-bottom:0pt;\"><span style=\"font-size:15px;font-family:Arial;color:#000000;background-color:transparent;font-weight:normal;font-style:normal;font-variant:normal;text-decoration:none;vertical-align:baseline;white-space:pre-wrap;\">A </span><a href=\"http://en.wikipedia.org/wiki/Link_(The_Legend_of_Zelda)\" style=\"text-decoration:none;\"><span style=\"font-size:15px;font-family:Arial;color:#1155cc;background-color:transparent;font-weight:normal;font-style:normal;font-variant:normal;text-decoration:underline;vertical-align:baseline;white-space:pre-wrap;\">link</span></a><span style=\"font-size:15px;font-family:Arial;color:#000000;background-color:transparent;font-weight:normal;font-style:normal;font-variant:normal;text-decoration:none;vertical-align:baseline;white-space:pre-wrap;\">.</span></p></b><br class=\"Apple-interchange-newline\">',\n                output: '<div><b>Bold</b></div><p><i>Italic</i></p><p><b><i>Bold and Italic</i></b></p><p>A <a href=\"http://en.wikipedia.org/wiki/Link_(The_Legend_of_Zelda)\">link</a>.</p>'\n            },\n            {\n                source: 'Inside editor',\n                paste: '<meta charset=\\'utf-8\\'><p style=\"margin-bottom: 40px; color: rgb(0, 0, 0); font-family: \\'Helvetica Neue\\', Helvetica, Arial, sans-serif; font-size: 22.22222328186035px; font-style: normal; font-variant: normal; font-weight: normal; letter-spacing: normal; line-height: 30px; orphans: auto; text-align: start; text-indent: 0px; text-transform: none; white-space: normal; widows: auto; word-spacing: 0px; -webkit-text-stroke-width: 0px;\"><b>Bold</b></p><p style=\"margin-bottom: 40px; color: rgb(0, 0, 0); font-family: \\'Helvetica Neue\\', Helvetica, Arial, sans-serif; font-size: 22.22222328186035px; font-style: normal; font-variant: normal; font-weight: normal; letter-spacing: normal; line-height: 30px; orphans: auto; text-align: start; text-indent: 0px; text-transform: none; white-space: normal; widows: auto; word-spacing: 0px; -webkit-text-stroke-width: 0px;\"><i>Italic</i></p><p style=\"margin-bottom: 40px; color: rgb(0, 0, 0); font-family: \\'Helvetica Neue\\', Helvetica, Arial, sans-serif; font-size: 22.22222328186035px; font-style: normal; font-variant: normal; font-weight: normal; letter-spacing: normal; line-height: 30px; orphans: auto; text-align: start; text-indent: 0px; text-transform: none; white-space: normal; widows: auto; word-spacing: 0px; -webkit-text-stroke-width: 0px;\"><b><i>Bold and Italic</i></b></p><p style=\"margin-bottom: 40px; color: rgb(0, 0, 0); font-family: \\'Helvetica Neue\\', Helvetica, Arial, sans-serif; font-size: 22.22222328186035px; font-style: normal; font-variant: normal; font-weight: normal; letter-spacing: normal; line-height: 30px; orphans: auto; text-align: start; text-indent: 0px; text-transform: none; white-space: normal; widows: auto; word-spacing: 0px; -webkit-text-stroke-width: 0px;\">A<span class=\"Apple-converted-space\"> </span><a href=\"http://en.wikipedia.org/wiki/Link_(The_Legend_of_Zelda)\" style=\"color: black;\">link</a>.</p>',\n                output: '<p><b>Bold</b></p><p><i>Italic</i></p><p><b><i>Bold and Italic</i></b></p><p>A <a href=\"http://en.wikipedia.org/wiki/Link_(The_Legend_of_Zelda)\">link</a>.</p>'\n            },\n            {\n                source: 'Messy paragraphs with spaces',\n                paste: '<meta charset=\\'utf-8\\'><p class=\"sub_buzz_desc\" style=\"margin: 0px; padding: 0px 0px 12px; border: 0px; outline: 0px; font-size: 18px; background-color: rgb(255, 255, 255); line-height: 26px; font-style: normal; font-variant: normal; font-weight: normal; font-family: cambria, georgia, serif; color: rgb(34, 34, 34); letter-spacing: normal; orphans: auto; text-align: start; text-indent: 0px; text-transform: none; white-space: normal; widows: auto; word-spacing: 0px; -webkit-text-stroke-width: 0px; background-position: initial initial; background-repeat: initial initial;\"><b>Bold</b></p><p style=\"margin: 0px; padding: 6px 0px 12px; border: 0px; outline: 0px; font-size: 18px; background-color: rgb(255, 255, 255); line-height: 26px; font-style: normal; font-variant: normal; font-weight: normal; font-family: cambria, georgia, serif; color: rgb(34, 34, 34); letter-spacing: normal; orphans: auto; text-align: start; text-indent: 0px; text-transform: none; white-space: normal; widows: auto; word-spacing: 0px; -webkit-text-stroke-width: 0px; background-position: initial initial; background-repeat: initial initial;\"><i>Italic</i></p><p class=\"sub_buzz_desc\" style=\"margin: 0px; padding: 0px 0px 12px; border: 0px; outline: 0px; font-size: 18px; background-color: rgb(255, 255, 255); line-height: 26px; font-style: normal; font-variant: normal; font-weight: normal; font-family: cambria, georgia, serif; color: rgb(34, 34, 34); letter-spacing: normal; orphans: auto; text-align: start; text-indent: 0px; text-transform: none; white-space: normal; widows: auto; word-spacing: 0px; -webkit-text-stroke-width: 0px; background-position: initial initial; background-repeat: initial initial;\"><b><i>Bold and Italic</i></b></p><p style=\"margin: 0px; padding: 6px 0px 12px; border: 0px; outline: 0px; font-size: 18px; background-color: rgb(255, 255, 255); line-height: 26px; font-style: normal; font-variant: normal; font-weight: normal; font-family: cambria, georgia, serif; color: rgb(34, 34, 34); letter-spacing: normal; orphans: auto; text-align: start; text-indent: 0px; text-transform: none; white-space: normal; widows: auto; word-spacing: 0px; -webkit-text-stroke-width: 0px; background-position: initial initial; background-repeat: initial initial;\">A<span class=\"Apple-converted-space\"> </span><a href=\"http://en.wikipedia.org/wiki/Link_(The_Legend_of_Zelda)\" style=\"margin: 0px; padding: 0px; border: 0px; outline: 0px; font-size: 18px; background-color: transparent; color: rgb(0, 119, 238); text-decoration: none; background-position: initial initial; background-repeat: initial initial;\">link</a>.</p>',\n                output: '<p><b>Bold</b></p><p><i>Italic</i></p><p><b><i>Bold and Italic</i></b></p><p>A <a href=\"http://en.wikipedia.org/wiki/Link_(The_Legend_of_Zelda)\">link</a>.</p>'\n            },\n            {\n                source: 'Paragraphs with internal linebreaks',\n                paste: '<meta charset=\\'utf-8\\'><p>One<br>Two</p><p>Three<br>Four</p>',\n                output: '<p>One<br>Two</p><p>Three<br>Four</p>'\n            },\n            {\n                source: 'Non <p> or <div> with only <br> elements',\n                paste: '<p>One</p><div><h1><br /></h1></div><p>Two</p><span><span><br /></span></span><p>Three</p>',\n                output: '<p>One</p><p>Two</p><p>Three</p>'\n            },\n            {\n                source: 'Microsoft Word - line breaks',\n                paste: '<p>One\\nTwo\\n</p>\\n\\n<p>Three Four</p>',\n                output: '<p>One Two </p><p>Three Four</p>'\n            },\n            {\n                source: 'Microsoft Word - Proprietary elements',\n                paste: '<p>One<o:p></o:p></p><p>Two<o:p></o:p></p>',\n                output: '<p>One</p><p>Two</p>'\n            },\n            {\n                source: 'Microsoft Word - full document paste',\n                paste: WORD_PASTE_EXAMPLE,\n                output: '<p>Mycomplicated <b>word document renders</b> <i>like this in the card</i> generator.</p><p>Testbig text</p><p>Testingsmaller text and <s>crossed out text.</s></p><p>·      Test list</p><p>·      Test</p><p>o  Testindented</p><p>·      Tes6t</p><p>1.     tes t test test</p><p>a.    tes t indented</p>'\n            }\n        ],\n        inlineTests = [\n            {\n                source: 'Google docs',\n                paste: '<meta charset=\\'utf-8\\'><meta charset=\"utf-8\"><b style=\"font-weight:normal;\" id=\"docs-internal-guid-2f060cc5-1888-a396-af95-bfb31478c7ae\"><span style=\"font-size:15px;font-family:Arial;color:#000000;background-color:transparent;font-weight:bold;font-style:normal;font-variant:normal;text-decoration:none;vertical-align:baseline;white-space:pre-wrap;\">Bold,</span><span style=\"font-size:15px;font-family:Arial;color:#000000;background-color:transparent;font-weight:normal;font-style:normal;font-variant:normal;text-decoration:none;vertical-align:baseline;white-space:pre-wrap;\"> </span><span style=\"font-size:15px;font-family:Arial;color:#000000;background-color:transparent;font-weight:normal;font-style:italic;font-variant:normal;text-decoration:none;vertical-align:baseline;white-space:pre-wrap;\">italic,</span><span style=\"font-size:15px;font-family:Arial;color:#000000;background-color:transparent;font-weight:normal;font-style:normal;font-variant:normal;text-decoration:none;vertical-align:baseline;white-space:pre-wrap;\"> </span><span style=\"font-size:15px;font-family:Arial;color:#000000;background-color:transparent;font-weight:bold;font-style:italic;font-variant:normal;text-decoration:none;vertical-align:baseline;white-space:pre-wrap;\">bold and italic</span><span style=\"font-size:15px;font-family:Arial;color:#000000;background-color:transparent;font-weight:normal;font-style:normal;font-variant:normal;text-decoration:none;vertical-align:baseline;white-space:pre-wrap;\">, and </span><a href=\"http://en.wikipedia.org/wiki/Link_(The_Legend_of_Zelda)\" style=\"text-decoration:none;\"><span style=\"font-size:15px;font-family:Arial;color:#1155cc;background-color:transparent;font-weight:normal;font-style:normal;font-variant:normal;text-decoration:underline;vertical-align:baseline;white-space:pre-wrap;\">a link</span></a><span style=\"font-size:15px;font-family:Arial;color:#000000;background-color:transparent;font-weight:normal;font-style:normal;font-variant:normal;text-decoration:none;vertical-align:baseline;white-space:pre-wrap;\">.</span></b>',\n                output: '<b>Bold,</b> <i>italic,</i> <b><i>bold and italic</i></b>, and <a href=\"http://en.wikipedia.org/wiki/Link_\\\\(The_Legend_of_Zelda\\\\)\">a link</a>\\\\.'\n            },\n            {\n                source: 'Google docs w/ numbered font weight',\n                paste: '<meta charset=\\'utf-8\\'><meta charset=\"utf-8\"><b style=\"font-weight:normal;\" id=\"docs-internal-guid-2f060cc5-1888-a396-af95-bfb31478c7ae\"><span style=\"font-size:15px;font-family:Arial;color:#000000;background-color:transparent;font-weight:700;font-style:normal;font-variant:normal;text-decoration:none;vertical-align:baseline;white-space:pre-wrap;\">Bold,</span><span style=\"font-size:15px;font-family:Arial;color:#000000;background-color:transparent;font-weight:normal;font-style:normal;font-variant:normal;text-decoration:none;vertical-align:baseline;white-space:pre-wrap;\"> </span><span style=\"font-size:15px;font-family:Arial;color:#000000;background-color:transparent;font-weight:normal;font-style:italic;font-variant:normal;text-decoration:none;vertical-align:baseline;white-space:pre-wrap;\">italic,</span><span style=\"font-size:15px;font-family:Arial;color:#000000;background-color:transparent;font-weight:normal;font-style:normal;font-variant:normal;text-decoration:none;vertical-align:baseline;white-space:pre-wrap;\"> </span><span style=\"font-size:15px;font-family:Arial;color:#000000;background-color:transparent;font-weight:700;font-style:italic;font-variant:normal;text-decoration:none;vertical-align:baseline;white-space:pre-wrap;\">bold and italic</span><span style=\"font-size:15px;font-family:Arial;color:#000000;background-color:transparent;font-weight:normal;font-style:normal;font-variant:normal;text-decoration:none;vertical-align:baseline;white-space:pre-wrap;\">, and </span><a href=\"http://en.wikipedia.org/wiki/Link_(The_Legend_of_Zelda)\" style=\"text-decoration:none;\"><span style=\"font-size:15px;font-family:Arial;color:#1155cc;background-color:transparent;font-weight:normal;font-style:normal;font-variant:normal;text-decoration:underline;vertical-align:baseline;white-space:pre-wrap;\">a link</span></a><span style=\"font-size:15px;font-family:Arial;color:#000000;background-color:transparent;font-weight:normal;font-style:normal;font-variant:normal;text-decoration:none;vertical-align:baseline;white-space:pre-wrap;\">.</span></b>',\n                output: '<b>Bold,</b> <i>italic,</i> <b><i>bold and italic</i></b>, and <a href=\"http://en.wikipedia.org/wiki/Link_\\\\(The_Legend_of_Zelda\\\\)\">a link</a>\\\\.'\n            },\n            {\n                source: 'Inside editor',\n                paste: '<meta charset=\\'utf-8\\'><b style=\"color: rgb(0, 0, 0); font-family: \\'Helvetica Neue\\', Helvetica, Arial, sans-serif; font-size: 22.22222328186035px; font-style: normal; font-variant: normal; letter-spacing: normal; line-height: 30px; orphans: auto; text-align: start; text-indent: 0px; text-transform: none; white-space: normal; widows: auto; word-spacing: 0px; -webkit-text-stroke-width: 0px;\">Bold,</b><span style=\"color: rgb(0, 0, 0); font-family: \\'Helvetica Neue\\', Helvetica, Arial, sans-serif; font-size: 22.22222328186035px; font-style: normal; font-variant: normal; font-weight: normal; letter-spacing: normal; line-height: 30px; orphans: auto; text-align: start; text-indent: 0px; text-transform: none; white-space: normal; widows: auto; word-spacing: 0px; -webkit-text-stroke-width: 0px; display: inline !important; float: none;\"> </span><i style=\"color: rgb(0, 0, 0); font-family: \\'Helvetica Neue\\', Helvetica, Arial, sans-serif; font-size: 22.22222328186035px; font-variant: normal; font-weight: normal; letter-spacing: normal; line-height: 30px; orphans: auto; text-align: start; text-indent: 0px; text-transform: none; white-space: normal; widows: auto; word-spacing: 0px; -webkit-text-stroke-width: 0px;\">italic,</i><span style=\"color: rgb(0, 0, 0); font-family: \\'Helvetica Neue\\', Helvetica, Arial, sans-serif; font-size: 22.22222328186035px; font-style: normal; font-variant: normal; font-weight: normal; letter-spacing: normal; line-height: 30px; orphans: auto; text-align: start; text-indent: 0px; text-transform: none; white-space: normal; widows: auto; word-spacing: 0px; -webkit-text-stroke-width: 0px; display: inline !important; float: none;\"><span class=\"Apple-converted-space\"> </span></span><b style=\"color: rgb(0, 0, 0); font-family: \\'Helvetica Neue\\', Helvetica, Arial, sans-serif; font-size: 22.22222328186035px; font-style: normal; font-variant: normal; letter-spacing: normal; line-height: 30px; orphans: auto; text-align: start; text-indent: 0px; text-transform: none; white-space: normal; widows: auto; word-spacing: 0px; -webkit-text-stroke-width: 0px;\"><i>bold and italic</i></b><span style=\"color: rgb(0, 0, 0); font-family: \\'Helvetica Neue\\', Helvetica, Arial, sans-serif; font-size: 22.22222328186035px; font-style: normal; font-variant: normal; font-weight: normal; letter-spacing: normal; line-height: 30px; orphans: auto; text-align: start; text-indent: 0px; text-transform: none; white-space: normal; widows: auto; word-spacing: 0px; -webkit-text-stroke-width: 0px; display: inline !important; float: none;\">, and<span class=\"Apple-converted-space\"> </span></span><a href=\"http://en.wikipedia.org/wiki/Link_(The_Legend_of_Zelda)\" style=\"color: black; font-family: \\'Helvetica Neue\\', Helvetica, Arial, sans-serif; font-size: 22.22222328186035px; font-style: normal; font-variant: normal; font-weight: normal; letter-spacing: normal; line-height: 30px; orphans: auto; text-align: start; text-indent: 0px; text-transform: none; white-space: normal; widows: auto; word-spacing: 0px; -webkit-text-stroke-width: 0px;\">a link</a><span style=\"color: rgb(0, 0, 0); font-family: \\'Helvetica Neue\\', Helvetica, Arial, sans-serif; font-size: 22.22222328186035px; font-style: normal; font-variant: normal; font-weight: normal; letter-spacing: normal; line-height: 30px; orphans: auto; text-align: start; text-indent: 0px; text-transform: none; white-space: normal; widows: auto; word-spacing: 0px; -webkit-text-stroke-width: 0px; display: inline !important; float: none;\">.</span>',\n                output: '<b>Bold,</b> <i>italic,</i> <b><i>bold and italic</i></b>, and <a href=\"http://en.wikipedia.org/wiki/Link_\\\\(The_Legend_of_Zelda\\\\)\">a link</a>\\\\.'\n            },\n            {\n                source: 'Copy and pasted anchor from chrome',\n                paste: '<meta charset=\\'utf-8\\'><a href=\"http://www.yahoo.com/\" style=\"box-sizing: border-box; color: rgb(8, 158, 0); text-decoration: none; outline: 0px; font-family: \\'Open Sans\\', HelveticaNeue, Helvetica, Arial, sans-serif; font-size: 16px; font-style: normal; font-variant: normal; font-weight: normal; letter-spacing: normal; line-height: 26px; orphans: auto; text-align: start; text-indent: 0px; text-transform: none; white-space: normal; widows: 1; word-spacing: 0px; -webkit-text-stroke-width: 0px; background-color: rgb(249, 249, 249);\">Yahoo</a><span style=\"color: rgb(62, 67, 62); font-family: \\'Open Sans\\', HelveticaNeue, Helvetica, Arial, sans-serif; font-size: 16px; font-style: normal; font-variant: normal; font-weight: normal; letter-spacing: normal; line-height: 26px; orphans: auto; text-align: start; text-indent: 0px; text-transform: none; white-space: normal; widows: 1; word-spacing: 0px; -webkit-text-stroke-width: 0px; display: inline !important; float: none; background-color: rgb(249, 249, 249);\"><span class=\"Apple-converted-space\"> </span>has been busy rebuilding its business around<span class=\"Apple-converted-space\"> </span></span><a href=\"http://www.fastcompany.com/3044281/marissa-mayer\" style=\"box-sizing: border-box; color: rgb(8, 158, 0); text-decoration: none; outline: 0px; font-family: \\'Open Sans\\', HelveticaNeue, Helvetica, Arial, sans-serif; font-size: 16px; font-style: normal; font-variant: normal; font-weight: normal; letter-spacing: normal; line-height: 26px; orphans: auto; text-align: start; text-indent: 0px; text-transform: none; white-space: normal; widows: 1; word-spacing: 0px; -webkit-text-stroke-width: 0px; background-color: rgb(249, 249, 249);\">mobile</a><span style=\"color: rgb(62, 67, 62); font-family: \\'Open Sans\\', HelveticaNeue, Helvetica, Arial, sans-serif; font-size: 16px; font-style: normal; font-variant: normal; font-weight: normal; letter-spacing: normal; line-height: 26px; orphans: auto; text-align: start; text-indent: 0px; text-transform: none; white-space: normal; widows: 1; word-spacing: 0px; -webkit-text-stroke-width: 0px; display: inline !important; float: none; background-color: rgb(249, 249, 249);\"> under CEO Marissa Mayer, and soon it could make one of its biggest bets yet on the platform.</span>',\n                output: '<a href=\"http://www.yahoo.com/\">Yahoo</a> has been busy rebuilding its business around <a href=\"http://www.fastcompany.com/3044281/marissa-mayer\">mobile</a> under CEO Marissa Mayer, and soon it could make one of its biggest bets yet on the platform.'\n            },\n            {\n                source: 'Copy and pasted span with multiple child nodes',\n                paste: '<span>| </span><span><a href=\"http://www.yahoo.com/\">Yahoo</a> | <a href=\"http://www.google.com/\">Google</a></span>',\n                output: '\\\\| <a href=\"http://www.yahoo.com/\">Yahoo</a> \\\\| <a href=\"http://www.google.com/\">Google</a>'\n            }\n        ],\n        textTests = [\n            {\n                source: 'Text single word',\n                paste: 'supercalifragilisticexpalidocious',\n                output: '<div id=\"editor-inner\">supercalifragilisticexpalidocious</div>'\n            },\n            {\n                source: 'Text single word with leading/trailing space',\n                paste: ' supercalifragilisticexpalidocious ',\n                output: '<div id=\"editor-inner\"> supercalifragilisticexpalidocious </div>'\n            },\n            {\n                source: 'Text multi-word with no line breaks',\n                paste: 'Their relationship consisted in discussing if it existed',\n                output: '<div id=\"editor-inner\">Their relationship consisted in discussing if it existed</div>'\n            },\n            {\n                source: 'Text with multiple line breaks',\n                paste: 'Only one thing made him happy\\nAnd now that it was gone\\nEverything made him happy\\r\\n',\n                output: '<div id=\"editor-inner\"><p>Only one thing made him happy</p><p>And now that it was gone</p><p>Everything made him happy</p></div>'\n            }\n        ];\n\n    beforeEach(function () {\n        setupTestHelpers.call(this);\n        this.el = this.createElement('div', 'editor', 'hhh');\n        this.el.id = 'paste-editor';\n    });\n\n    afterEach(function () {\n        this.cleanupTest();\n    });\n\n    describe('using cleanPastedHTML option', function () {\n        it('should filter multi-line rich-text pastes', function () {\n            var i,\n                editorEl = this.el,\n                editor = this.newMediumEditor('.editor', {\n                    delay: 200,\n                    paste: {\n                        forcePlainText: false,\n                        cleanPastedHTML: true\n                    }\n                }),\n                pasteHandler = editor.getExtensionByName('paste'),\n\n                // mock event with clipboardData API\n                // test requires creating a function, so can't loop or jslint balks\n                evt = {\n                    pasteText: null,\n                    preventDefault: function () {\n                        return;\n                    },\n                    clipboardData: {\n                        types: ['text/plain', 'text/html'],\n                        getData: function () {\n                            // do we need to return different results for the different types? text/plain, text/html\n                            return this.pasteText;\n                        }\n                    }\n                };\n\n            for (i = 0; i < multiLineTests.length; i += 1) {\n\n                // move caret to editor\n                editorEl.innerHTML = '<span id=\"editor-inner\">&nbsp</span>';\n\n                selectElementContentsAndFire(editorEl);\n\n                evt.clipboardData.pasteText = multiLineTests[i].paste;\n                pasteHandler.handlePaste(evt, editorEl);\n                jasmine.clock().tick(100);\n                expect(editorEl.innerHTML).toEqual(multiLineTests[i].output);\n            }\n        });\n\n        it('should add paragraphs to pasted plain-text', function () {\n            var editorEl = this.el,\n                editor = this.newMediumEditor('.editor', {\n                    delay: 200,\n                    paste: {\n                        forcePlainText: false,\n                        cleanPastedHTML: true\n                    }\n                }),\n                pasteHandler = editor.getExtensionByName('paste'),\n\n                // mock event with clipboardData API\n                // test requires creating a function, so can't loop or jslint balks\n                evt = {\n                    preventDefault: function () {\n                        return;\n                    },\n                    clipboardData: {\n                        types: ['text/plain'],\n                        getData: function (clipboardType) {\n                            if (clipboardType === 'text/plain') {\n                                return 'One\\n\\nTwo\\n\\nThree';\n                            }\n                        }\n                    }\n                };\n\n            // move caret to editor\n            editorEl.innerHTML = '<span id=\"editor-inner\">&nbsp</span>';\n\n            selectElementContentsAndFire(editorEl);\n\n            pasteHandler.handlePaste(evt, editorEl);\n            jasmine.clock().tick(100);\n            expect(editorEl.innerHTML).toEqual('<p>One</p><p>Two</p><p>Three</p>');\n\n        });\n\n        it('should trigger editablePaste', function () {\n            var editorEl = this.el,\n                editor = this.newMediumEditor('.editor', {\n                    paste: {\n                        forcePlainText: false,\n                        cleanPastedHTML: true\n                    }\n                }),\n                spy = jasmine.createSpy('handler');\n\n            editor.subscribe('editablePaste', spy);\n\n            // move caret to editor\n            editorEl.innerHTML = '<span id=\"editor-inner\">&nbsp</span>';\n\n            selectElementContentsAndFire(editorEl);\n\n            expect(spy).not.toHaveBeenCalled();\n            var evt = prepareEvent(editorEl, 'paste');\n            firePreparedEvent(evt, editorEl, 'paste');\n            jasmine.clock().tick(1);\n            expect(spy).toHaveBeenCalledWith(evt, this.el);\n        });\n\n        it('should filter multi-line rich-text pastes when \"insertHTML\" command is not supported', function () {\n            var editor = this.newMediumEditor('.editor', {\n                paste: {\n                    forcePlainText: false,\n                    cleanPastedHTML: true\n                }\n            });\n\n            spyOn(document, 'queryCommandSupported').and.returnValue(false);\n\n            multiLineTests.forEach(function (test) {\n                this.el.innerHTML = '<span id=\"editor-inner\">lorem ipsum</span>';\n\n                selectElementContentsAndFire(this.el);\n\n                editor.cleanPaste(test.paste);\n                expect(this.el.innerHTML).toEqual(test.output);\n            }.bind(this));\n        });\n\n        it('should add target=\"_blank\" on anchor', function () {\n            var editor = this.newMediumEditor('.editor', {\n                targetBlank: true,\n                paste: {\n                    forcePlainText: false,\n                    cleanPastedHTML: true\n                }\n            });\n\n            spyOn(document, 'queryCommandSupported').and.returnValue(false);\n\n            this.el.innerHTML = '<span id=\"editor-inner\">lorem ipsum</span>';\n\n            selectElementContentsAndFire(this.el);\n\n            editor.cleanPaste('<a href=\"http://0.0.0.0/bar.html\">foo<a>');\n            expect(this.el.innerHTML).toContain('target=\"_blank\"');\n        });\n    });\n\n    describe('using keyboard', function () {\n        it('should insert a custom paste-bin on keydown of CTRL + V', function () {\n            var editor = this.newMediumEditor('.editor', {\n                    paste: {\n                        forcePlainText: false,\n                        cleanPastedHTML: true\n                    }\n                });\n\n            selectElementContentsAndFire(editor.elements[0].firstChild);\n\n            var contentEditables = document.body.querySelectorAll('[contentEditable=true]');\n            expect(contentEditables.length).toBe(1);\n\n            fireEvent(this.el, 'keydown', {\n                keyCode: MediumEditor.util.keyCode.V,\n                ctrlKey: true,\n                metaKey: true\n            });\n\n            contentEditables = document.body.querySelectorAll('[contentEditable=true]');\n            expect(contentEditables.length).toBe(2);\n\n            var pasteBin = contentEditables[1];\n            expect(pasteBin.innerHTML).toBe('%ME_PASTEBIN%');\n            expect(pasteBin.parentNode).toBe(document.body);\n\n            var range = document.getSelection().getRangeAt(0);\n            expect(MediumEditor.util.isDescendant(pasteBin, range.commonAncestorContainer, true)).toBe(true, 'Select is not within the paste bin');\n            expect(range.toString()).toBe('%ME_PASTEBIN%');\n        });\n\n        it('should trigger handlePasteBinPaste when pasting into paste-bin', function () {\n            spyOn(MediumEditor.extensions.paste.prototype, 'handlePasteBinPaste').and.callThrough();\n            var editor = this.newMediumEditor('.editor', {\n                    paste: {\n                        forcePlainText: false,\n                        cleanPastedHTML: true\n                    }\n                }),\n                pasteHandler = editor.getExtensionByName('paste');\n\n            selectElementContentsAndFire(editor.elements[0].firstChild);\n\n            var contentEditables = document.body.querySelectorAll('[contentEditable=true]');\n            expect(contentEditables.length).toBe(1);\n\n            fireEvent(this.el, 'keydown', {\n                keyCode: MediumEditor.util.keyCode.V,\n                ctrlKey: true,\n                metaKey: true\n            });\n\n            contentEditables = document.body.querySelectorAll('[contentEditable=true]');\n            expect(contentEditables.length).toBe(2);\n\n            var pasteBin = contentEditables[1],\n                evt = prepareEvent(pasteBin, 'paste');\n\n            firePreparedEvent(evt, pasteBin, 'paste');\n            expect(pasteHandler.handlePasteBinPaste).toHaveBeenCalledWith(evt);\n        });\n\n        it('should fire editablePaste event when pasting', function () {\n            var editor = this.newMediumEditor('.editor', {\n                    paste: {\n                        forcePlainText: false,\n                        cleanPastedHTML: true\n                    }\n                }),\n                spy = jasmine.createSpy('handler');\n\n            editor.subscribe('editablePaste', spy);\n\n            selectElementContentsAndFire(editor.elements[0].firstChild);\n            expect(spy).not.toHaveBeenCalled();\n\n            fireEvent(this.el, 'keydown', {\n                keyCode: MediumEditor.util.keyCode.V,\n                ctrlKey: true,\n                metaKey: true\n            });\n\n            var contentEditables = document.body.querySelectorAll('[contentEditable=true]');\n            expect(contentEditables.length).toBe(2);\n\n            var evt = {\n                    type: 'paste',\n                    defaultPrevented: false,\n                    preventDefault: function () {},\n                    clipboardData: {\n                        types: ['text/plain', 'text/html'],\n                        getData: function () {\n                            // do we need to return different results for the different types? text/plain, text/html\n                            return 'pasted content';\n                        }\n                    }\n                },\n                pasteExtension = editor.getExtensionByName('paste');\n\n            pasteExtension.handlePasteBinPaste(evt);\n            jasmine.clock().tick(1);\n            expect(spy).toHaveBeenCalledWith({ currentTarget: editor.elements[0], target: editor.elements[0] }, editor.elements[0]);\n        });\n\n        it('should do nothing if default was prevented on paste event of the paste-bin', function () {\n            var editor = this.newMediumEditor('.editor', {\n                    paste: {\n                        forcePlainText: false,\n                        cleanPastedHTML: true\n                    }\n                }),\n                pasteExtension = editor.getExtensionByName('paste');\n\n            selectElementContentsAndFire(editor.elements[0].firstChild);\n\n            var contentEditables = document.body.querySelectorAll('[contentEditable=true]');\n            expect(contentEditables.length).toBe(1);\n\n            fireEvent(this.el, 'keydown', {\n                keyCode: MediumEditor.util.keyCode.V,\n                ctrlKey: true,\n                metaKey: true\n            });\n\n            contentEditables = document.body.querySelectorAll('[contentEditable=true]');\n            expect(contentEditables.length).toBe(2);\n\n            var evt = {\n                    type: 'paste',\n                    defaultPrevented: true,\n                    preventDefault: function () {},\n                    clipboardData: {\n                        types: ['text/plain', 'text/html'],\n                        getData: function () {\n                            // do we need to return different results for the different types? text/plain, text/html\n                            return 'pasted content';\n                        }\n                    }\n                };\n\n            spyOn(evt, 'preventDefault');\n\n            // Paste should insert data from the clipboard, and prevent paste from happening in the paste-bin\n            pasteExtension.handlePasteBinPaste(evt);\n            expect(evt.preventDefault).not.toHaveBeenCalled();\n            expect(this.el.innerHTML).toBe('hhh');\n\n            // paste-bin should be gone\n            contentEditables = document.body.querySelectorAll('[contentEditable=true]');\n            expect(contentEditables.length).toBe(1);\n        });\n\n        it('should use clipboard data if available and not allow paste to happen', function () {\n            var editor = this.newMediumEditor('.editor', {\n                    paste: {\n                        forcePlainText: false,\n                        cleanPastedHTML: true\n                    }\n                }),\n                pasteExtension = editor.getExtensionByName('paste');\n\n            selectElementContentsAndFire(editor.elements[0].firstChild);\n\n            var contentEditables = document.body.querySelectorAll('[contentEditable=true]');\n            expect(contentEditables.length).toBe(1);\n\n            fireEvent(this.el, 'keydown', {\n                keyCode: MediumEditor.util.keyCode.V,\n                ctrlKey: true,\n                metaKey: true\n            });\n\n            contentEditables = document.body.querySelectorAll('[contentEditable=true]');\n            expect(contentEditables.length).toBe(2);\n\n            var evt = {\n                    type: 'paste',\n                    preventDefault: function () {},\n                    clipboardData: {\n                        types: ['text/plain', 'text/html'],\n                        getData: function () {\n                            // do we need to return different results for the different types? text/plain, text/html\n                            return 'pasted content';\n                        }\n                    }\n                };\n\n            spyOn(evt, 'preventDefault');\n\n            // Paste should insert data from the clipboard, and prevent paste from happening in the paste-bin\n            pasteExtension.handlePasteBinPaste(evt);\n            expect(evt.preventDefault).toHaveBeenCalled();\n            expect(this.el.innerHTML).toBe('pasted content');\n\n            // paste-bin should be gone\n            contentEditables = document.body.querySelectorAll('[contentEditable=true]');\n            expect(contentEditables.length).toBe(1);\n        });\n\n        it('should use html from the paste bin when clipboard data is not available', function () {\n            var editor = this.newMediumEditor('.editor', {\n                    paste: {\n                        forcePlainText: false,\n                        cleanPastedHTML: true\n                    }\n                }),\n                pasteExtension = editor.getExtensionByName('paste');\n\n            selectElementContentsAndFire(editor.elements[0].firstChild);\n\n            var contentEditables = document.body.querySelectorAll('[contentEditable=true]');\n            expect(contentEditables.length).toBe(1);\n\n            fireEvent(this.el, 'keydown', {\n                keyCode: MediumEditor.util.keyCode.V,\n                ctrlKey: true,\n                metaKey: true\n            });\n\n            contentEditables = document.body.querySelectorAll('[contentEditable=true]');\n            expect(contentEditables.length).toBe(2);\n\n            var evt = {\n                    type: 'paste',\n                    preventDefault: function () {},\n                    clipboardData: {\n                        types: ['text/plain'],\n                        getData: function () {\n                            // do we need to return different results for the different types? text/plain, text/html\n                            return null;\n                        }\n                    }\n                },\n                testHTML = '<b>HTML from <u>paste bin</u></b>',\n                pasteBin = contentEditables[1];\n\n            pasteBin.innerHTML = testHTML;\n            spyOn(evt, 'preventDefault');\n\n            // Paste should not be prevented on the paste bin\n            pasteExtension.handlePasteBinPaste(evt);\n            expect(evt.preventDefault).not.toHaveBeenCalled();\n            jasmine.clock().tick(1);\n\n            // HTML from paste-bin should now be in the editor\n            expect(this.el.innerHTML).toMatch(new RegExp(testHTML + '(<br/?>)?'));\n\n            // paste-bin should be gone\n            contentEditables = document.body.querySelectorAll('[contentEditable=true]');\n            expect(contentEditables.length).toBe(1);\n        });\n\n        it('should use plain text when clipboard data is not available and paste-bin does not have real content', function () {\n            var editor = this.newMediumEditor('.editor', {\n                    paste: {\n                        forcePlainText: false,\n                        cleanPastedHTML: true\n                    }\n                }),\n                pasteExtension = editor.getExtensionByName('paste');\n\n            selectElementContentsAndFire(editor.elements[0].firstChild);\n\n            var contentEditables = document.body.querySelectorAll('[contentEditable=true]');\n            expect(contentEditables.length).toBe(1);\n\n            fireEvent(this.el, 'keydown', {\n                keyCode: MediumEditor.util.keyCode.V,\n                ctrlKey: true,\n                metaKey: true\n            });\n\n            contentEditables = document.body.querySelectorAll('[contentEditable=true]');\n            expect(contentEditables.length).toBe(2);\n\n            var evt = {\n                    type: 'paste',\n                    preventDefault: function () {},\n                    clipboardData: {\n                        types: ['text/plain'],\n                        getData: function () {\n                            // do we need to return different results for the different types? text/plain, text/html\n                            return 'Plain Text';\n                        }\n                    }\n                },\n                pasteBin = contentEditables[1];\n\n            pasteBin.innerHTML = '';\n            spyOn(evt, 'preventDefault');\n\n            // Paste should not be prevented on the paste bin\n            pasteExtension.handlePasteBinPaste(evt);\n            expect(evt.preventDefault).not.toHaveBeenCalled();\n            jasmine.clock().tick(1);\n\n            // plaintext from the clipboard should now be in the editor\n            expect(this.el.innerHTML).toBe('Plain Text');\n\n            // paste-bin should be gone\n            contentEditables = document.body.querySelectorAll('[contentEditable=true]');\n            expect(contentEditables.length).toBe(1);\n        });\n    });\n\n    describe('using cleanPaste', function () {\n        it('should filter inline rich-text', function () {\n            var i,\n                editorEl = this.el,\n                editor = this.newMediumEditor('.editor', {\n                    delay: 200,\n                    paste: {\n                        forcePlainText: false,\n                        cleanPastedHTML: true\n                    }\n                });\n\n            for (i = 0; i < inlineTests.length; i += 1) {\n\n                // move caret to editor\n                editorEl.innerHTML = 'Before&nbsp;<span id=\"editor-inner\">&nbsp;</span>&nbsp;after.';\n\n                selectElementContents(document.getElementById('editor-inner'));\n\n                editor.cleanPaste(inlineTests[i].paste);\n                jasmine.clock().tick(100);\n\n                // Firefox and IE: doing an insertHTML while this <span> is selected results in the html being inserted inside of the span\n                // Firefox replace the &nbsp; other either side of the <span> with a space\n                // Webkit: doing an insertHTML while this <span> is selected results in the span being replaced completely\n                expect(editorEl.innerHTML).toMatch(new RegExp('^Before(&nbsp;|\\\\s)(<span id=\"editor-inner\">)?' + inlineTests[i].output + '(</span>)?(&nbsp;|\\\\s)after\\\\.$'));\n            }\n        });\n\n        it('should filter inline rich-text when \"insertHTML\" command is not supported', function () {\n            var editor = this.newMediumEditor('.editor', {\n                    paste: {\n                        forcePlainText: false,\n                        cleanPastedHTML: true\n                    }\n                });\n\n            spyOn(document, 'queryCommandSupported').and.returnValue(false);\n\n            inlineTests.forEach(function (test) {\n                this.el.innerHTML = 'Before&nbsp;<span id=\"editor-inner\">&nbsp;</span>&nbsp;after.';\n\n                selectElementContents(document.getElementById('editor-inner'));\n\n                editor.cleanPaste(test.paste);\n\n                // Firefox and IE: doing an insertHTML while this <span> is selected results in the html being inserted inside of the span\n                // Firefox replace the &nbsp; other either side of the <span> with a space\n                // Webkit: doing an insertHTML while this <span> is selected results in the span being replaced completely\n                expect(this.el.innerHTML).toMatch(new RegExp('^Before(&nbsp;|\\\\s)(<span id=\"editor-inner\">)?' + test.output + '(</span>)?(&nbsp;|\\\\s)after\\\\.$'));\n            }.bind(this));\n        });\n\n        it('should respect custom replacments when passed during instantiation', function () {\n            var editor = this.newMediumEditor('.editor', {\n                paste: {\n                    forcePlainText: false,\n                    cleanPastedHTML: true,\n                    cleanReplacements: [[new RegExp(/<label>/gi), '<sub>'], [new RegExp(/<\\/label>/gi), '</sub>']]\n                }\n            });\n\n            this.el.innerHTML = 'Before&nbsp;<span id=\"editor-inner\">&nbsp;</span>&nbsp;after.';\n            selectElementContents(document.getElementById('editor-inner'));\n\n            editor.cleanPaste('<label>div one</label><label>div two</label>');\n            expect(this.el.innerHTML).toMatch(new RegExp('^Before(&nbsp;|\\\\s)(<span id=\"editor-inner\">)?<sub>div one</sub><sub>div two</sub>(</span>)?(&nbsp;|\\\\s)after\\\\.$'));\n        });\n\n        it('should respect custom replacements before builtin replacements.', function () {\n            var editor = this.newMediumEditor('.editor', {\n                paste: {\n                    forcePlainText: false,\n                    cleanPastedHTML: true,\n                    preCleanReplacements: [[new RegExp(/<\\/?o:[a-z]*>/gi), 'foo']]\n                }\n            });\n\n            this.el.innerHTML = 'Before&nbsp;<span id=\"editor-inner\">&nbsp;</span>&nbsp;after.';\n            selectElementContents(document.getElementById('editor-inner'));\n\n            // Normally, the paste extension's regular expressions would clear the `<o:p></o:p>` tags,\n            // but our `preCleanReplacements` should transform them each to \"foo\" before the default\n            // cleanReplacement has a chance to see it.\n            editor.cleanPaste('<div><o:p></o:p></div>');\n\n            expect(this.el.innerHTML).toMatch(new RegExp('foofoo'));\n        });\n\n        it('should cleanup only pasted element on multi-line when nothing is selected', function () {\n            var editor = this.newMediumEditor('.editor', {\n                paste: {\n                    forcePlainText: false,\n                    cleanPastedHTML: true\n                }\n            });\n\n            this.el.innerHTML = '<div><img src=\"http://0.0.0.0/ohyeah.png\" /></div>';\n\n            selectElementContents(this.el.firstChild, { collapse: true });\n\n            editor.cleanPaste('<table><tr><td>test</td><td><br/></td></tr></table>');\n\n            expect(this.el.innerHTML).toContain('<img src=\"http://0.0.0.0/ohyeah.png\"></div>');\n        });\n    });\n\n    describe('using pasteHTML', function () {\n        it('should remove certain attributes and tags by default', function () {\n            var editor = this.newMediumEditor('.editor');\n            selectElementContents(this.el.firstChild);\n            editor.pasteHTML('<p class=\"some-class\" style=\"font-weight: bold\" dir=\"ltr\"><meta name=\"description\" content=\"test\" />test</p>');\n            expect(editor.elements[0].innerHTML).toBe('<p>test</p>');\n        });\n\n        it('should not remove node with \"empty\" content', function () {\n            var editor = this.newMediumEditor('.editor', {\n                paste: {\n                    forcePlainText: false,\n                    cleanPastedHTML: true\n                }\n            });\n\n            this.el.innerHTML = '<div>this is a div</div><figure id=\"editor-inner\">and this is a figure</figure>.';\n\n            selectElementContents(this.el.firstChild);\n\n            editor.pasteHTML('<table><tr><td>test</td><td><br/></td></tr></table>');\n\n            expect(this.el.innerHTML).toContain('<table><tbody><tr><td>test</td><td><br></td></tr></tbody></table>');\n        });\n\n        it('should accept a list of attrs to clean up', function () {\n            var editor = this.newMediumEditor('.editor');\n            selectElementContents(this.el.firstChild);\n            editor.pasteHTML(\n                '<table class=\"medium-editor-table\" dir=\"ltr\" style=\"border: 1px solid red;\"><tbody><tr><td>test</td></tr></tbody></table>',\n                { cleanAttrs: ['style', 'dir'] }\n            );\n            expect(editor.elements[0].innerHTML).toBe('<table class=\"medium-editor-table\"><tbody><tr><td>test</td></tr></tbody></table>');\n        });\n\n        it('should accept a list of tags to clean up', function () {\n            var editor = this.newMediumEditor('.editor');\n            selectElementContents(this.el.firstChild);\n            editor.pasteHTML(\n                '<div><i>test</i><meta name=\"description\" content=\"test\" /><b>test</b></div>',\n                { cleanTags: ['meta', 'b'] }\n            );\n            expect(editor.elements[0].innerHTML).toBe('<div><i>test</i></div>');\n        });\n\n        it('should accept a list of tags to unwrap', function () {\n            var editor = this.newMediumEditor('.editor');\n            selectElementContents(this.el.firstChild);\n            editor.pasteHTML(\n                '<div><i>test</i><sub><b>test</b></sub><sup>test</sup></div>',\n                { unwrapTags: ['sub', 'sup'] }\n            );\n            expect(editor.elements[0].innerHTML).toBe('<div><i>test</i><b>test</b>test</div>');\n        });\n\n        it('should respect custom clean up options passed during instantiation', function () {\n            var editor = this.newMediumEditor('.editor', {\n                paste: {\n                    cleanAttrs: ['style', 'dir'],\n                    cleanTags: ['meta', 'b'],\n                    unwrapTags: ['sub', 'sup']\n                }\n            });\n            selectElementContents(this.el.firstChild);\n            editor.pasteHTML(\n                '<table class=\"medium-editor-table\" dir=\"ltr\" style=\"border: 1px solid red;\"><tbody><tr><td>test</td></tr></tbody></table>' +\n                '<div><i>test</i><meta name=\"description\" content=\"test\" /><b>test</b></div>' +\n                '<div><i>test</i><sub><b>test</b></sub><sup>test</sup></div>'\n            );\n            expect(editor.elements[0].innerHTML).toBe(\n                '<table class=\"medium-editor-table\"><tbody><tr><td>test</td></tr></tbody></table>' +\n                '<div><i>test</i></div>' +\n                '<div><i>test</i>test</div>'\n            );\n        });\n    });\n\n    describe('text with/without linebreaks', function () {\n        it('should be handled consistantly', function () {\n            var range, i,\n                editorEl = this.el,\n                sel = window.getSelection(),\n                editor = this.newMediumEditor('.editor', {\n                    delay: 200,\n                    disableReturn: false\n                }),\n                pasteHandler = editor.getExtensionByName('paste'),\n\n                // mock event with clipboardData API\n                // test requires creating a function, so can't loop or jslint balks\n                evt = {\n                    pasteText: null,\n                    preventDefault: function () {\n                        return;\n                    },\n                    clipboardData: {\n                        types: ['text/plain', 'text/html'],\n                        getData: function () {\n                            // do we need to return different results for the different types? text/plain, text/html\n                            return this.pasteText;\n                        }\n                    }\n                };\n\n            for (i = 0; i < textTests.length; i += 1) {\n                editorEl.innerHTML = '<div id=\"editor-inner\">&nbsp;</div>';\n\n                range = document.createRange();\n                range.selectNodeContents(editorEl.firstChild);\n                sel.removeAllRanges();\n                sel.addRange(range);\n\n                evt.clipboardData.pasteText = textTests[i].paste;\n                pasteHandler.handlePaste(evt, editorEl);\n                jasmine.clock().tick(100);\n                expect(editorEl.innerHTML).toEqual(textTests[i].output);\n            }\n        });\n    });\n\n    describe('using custom paste handler', function () {\n        it('should be overrideable via paste options', function () {\n            var origInit = spyOn(MediumEditor.extensions.paste.prototype, 'init'),\n                newInit = jasmine.createSpy('spy'),\n                editor = this.newMediumEditor('.editor', {\n                    paste: {\n                        forcePlainText: false,\n                        cleanPastedHTML: true,\n                        init: newInit\n                    }\n                });\n            expect(origInit).not.toHaveBeenCalled();\n            expect(newInit).toHaveBeenCalled();\n\n            selectElementContents(this.el.firstChild);\n            editor.cleanPaste('<p>One\\nTwo\\n</p>\\n\\n<p>Three Four</p>');\n            expect(editor.elements[0].innerHTML).toBe('<p>One Two </p><p>Three Four</p>');\n        });\n\n        it('should be overrideable via custom extension', function () {\n            var origInit = spyOn(MediumEditor.extensions.paste.prototype, 'init'),\n                newInit = jasmine.createSpy('spy'),\n                origPasteHTML = spyOn(MediumEditor.extensions.paste.prototype, 'pasteHTML'),\n                newPasteHTML = jasmine.createSpy('spy'),\n                customPasteHandler = {\n                    name: 'paste',\n                    init: newInit,\n                    pasteHTML: newPasteHTML\n                },\n                editor = this.newMediumEditor('.editor', {\n                    extensions: {\n                        paste: customPasteHandler\n                    }\n                }),\n                testString = '<p>test</p>';\n\n            expect(origInit).not.toHaveBeenCalled();\n            expect(newInit).toHaveBeenCalled();\n            expect(editor.getExtensionByName('paste')).toBe(customPasteHandler);\n\n            editor.pasteHTML(testString);\n            expect(origPasteHTML).not.toHaveBeenCalled();\n            expect(newPasteHTML).toHaveBeenCalledWith(testString, undefined);\n        });\n    });\n});\n"
  },
  {
    "path": "spec/placeholder.spec.js",
    "content": "/*global fireEvent, selectElementContentsAndFire */\n\ndescribe('MediumEditor.extensions.placeholder TestCase', function () {\n    'use strict';\n\n    beforeEach(function () {\n        setupTestHelpers.call(this);\n        this.el = this.createElement('div', 'editor', '');\n    });\n\n    afterEach(function () {\n        this.cleanupTest();\n    });\n\n    it('should set placeholder for empty elements', function () {\n        var editor = this.newMediumEditor('.editor');\n        expect(editor.elements[0].className).toContain('medium-editor-placeholder');\n    });\n\n    it('should not set a placeholder for elements with text content', function () {\n        this.el.innerHTML = 'some text';\n        var editor = this.newMediumEditor('.editor');\n        expect(editor.elements[0].className).not.toContain('medium-editor-placeholder');\n    });\n\n    it('should not set a placeholder for elements with images only', function () {\n        this.el.innerHTML = '<img src=\"../demo/img/roman.jpg\">';\n        var editor = this.newMediumEditor('.editor');\n        expect(editor.elements[0].className).not.toContain('medium-editor-placeholder');\n    });\n\n    it('should not set a placeholder for elements with unorderedlist', function () {\n        this.el.innerHTML = '<ul><li></li></ul>';\n        var editor = this.newMediumEditor('.editor');\n        expect(editor.elements[0].className).not.toContain('medium-editor-placeholder');\n    });\n\n    it('should not set a placeholder for elements with orderedlist', function () {\n        this.el.innerHTML = '<ol><li></li></ol>';\n        var editor = this.newMediumEditor('.editor');\n        expect(editor.elements[0].className).not.toContain('medium-editor-placeholder');\n    });\n\n    it('should not set a placeholder for elements with table', function () {\n        this.el.innerHTML = '<table></table>';\n        var editor = this.newMediumEditor('.editor');\n        expect(editor.elements[0].className).not.toContain('medium-editor-placeholder');\n    });\n\n    it('should set placeholder for elements with empty children', function () {\n        this.el.innerHTML = '<p><br></p><div class=\"empty\"></div>';\n        var editor = this.newMediumEditor('.editor');\n        expect(editor.elements[0].className).toContain('medium-editor-placeholder');\n    });\n\n    it('should remove the placeholder on focus', function () {\n        var editor = this.newMediumEditor('.editor');\n        expect(editor.elements[0].className).toContain('medium-editor-placeholder');\n        selectElementContentsAndFire(editor.elements[0]);\n        expect(editor.elements[0].className).not.toContain('medium-editor-placeholder');\n    });\n\n    it('should remove the placeholder on click', function () {\n        var editor = this.newMediumEditor('.editor');\n        expect(editor.elements[0].className).toContain('medium-editor-placeholder');\n        fireEvent(editor.elements[0], 'click');\n        expect(editor.elements[0].className).not.toContain('medium-editor-placeholder');\n        fireEvent(document.body, 'mousedown');\n        fireEvent(document.body, 'mouseup');\n        fireEvent(document.body, 'click');\n        expect(editor.elements[0].className).toContain('medium-editor-placeholder');\n        this.el.innerHTML = '<p>lorem</p><p id=\"target\">ipsum</p><p>dolor</p>';\n        fireEvent(document.getElementById('target'), 'click');\n        expect(editor.elements[0].className).not.toContain('medium-editor-placeholder');\n    });\n\n    it('should remove the placeholder on input, and NOT on click', function () {\n        var editor = this.newMediumEditor('.editor', { placeholder: { hideOnClick: false }});\n        expect(editor.elements[0].className).toContain('medium-editor-placeholder');\n        fireEvent(editor.elements[0], 'click');\n        expect(editor.elements[0].className).toContain('medium-editor-placeholder');\n        fireEvent(document.body, 'mousedown');\n        fireEvent(document.body, 'mouseup');\n        fireEvent(document.body, 'click');\n        expect(editor.elements[0].className).toContain('medium-editor-placeholder');\n        this.el.innerHTML = '<p>lorem</p><p id=\"target\">ipsum</p><p>dolor</p>';\n        editor.trigger('editableInput', {}, editor.elements[0]);\n        expect(editor.elements[0].className).not.toContain('medium-editor-placeholder');\n        this.el.innerHTML = '';\n        editor.trigger('editableInput', {}, editor.elements[0]);\n        expect(editor.elements[0].className).toContain('medium-editor-placeholder');\n    });\n\n    it('should add a placeholder to empty elements on blur', function () {\n        this.el.innerHTML = 'some text';\n        var editor = this.newMediumEditor('.editor');\n        editor.elements[0].focus();\n        fireEvent(editor.elements[0], 'focus');\n        expect(editor.elements[0].className).not.toContain('medium-editor-placeholder');\n        editor.elements[0].innerHTML = '';\n        fireEvent(document.body, 'mousedown');\n        fireEvent(document.body, 'mouseup');\n        fireEvent(document.body, 'click');\n        expect(editor.elements[0].className).toContain('medium-editor-placeholder');\n    });\n\n    it('should not add a placeholder to elements with text on blur', function () {\n        var editor = this.newMediumEditor('.editor');\n        expect(editor.elements[0].className).toContain('medium-editor-placeholder');\n        editor.elements[0].innerHTML = 'some text';\n        editor.selectElement(this.el.firstChild);\n        fireEvent(document.body, 'mousedown');\n        fireEvent(document.body, 'mouseup');\n        fireEvent(document.body, 'click');\n        expect(editor.elements[0].className).not.toContain('medium-editor-placeholder');\n    });\n\n    // https://github.com/yabwe/medium-editor/issues/768\n    it('should remove the placeholder when the content is updated manually', function () {\n        var editor = this.newMediumEditor('.editor');\n        expect(editor.elements[0].className).toContain('medium-editor-placeholder');\n        editor.setContent('<p>lorem ipsum</p>');\n        expect(editor.elements[0].className).not.toContain('medium-editor-placeholder');\n    });\n\n    // https://github.com/yabwe/medium-editor/issues/783\n    it('should not show a placeholder when input changes but editor is still empty', function () {\n        var editor = this.newMediumEditor('.editor');\n        expect(editor.elements[0].className).toContain('medium-editor-placeholder');\n        fireEvent(editor.elements[0], 'click');\n        editor.elements[0].focus();\n        expect(editor.elements[0].className).not.toContain('medium-editor-placeholder');\n        var toolbar = editor.getExtensionByName('toolbar');\n        fireEvent(editor.elements[0], 'blur');\n        fireEvent(toolbar.getToolbarElement().querySelector('[data-action=\"append-h2\"]'), 'click');\n        expect(editor.elements[0].className).not.toContain('medium-editor-placeholder');\n    });\n\n    /*jslint regexp: true*/\n    function validatePlaceholderContent(element, expectedValue) {\n        var placeholder = window.getComputedStyle(element, ':after').getPropertyValue('content'),\n            regex = /^attr\\(([^\\)]+)\\)$/g,\n            match = regex.exec(placeholder);\n        if (match) {\n            // In firefox, getComputedStyle().getPropertyValue('content') can return attr() instead of what attr() evaluates to\n            expect(match[1]).toBe('data-placeholder');\n        }\n        // When these tests run in firefox in saucelabs, for some reason the content property of the\n        // placeholder is 'none'.  Not sure why this happens, or why this is specific to saucelabs\n        // but for now, just skipping the assertion in this case\n        else if (placeholder !== 'none') {\n            expect(placeholder).toMatch(new RegExp('^[\\'\"]' + expectedValue + '[\\'\"]$'));\n        }\n    }\n    /*jslint regexp: false*/\n\n    it('should add the default placeholder text when data-placeholder is not present', function () {\n        var editor = this.newMediumEditor('.editor');\n        validatePlaceholderContent(editor.elements[0], MediumEditor.extensions.placeholder.prototype.text);\n    });\n\n    it('should add the default placeholder text when data-placeholder is not present on dynamically added elements', function () {\n        var editor = this.newMediumEditor('.editor');\n        expect(editor.elements.length).toBe(1);\n\n        var newEl = this.createElement('div', 'other-element');\n        editor.addElements(newEl);\n        validatePlaceholderContent(newEl, MediumEditor.extensions.placeholder.prototype.text);\n    });\n\n    it('should remove the added data-placeholder attribute when destroyed', function () {\n        expect(this.el.hasAttribute('data-placeholder')).toBe(false);\n\n        var editor = this.newMediumEditor('.editor');\n        expect(this.el.getAttribute('data-placeholder')).toBe(MediumEditor.extensions.placeholder.prototype.text);\n\n        editor.destroy();\n        expect(this.el.hasAttribute('data-placeholder')).toBe(false);\n    });\n\n    it('should remove the added data-placeholder attribute when elements are removed dynamically from the editor', function () {\n        var editor = this.newMediumEditor('.editor'),\n            newEl = this.createElement('div', 'other-element');\n\n        expect(newEl.hasAttribute('other-element')).toBe(false);\n        editor.addElements(newEl);\n        expect(newEl.getAttribute('data-placeholder')).toBe(MediumEditor.extensions.placeholder.prototype.text);\n\n        editor.removeElements('.other-element');\n        expect(newEl.hasAttribute('data-placeholder')).toBe(false);\n    });\n\n    it('should not remove custom data-placeholder attribute when destroyed', function () {\n        var placeholderText = 'Custom placeholder';\n        this.el.setAttribute('data-placeholder', placeholderText);\n\n        var editor = this.newMediumEditor('.editor');\n        expect(this.el.getAttribute('data-placeholder')).toBe(placeholderText);\n\n        editor.destroy();\n        expect(this.el.getAttribute('data-placeholder')).toBe(placeholderText);\n    });\n\n    it('should use the data-placeholder when it is present', function () {\n        var editor,\n            placeholderText = 'Custom placeholder';\n        this.el.setAttribute('data-placeholder', placeholderText);\n        editor = this.newMediumEditor('.editor');\n        validatePlaceholderContent(editor.elements[0], placeholderText);\n    });\n\n    it('should use custom placeholder text when passed as the placeholder.text option', function () {\n        var placeholderText = 'Custom placeholder',\n            editor = this.newMediumEditor('.editor', {\n            placeholder: {\n                text: placeholderText\n            }\n        });\n        validatePlaceholderContent(editor.elements[0], placeholderText);\n    });\n\n    it('should not set placeholder for empty elements when placeholder is set to false', function () {\n        var editor = this.newMediumEditor('.editor', {\n            placeholder: false\n        });\n        expect(editor.elements[0].className).not.toContain('medium-editor-placeholder');\n    });\n\n    it('should not add a placeholder to empty elements on blur when placeholder is set to false', function () {\n        this.el.innerHTML = 'some text';\n        var editor = this.newMediumEditor('.editor', {\n            placeholder: false\n        });\n        expect(editor.elements[0].className).not.toContain('medium-editor-placeholder');\n        editor.elements[0].innerHTML = '';\n        fireEvent(document.querySelector('div'), 'click');\n        expect(editor.elements[0].className).not.toContain('medium-editor-placeholder');\n    });\n});\n"
  },
  {
    "path": "spec/selection.spec.js",
    "content": "/*global selectElementContents, placeCursorInsideElement, isSafari */\n\ndescribe('MediumEditor.selection TestCase', function () {\n    'use strict';\n\n    beforeEach(function () {\n        setupTestHelpers.call(this);\n        this.el = this.createElement('div', 'editor', 'lorem ipsum');\n    });\n\n    afterEach(function () {\n        this.cleanupTest();\n    });\n\n    describe('Exposure', function () {\n        it('is exposed on the MediumEditor ctor', function () {\n            expect(MediumEditor.selection).toBeTruthy();\n        });\n    });\n\n    describe('exportSelection', function () {\n        it('should not export a position indicating the cursor is before an empty paragraph', function () {\n            this.el.innerHTML = '<p><span>www.google.com</span></p><p><br /></p><p>Whatever</p>';\n            placeCursorInsideElement(this.el.querySelector('span'), 1); // end of first span\n            var exportedSelection = MediumEditor.selection.exportSelection(this.el, document);\n            expect(exportedSelection.emptyBlocksIndex).toEqual(undefined);\n        });\n\n        it('should export a position indicating the cursor is at the beginning of a paragraph', function () {\n            this.el.innerHTML = '<p><span>www.google.com</span></p><p><b>Whatever</b></p>';\n            placeCursorInsideElement(this.el.querySelector('b'), 0); // beginning of <b> tag\n            var exportedSelection = MediumEditor.selection.exportSelection(this.el, document);\n            expect(exportedSelection.emptyBlocksIndex).toEqual(0);\n        });\n\n        it('should not export a position indicating the cursor is after an empty paragraph', function () {\n            this.el.innerHTML = '<p><span>www.google.com</span></p><p><br /></p>' +\n                '<p class=\"target\">Whatever</p>';\n            // After the 'W' in whatever\n            placeCursorInsideElement(this.el.querySelector('p.target').firstChild, 1);\n            var exportedSelection = MediumEditor.selection.exportSelection(this.el, document);\n            expect(exportedSelection.emptyBlocksIndex).toEqual(undefined);\n        });\n\n        it('should not export a position indicating the cursor is after an empty paragraph (in a complicated markup case)',\n                function () {\n            this.el.innerHTML = '<p><span>www.google.com</span></p><p><br /></p>' +\n                '<p>What<span class=\"target\">ever</span></p>';\n            // Before the 'e' in whatever\n            placeCursorInsideElement(this.el.querySelector('span.target').firstChild, 0);\n            var exportedSelection = MediumEditor.selection.exportSelection(this.el, document);\n            expect(exportedSelection.emptyBlocksIndex).toEqual(undefined);\n        });\n\n        it('should not export a position indicating the cursor is after an empty paragraph ' +\n                '(in a complicated markup with selection on the element)', function () {\n            this.el.innerHTML = '<p><span>www.google.com</span></p><p><br /></p>' +\n                '<p>What<span class=\"target\">ever</span></p>';\n            // Before the 'e' in whatever\n            placeCursorInsideElement(this.el.querySelector('span.target'), 0);\n            var exportedSelection = MediumEditor.selection.exportSelection(this.el, document);\n            expect(exportedSelection.emptyBlocksIndex).toEqual(undefined);\n        });\n\n        it('should export a position indicating the cursor is in an empty paragraph', function () {\n            this.el.innerHTML = '<p><span>www.google.com</span></p><p><br /></p><p>Whatever</p>';\n            placeCursorInsideElement(this.el.getElementsByTagName('p')[1], 0);\n            var exportedSelection = MediumEditor.selection.exportSelection(this.el, document);\n            expect(exportedSelection.emptyBlocksIndex).toEqual(1);\n        });\n\n        it('should export a position indicating the cursor is after an empty paragraph', function () {\n            this.el.innerHTML = '<p><span>www.google.com</span></p><p><br /></p><p>Whatever</p>';\n            placeCursorInsideElement(this.el.getElementsByTagName('p')[2], 0);\n            var exportedSelection = MediumEditor.selection.exportSelection(this.el, document);\n            expect(exportedSelection.emptyBlocksIndex).toEqual(2);\n        });\n\n        it('should export a position indicating the cursor is after an empty block element', function () {\n            this.el.innerHTML = '<p><span>www.google.com</span></p><h1><br /></h1><h2><br /></h2><p>Whatever</p>';\n            placeCursorInsideElement(this.el.querySelector('h2'), 0);\n            var exportedSelection = MediumEditor.selection.exportSelection(this.el, document);\n            expect(exportedSelection.emptyBlocksIndex).toEqual(2);\n        });\n\n        it('should export a selection that specifies an image is the selection', function () {\n            this.el.innerHTML = '<p>lorem ipsum <a href=\"#\"><img src=\"../demo/img/medium-editor.jpg\" /></a> dolor</p>';\n            selectElementContents(this.el.querySelector('a'));\n            var exportedSelection = MediumEditor.selection.exportSelection(this.el, document);\n            expect(exportedSelection.trailingImageCount).toBe(1);\n            expect(exportedSelection.start).toBe(12);\n            expect(exportedSelection.end).toBe(12);\n        });\n\n        it('should export a selection that can be imported when the selection starts with an image', function () {\n            this.el.innerHTML = '<p>lorem ipsum <a href=\"#\"><img src=\"../demo/img/medium-editor.jpg\" />img</a> dolor</p>';\n            selectElementContents(this.el.querySelector('a'));\n            var exportedSelection = MediumEditor.selection.exportSelection(this.el, document);\n            expect(exportedSelection.trailingImageCount).toBe(undefined);\n            expect(exportedSelection.start).toBe(12);\n            expect(exportedSelection.end).toBe(15);\n\n            selectElementContents(this.el);\n            MediumEditor.selection.importSelection(exportedSelection, this.el, document);\n            var range = window.getSelection().getRangeAt(0);\n            expect(range.toString()).toBe('img');\n            if (range.startContainer.nodeName.toLowerCase() === 'a') {\n                expect(range.startContainer).toBe(this.el.querySelector('a'));\n                expect(range.startOffset).toBe(0);\n            } else {\n                expect(range.startContainer.nextSibling).toBe(this.el.querySelector('a'));\n                expect(range.startOffset).toBe(12);\n            }\n        });\n\n        it('should export a selection that specifies an image is at the end of a selection', function () {\n            this.el.innerHTML = '<p>lorem ipsum <a href=\"#\">img<b><img src=\"../demo/img/medium-editor.jpg\" /><img src=\"../demo/img/roman.jpg\" /></b></a> dolor</p>';\n            selectElementContents(this.el.querySelector('a'));\n            var exportedSelection = MediumEditor.selection.exportSelection(this.el, document);\n            expect(exportedSelection.trailingImageCount).toBe(2);\n            expect(exportedSelection.start).toBe(12);\n            expect(exportedSelection.end).toBe(15);\n        });\n    });\n\n    describe('importSelection', function () {\n        it('should be able to import an exported selection', function () {\n            this.el.innerHTML = 'lorem <i>ipsum</i> dolor';\n\n            selectElementContents(this.el.querySelector('i'));\n            var exportedSelection = MediumEditor.selection.exportSelection(this.el, document);\n            expect(Object.keys(exportedSelection).sort()).toEqual(['end', 'start']);\n\n            selectElementContents(this.el);\n            expect(exportedSelection).not.toEqual(MediumEditor.selection.exportSelection(this.el, document));\n\n            MediumEditor.selection.importSelection(exportedSelection, this.el, document);\n            expect(exportedSelection).toEqual(MediumEditor.selection.exportSelection(this.el, document));\n        });\n\n        it('should be able to import an exported selection that contain nodeTypes > 3', function (done) {\n            this.el.innerHTML = '<div><p>stuff here <!-- comment nodeType is 8 --> additional stuff here </p></div>';\n            selectElementContents(this.el.querySelector('p'));\n            var exportedSelection = MediumEditor.selection.exportSelection(this.el, document);\n            expect(Object.keys(exportedSelection).sort()).toEqual(['end', 'start']);\n\n            selectElementContents(this.el);\n            expect(exportedSelection).toEqual(MediumEditor.selection.exportSelection(this.el, document));\n\n            MediumEditor.selection.importSelection(exportedSelection, this.el, document);\n            expect(exportedSelection).toEqual(MediumEditor.selection.exportSelection(this.el, document));\n            done();\n        });\n\n        it('should import an exported selection outside any anchor tag', function () {\n            this.el.innerHTML = '<p id=1>Hello world: <a href=\"#\">http://www.example.com</a></p><p id=2><br></p>';\n            var link = this.el.getElementsByTagName('a')[0];\n\n            placeCursorInsideElement(link.childNodes[0], link.childNodes[0].nodeValue.length);\n            expect(MediumEditor.util.isDescendant(link, window.getSelection().getRangeAt(0).startContainer, true)).toBe(true);\n\n            var exportedSelection = MediumEditor.selection.exportSelection(this.el, document);\n            MediumEditor.selection.importSelection(exportedSelection, this.el, document, true);\n            var range = window.getSelection().getRangeAt(0),\n                node = range.startContainer;\n\n            // For some reason, Safari mucks with the selection range and makes this case not hold\n            // since we only really care about whether this works in IE, and it works as expected\n            // in other browsers, just skip this assertion for Safari\n            if (!isSafari()) {\n                expect(MediumEditor.util.isDescendant(link, node, true)).toBe(false);\n            }\n            // Even though we set the range to use the P tag as the start container, Safari normalizes the range\n            // down to the text node. Setting the range to use the P tag for the start is necessary to support\n            // MSIE, where it removes the link when the cursor is placed at the end of the text node in the anchor.\n            while (node.nodeName.toLowerCase() !== 'p') {\n                node = node.parentNode;\n            }\n            expect(node.nodeName.toLowerCase()).toBe('p');\n            expect(node.getAttribute('id')).toBe('1');\n        });\n\n        // https://github.com/yabwe/medium-editor/issues/738\n        it('should import an exported non-collapsed selection after an empty paragraph', function () {\n            this.el.innerHTML = '<p>This is <a href=\"#\">a link</a></p><p><br/></p><p>not a link</p>';\n            var lastTextNode = this.el.childNodes[2].firstChild;\n\n            MediumEditor.selection.select(document, lastTextNode, 0, lastTextNode, 'not a link'.length);\n\n            var exportedSelection = MediumEditor.selection.exportSelection(this.el, document);\n            expect(exportedSelection).toEqual({ start: 14, end: 24, emptyBlocksIndex: 2 });\n            MediumEditor.selection.importSelection(exportedSelection, this.el, document);\n\n            var range = window.getSelection().getRangeAt(0);\n            expect(range.startContainer === lastTextNode || range.startContainer === lastTextNode.parentNode)\n                .toBe(true, 'The selection is starting at the wrong element');\n            expect(range.startOffset).toBe(0, 'The start of the selection is not at the beginning of the text node');\n            expect(range.endContainer === lastTextNode || range.endContainer === lastTextNode.parentNode)\n                .toBe(true, 'The selection is ending at the wrong element');\n            expect(range.endOffset).toBe('not a link'.length, 'The end of the selection is not at the end of the text node');\n        });\n\n        it('should import a position with the cursor in an empty paragraph', function () {\n            this.el.innerHTML = '<p><span>www.google.com</span></p><p><br /></p><p>Whatever</p>';\n            MediumEditor.selection.importSelection({\n                'start': 14,\n                'end': 14,\n                'emptyBlocksIndex': 1\n            }, this.el, document);\n\n            var startParagraph = MediumEditor.util.getClosestTag(window.getSelection().getRangeAt(0).startContainer, 'p');\n            expect(startParagraph).toBe(this.el.getElementsByTagName('p')[1], 'empty paragraph');\n        });\n\n        it('should import a position with the cursor after an empty paragraph', function () {\n            this.el.innerHTML = '<p><span>www.google.com</span></p><p><br /></p><p>Whatever</p>';\n            MediumEditor.selection.importSelection({\n                'start': 14,\n                'end': 14,\n                'emptyBlocksIndex': 2\n            }, this.el, document);\n\n            var startParagraph = MediumEditor.util.getClosestTag(window.getSelection().getRangeAt(0).startContainer, 'p');\n            expect(startParagraph).toBe(this.el.getElementsByTagName('p')[2], 'paragraph after empty paragraph');\n        });\n\n        it('should import a position with the cursor after an empty paragraph when there are multipled editable elements', function () {\n            var el = this.createElement('div', 'editor', '<p><span>www.google.com</span></p><p><br /></p><p>Whatever</p>');\n            MediumEditor.selection.importSelection({\n                'start': 14,\n                'end': 14,\n                'editableElementIndex': 1,\n                'emptyBlocksIndex': 2\n            }, el, document);\n\n            var startParagraph = MediumEditor.util.getClosestTag(window.getSelection().getRangeAt(0).startContainer, 'p');\n            expect(startParagraph).toBe(el.getElementsByTagName('p')[2], 'paragraph after empty paragraph');\n        });\n\n        it('should import a position with the cursor after an empty block element', function () {\n            this.el.innerHTML = '<p><span>www.google.com</span></p><h1><br /></h1><h2><br /></h2><p>Whatever</p>';\n            MediumEditor.selection.importSelection({\n                'start': 14,\n                'end': 14,\n                'emptyBlocksIndex': 2\n            }, this.el, document);\n\n            var startParagraph = MediumEditor.util.getClosestTag(window.getSelection().getRangeAt(0).startContainer, 'h2');\n            expect(startParagraph).toBe(this.el.querySelector('h2'), 'block element after empty block element');\n        });\n\n        it('should import a position with the cursor after an empty block element when there are nested block elements', function () {\n            this.el.innerHTML = '<blockquote><p><span>www.google.com</span></p></blockquote><h1><br /></h1><h2><br /></h2><p>Whatever</p>';\n            MediumEditor.selection.importSelection({\n                'start': 14,\n                'end': 14,\n                'emptyBlocksIndex': 2\n            }, this.el, document);\n\n            var startParagraph = MediumEditor.util.getClosestTag(window.getSelection().getRangeAt(0).startContainer, 'h2');\n            expect(startParagraph).toBe(this.el.querySelector('h2'), 'block element after empty block element');\n        });\n\n        it('should import a position with the cursor after an empty block element inside an element with various children', function () {\n            this.el.innerHTML = '<p><span>www.google.com</span></p><h1><br /></h1><h2><br /></h2><p><b><i>Whatever</i></b></p>';\n            MediumEditor.selection.importSelection({\n                'start': 14,\n                'end': 14,\n                'emptyBlocksIndex': 3\n            }, this.el, document);\n\n            var innerElement = window.getSelection().getRangeAt(0).startContainer;\n            expect(MediumEditor.util.isDescendant(this.el.querySelector('i'), innerElement, true)).toBe(true, 'nested inline elment inside block element after empty block element');\n        });\n\n        ['br', 'img'].forEach(function (tagName) {\n            it('should not import a selection into focusing on the element \\'' + tagName + '\\' that cannot have children', function () {\n                this.el.innerHTML = '<p>Hello</p><p><' + tagName + ' /></p><p>World<p>';\n                MediumEditor.selection.importSelection({\n                    'start': 5,\n                    'end': 5,\n                    'emptyBlocksIndex': 1\n                }, this.el, document);\n\n                var innerElement = window.getSelection().getRangeAt(0).startContainer;\n                expect(innerElement.nodeName.toLowerCase()).toBe('p', 'focused element nodeName');\n                expect(innerElement).toBe(window.getSelection().getRangeAt(0).endContainer);\n                expect(innerElement.previousSibling.nodeName.toLowerCase()).toBe('p', 'previous sibling name');\n                expect(innerElement.nextSibling.nodeName.toLowerCase()).toBe('p', 'next sibling name');\n            });\n        });\n\n        it('should not import a selection into focusing on an empty element in a table', function () {\n            this.el.innerHTML = '<p>Hello</p><table><colgroup><col /></colgroup>' +\n                '<thead><tr><th>Head</th></tr></thead>' +\n                '<tbody><tr><td>Body</td></tr></tbody></table><p>World<p>';\n            MediumEditor.selection.importSelection({\n                'start': 5,\n                'end': 5,\n                'emptyBlocksIndex': 1\n            }, this.el, document);\n\n            var innerElement = window.getSelection().getRangeAt(0).startContainer;\n            // The behavior varies from browser to browser for this case, some select TH, some #textNode\n            expect(MediumEditor.util.isDescendant(this.el.querySelector('th'), innerElement, true))\n                .toBe(true, 'expect selection to be of TH or a descendant');\n            expect(innerElement).toBe(window.getSelection().getRangeAt(0).endContainer);\n        });\n\n        it('should not import a selection beyond any block elements that have text, even when emptyBlocksIndex indicates it should ', function () {\n            this.el.innerHTML = '<p><span>www.google.com</span></p><h1><br /></h1><h2>Not Empty</h2><p><b><i>Whatever</i></b></p>';\n            // Import a selection that indicates the text should be at the end of the 'www.google.com' word, but in the 3rd paragraph (at the beginning of 'Whatever')\n            MediumEditor.selection.importSelection({\n                'start': 14,\n                'end': 14,\n                'emptyBlocksIndex': 3\n            }, this.el, document);\n\n            var innerElement = window.getSelection().getRangeAt(0).startContainer;\n            expect(MediumEditor.util.isDescendant(this.el.querySelectorAll('p')[1], innerElement, true)).toBe(false, 'moved selection beyond non-empty block element');\n            expect(MediumEditor.util.isDescendant(this.el.querySelector('h2'), innerElement, true)).toBe(true, 'moved selection to element to incorrect block element');\n        });\n\n        // https://github.com/yabwe/medium-editor/issues/732\n        it('should support a selection correctly when space + newlines are separating block elements', function () {\n            this.el.innerHTML = '<ul>\\n' +\n                                '    <li><a href=\"#\">a link</a></li>\\n' +\n                                '    <li>a list item</li>\\n' +\n                                '    <li>target</li>\\n' +\n                                '</ul>';\n            var lastLi = this.el.querySelectorAll('ul > li')[2];\n\n            // Select the <li> with 'target'\n            selectElementContents(lastLi.firstChild);\n\n            var selectionData = MediumEditor.selection.exportSelection(this.el, document);\n            expect(selectionData.emptyBlocksIndex).toBeUndefined();\n\n            MediumEditor.selection.importSelection(selectionData, this.el, document);\n            var range = window.getSelection().getRangeAt(0);\n\n            expect(range.toString()).toBe('target', 'The selection is around the wrong element');\n            expect(MediumEditor.util.isDescendant(lastLi, range.startContainer, true)).toBe(true, 'The start of the selection is invalid');\n            expect(MediumEditor.util.isDescendant(lastLi, range.endContainer, true)).toBe(true, 'The end of the selection is invalid');\n        });\n\n        it('should support a selection that specifies an image is the selection', function () {\n            this.el.innerHTML = '<p>lorem ipsum <a href=\"#\"><img src=\"../demo/img/medium-editor.jpg\" /></a> dolor</p>';\n            MediumEditor.selection.importSelection({ start: 12, end: 12, startsWithImage: true, trailingImageCount: 1 }, this.el, document);\n            var range = window.getSelection().getRangeAt(0);\n            expect(range.toString()).toBe('');\n            expect(MediumEditor.util.isDescendant(range.endContainer, this.el.querySelector('img'), true)).toBe(true, 'the image is not within the selection');\n        });\n\n        it('should support a selection that starts with an image', function () {\n            this.el.innerHTML = '<p>lorem ipsum <a href=\"#\"><img src=\"../demo/img/medium-editor.jpg\" />img</a> dolor</p>';\n            MediumEditor.selection.importSelection({ start: 12, end: 15, startsWithImage: true }, this.el, document);\n            var range = window.getSelection().getRangeAt(0);\n            expect(range.toString()).toBe('img');\n            if (range.startContainer.nodeName.toLowerCase() === 'a') {\n                expect(range.startContainer).toBe(this.el.querySelector('a'));\n                expect(range.startOffset).toBe(0);\n            } else {\n                expect(range.startContainer.nextSibling).toBe(this.el.querySelector('a'));\n                expect(range.startOffset).toBe(12);\n            }\n        });\n\n        it('should support a selection that ends with an image', function () {\n            this.el.innerHTML = '<p>lorem ipsum <a href=\"#\">img<img src=\"../demo/img/medium-editor.jpg\" /><img src=\"../demo/img/roman.jpg\" /></a> dolor</p>';\n            MediumEditor.selection.importSelection({ start: 12, end: 15, trailingImageCount: 2 }, this.el, document);\n            var range = window.getSelection().getRangeAt(0);\n            expect(range.toString()).toBe('img');\n            expect(MediumEditor.util.isDescendant(range.endContainer, this.el.querySelector('img'), true)).toBe(true, 'the image is not within the selection');\n        });\n\n        // https://github.com/yabwe/medium-editor/issues/935\n        it('should support a selection that is after white-space at the beginning of a paragraph', function () {\n            this.el.innerHTML = '   <p>one two<br><a href=\"transindex.hu\">three</a><br></p><p><a href=\"amazon.com\">one</a> two three</p>';\n            this.newMediumEditor(this.el);\n            var firstText = this.el.querySelector('p').firstChild;\n            MediumEditor.selection.select(document, firstText, 0, firstText, 'one'.length);\n            var exported = MediumEditor.selection.exportSelection(this.el, document);\n            MediumEditor.selection.importSelection(exported, this.el, document);\n            var range = window.getSelection().getRangeAt(0);\n            expect(range.toString()).toBe('one');\n        });\n\n        it('should support importing a collapsed selection at the end of all content', function () {\n            this.el.innerHTML = '<p>lorem ipsum <b>dolor</b></p>';\n            var boldText = this.el.querySelector('b').firstChild;\n\n            placeCursorInsideElement(boldText, boldText.length);\n            var range = window.getSelection().getRangeAt(0);\n            expect(range.collapsed).toBe(true);\n            expect(MediumEditor.util.isDescendant(boldText.parentNode, range.startContainer, true)).toBe(true);\n            expect(MediumEditor.util.isDescendant(boldText.parentNode, range.endContainer, true)).toBe(true);\n\n            var exported = MediumEditor.selection.exportSelection(this.el, document);\n            expect(exported.start).toBe('lorem ipsum dolor'.length);\n            expect(exported.end).toBe('lorem ipsum dolor'.length);\n\n            MediumEditor.selection.importSelection(exported, this.el, document);\n            range = window.getSelection().getRangeAt(0);\n            expect(range.collapsed).toBe(true);\n            expect(MediumEditor.util.isDescendant(boldText.parentNode, range.startContainer, true)).toBe(true);\n            expect(MediumEditor.util.isDescendant(boldText.parentNode, range.endContainer, true)).toBe(true);\n        });\n    });\n\n    describe('getSelectedElements', function () {\n        it('no selected elements on empty selection', function () {\n            window.getSelection().removeAllRanges();\n            var elements = MediumEditor.selection.getSelectedElements(document);\n\n            expect(elements.length).toBe(0);\n        });\n\n        it('should select element from selection', function () {\n            this.el.innerHTML = 'lorem <i>ipsum</i> dolor';\n            selectElementContents(this.el.querySelector('i').firstChild);\n            var elements = MediumEditor.selection.getSelectedElements(document);\n\n            expect(elements.length).toBe(1);\n            expect(elements[0].nodeName.toLowerCase()).toBe('i');\n            expect(elements[0].innerHTML).toBe('ipsum');\n        });\n\n        it('should select first element when selection is global (ie: all the editor)', function () {\n            this.el.innerHTML = 'lorem <i>ipsum</i> dolor';\n            selectElementContents(this.el);\n            var elements = MediumEditor.selection.getSelectedElements(document);\n\n            expect(elements.length).toBe(1);\n            expect(elements[0].nodeName.toLowerCase()).toBe('i');\n            expect(elements[0].innerHTML).toBe('ipsum');\n        });\n    });\n\n    describe('getSelectedParentElement', function () {\n        it('should return null on bad range', function () {\n            expect(MediumEditor.selection.getSelectedParentElement(null)).toBe(null);\n            expect(MediumEditor.selection.getSelectedParentElement(false)).toBe(null);\n        });\n\n        it('should select the document', function () {\n            this.el.innerHTML = '<p>lorem <i>ipsum</i> dolor <span>hello</span> <b>you</b> </p>';\n            var range = document.createRange(),\n                sel = window.getSelection(),\n                element;\n\n            range.setStart(document, 0);\n            range.setEnd(this.el.querySelector('b').firstChild, 2);\n\n            sel.removeAllRanges();\n            sel.addRange(range);\n\n            element = MediumEditor.selection.getSelectedParentElement(range);\n\n            expect(element).toBe(document);\n        });\n    });\n\n    describe('selectionContainsContent', function () {\n        it('should return true for non-empty text', function () {\n            this.el.innerHTML = '<p>this is<span> </span>text</p>';\n            selectElementContents(this.el.querySelector('p'));\n\n            expect(MediumEditor.selection.selectionContainsContent(document)).toBe(true);\n        });\n\n        it('should return false for white-space only selections', function () {\n            this.el.innerHTML = '<p>this is<span> </span>text</p>';\n            selectElementContents(this.el.querySelector('span'));\n\n            expect(MediumEditor.selection.selectionContainsContent(document)).toBe(false);\n        });\n\n        it('should return true for image with link selections', function () {\n            this.el.innerHTML = '<p>this is <a href=\"#\"><img src=\"../demo/img/medium-editor.jpg\" /></a> image test</p>';\n            selectElementContents(this.el.querySelector('a'));\n\n            expect(MediumEditor.selection.selectionContainsContent(document)).toBe(true);\n        });\n    });\n\n    describe('clearSelection', function () {\n        it('should clear the selection and set the caret to the start of the prior range when specified', function () {\n            this.el.innerHTML = '<p>this is<span> </span>text</p>';\n            selectElementContents(this.el.querySelector('p'));\n            var selectionStart = document.getSelection().anchorOffset;\n\n            MediumEditor.selection.clearSelection(document, true);\n            expect(document.getSelection().focusOffset).toBe(selectionStart);\n\n            var newSelectionEnd = document.getSelection().focusOffset;\n            expect(newSelectionEnd).toBe(selectionStart);\n        });\n\n        it('should clear the selection and set the caret to the end of the prior range by default', function () {\n            this.el.innerHTML = '<p>this is<span> </span>text</p>';\n            selectElementContents(this.el.querySelector('p'));\n            var selectionEnd = document.getSelection().focusOffset;\n\n            MediumEditor.selection.clearSelection(document);\n            expect(document.getSelection().anchorOffset).toBe(selectionEnd);\n\n            var newSelectionStart = document.getSelection().anchorOffset;\n            expect(newSelectionStart).toBe(selectionEnd);\n        });\n    });\n});\n"
  },
  {
    "path": "spec/serialize.spec.js",
    "content": "describe('Anchor Button TestCase', function () {\n    'use strict';\n\n    beforeEach(function () {\n        setupTestHelpers.call(this);\n        this.el = this.createElement('div', 'editor', '<p>lorem <strong>ipsum</strong></p>');\n        this.el.id = 'medium-editor-test';\n    });\n\n    afterEach(function () {\n        this.cleanupTest();\n    });\n\n    it('should return the editor content as a JSON object', function () {\n        var editor = this.newMediumEditor('.editor'),\n            json = editor.serialize();\n        expect(json).toEqual({\n            'medium-editor-test': {\n                value: '<p>lorem <strong>ipsum</strong></p>'\n            }\n        });\n    });\n\n    it('should set a custom id when elements have no ids', function () {\n        this.el.removeAttribute('id');\n        var editor = this.newMediumEditor('.editor'),\n            json = editor.serialize();\n        expect(json).toEqual({\n            'element-0': {\n                value: '<p>lorem <strong>ipsum</strong></p>'\n            }\n        });\n    });\n\n});\n"
  },
  {
    "path": "spec/setup.spec.js",
    "content": "/*global fireEvent, selectElementContentsAndFire */\n\ndescribe('Setup/Destroy TestCase', function () {\n    'use strict';\n\n    beforeEach(function () {\n        setupTestHelpers.call(this);\n        this.el = this.createElement('div', 'editor', 'lore ipsum');\n    });\n\n    afterEach(function () {\n        this.cleanupTest();\n    });\n\n    it('should toggle the isActive property', function () {\n        var editor = this.newMediumEditor('.editor');\n        editor.destroy();\n        expect(editor.isActive).toBe(false);\n        editor.setup();\n        expect(editor.isActive).toBe(true);\n        editor.destroy();\n        expect(editor.isActive).toBe(false);\n    });\n\n    describe('Setup', function () {\n        it('should init the toolbar and editor elements', function () {\n            var editor = this.newMediumEditor('.editor');\n            editor.destroy();\n            spyOn(MediumEditor.prototype, 'setup').and.callThrough();\n            editor.setup();\n            expect(editor.setup).toHaveBeenCalled();\n            expect(document.querySelector('[data-medium-editor-element]')).toBeTruthy();\n            expect(document.querySelector('[aria-multiline]')).toBeTruthy();\n            expect(document.querySelector('[medium-editor-index]')).toBeTruthy();\n            expect(document.querySelector('[role]')).toBeTruthy();\n            expect(document.querySelector('[spellcheck]')).toBeTruthy();\n            expect(document.querySelector('[contenteditable]')).toBeTruthy();\n        });\n\n        it('should know about defaults', function () {\n            expect(MediumEditor.prototype.defaults).toBeTruthy();\n        });\n    });\n\n    describe('Destroy', function () {\n        it('should remove mediumEditor elements from DOM', function () {\n            var editor = this.newMediumEditor('.editor');\n            expect(document.querySelector('.medium-editor-toolbar')).toBeTruthy();\n            editor.destroy();\n            expect(document.querySelector('.medium-editor-toolbar')).toBeFalsy();\n\n            // ensure only initial attributes are here: the editor class\n            expect(this.el.getAttribute('class')).toBe('editor');\n            expect(this.el.attributes.length).toBe(1);\n        });\n\n        it('should remove all the added events', function () {\n            var editor = this.newMediumEditor('.editor');\n            expect(editor.events.events.length).toBeGreaterThan(0);\n            editor.destroy();\n            expect(editor.events.events.length).toBe(0);\n        });\n\n        it('should abort any pending throttled event handlers', function () {\n            var editor, triggerEvents, toolbar;\n\n            editor = this.newMediumEditor('.editor', { delay: 5 });\n            triggerEvents = function () {\n                fireEvent(window, 'resize');\n                fireEvent(document.body, 'click', {\n                    target: document.body\n                });\n                fireEvent(document.body, 'blur');\n            };\n            // Store toolbar, since destroy will remove the reference from the editor\n            toolbar = editor.getExtensionByName('toolbar');\n\n            // fire event (handler executed immediately)\n            triggerEvents();\n            jasmine.clock().tick(1);\n\n            // fire event again (handler delayed because of throttle)\n            triggerEvents();\n\n            spyOn(toolbar, 'positionToolbarIfShown').and.callThrough(); // via: handleResize\n            spyOn(editor, 'checkSelection').and.callThrough(); // via: handleBlur\n            editor.destroy();\n            jasmine.clock().tick(1000); // arbitrary – must be longer than THROTTLE_INTERVAL\n            expect(toolbar.positionToolbarIfShown).not.toHaveBeenCalled();\n            expect(editor.checkSelection).not.toHaveBeenCalled();\n        });\n\n        // regression test for https://github.com/yabwe/medium-editor/issues/197\n        it('should not crash when destroy immediately after a mouse click', function () {\n            var editor = this.newMediumEditor('.editor');\n            // selected some content and let the toolbar appear\n            selectElementContentsAndFire(editor.elements[0], { testDelay: 501 });\n\n            // fire a mouse up somewhere else (i.e. a button which click handler could have called destroy() )\n            fireEvent(document.documentElement, 'mouseup');\n            editor.destroy();\n\n            jasmine.clock().tick(501);\n            expect(true).toBe(true);\n        });\n    });\n});\n"
  },
  {
    "path": "spec/textarea.spec.js",
    "content": "/*global fireEvent */\n\ndescribe('Textarea TestCase', function () {\n    'use strict';\n\n    describe('MediumEditor constructor', function () {\n        beforeEach(function () {\n            setupTestHelpers.call(this);\n            this.el = this.createElement('textarea', 'editor');\n            this.el.value = 'test content';\n            this.el.setAttribute('data-disable-toolbar', false);\n            this.el.setAttribute('data-placeholder', 'Something');\n            this.el.setAttribute('data-disable-return', false);\n            this.el.setAttribute('data-disable-double-return', false);\n            this.el.setAttribute('data-disable-preview', false);\n            this.el.setAttribute('spellcheck', true);\n            this.el.setAttribute('data-imhere', 'ohyeah');\n        });\n\n        afterEach(function () {\n            this.cleanupTest();\n        });\n\n        it('should accept a textarea element and \"convert\" it to a div, preserving all attributes', function () {\n            var editor = this.newMediumEditor('.editor'),\n                textarea = this.el;\n            expect(editor.elements[0].nodeName.toLowerCase()).toBe('div');\n\n            var attributes = [\n                'data-disable-editing',\n                'data-disable-toolbar',\n                'data-placeholder',\n                'data-disable-return',\n                'data-disable-double-return',\n                'data-disable-preview',\n                'spellcheck',\n                'data-imhere'\n            ];\n            attributes.forEach(function (attr) {\n                expect(editor.elements[0].getAttribute(attr)).toBe(textarea.getAttribute(attr));\n            });\n        });\n\n        it('should sync editor changes with the original textarea element', function () {\n            var editor = this.newMediumEditor('.editor');\n            expect(this.el.value).toEqual('test content');\n            editor.elements[0].innerHTML = 'new content';\n            fireEvent(editor.elements[0], 'input');\n            fireEvent(editor.elements[0], 'keypress');\n            jasmine.clock().tick(1);\n            expect(this.el.value).toEqual('new content');\n        });\n\n        it('should preserve textarea className', function () {\n            this.el.className += ' test-class test-class-2';\n            var editor = this.newMediumEditor('.editor');\n            expect(editor.elements[0].className).toBe('editor test-class test-class-2 medium-editor-element');\n        });\n\n        it('should create unique div ids for multiple textareas', function () {\n            var origDateNow = Date.now;\n            Date.now = function () {\n                return 1464448478887;\n            };\n            for (var i = 0; i < 12; i++) {\n                var ta = this.createElement('textarea', 'editor');\n                ta.value = 'test content';\n            }\n            var editor = this.newMediumEditor('.editor');\n            editor.elements.forEach(function (el) {\n                expect(document.querySelectorAll('div#' + el.id).length).toEqual(1);\n            });\n            Date.now = origDateNow;\n        });\n\n        it('should create unique medium-editor-textarea-ids across all editor instances', function () {\n            var origDateNow = Date.now;\n            Date.now = function () {\n                return 1464448478887;\n            };\n            var tas = [];\n            for (var i = 0; i < 12; i++) {\n                var ta = this.createElement('textarea', 'editor');\n                ta.value = 'test content';\n                tas.push(ta);\n            }\n            tas.forEach(function (el) {\n                this.newMediumEditor(el);\n            }, this);\n            this.editors.forEach(function (editor) {\n                expect(document.querySelectorAll('textarea[medium-editor-textarea-id=\"' +\n                    editor.elements[0].getAttribute('medium-editor-textarea-id') + '\"]').length).toEqual(1);\n            });\n            Date.now = origDateNow;\n        });\n\n        it('should cleanup after destroy', function () {\n            var editor = this.newMediumEditor('.editor');\n            expect(this.el.classList.contains('medium-editor-hidden')).toBe(true);\n            editor.destroy();\n            expect(this.el.classList.contains('medium-editor-hidden')).toBe(false);\n        });\n\n        it('should reset the value of created div when form containing textarea is reset', function () {\n            var form = this.createElement('form', null, null, true),\n                initialContent = this.el.value;\n            form.appendChild(this.el);\n            document.body.appendChild(form);\n            var editor = this.newMediumEditor('.editor');\n            expect(editor.elements[0].innerHTML).toEqual(initialContent);\n\n            editor.setContent('<p>custom content</p>');\n            expect(editor.elements[0].innerHTML).not.toEqual(initialContent);\n\n            form.reset();\n            expect(editor.elements[0].innerHTML).toEqual(initialContent);\n        });\n    });\n\n    describe('Dynamically adding textarea elements to the editor', function () {\n        beforeEach(function () {\n            setupTestHelpers.call(this);\n            this.div = this.createElement('div', 'editable-div', 'sample div');\n            this.el = this.createElement('textarea', 'editor');\n            this.el.value = 'test content';\n            this.el.setAttribute('data-disable-toolbar', false);\n            this.el.setAttribute('data-placeholder', 'Something');\n            this.el.setAttribute('data-disable-return', false);\n            this.el.setAttribute('data-disable-double-return', false);\n            this.el.setAttribute('data-disable-preview', false);\n            this.el.setAttribute('spellcheck', true);\n            this.el.setAttribute('data-imhere', 'ohyeah');\n        });\n\n        afterEach(function () {\n            this.cleanupTest();\n        });\n\n        it('should \"convert\" it to a div, preserving all attributes', function () {\n            var editor = this.newMediumEditor('.editable-div'),\n                textarea = this.el;\n            expect(editor.elements[0]).toBe(this.div);\n            expect(editor.elements.length).toBe(1);\n\n            editor.addElements(this.el);\n            expect(editor.elements.length).toBe(2);\n\n            var attributes = [\n                    'data-disable-editing',\n                    'data-disable-toolbar',\n                    'data-placeholder',\n                    'data-disable-return',\n                    'data-disable-double-return',\n                    'data-disable-preview',\n                    'spellcheck',\n                    'data-imhere'\n                ],\n                tempDiv = editor.elements[1];\n\n            attributes.forEach(function (attr) {\n                expect(tempDiv.getAttribute(attr)).toBe(textarea.getAttribute(attr));\n            });\n        });\n\n        it('should sync editor changes with the original textarea element', function () {\n            var editor = this.newMediumEditor('.editable-div');\n            editor.addElements(this.el);\n\n            expect(this.el.value).toEqual('test content');\n            expect(editor.elements[1].innerHTML).toEqual('test content');\n\n            editor.elements[1].innerHTML = 'new content';\n            fireEvent(editor.elements[1], 'input');\n            fireEvent(editor.elements[1], 'keypress');\n            jasmine.clock().tick(1);\n            expect(this.el.value).toEqual('new content');\n        });\n\n        it('should preserve textarea className', function () {\n            this.el.className += ' test-class test-class-2';\n            var editor = this.newMediumEditor('.editable-div');\n            editor.addElements(this.el);\n            expect(editor.elements[1].className).toBe('editor test-class test-class-2 medium-editor-element');\n        });\n\n        it('should create unique div ids for multiple textareas', function () {\n            this.div.setAttribute('id', 'medium-editor-12345');\n            for (var i = 0; i < 12; i++) {\n                var ta = this.createElement('textarea', 'editor');\n                ta.value = 'test content';\n            }\n            var editor = this.newMediumEditor('.editable-div');\n            expect(editor.elements.length).toBe(1);\n\n            editor.addElements(document.querySelectorAll('.editor'));\n            expect(editor.elements.length).toBe(14);\n\n            editor.elements.forEach(function (el) {\n                expect(document.querySelectorAll('div#' + el.id).length).toEqual(1);\n            });\n        });\n\n        it('should create unique medium-editor-textarea-ids across all editor instances', function () {\n            var tas = [];\n            for (var i = 0; i < 12; i++) {\n                var ta = this.createElement('textarea', 'editor');\n                ta.value = 'test content';\n                tas.push(ta);\n\n                var div = this.createElement('div', 'editable-div');\n                div.innerHTML = 'test div';\n                this.newMediumEditor(div);\n            }\n\n            tas.forEach(function (el, idx) {\n                this.editors[idx].addElements(el);\n            }, this);\n            this.editors.forEach(function (editor) {\n                expect(document.querySelectorAll('textarea[medium-editor-textarea-id=\"' +\n                    editor.elements[1].getAttribute('medium-editor-textarea-id') + '\"]').length).toEqual(1);\n            });\n        });\n\n        it('should cleanup after destroy', function () {\n            var editor = this.newMediumEditor('.editable-div');\n            expect(this.el.classList.contains('medium-editor-hidden')).toBe(false);\n            editor.addElements(this.el);\n            expect(this.el.classList.contains('medium-editor-hidden')).toBe(true);\n            editor.destroy();\n            expect(this.el.classList.contains('medium-editor-hidden')).toBe(false);\n        });\n\n        it('should reset the value of created div when form containing textarea is reset', function () {\n            var form = this.createElement('form', null, null, true),\n                initialContent = 'initial text',\n                otherTextarea = this.createElement('textarea', 'editor', initialContent, true),\n                editor = this.newMediumEditor('.editor');\n            otherTextarea.value = initialContent;\n            expect(editor.elements.length).toBe(1);\n\n            form.appendChild(otherTextarea);\n            document.body.appendChild(form);\n\n            editor.addElements(otherTextarea);\n            var createdDiv = editor.elements[1];\n            expect(createdDiv.innerHTML).toEqual(initialContent);\n\n            editor.setContent('<p>custom content</p>', 1);\n            expect(createdDiv.innerHTML).not.toEqual(initialContent);\n\n            form.reset();\n            expect(createdDiv.innerHTML).toEqual(initialContent);\n        });\n    });\n\n    describe('Calling removeElements', function () {\n        beforeEach(function () {\n            setupTestHelpers.call(this);\n            this.div = this.createElement('div', 'editable-div', 'sample div');\n            this.el = this.createElement('textarea', 'editor');\n            this.el.value = 'test content';\n            this.el.id = 'editor-textarea';\n        });\n\n        afterEach(function () {\n            this.cleanupTest();\n        });\n\n        it('should remove the created div and show the textarea when passed a div created for a textarea', function () {\n            var editor = this.newMediumEditor('.editor'),\n                createdDiv = editor.elements[0],\n                divParent = createdDiv.parentNode;\n            expect(editor.elements.length).toBe(1);\n            expect(this.el.classList.contains('medium-editor-hidden')).toBe(true);\n\n            editor.addElements(this.div);\n            expect(editor.elements.length).toBe(2);\n\n            editor.removeElements(createdDiv);\n            expect(editor.elements.length).toBe(1);\n            expect(createdDiv.parentNode).not.toBe(divParent, 'The div created for the textarea should have been removed');\n            expect(this.el.hasAttribute('medium-editor-textarea-id')).toBe(false,\n                'The textarea should not have a medium-editor-textarea-id attribute when its editor div is removed');\n            expect(this.el.classList.contains('medium-editor-hidden')).toBe(false,\n                'The textarea should be hidden when its editor div is removed');\n        });\n\n        it('should remove the created div and show the textaarea when passed the actual textarea', function () {\n            var editor = this.newMediumEditor('.editor'),\n                createdDiv = editor.elements[0],\n                divParent = createdDiv.parentNode;\n            expect(editor.elements.length).toBe(1);\n            expect(this.el.classList.contains('medium-editor-hidden')).toBe(true);\n\n            editor.addElements(this.div);\n            expect(editor.elements.length).toBe(2);\n\n            editor.removeElements(this.el);\n            expect(editor.elements.length).toBe(1);\n            expect(createdDiv.parentNode).not.toBe(divParent, 'The div created for the textarea should have been removed');\n            expect(this.el.hasAttribute('medium-editor-textarea-id')).toBe(false,\n                'The textarea should not have a medium-editor-textarea-id attribute when its editor div is removed');\n            expect(this.el.classList.contains('medium-editor-hidden')).toBe(false,\n                'The textarea should be hidden when its editor div is removed');\n        });\n    });\n});\n"
  },
  {
    "path": "spec/toolbar.spec.js",
    "content": "/*global fireEvent, selectElementContents,\n         selectElementContentsAndFire,\n         placeCursorInsideElement */\n\ndescribe('MediumEditor.extensions.toolbar TestCase', function () {\n    'use strict';\n\n    beforeEach(function () {\n        setupTestHelpers.call(this);\n        this.el = this.createElement('div', 'editor');\n    });\n\n    afterEach(function () {\n        this.cleanupTest();\n    });\n\n    describe('Initialization', function () {\n        it('should call the createToolbar method', function () {\n            spyOn(MediumEditor.extensions.toolbar.prototype, 'createToolbar').and.callThrough();\n            var editor = this.newMediumEditor('.editor'),\n                toolbar = editor.getExtensionByName('toolbar');\n            expect(toolbar).not.toBeUndefined();\n            expect(toolbar.createToolbar).toHaveBeenCalled();\n        });\n\n        it('should create a new element for the editor toolbar', function () {\n            expect(document.querySelectorAll('.medium-editor-toolbar').length).toBe(0);\n            var editor = this.newMediumEditor('.editor'),\n                toolbar = editor.getExtensionByName('toolbar').getToolbarElement();\n            expect(toolbar.className).toMatch(/medium-editor-toolbar/);\n            expect(document.querySelectorAll('.medium-editor-toolbar').length).toBe(1);\n        });\n\n        it('should not create an anchor form element or anchor extension if anchor is not passed as a button', function () {\n            expect(document.querySelectorAll('.medium-editor-toolbar-form-anchor').length).toBe(0);\n            var editor = this.newMediumEditor('.editor', {\n                    toolbar: {\n                        buttons: ['bold', 'italic', 'underline']\n                    }\n                }),\n                toolbar = editor.getExtensionByName('toolbar');\n            expect(toolbar.getToolbarElement().querySelectorAll('.medium-editor-toolbar-form-anchor').length).toBe(0);\n            expect(editor.getExtensionByName('anchor')).toBeUndefined();\n        });\n    });\n\n    describe('Toolbars', function () {\n        it('should enable bold button in toolbar when bold text is selected', function () {\n            var editor = null,\n                newElement = this.createElement('div', '', 'lorem ipsum <b><div id=\"bold_dolorOne\">dolor</div></b>');\n\n            newElement.id = 'editor-for-toolbar-test';\n\n            editor = this.newMediumEditor(document.getElementById('editor-for-toolbar-test'), { delay: 0 });\n            var toolbar = editor.getExtensionByName('toolbar');\n            selectElementContentsAndFire(document.getElementById('bold_dolorOne'));\n\n            expect(toolbar.getToolbarElement().querySelector('button[data-action=\"bold\"]').classList.contains('medium-editor-button-active')).toBe(true);\n        });\n\n        it('should not activate buttons in toolbar when stopSelectionUpdates has been called, but should activate buttons after startSelectionUpdates is called', function () {\n            this.el.innerHTML = 'lorem ipsum <b><div id=\"bold_dolorTwo\">dolor</div></b>';\n\n            var editor = this.newMediumEditor(document.querySelectorAll('.editor'), { delay: 0 }),\n                toolbar = editor.getExtensionByName('toolbar');\n\n            editor.stopSelectionUpdates();\n            selectElementContentsAndFire(document.getElementById('bold_dolorTwo'));\n            expect(toolbar.getToolbarElement().querySelector('button[data-action=\"bold\"]').classList.contains('medium-editor-button-active')).toBe(false);\n\n            editor.startSelectionUpdates();\n            selectElementContentsAndFire(document.getElementById('bold_dolorTwo'), { eventToFire: 'mouseup' });\n            expect(toolbar.getToolbarElement().querySelector('button[data-action=\"bold\"]').classList.contains('medium-editor-button-active')).toBe(true);\n        });\n\n        it('should trigger the showToolbar custom event when toolbar is shown', function () {\n            var editor = this.newMediumEditor('.editor'),\n                callback = jasmine.createSpy();\n\n            this.el.innerHTML = 'specOnShowToolbarTest';\n\n            editor.subscribe('showToolbar', callback);\n\n            selectElementContentsAndFire(this.el);\n\n            expect(callback).toHaveBeenCalledWith({}, this.el);\n        });\n\n        it('should trigger positionToolbar custom event when toolbar is moved', function () {\n            var editor = this.newMediumEditor('.editor'),\n                callback = jasmine.createSpy();\n\n            this.el.innerHTML = 'specOnUpdateToolbarTest';\n            editor.subscribe('positionToolbar', callback);\n\n            selectElementContentsAndFire(this.el);\n\n            expect(callback).toHaveBeenCalledWith({}, this.el);\n        });\n\n        it('should trigger positionedToolbar custom event when toolbar is moved', function () {\n            var editor = this.newMediumEditor('.editor'),\n                callback = jasmine.createSpy();\n\n            this.el.innerHTML = 'specOnUpdateToolbarTest';\n            editor.subscribe('positionedToolbar', callback);\n\n            selectElementContentsAndFire(this.el);\n\n            expect(callback).toHaveBeenCalledWith({}, this.el);\n        });\n\n        it('should trigger positionToolbar before setToolbarPosition is called', function () {\n            this.el.innerHTML = 'position sanity check';\n            var editor = this.newMediumEditor('.editor'),\n                toolbar = editor.getExtensionByName('toolbar'),\n                triggerCount = 0,\n                temp = {\n                    update: function () {\n                        // selectElementContents will select the contents and trigger 'click'\n                        // Since we're manually triggering things, in Edge we have to trigger\n                        // 'click' or the toolbar won't detect the change.  However, in some\n                        // browsers the selection action itself triggers a 'focus' which the toolbar\n                        // picks up.  This means for some browsers (ie Chrome, FF) the 'positionToolbar'\n                        // event will trigger twice.  So, let's just make sure the first time the\n                        // event triggers, that it was triggered BEFORE the first call to setToolbarPosition\n                        if (triggerCount === 0) {\n                            expect(toolbar.setToolbarPosition).not.toHaveBeenCalled();\n                        }\n                        triggerCount++;\n                    }\n                };\n\n            selectElementContents(this.el);\n            jasmine.clock().tick(1);\n\n            spyOn(toolbar, 'setToolbarPosition').and.callThrough();\n            spyOn(temp, 'update').and.callThrough();\n            editor.subscribe('positionToolbar', temp.update);\n            selectElementContentsAndFire(this.el);\n            expect(temp.update).toHaveBeenCalled();\n            expect(toolbar.setToolbarPosition).toHaveBeenCalled();\n        });\n\n        it('should trigger positionedToolbar after setToolbarPosition and showToolbar is called', function () {\n            this.el.innerHTML = 'position sanity check';\n            var editor = this.newMediumEditor('.editor'),\n                toolbar = editor.getExtensionByName('toolbar'),\n                temp = {\n                    update: function () {\n                        expect(toolbar.setToolbarPosition).toHaveBeenCalled();\n                        expect(toolbar.showToolbar).toHaveBeenCalled();\n                    }\n                };\n\n            selectElementContents(this.el);\n            jasmine.clock().tick(1);\n\n            spyOn(toolbar, 'setToolbarPosition').and.callThrough();\n            spyOn(toolbar, 'showToolbar').and.callThrough();\n            spyOn(temp, 'update').and.callThrough();\n            editor.subscribe('positionedToolbar', temp.update);\n            selectElementContentsAndFire(this.el);\n            expect(temp.update).toHaveBeenCalled();\n            expect(toolbar.setToolbarPosition).toHaveBeenCalled();\n        });\n\n        it('should trigger the hideToolbar custom event when toolbar is hidden', function () {\n            var editor = this.newMediumEditor('.editor'),\n                callback = jasmine.createSpy();\n\n            this.el.innerHTML = 'specOnShowToolbarTest';\n\n            editor.subscribe('hideToolbar', callback);\n\n            selectElementContentsAndFire(this.el);\n\n            // Remove selection and call check selection, which should make the toolbar be hidden\n            window.getSelection().removeAllRanges();\n            editor.checkSelection();\n\n            expect(callback).toHaveBeenCalledWith({}, this.el);\n        });\n\n        it('should be possible to listen to toolbar events from extensions', function () {\n            var callbackShow = jasmine.createSpy('show'),\n                callbackHide = jasmine.createSpy('hide'),\n                TestExtension = MediumEditor.Extension.extend({\n                    parent: true,\n                    init: function () {\n                        this.base.subscribe('showToolbar', callbackShow);\n                        this.base.subscribe('hideToolbar', callbackHide);\n                    }\n                }),\n                editor = this.newMediumEditor('.editor', {\n                    extensions: { 'testExtension': new TestExtension() }\n                });\n\n            this.el.innerHTML = 'specOnShowToolbarTest';\n\n            selectElementContentsAndFire(this.el);\n            expect(callbackShow).toHaveBeenCalledWith({}, this.el);\n\n            // Remove selection and call check selection, which should make the toolbar be hidden\n            window.getSelection().removeAllRanges();\n            editor.checkSelection();\n\n            expect(callbackHide).toHaveBeenCalledWith({}, this.el);\n        });\n\n        // regression test for https://github.com/yabwe/medium-editor/issues/390\n        it('should work with multiple elements of the same class', function () {\n            var editor,\n                el,\n                i;\n\n            this.el.textContent = '0. Lorem ipsum dolor sit amet';\n            for (i = 1; i < 3; i += 1) {\n                el = this.createElement('div', 'editor');\n                el.textContent = i + '. Lorem ipsum dolor sit amet';\n            }\n\n            expect(document.querySelectorAll('.editor').length).toBe(3);\n\n            editor = this.newMediumEditor('.editor');\n            var toolbar = editor.getExtensionByName('toolbar');\n\n            selectElementContentsAndFire(editor.elements[0]);\n            expect(toolbar.isDisplayed()).toBe(true);\n\n            selectElementContentsAndFire(editor.elements[1]);\n            expect(toolbar.isDisplayed()).toBe(true);\n\n            selectElementContentsAndFire(editor.elements[2]);\n            expect(toolbar.isDisplayed()).toBe(true);\n        });\n\n        it('should not hide when selecting a link containing only an image', function () {\n            this.el.innerHTML = '<p>Here is an <a href=\"#\"><img src=\"../demo/img/medium-editor.jpg\"></a> image</p>';\n            var editor = this.newMediumEditor('.editor'),\n                toolbar = editor.getExtensionByName('toolbar');\n\n            selectElementContentsAndFire(editor.elements[0].querySelector('a'));\n            fireEvent(editor.elements[0], 'mousedown');\n            fireEvent(document.body, 'mouseup');\n            fireEvent(document.body, 'click');\n            jasmine.clock().tick(51);\n\n            expect(toolbar.isDisplayed()).toBe(true);\n        });\n\n        it('should not hide when selecting text within editor, but release mouse outside of editor', function () {\n            this.el.innerHTML = 'lorem ipsum';\n            var editor = this.newMediumEditor('.editor'),\n                toolbar = editor.getExtensionByName('toolbar');\n\n            selectElementContentsAndFire(editor.elements[0].firstChild);\n            fireEvent(editor.elements[0], 'mousedown');\n            fireEvent(document.body, 'mouseup');\n            fireEvent(document.body, 'click');\n            jasmine.clock().tick(51);\n\n            expect(toolbar.isDisplayed()).toBe(true);\n        });\n\n        it('should hide when clicking outside the toolbar on an element that does not clear selection', function () {\n            this.el.innerHTML = 'lorem ipsum';\n            var outsideElement = this.createElement('div', '', 'Click Me, I don\\'t clear selection'),\n                editor = this.newMediumEditor('.editor'),\n                toolbar = editor.getExtensionByName('toolbar');\n\n            outsideElement.setAttribute('style', '-webkit-user-select: none; -moz-user-select: none; -ms-user-select: none; user-select: none;');\n\n            selectElementContentsAndFire(editor.elements[0].firstChild);\n            expect(toolbar.isDisplayed()).toBe(true);\n\n            fireEvent(outsideElement, 'mousedown');\n            fireEvent(outsideElement, 'mouseup');\n            fireEvent(outsideElement, 'click');\n            jasmine.clock().tick(51);\n\n            expect(document.getSelection().rangeCount).toBe(1);\n            expect(toolbar.isDisplayed()).toBe(false);\n        });\n\n        it('should hide when selecting multiple paragraphs and the allowMultiParagraphSelection option is false', function () {\n            this.el.innerHTML = '<p id=\"p-one\">lorem ipsum</p><p id=\"p-two\">lorem ipsum</p>';\n            var editor = this.newMediumEditor('.editor', {\n                    toolbar: {\n                        allowMultiParagraphSelection: false\n                    }\n                }),\n                toolbar = editor.getExtensionByName('toolbar');\n            selectElementContentsAndFire(document.getElementById('p-one'));\n            expect(toolbar.getToolbarElement().classList.contains('medium-editor-toolbar-active')).toBe(true);\n            selectElementContentsAndFire(this.el, { eventToFire: 'mouseup' });\n            expect(toolbar.getToolbarElement().classList.contains('medium-editor-toolbar-active')).toBe(false);\n        });\n\n        it('should check for selection on mouseup event', function () {\n            spyOn(MediumEditor.extensions.toolbar.prototype, 'checkState');\n            var editor = this.newMediumEditor('.editor'),\n                toolbar = editor.getExtensionByName('toolbar');\n            fireEvent(editor.elements[0], 'mouseup');\n            expect(toolbar.checkState).toHaveBeenCalled();\n        });\n\n        it('should check for selection on keyup', function () {\n            spyOn(MediumEditor.extensions.toolbar.prototype, 'checkState');\n            var editor = this.newMediumEditor('.editor'),\n                toolbar = editor.getExtensionByName('toolbar');\n            fireEvent(editor.elements[0], 'keyup');\n            expect(toolbar.checkState).toHaveBeenCalled();\n        });\n\n        it('should hide if selection is empty', function () {\n            spyOn(MediumEditor.extensions.toolbar.prototype, 'setToolbarPosition').and.callThrough();\n            spyOn(MediumEditor.extensions.toolbar.prototype, 'setToolbarButtonStates').and.callThrough();\n            spyOn(MediumEditor.extensions.toolbar.prototype, 'showAndUpdateToolbar').and.callThrough();\n            var editor = this.newMediumEditor('.editor'),\n                toolbar = editor.getExtensionByName('toolbar');\n            toolbar.getToolbarElement().style.display = 'block';\n            toolbar.getToolbarElement().classList.add('medium-editor-toolbar-active');\n            expect(toolbar.getToolbarElement().classList.contains('medium-editor-toolbar-active')).toBe(true);\n            editor.checkSelection();\n            expect(toolbar.getToolbarElement().classList.contains('medium-editor-toolbar-active')).toBe(false);\n            expect(toolbar.setToolbarPosition).not.toHaveBeenCalled();\n            expect(toolbar.setToolbarButtonStates).not.toHaveBeenCalled();\n            expect(toolbar.showAndUpdateToolbar).not.toHaveBeenCalled();\n        });\n\n        it('should hide when selecting multiple paragraphs and the deprecated allowMultiParagraphSelection option is false', function () {\n            this.el.innerHTML = '<p id=\"p-one\">lorem ipsum</p><p id=\"p-two\">lorem ipsum</p>';\n            var editor = this.newMediumEditor('.editor', {\n                    toolbar: {\n                        allowMultiParagraphSelection: false\n                    }\n                }),\n                toolbar = editor.getExtensionByName('toolbar');\n            selectElementContentsAndFire(document.getElementById('p-one'));\n            expect(toolbar.getToolbarElement().classList.contains('medium-editor-toolbar-active')).toBe(true);\n            selectElementContentsAndFire(this.el, { eventToFire: 'mouseup' });\n            expect(toolbar.getToolbarElement().classList.contains('medium-editor-toolbar-active')).toBe(false);\n        });\n\n        it('should show when something is selected', function () {\n            this.el.innerHTML = 'lorem ipsum';\n            var editor = this.newMediumEditor('.editor'),\n                toolbar = editor.getExtensionByName('toolbar');\n            expect(toolbar.getToolbarElement().classList.contains('medium-editor-toolbar-active')).toBe(false);\n            selectElementContentsAndFire(this.el);\n            expect(toolbar.getToolbarElement().classList.contains('medium-editor-toolbar-active')).toBe(true);\n        });\n\n        it('should update position and button states when something is selected', function () {\n            this.el.innerHTML = 'lorem ipsum';\n            spyOn(MediumEditor.extensions.toolbar.prototype, 'setToolbarPosition').and.callThrough();\n            spyOn(MediumEditor.extensions.toolbar.prototype, 'setToolbarButtonStates').and.callThrough();\n            spyOn(MediumEditor.extensions.toolbar.prototype, 'showAndUpdateToolbar').and.callThrough();\n            var editor = this.newMediumEditor('.editor'),\n                toolbar = editor.getExtensionByName('toolbar');\n            selectElementContentsAndFire(this.el);\n            expect(toolbar.setToolbarPosition).toHaveBeenCalled();\n            expect(toolbar.setToolbarButtonStates).toHaveBeenCalled();\n            expect(toolbar.showAndUpdateToolbar).toHaveBeenCalled();\n        });\n\n        it('should correctly update position even if elementsContainer is absolute', function () {\n            var container = document.createElement('div'),\n                editor, toolbar;\n\n            container.style.position = 'absolute';\n            container.style.left = '200px';\n            container.style.top = '200px';\n            document.body.appendChild(container);\n\n            this.el.innerHTML = 'lorem';\n\n            editor = this.newMediumEditor('.editor', {\n                elementsContainer: container\n            });\n            toolbar = editor.getExtensionByName('toolbar').getToolbarElement();\n\n            selectElementContentsAndFire(this.el);\n            expect(toolbar.classList.contains('medium-editor-toolbar-active')).toBe(true);\n            expect(parseInt(toolbar.style.left, 10)).toBeLessThan(200);\n            expect(parseInt(toolbar.style.top, 10)).toBeLessThan(200);\n\n            document.body.removeChild(container);\n        });\n    });\n\n    describe('Static Toolbars', function () {\n        it('should let the user click outside of the selected area to leave', function () {\n            this.el.innerHTML = 'This is my text<span>and this is some other text</span>';\n            var editor = this.newMediumEditor('.editor', {\n                    toolbar: {\n                        static: true,\n                        updateOnEmptySelection: true,\n                        standardizeSelectionStart: true\n                    }\n                }),\n                toolbar = editor.getExtensionByName('toolbar');\n\n            placeCursorInsideElement(this.el.firstChild, 'This is my text'.length);\n            fireEvent(this.el.parentNode, 'click', {\n                target: this.el.parentNode,\n                currentTarget: this.el\n            });\n\n            jasmine.clock().tick(1);\n            expect(toolbar.getToolbarElement().classList.contains('medium-editor-toolbar-active')).toBe(false);\n            expect(this.el.getAttribute('medium-editor-focused')).not.toBeTruthy();\n        });\n\n        it('should not throw an error when check selection is called when there is an empty selection', function () {\n            this.el.innerHTML = '<b>lorem ipsum</b>';\n            var editor = this.newMediumEditor('.editor', {\n                    toolbar: {\n                        static: true,\n                        sticky: true\n                    }\n                }),\n                toolbar = editor.getExtensionByName('toolbar');\n\n            selectElementContentsAndFire(this.el.querySelector('b'));\n            expect(toolbar.getToolbarElement().classList.contains('medium-editor-toolbar-active')).toBe(true);\n            expect(toolbar.getToolbarElement().querySelector('[data-action=\"bold\"]').classList.contains('medium-editor-button-active')).toBe(true);\n            window.getSelection().removeAllRanges();\n            editor.checkSelection();\n            jasmine.clock().tick(1); // checkSelection delay\n            expect(true).toBe(true);\n        });\n\n        it('should show and update toolbar buttons when toolbar is static and updateOnEmptySelection option is set to true', function () {\n            this.el.innerHTML = '<b>lorem ipsum</b>';\n            var editor = this.newMediumEditor('.editor', {\n                    toolbar: {\n                        static: true,\n                        sticky: true,\n                        updateOnEmptySelection: true\n                    }\n                }),\n                toolbar = editor.getExtensionByName('toolbar');\n\n            selectElementContentsAndFire(this.el.querySelector('b'), { eventToFire: 'focus', testDelay: -1 });\n            window.getSelection().removeAllRanges();\n            editor.checkSelection();\n            jasmine.clock().tick(1); // checkSelection delay\n            expect(toolbar.getToolbarElement().classList.contains('medium-editor-toolbar-active')).toBe(true);\n            expect(toolbar.getToolbarElement().querySelector('[data-action=\"bold\"]').classList.contains('medium-editor-button-active')).toBe(true);\n        });\n\n        it('should be hidden for one medium-editor instance when another medium-editor instance shows its toolbar', function () {\n            var editorOne,\n                editorTwo,\n                elTwo = this.createElement('div', '', '<span id=\"editor-span-2\">lorem ipsum</span>');\n\n            elTwo.id = 'editor-div-two';\n            this.el.innerHTML = '<span id=\"editor-span-1\">lorem ipsum</span>';\n\n            editorOne = this.newMediumEditor('.editor', {\n                toolbar: {\n                    static: true\n                }\n            });\n            editorTwo = this.newMediumEditor(document.getElementById('editor-div-two'), {\n                toolbar: {\n                    static: true\n                }\n            });\n            var toolbarOne = editorOne.getExtensionByName('toolbar'),\n                toolbarTwo = editorTwo.getExtensionByName('toolbar');\n\n            selectElementContents(document.getElementById('editor-span-1'));\n            fireEvent(this.el, 'click', {\n                target: this.el,\n                relatedTarget: elTwo\n            });\n\n            jasmine.clock().tick(1); // checkSelection delay\n\n            expect(toolbarOne.getToolbarElement().classList.contains('medium-editor-toolbar-active')).toBe(true);\n            expect(toolbarTwo.getToolbarElement().classList.contains('medium-editor-toolbar-active')).toBe(false);\n\n            selectElementContents(document.getElementById('editor-span-2'));\n            fireEvent(elTwo, 'click', {\n                target: elTwo,\n                relatedTarget: this.el\n            });\n\n            jasmine.clock().tick(1); // checkSelection delay\n\n            expect(toolbarOne.getToolbarElement().classList.contains('medium-editor-toolbar-active')).toBe(false);\n            expect(toolbarTwo.getToolbarElement().classList.contains('medium-editor-toolbar-active')).toBe(true);\n        });\n\n        it('should update button states when updateOnEmptySelection is true and the selection is empty', function () {\n            spyOn(MediumEditor.extensions.toolbar.prototype, 'setToolbarButtonStates').and.callThrough();\n\n            var editor = this.newMediumEditor('.editor', {\n                toolbar: {\n                    updateOnEmptySelection: true,\n                    static: true\n                }\n            });\n\n            selectElementContentsAndFire(this.el, { collapse: 'toStart' });\n            expect(editor.getExtensionByName('toolbar').setToolbarButtonStates).toHaveBeenCalled();\n        });\n    });\n\n    describe('Deactive', function () {\n        it('should remove select event from elements', function () {\n            spyOn(this.el, 'addEventListener');\n            var editor = this.newMediumEditor('.editor');\n            expect(this.el.addEventListener).toHaveBeenCalled();\n            spyOn(this.el, 'removeEventListener');\n            editor.destroy();\n            expect(this.el.removeEventListener).toHaveBeenCalled();\n        });\n    });\n\n    describe('Disable', function () {\n        it('should not show the toolbar on elements when toolbar option is set to false', function () {\n            var editor = this.newMediumEditor('.editor', {\n                toolbar: false\n            });\n            expect(editor.options.toolbar).toBe(false);\n            expect(document.getElementsByClassName('medium-editor-toolbar-actions').length).toBe(0);\n        });\n\n        it('should not create the toolbar if all elements has data attr of disable-toolbar', function () {\n            this.el.setAttribute('data-disable-toolbar', 'true');\n            var editor = this.newMediumEditor('.editor');\n            expect(document.getElementsByClassName('medium-editor-toolbar-actions').length).toBe(0);\n            expect(editor.getExtensionByName('toolbar')).toBeUndefined();\n        });\n\n        it('should not show the toolbar when one element has a data attr of disable-toolbar set and text is selected', function () {\n            var element = this.createElement('div', 'editor', 'lorem ipsum'),\n                editor = null;\n\n            element.setAttribute('data-disable-toolbar', 'true');\n\n            editor = this.newMediumEditor(document.querySelectorAll('.editor'));\n            var toolbar = editor.getExtensionByName('toolbar');\n\n            expect(editor.elements.length).toBe(2);\n            expect(toolbar.getToolbarElement().style.display).toBe('');\n            selectElementContentsAndFire(element);\n\n            expect(toolbar.getToolbarElement().style.display).toBe('');\n        });\n\n        it('should not display toolbar when selected text within an element with contenteditable=\"false\"', function () {\n            this.createElement('div', 'editor');\n            this.el.innerHTML = 'lorem ipsum <div id=\"cef_el\" contenteditable=\"false\">dolor</div>';\n\n            var editor = this.newMediumEditor(document.querySelectorAll('.editor'), { delay: 0 }),\n                toolbar = editor.getExtensionByName('toolbar');\n\n            selectElementContentsAndFire(document.getElementById('cef_el'));\n            expect(toolbar.getToolbarElement().classList.contains('medium-editor-toolbar-active')).toBe(false);\n        });\n\n        it('should show the toolbar if its text are selected even though one or more elements that has a data attr of disable-toolbar', function () {\n            var editor,\n                element = this.createElement('div', 'editor');\n\n            element.setAttribute('data-disable-toolbar', 'true');\n            this.el.innerHTML = 'lorem ipsum';\n            editor = this.newMediumEditor(document.querySelectorAll('.editor'));\n            var toolbar = editor.getExtensionByName('toolbar');\n            expect(editor.elements.length).toBe(2);\n            expect(toolbar.getToolbarElement().style.display).toBe('');\n            selectElementContentsAndFire(this.el);\n\n            expect(toolbar.getToolbarElement().classList.contains('medium-editor-toolbar-active')).toBe(true);\n        });\n\n        it('should not try to toggle toolbar when toolbar option is set to false', function () {\n            this.createElement('div', 'editor');\n            this.el.innerHTML = 'lorem ipsum';\n\n            var editor = this.newMediumEditor(document.querySelectorAll('.editor'), {\n                    toolbar: false\n                }),\n                toolbar = editor.getExtensionByName('toolbar');\n\n            expect(toolbar).toBeUndefined();\n\n            selectElementContents(this.el);\n            editor.checkSelection();\n        });\n    });\n\n    describe('Scroll', function () {\n        it('should position static + sticky toolbar', function () {\n            var editor = this.newMediumEditor('.editor', {\n                    toolbar: {\n                        static: true,\n                        sticky: true\n                    }\n                }),\n                toolbar = editor.getExtensionByName('toolbar');\n            spyOn(MediumEditor.extensions.toolbar.prototype, 'positionToolbarIfShown');\n            fireEvent(window, 'scroll');\n            expect(toolbar.positionToolbarIfShown).toHaveBeenCalled();\n        });\n    });\n\n    describe('Resizing', function () {\n        beforeEach(function () {\n            this.el.innerHTML = 'test content';\n        });\n\n        it('should reset toolbar position', function () {\n            var editor = this.newMediumEditor('.editor'),\n                toolbar = editor.getExtensionByName('toolbar');\n            selectElementContentsAndFire(editor.elements[0]);\n            expect(toolbar.getToolbarElement().className.indexOf('active')).toBeGreaterThan(-1);\n            spyOn(toolbar, 'setToolbarPosition');\n            fireEvent(window, 'resize');\n            jasmine.clock().tick(1);\n            expect(toolbar.setToolbarPosition).toHaveBeenCalled();\n        });\n\n        it('should not call setToolbarPosition when toolbar is not visible', function () {\n            var editor = this.newMediumEditor('.editor'),\n                toolbar = editor.getExtensionByName('toolbar');\n            spyOn(toolbar, 'setToolbarPosition').and.callThrough();\n            fireEvent(window, 'resize');\n            jasmine.clock().tick(1);\n            expect(toolbar.getToolbarElement().className.indexOf('active')).toBe(-1);\n            expect(toolbar.setToolbarPosition).not.toHaveBeenCalled();\n        });\n\n        it('should throttle multiple calls to position toolbar', function () {\n            var editor = this.newMediumEditor('.editor'),\n                toolbar = editor.getExtensionByName('toolbar'),\n                tickTime = 60,\n                totalTicks;\n\n            selectElementContentsAndFire(editor.elements[0]);\n            expect(toolbar.getToolbarElement().className.indexOf('active')).toBeGreaterThan(-1);\n\n            spyOn(toolbar, 'setToolbarPosition').and.callThrough();\n            for (totalTicks = 0; totalTicks < tickTime; totalTicks += 10) {\n                fireEvent(window, 'resize');\n                jasmine.clock().tick(10);\n            }\n            expect(toolbar.setToolbarPosition.calls.count()).toBeLessThan(3);\n        });\n    });\n\n    describe('Static & sticky toolbar position', function () {\n        it('should position static + sticky toolbar on the left', function () {\n            this.el.innerHTML = '<b>lorem ipsum</b>';\n            var editor = this.newMediumEditor('.editor', {\n                    toolbar: {\n                        static: true,\n                        sticky: true,\n                        align: 'left'\n                    }\n                }),\n                toolbar = editor.getExtensionByName('toolbar').getToolbarElement();\n\n            selectElementContentsAndFire(this.el.querySelector('b'));\n            window.getSelection().getRangeAt(0).collapse(false);\n            editor.checkSelection();\n            jasmine.clock().tick(1); // checkSelection delay\n\n            expect(toolbar.style.left).not.toBe('');\n        });\n\n        it('should position static + sticky toolbar on the right', function () {\n            this.el.innerHTML = '<b>lorem ipsum</b>';\n            var editor = this.newMediumEditor('.editor', {\n                    toolbar: {\n                        static: true,\n                        sticky: true,\n                        align: 'right'\n                    }\n                }),\n                toolbar = editor.getExtensionByName('toolbar').getToolbarElement();\n\n            selectElementContentsAndFire(this.el.querySelector('b'));\n            window.getSelection().getRangeAt(0).collapse(false);\n            editor.checkSelection();\n            jasmine.clock().tick(1); // checkSelection delay\n\n            expect(toolbar.style.left).not.toBe('');\n        });\n\n        it('should position static + sticky toolbar on the center', function () {\n            this.el.innerHTML = '<b>lorem ipsum</b>';\n            var editor = this.newMediumEditor('.editor', {\n                    toolbar: {\n                        static: true,\n                        sticky: true,\n                        align: 'center'\n                    }\n                }),\n                toolbar = editor.getExtensionByName('toolbar').getToolbarElement();\n\n            selectElementContentsAndFire(this.el.querySelector('b'));\n            window.getSelection().getRangeAt(0).collapse(false);\n            editor.checkSelection();\n            jasmine.clock().tick(1); // checkSelection delay\n\n            expect(toolbar.style.left).not.toBe('');\n        });\n    });\n\n    describe('Relative Toolbars', function () {\n        it('should contain relative toolbar class', function () {\n            var relativeContainer = this.createElement('div');\n            relativeContainer.setAttribute('id', 'someRelativeDiv');\n            window.document.body.appendChild(relativeContainer);\n\n            var editor = this.newMediumEditor('.editor', {\n                    toolbar: {\n                        relativeContainer: document.getElementById('someRelativeDiv')\n                    }\n                }),\n                toolbar = editor.getExtensionByName('toolbar');\n\n            expect(toolbar.getToolbarElement().classList.contains('medium-editor-relative-toolbar')).toBe(true);\n        });\n\n        it('should be included in relative node', function () {\n            var relativeContainer = this.createElement('div');\n            relativeContainer.setAttribute('id', 'someRelativeDiv');\n            window.document.body.appendChild(relativeContainer);\n\n            var editor = this.newMediumEditor('.editor', {\n                toolbar: {\n                    relativeContainer: document.getElementById('someRelativeDiv')\n                }\n            }),\n            toolbarHTML = editor.getExtensionByName('toolbar').getToolbarElement().outerHTML;\n\n            expect(document.getElementById('someRelativeDiv').innerHTML).toBe(toolbarHTML);\n        });\n    });\n});\n"
  },
  {
    "path": "spec/util.spec.js",
    "content": "/*global selectElementContents, selectElementContentsAndFire */\n\ndescribe('MediumEditor.util', function () {\n    'use strict';\n\n    beforeEach(function () {\n        setupTestHelpers.call(this);\n    });\n\n    afterEach(function () {\n        this.cleanupTest();\n    });\n\n    describe('Exposure', function () {\n        it('is exposed on the MediumEditor ctor', function () {\n            expect(MediumEditor.util).toBeTruthy();\n        });\n\n    });\n\n    describe('Extend', function () {\n        it('should overwrite values from right to left', function () {\n            var objOne = { one: 'one' },\n                objTwo = { one: 'two', three: 'three' },\n                objThree = { three: 'four', five: 'six' },\n                objFour,\n                result = MediumEditor.util.extend({}, objOne, objTwo, objThree, objFour);\n            expect(result).toEqual({ one: 'two', three: 'four', five: 'six' });\n        });\n    });\n\n    describe('Defaults', function () {\n        it('should overwrite values from left to right', function () {\n            var objOne = { one: 'one' },\n                objTwo = { one: 'two', three: 'three' },\n                objThree = { three: 'four', five: 'six' },\n                objFour,\n                result = MediumEditor.util.defaults({}, objOne, objTwo, objThree, objFour);\n            expect(result).toEqual({ one: 'one', three: 'three', five: 'six' });\n        });\n\n        it('should overwrite nothing without args', function () {\n            var result = MediumEditor.util.defaults();\n\n            expect(result).toEqual({});\n        });\n    });\n\n    describe('Deprecated', function () {\n        it('should warn when a method is deprecated', function () {\n            var testObj = {\n                newMethod: function () {}\n            };\n            spyOn(testObj, 'newMethod').and.callThrough();\n            spyOn(MediumEditor.util, 'warn').and.callThrough();\n            MediumEditor.util.deprecatedMethod.call(testObj, 'test', 'newMethod', ['arg1', true], 'some version');\n            expect(testObj.newMethod).toHaveBeenCalledWith('arg1', true);\n            expect(MediumEditor.util.warn).toHaveBeenCalledWith(\n                'test is deprecated, please use newMethod instead. Will be removed in some version'\n            );\n        });\n\n        it('should warn when an option is deprecated', function () {\n            spyOn(MediumEditor.util, 'warn').and.callThrough();\n            MediumEditor.util.deprecated('oldOption', 'sub.newOption');\n            expect(MediumEditor.util.warn).toHaveBeenCalledWith(\n                'oldOption is deprecated, please use sub.newOption instead.'\n            );\n        });\n\n        it('should allow passing a version when the removal will happen', function () {\n            spyOn(MediumEditor.util, 'warn').and.callThrough();\n            MediumEditor.util.deprecated('old', 'new', '11tybillion');\n            expect(MediumEditor.util.warn).toHaveBeenCalledWith(\n                'old is deprecated, please use new instead. Will be removed in 11tybillion'\n            );\n        });\n    });\n\n    describe('settargetblank', function () {\n        it('sets target blank on a A element from a A element', function () {\n            var el = this.createElement('a', '', 'lorem ipsum');\n            el.attributes.href = 'http://0.0.0.0/bar.html';\n\n            MediumEditor.util.setTargetBlank(el);\n\n            expect(el.target).toBe('_blank');\n        });\n\n        it('sets target blank on a A element from a DIV element', function () {\n            var el = this.createElement('div', '', '<a href=\"http://1.1.1.1/foo.html\">foo</a> <a href=\"http://0.0.0.0/bar.html\">bar</a>');\n\n            MediumEditor.util.setTargetBlank(el, 'http://0.0.0.0/bar.html');\n\n            var nodes = el.getElementsByTagName('a');\n\n            expect(nodes[0].target).not.toBe('_blank');\n            expect(nodes[1].target).toBe('_blank');\n        });\n    });\n\n    describe('removetargetblank', function () {\n        it('removes target blank from a A element', function () {\n            var el = this.createElement('a', '', 'lorem ipsum');\n            el.attributes.href = 'http://0.0.0.0/bar.html';\n            el.attributes.target = '_blank';\n\n            MediumEditor.util.removeTargetBlank(el, 'http://0.0.0.0/bar.html');\n\n            expect(el.target).toBe('');\n        });\n\n        it('removes target blank on a A element from a DIV element', function () {\n            var el = this.createElement('div', '', '<a href=\"http://1.1.1.1/foo.html\" target=\"_blank\">foo</a> <a href=\"http://0.0.0.0/bar.html\" target=\"_blank\">bar</a>');\n\n            MediumEditor.util.removeTargetBlank(el, 'http://0.0.0.0/bar.html');\n\n            var nodes = el.getElementsByTagName('a');\n\n            expect(nodes[0].target).toBe('_blank');\n            expect(nodes[1].target).toBe('');\n        });\n    });\n\n    describe('addClassToAnchors', function () {\n        it('add class to anchors on a A element from a A element', function () {\n            var el = this.createElement('a', '', 'lorem ipsum');\n            el.attributes.href = 'http://0.0.0.0/bar.html';\n\n            MediumEditor.util.addClassToAnchors(el, 'firstclass');\n\n            expect(el.classList.length).toBe(1);\n            expect(el.classList.contains('firstclass')).toBe(true);\n        });\n\n        it('add class to anchors on a A element from a DIV element', function () {\n            var el = this.createElement('div', '', '<a href=\"http://1.1.1.1/foo.html\">foo</a> <a href=\"http://0.0.0.0/bar.html\">bar</a>');\n\n            MediumEditor.util.addClassToAnchors(el, 'firstclass');\n\n            var nodes = el.getElementsByTagName('a');\n\n            expect(nodes[0].classList.length).toBe(1);\n            expect(nodes[1].classList.length).toBe(1);\n\n            expect(nodes[0].classList.contains('firstclass')).toBe(true);\n            expect(nodes[1].classList.contains('firstclass')).toBe(true);\n        });\n    });\n\n    describe('warn', function () {\n\n        it('exists', function () {\n            expect(typeof MediumEditor.util.warn).toBe('function');\n        });\n\n        it('ends up calling console.warn', function () {\n            // IE9 mock for SauceLabs\n            if (window.console === undefined) {\n                window.console = {\n                    warn: function (msg) {\n                        return msg;\n                    }\n                };\n            } else if (typeof window.console.warn !== 'function') {\n                window.console.warn = function (msg) {\n                    return msg;\n                };\n            }\n\n            var spy = spyOn(window.console.warn, 'apply').and.callThrough();\n            MediumEditor.util.warn('message');\n            expect(spy).toHaveBeenCalled();\n        });\n\n    });\n\n    describe('splitOffDOMTree', function () {\n        /* start:\n         *\n         *         <div>\n         *      /    |   \\\n         *  <span> <span> <span>\n         *   / \\    / \\    / \\\n         *  1   2  3   4  5   6\n         *\n         * result:\n         *\n         *     <div>            <div>'\n         *      / \\              / \\\n         * <span> <span>   <span>' <span>\n         *   / \\    |        |      / \\\n         *  1   2   3        4     5   6\n         */\n        it('should split a complex tree correctly when splitting off right part of tree', function () {\n            var el = this.createElement('div', '',\n                '<span><b>1</b><i>2</i></span><span><b>3</b><u>4</u></span><span><b>5</b><i>6</i></span>'),\n                splitOn = el.querySelector('u').firstChild,\n                result = MediumEditor.util.splitOffDOMTree(el, splitOn);\n\n            expect(el.outerHTML).toBe('<div><span><b>1</b><i>2</i></span><span><b>3</b></span></div>');\n            expect(result.outerHTML).toBe('<div><span><u>4</u></span><span><b>5</b><i>6</i></span></div>');\n        });\n\n        /* start:\n         *\n         *         <div>\n         *      /    |   \\\n         *  <span> <span> <span>\n         *   / \\    / \\    / \\\n         *  1   2  3   4  5   6\n         *\n         * result:\n         *\n         *     <div>'      <div>\n         *      / \\          |\n         * <span> <span>   <span>\n         *   /\\     /\\       /\\\n         *  1  2   3  4     5  6\n         */\n        it('should split a complex tree correctly when splitting off left part of tree', function () {\n            var el = this.createElement('div', '',\n                '<span><b>1</b><i>2</i></span><span><b>3</b><u>4</u></span><span><b>5</b><i>6</i></span>'),\n                splitOn = el.querySelector('u').firstChild,\n                result = MediumEditor.util.splitOffDOMTree(el, splitOn, true);\n\n            expect(el.outerHTML).toBe('<div><span><b>5</b><i>6</i></span></div>');\n            expect(result.outerHTML).toBe('<div><span><b>1</b><i>2</i></span><span><b>3</b><u>4</u></span></div>');\n        });\n    });\n\n    describe('moveTextRangeIntoElement', function () {\n        it('should return false and bail if no elements are passed', function () {\n            expect(MediumEditor.util.moveTextRangeIntoElement(null, null)).toBe(false);\n        });\n\n        it('should return false and bail if elemenets do not share a root', function () {\n            var el = this.createElement('div', '', 'text'),\n                elTwo = this.createElement('div', '', 'more text', true),\n                temp = this.createElement('div', '');\n            expect(MediumEditor.util.moveTextRangeIntoElement(el, elTwo, temp)).toBe(false);\n            expect(temp.innerHTML).toBe('');\n        });\n\n        it('should create a parent element that spans multiple root elements', function () {\n            var el = this.createElement('div', '',\n                    '<span>Link = http</span>' +\n                    '<span>://</span>' +\n                    '<span>www.exam</span>' +\n                    '<span>ple.com</span>' +\n                    '<span>:443/</span>' +\n                    '<span>path/to</span>' +\n                    '<span>somewhere#</span>' +\n                    '<span>index notLink</span>'),\n                firstText = el.firstChild.firstChild.splitText('Link = '.length),\n                lastText = el.lastChild.firstChild,\n                para = this.createElement('p', '');\n            lastText.splitText('index'.length);\n            MediumEditor.util.moveTextRangeIntoElement(firstText, lastText, para);\n            expect(el.innerHTML).toBe(\n                '<span>Link = </span>' +\n                '<p>' +\n                    '<span>http</span>' +\n                    '<span>://</span>' +\n                    '<span>www.exam</span>' +\n                    '<span>ple.com</span>' +\n                    '<span>:443/</span>' +\n                    '<span>path/to</span>' +\n                    '<span>somewhere#</span>' +\n                    '<span>index</span>' +\n                '</p>' +\n                '<span> notLink</span>'\n            );\n        });\n    });\n\n    describe('getClosestTag', function () {\n        it('should get closed tag', function () {\n            var el = this.createElement('div', '', '<span>my <b>text</b></span>'),\n                tag = el.querySelector('b').firstChild,\n                closestTag = MediumEditor.util.getClosestTag(tag, 'span');\n\n            expect(closestTag.nodeName.toLowerCase()).toBe('span');\n        });\n\n        it('should not get closed tag with data-medium-editor-element', function () {\n            var el = this.createElement('div', '', '<p>youpi<span data-medium-editor-element=\"true\">my <b>text</b></span></p>'),\n                tag = el.querySelector('b').firstChild,\n                closestTag = MediumEditor.util.getClosestTag(tag, 'p');\n\n            expect(closestTag).toBe(false);\n        });\n\n        it('should not get closed tag from empty element', function () {\n            var closestTag = MediumEditor.util.getClosestTag(false, 'span');\n\n            expect(closestTag).toBe(false);\n        });\n    });\n\n    describe('isListItem', function () {\n        it('should be a list item but inside a ul', function () {\n            var el = this.createElement('ul', '', '<li>test</li>'),\n                result = MediumEditor.util.isListItem(el);\n\n            expect(result).toBe(false);\n        });\n\n        it('should be a list item', function () {\n            var el = this.createElement('ul', '', '<li>test</li>'),\n                li = el.querySelector('li'),\n                result = MediumEditor.util.isListItem(li);\n\n            expect(result).toBe(true);\n        });\n\n        it('should not be a list item', function () {\n            var result = MediumEditor.util.isListItem(false);\n\n            expect(result).toBe(false);\n        });\n\n        it('should not be a list item', function () {\n            var el = this.createElement('p', '', '<b>test</b>'),\n                result = MediumEditor.util.isListItem(el);\n\n            expect(result).toBe(false);\n        });\n    });\n\n    describe('isKey', function () {\n        it('should return true no matter how the key associated to the event is defined', function () {\n            var event;\n\n            event = {\n                which: 13,\n                keyCode: null,\n                charCode: null\n            };\n            expect(MediumEditor.util.isKey(event, 13)).toBeTruthy();\n\n            event = {\n                which: null,\n                keyCode: 13,\n                charCode: null\n            };\n            expect(MediumEditor.util.isKey(event, 13)).toBeTruthy();\n\n            event = {\n                which: null,\n                keyCode: null,\n                charCode: 13\n            };\n            expect(MediumEditor.util.isKey(event, 13)).toBeTruthy();\n        });\n\n        it('should return true when a key associated to event is listed to one we are looking for', function () {\n            var event = {\n                which: 13\n            };\n\n            expect(MediumEditor.util.isKey(event, 13)).toBeTruthy();\n            expect(MediumEditor.util.isKey(event, [13])).toBeTruthy();\n            expect(MediumEditor.util.isKey(event, [13, 12])).toBeTruthy();\n            expect(MediumEditor.util.isKey(event, [12, 13])).toBeTruthy();\n        });\n\n        it('should return false when a key associated to event is NOT listed to one we are looking for', function () {\n            var event = {\n                which: 13\n            };\n\n            expect(MediumEditor.util.isKey(event, 65)).toBeFalsy();\n            expect(MediumEditor.util.isKey(event, [65])).toBeFalsy();\n            expect(MediumEditor.util.isKey(event, [65, 66])).toBeFalsy();\n        });\n    });\n\n    describe('execFormatBlock', function () {\n        it('should execute indent command when called with blockquote when selection is inside a nested block element within a blockquote', function () {\n            var el = this.createElement('div', '', '<blockquote><p>Some <b>Text</b></p></blockquote>');\n            el.setAttribute('contenteditable', true);\n            selectElementContents(el.querySelector('b'));\n            spyOn(document, 'execCommand');\n\n            MediumEditor.util.execFormatBlock(document, 'blockquote');\n            expect(document.execCommand).toHaveBeenCalledWith('outdent', false, null);\n        });\n\n        it('should execute indent command when called with blockquote when isIE is true', function () {\n            var origIsIE = MediumEditor.util.isIE,\n                el = this.createElement('div', '', '<p>Some <b>Text</b></p>');\n            MediumEditor.util.isIE = true;\n            el.setAttribute('contenteditable', true);\n            selectElementContents(el.querySelector('b'));\n            spyOn(document, 'execCommand');\n\n            MediumEditor.util.execFormatBlock(document, 'blockquote');\n            expect(document.execCommand).toHaveBeenCalledWith('indent', false, 'blockquote');\n\n            MediumEditor.util.isIE = origIsIE;\n        });\n    });\n\n    describe('insertHTMLCommand', function () {\n        it('should not remove the contenteditable element when calling insert into an empty contenteditable element', function () {\n            var el = this.createElement('div', 'editable', ''),\n                origQCS = document.queryCommandSupported;\n            // Force our custom implementation to run\n            spyOn(document, 'queryCommandSupported').and.callFake(function (command) {\n                if (command === 'insertHTML') {\n                    return false;\n                }\n                return origQCS.apply(document, arguments);\n            });\n            // Mimic an editor element\n            el.setAttribute('contenteditable', true);\n            el.setAttribute('data-medium-editor-element', true);\n\n            // Make sure the element has 0 child nodes\n            while (el.firstChild) {\n                el.remove(el.firstChild);\n            }\n            selectElementContents(el);\n            MediumEditor.util.insertHTMLCommand(document, '<p>some pasted html</p>', true);\n\n            expect(el.innerHTML).toBe('<p>some pasted html</p>');\n            expect(document.body.contains(el)).toBe(true, 'The editor element has been removed from the page');\n        });\n\n        // https://github.com/yabwe/medium-editor/issues/992\n        it('should trigger editableInput when using custom insertHTML implementation and contenteditable does not support input event', function () {\n            // Ensure custom implementation\n            var originalInputSupport = MediumEditor.Events.prototype.InputEventOnContenteditableSupported;\n            MediumEditor.Events.prototype.InputEventOnContenteditableSupported = false;\n\n            var el = this.createElement('div', 'editable', '<p>orig text</p>'),\n                origQCS = document.queryCommandSupported;\n            // Force our custom implementation to run\n            spyOn(document, 'queryCommandSupported').and.callFake(function (command) {\n                if (command === 'insertHTML') {\n                    return false;\n                }\n                return origQCS.apply(document, arguments);\n            });\n\n            var editor = this.newMediumEditor('.editable'),\n                spy = jasmine.createSpy('handler');\n\n            editor.subscribe('editableInput', spy);\n            selectElementContentsAndFire(el.firstChild);\n            MediumEditor.util.insertHTMLCommand(document, '<p>some pasted html</p>', true);\n\n            var obj = { target: el, currentTarget: el };\n            expect(spy).toHaveBeenCalledWith(obj, el);\n\n            expect(el.innerHTML).toBe('<p>some pasted html</p>');\n            expect(document.body.contains(el)).toBe(true, 'The editor element has been removed from the page');\n\n            MediumEditor.Events.prototype.InputEventOnContenteditableSupported = originalInputSupport;\n        });\n    });\n\n    describe('isDescendant', function () {\n        it('should return true for an element which is a descendant of another', function () {\n            var parent = this.createElement('div'),\n                child = parent.appendChild(document.createTextNode('text'));\n            expect(MediumEditor.util.isDescendant(parent, child)).toBe(true);\n        });\n\n        it('should return false for an element which is not a descendant of another', function () {\n            var parent = this.createElement('div'),\n                child = document.createTextNode('text');\n            expect(MediumEditor.util.isDescendant(parent, child)).toBe(false);\n        });\n\n        it('should return false when checking the same element', function () {\n            var parent = this.createElement('div');\n            expect(MediumEditor.util.isDescendant(parent, parent)).toBe(false);\n        });\n\n        it('should return true when checking the same element but using the equality param', function () {\n            var parent = this.createElement('div');\n            expect(MediumEditor.util.isDescendant(parent, parent, true)).toBe(true);\n        });\n\n        it('should return false when the elements are null', function () {\n            var parent = this.createElement('div');\n            expect(MediumEditor.util.isDescendant(parent, null)).toBe(false);\n            expect(MediumEditor.util.isDescendant(null, parent)).toBe(false);\n            expect(MediumEditor.util.isDescendant(null, null)).toBe(false);\n        });\n\n        it('should return false when the parent element is a text node', function () {\n            var parent = document.createTextNode('text'),\n                child = this.createElement('div');\n            expect(MediumEditor.util.isDescendant(parent, child)).toBe(false);\n        });\n    });\n\n    describe('splitByBlockElements', function () {\n        it('should return block elements without block elements as children', function () {\n            var el = this.createElement('div');\n            el.innerHTML = '' +\n                '<blockquote><ol>' +\n                        '<li><div><table><thead><tr><th>Head</th></tr></thead></table></div></li>' +\n                        '<li>List Item</li>' +\n                    '</ol>' +\n                    '<p>paragraph</p>' +\n                '</blockquote>';\n\n            var parts = MediumEditor.util.splitByBlockElements(el);\n            expect(parts.length).toBe(3);\n\n            // <th>Head</th>\n            expect(parts[0].nodeName.toLowerCase()).toBe('th');\n            expect(parts[0].textContent).toBe('Head');\n            // <li>List Item</li>\n            expect(parts[1].nodeName.toLowerCase()).toBe('li');\n            expect(parts[1].textContent).toBe('List Item');\n            // <p>paragraph</p>\n            expect(parts[2].nodeName.toLowerCase()).toBe('p');\n            expect(parts[2].textContent).toBe('paragraph');\n        });\n\n        it('should return inline elements and text nodes if they are siblings of blocks', function () {\n            var el = this.createElement('div');\n            el.innerHTML = '' +\n                '<blockquote>' +\n                    '<span>Text <b>bold <i>bold + italics</i></b> <u>underlined</u></span>' +\n                    '<ol><li>List Item</li></ol>' +\n                    'Text Node' +\n                '</blockquote>';\n            var parts = MediumEditor.util.splitByBlockElements(el);\n            expect(parts.length).toBe(3);\n\n            // <span>Text <b>bold <i>bold + italics</i></b> <u>underlined</u></span>\n            expect(parts[0].nodeName.toLowerCase()).toBe('span');\n            expect(parts[0].textContent).toBe('Text bold bold + italics underlined');\n            // <li>List Item</li>\n            expect(parts[1].nodeName.toLowerCase()).toBe('li');\n            expect(parts[1].textContent).toBe('List Item');\n            // Text Node\n            expect(parts[2].nodeName.toLowerCase()).toBe('#text');\n            expect(parts[2].textContent).toBe('Text Node');\n        });\n\n        it('should ignore comments', function () {\n            var comment = document.createComment('comment'),\n                parts = MediumEditor.util.splitByBlockElements(comment);\n            expect(parts.length).toBe(0);\n        });\n\n        it('should ignore nested comments', function () {\n            var el = this.createElement('div');\n            el.innerHTML = '' +\n                  '<p>Text</p>' +\n                  '<!---->';\n            var parts = MediumEditor.util.splitByBlockElements(el);\n            expect(parts.length).toBe(1);\n        });\n    });\n\n    describe('getClosestBlockContainer', function () {\n        it('should return the closest block container', function () {\n            var el = this.createElement('div', '', '<blockquote><p>paragraph</p><ul><li><span>list item</span></li></ul></blockquote>'),\n                span = el.querySelector('span'),\n                container = MediumEditor.util.getClosestBlockContainer(span);\n            expect(container).toBe(el.querySelector('li'));\n        });\n\n        it('should return the parent editable if element is a text node child of the editor', function () {\n            var el = this.createElement('div', 'editable', ' <p>text</p>'),\n                emptyTextNode = el.firstChild;\n            this.newMediumEditor('.editable');\n            var container = MediumEditor.util.getClosestBlockContainer(emptyTextNode);\n            expect(container).toBe(el);\n        });\n    });\n\n    describe('getTopBlockContainer', function () {\n        it('should return the highest level block container', function () {\n            var el = this.createElement('div', '', '<blockquote><p>paragraph</p><ul><li><span>list item</span></li></ul></blockquote>'),\n                span = el.querySelector('span'),\n                container = MediumEditor.util.getTopBlockContainer(span);\n            expect(container).toBe(el.querySelector('blockquote'));\n        });\n\n        it('should return the parent editable if element is a text node child of the editor', function () {\n            var el = this.createElement('div', 'editable', ' <p>text</p>'),\n                emptyTextNode = el.firstChild;\n            this.newMediumEditor('.editable');\n            var container = MediumEditor.util.getTopBlockContainer(emptyTextNode);\n            expect(container).toBe(el);\n        });\n    });\n\n    describe('findPreviousSibling', function () {\n        it('should return the previous sibling of an element if it exists', function () {\n            var el = this.createElement('div', '', '<p>first <b>second </b><i>third</i></p><ul><li>fourth</li></ul>'),\n                second = el.querySelector('b'),\n                third = el.querySelector('i'),\n                prevSibling = MediumEditor.util.findPreviousSibling(third);\n            expect(prevSibling).toBe(second);\n        });\n\n        it('should return the previous sibling on an ancestor if a previous sibling does not exist', function () {\n            var el = this.createElement('div', '', '<p>first <b>second </b><i>third</i></p><ul><li>fourth</li></ul>'),\n                fourth = el.querySelector('li').firstChild,\n                p = el.querySelector('p'),\n                prevSibling = MediumEditor.util.findPreviousSibling(fourth);\n            expect(prevSibling).toBe(p);\n        });\n\n        it('should not find a previous sibling if the element is at the beginning of an editor element', function () {\n            var el = this.createElement('div', 'editable', '<p>first <b>second </b><i>third</i></p><ul><li>fourth</li></ul>'),\n                first = el.querySelector('p').firstChild;\n            this.newMediumEditor('.editable');\n            var prevSibling = MediumEditor.util.findPreviousSibling(first);\n            expect(prevSibling).toBeFalsy();\n        });\n    });\n\n    describe('findOrCreateMatchingTextNodes', function () {\n        it('should return text nodes within an element', function () {\n            var el = this.createElement('div');\n            el.innerHTML = '<p>Plain <b>bold</b> <a href=\"#\">link</a> <i>italic</i> <u>underline</u> <span>span1 <span>span2</span></span></p>';\n            var textNodes = MediumEditor.util.findOrCreateMatchingTextNodes(document, el, { start: 0, end: el.textContent.length });\n            expect(textNodes.length).toBe(11);\n            expect(textNodes[0].nodeValue).toBe('Plain ');\n            expect(textNodes[9].nodeValue).toBe('span1 ');\n            expect(textNodes[10].nodeValue).toBe('span2');\n        });\n\n        it('should return text nodes within an element given a start and end range', function () {\n            var el = this.createElement('div');\n            el.innerHTML = '<p>Plain <b>bold</b> <a href=\"#\">link</a> <i>italic</i> <u>underline</u> <span>span1 <span>span2</span></span></p>';\n            var textNodes = MediumEditor.util.findOrCreateMatchingTextNodes(document, el, { start: 11, end: 22 });\n            expect(textNodes.length).toBe(3);\n            expect(textNodes[0].nodeValue).toBe('link');\n            expect(textNodes[1].nodeValue).toBe(' ');\n            expect(textNodes[2].nodeValue).toBe('italic');\n        });\n\n        it('should split text nodes if start and end range are in the middle of a text node', function () {\n            var el = this.createElement('div');\n            el.innerHTML = '<p>Plain <b>bold</b> <a href=\"#\">link</a> <i>italic</i> <u>underline</u> <span>span1 <span>span2</span></span></p>';\n            var textNodes = MediumEditor.util.findOrCreateMatchingTextNodes(document, el, { start: 13, end: 19 });\n\n            expect(el.querySelector('a').childNodes.length).toBe(2);\n            expect(el.querySelector('i').childNodes.length).toBe(2);\n            expect(textNodes.length).toBe(3);\n            expect(textNodes[0].nodeValue).toBe('nk');\n            expect(textNodes[1].nodeValue).toBe(' ');\n            expect(textNodes[2].nodeValue).toBe('ita');\n        });\n\n        it('should return an image when it falls within the specified range', function () {\n            var el = this.createElement('div');\n            el.innerHTML = '<p>Plain <b>bold</b> <a href=\"#\">li<img src=\"../demo/img/medium-editor.jpg\" />nk</a> <i>italic</i> <u>underline</u> <span>span1 <span>span2</span></span></p>';\n            var textNodes = MediumEditor.util.findOrCreateMatchingTextNodes(document, el, { start: 11, end: 15 });\n            expect(textNodes.length).toBe(3);\n            expect(textNodes[0].nodeValue).toBe('li');\n            expect(textNodes[1].nodeName.toLowerCase()).toBe('img');\n            expect(textNodes[2].nodeValue).toBe('nk');\n        });\n\n        it('should return an image when it is at the end of the specified range', function () {\n            var el = this.createElement('div');\n            el.innerHTML = '<p>Plain <b>bold</b> <a href=\"#\">link<img src=\"../demo/img/medium-editor.jpg\" /></a> <i>italic</i> <u>underline</u> <span>span1 <span>span2</span></span></p>';\n            var textNodes = MediumEditor.util.findOrCreateMatchingTextNodes(document, el, { start: 11, end: 15 });\n            expect(textNodes.length).toBe(2);\n            expect(textNodes[0].nodeValue).toBe('link');\n            expect(textNodes[1].nodeName.toLowerCase()).toBe('img');\n        });\n\n        it('should return an image when it is the only content in the specified range', function () {\n            var el = this.createElement('div');\n            el.innerHTML = '<p>Plain <b>bold</b> <a href=\"#\"><img src=\"../demo/img/medium-editor.jpg\" /></a> <i>italic</i> <u>underline</u> <span>span1 <span>span2</span></span></p>';\n            var textNodes = MediumEditor.util.findOrCreateMatchingTextNodes(document, el, { start: 11, end: 11 });\n            expect(textNodes.length).toBe(1);\n            expect(textNodes[0].nodeName.toLowerCase()).toBe('img');\n        });\n\n        it('should return images when they are at the beginning of the specified range', function () {\n            var el = this.createElement('div');\n            el.innerHTML = '<p>Plain <b>bold</b> <a href=\"#\"><img src=\"../demo/img/medium-editor.jpg\" /><img src=\"../demo/img/roman.jpg\" />link</a> <i>italic</i> <u>underline</u> <span>span1 <span>span2</span></span></p>';\n            var textNodes = MediumEditor.util.findOrCreateMatchingTextNodes(document, el, { start: 11, end: 15 });\n            expect(textNodes.length).toBe(3);\n            expect(textNodes[0].nodeName.toLowerCase()).toBe('img');\n            expect(textNodes[1].nodeName.toLowerCase()).toBe('img');\n            expect(textNodes[2].nodeValue).toBe('link');\n        });\n    });\n\n    // TODO: Remove these tests when getFirstTextNode is deprecated in 6.0.0\n    describe('getFirstTextNode', function () {\n        it('should find the first text node within an element', function () {\n            var el = this.createElement('div', '', '<p><b><i><u><a href=\"#\">First</a> text</u> in</i> editor</b>!</p>'),\n                anchorText = el.querySelector('a').firstChild,\n                firstText = MediumEditor.util.getFirstTextNode(el);\n\n            expect(firstText).toBe(anchorText);\n        });\n\n        it('should return the text node if passed a text node', function () {\n            var el = this.createElement('div', '', '<p>text</p>'),\n                textNode = el.querySelector('p').firstChild,\n                firstText = MediumEditor.util.getFirstTextNode(textNode);\n\n            expect(firstText).toBe(textNode);\n        });\n\n        it('should return null if no text node exists in element', function () {\n            var el = this.createElement('div'),\n                firstText = MediumEditor.util.getFirstTextNode(el);\n\n            expect(firstText).toBeNull();\n        });\n    });\n});\n"
  },
  {
    "path": "spec/vendor/jasmine-jsreporter-script.js",
    "content": "(function () {\n    jasmine.getEnv().addReporter(new jasmine.JSReporter2());                   //< for jsreporter\n\n    var oldFunc = window.jasmine.getJSReport;\n\n    window.jasmine.getJSReport = function () {\n     var results = oldFunc();\n     return removePassing(results);\n    };\n\n    function removePassing(results) {\n      if (typeof results === \"undefined\") {\n        return false;\n      }\n\n      var suites = [];\n\n      for (var i = 0; i < results.length; i++) {\n        if (!results.suites[i].passed) {\n          suites.push(results.suites[i]);\n        }\n      }\n\n      results.suites = suites;\n\n      return results;\n    }\n})();\n"
  },
  {
    "path": "spec/vendor/jasmine-jsreporter.js",
    "content": "/*\n  This file is part of the Jasmine JSReporter project from Ivan De Marino.\n\n  Copyright (C) 2011-2014 Ivan De Marino <http://ivandemarino.me>\n  Copyright (C) 2014 Alex Treppass <http://alextreppass.co.uk>\n\n  Redistribution and use in source and binary forms, with or without\n  modification, are permitted provided that the following conditions are met:\n\n    * Redistributions of source code must retain the above copyright\n      notice, this list of conditions and the following disclaimer.\n    * Redistributions in binary form must reproduce the above copyright\n      notice, this list of conditions and the following disclaimer in the\n      documentation and/or other materials provided with the distribution.\n    * Neither the name of the <organization> nor the\n      names of its contributors may be used to endorse or promote products\n      derived from this software without specific prior written permission.\n\n  THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS \"AS IS\"\n  AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE\n  IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE\n  ARE DISCLAIMED. IN NO EVENT SHALL IVAN DE MARINO BE LIABLE FOR ANY\n  DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES\n  (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;\n  LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND\n  ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT\n  (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF\n  THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.\n*/\n(function (jasmine) {\n\n  if (!jasmine) {\n    throw new Error(\"[Jasmine JSReporter] 'Jasmine' library not found\");\n  }\n\n  // ------------------------------------------------------------------------\n  // Jasmine JSReporter for Jasmine 1.x\n  // ------------------------------------------------------------------------\n\n  /**\n   * Calculate elapsed time, in Seconds.\n   * @param startMs Start time in Milliseconds\n   * @param finishMs Finish time in Milliseconds\n   * @return Elapsed time in Seconds */\n  function elapsedSec (startMs, finishMs) {\n      return (finishMs - startMs) / 1000;\n  }\n\n  /**\n   * Round an amount to the given number of Digits.\n   * If no number of digits is given, than '2' is assumed.\n   * @param amount Amount to round\n   * @param numOfDecDigits Number of Digits to round to. Default value is '2'.\n   * @return Rounded amount */\n  function round (amount, numOfDecDigits) {\n      numOfDecDigits = numOfDecDigits || 2;\n      return Math.round(amount * Math.pow(10, numOfDecDigits)) / Math.pow(10, numOfDecDigits);\n  }\n\n  /**\n   * Create a new array which contains only the failed items.\n   * @param items Items which will be filtered\n   * @returns {Array} of failed items */\n  function failures (items) {\n      var fs = [], i, v;\n      for (i = 0; i < items.length; i += 1) {\n          v = items[i];\n          if (!v.passed_) {\n              fs.push(v);\n          }\n      }\n      return fs;\n  }\n\n  /**\n   * Collect information about a Suite, recursively, and return a JSON result.\n   * @param suite The Jasmine Suite to get data from\n   */\n  function getSuiteData (suite) {\n      var suiteData = {\n              description : suite.description,\n              durationSec : 0,\n              specs: [],\n              suites: [],\n              passed: true\n          },\n          specs = suite.specs(),\n          suites = suite.suites(),\n          i, ilen;\n\n      // Loop over all the Suite's Specs\n      for (i = 0, ilen = specs.length; i < ilen; ++i) {\n          suiteData.specs[i] = {\n              description : specs[i].description,\n              durationSec : specs[i].durationSec,\n              passed : specs[i].results().passedCount === specs[i].results().totalCount,\n              skipped : specs[i].results().skipped,\n              passedCount : specs[i].results().passedCount,\n              failedCount : specs[i].results().failedCount,\n              totalCount : specs[i].results().totalCount,\n              failures: failures(specs[i].results().getItems())\n          };\n          suiteData.passed = !suiteData.specs[i].passed ? false : suiteData.passed;\n          suiteData.durationSec += suiteData.specs[i].durationSec;\n      }\n\n      // Loop over all the Suite's sub-Suites\n      for (i = 0, ilen = suites.length; i < ilen; ++i) {\n          suiteData.suites[i] = getSuiteData(suites[i]); //< recursive population\n          suiteData.passed = !suiteData.suites[i].passed ? false : suiteData.passed;\n          suiteData.durationSec += suiteData.suites[i].durationSec;\n      }\n\n      // Rounding duration numbers to 3 decimal digits\n      suiteData.durationSec = round(suiteData.durationSec, 4);\n\n      return suiteData;\n  }\n\n  var JSReporter =  function () {\n  };\n\n  JSReporter.prototype = {\n      reportRunnerStarting: function (runner) {\n          // Nothing to do\n      },\n\n      reportSpecStarting: function (spec) {\n          // Start timing this spec\n          spec.startedAt = new Date();\n      },\n\n      reportSpecResults: function (spec) {\n          // Finish timing this spec and calculate duration/delta (in sec)\n          spec.finishedAt = new Date();\n          // If the spec was skipped, reportSpecStarting is never called and spec.startedAt is undefined\n          spec.durationSec = spec.startedAt ? elapsedSec(spec.startedAt.getTime(), spec.finishedAt.getTime()) : 0;\n      },\n\n      reportSuiteResults: function (suite) {\n          // Nothing to do\n      },\n\n      reportRunnerResults: function (runner) {\n          var suites = runner.suites(),\n              i, j, ilen;\n\n          // Attach results to the \"jasmine\" object to make those results easy to scrap/find\n          jasmine.runnerResults = {\n              suites: [],\n              durationSec : 0,\n              passed : true\n          };\n\n          // Loop over all the Suites\n          for (i = 0, ilen = suites.length, j = 0; i < ilen; ++i) {\n              if (suites[i].parentSuite === null) {\n                  jasmine.runnerResults.suites[j] = getSuiteData(suites[i]);\n                  // If 1 suite fails, the whole runner fails\n                  jasmine.runnerResults.passed = !jasmine.runnerResults.suites[j].passed ? false : jasmine.runnerResults.passed;\n                  // Add up all the durations\n                  jasmine.runnerResults.durationSec += jasmine.runnerResults.suites[j].durationSec;\n                  j++;\n              }\n          }\n\n          // Decorate the 'jasmine' object with getters\n          jasmine.getJSReport = function () {\n              if (jasmine.runnerResults) {\n                  return jasmine.runnerResults;\n              }\n              return null;\n          };\n          jasmine.getJSReportAsString = function () {\n              return JSON.stringify(jasmine.getJSReport());\n          };\n      }\n  };\n\n  // export public\n  jasmine.JSReporter = JSReporter;\n\n\n  // ------------------------------------------------------------------------\n  // Jasmine JSReporter for Jasmine 2.0\n  // ------------------------------------------------------------------------\n\n  /*\n    Simple timer implementation\n  */\n  var Timer = function () {};\n\n  Timer.prototype.start = function () {\n    this.startTime = new Date().getTime();\n    return this;\n  };\n\n  Timer.prototype.elapsed = function () {\n    if (this.startTime == null) {\n      return -1;\n    }\n    return new Date().getTime() - this.startTime;\n  };\n\n  /*\n    Utility methods\n  */\n  var _extend = function (obj1, obj2) {\n    for (var prop in obj2) {\n      obj1[prop] = obj2[prop];\n    }\n    return obj1;\n  };\n  var _clone = function (obj) {\n    if (obj !== Object(obj)) {\n      return obj;\n    }\n    return _extend({}, obj);\n  };\n\n  jasmine.JSReporter2 = function () {\n    this.specs  = {};\n    this.suites = {};\n    this.rootSuites = [];\n    this.suiteStack = [];\n\n    // export methods under jasmine namespace\n    jasmine.getJSReport = this.getJSReport;\n    jasmine.getJSReportAsString = this.getJSReportAsString;\n  };\n\n  var JSR = jasmine.JSReporter2.prototype;\n\n  // Reporter API methods\n  // --------------------\n\n  JSR.suiteStarted = function (suite) {\n    suite = this._cacheSuite(suite);\n    // build up suite tree as we go\n    suite.specs = [];\n    suite.suites = [];\n    suite.passed = true;\n    suite.parentId = this.suiteStack.slice(this.suiteStack.length -1)[0];\n    if (suite.parentId) {\n      this.suites[suite.parentId].suites.push(suite);\n    } else {\n      this.rootSuites.push(suite.id);\n    }\n    this.suiteStack.push(suite.id);\n    suite.timer = new Timer().start();\n  };\n\n  JSR.suiteDone = function (suite) {\n    suite = this._cacheSuite(suite);\n    suite.duration = suite.timer.elapsed();\n    suite.durationSec = suite.duration / 1000;\n    this.suiteStack.pop();\n\n    // maintain parent suite state\n    var parent = this.suites[suite.parentId];\n    if (parent) {\n      parent.passed = parent.passed && suite.passed;\n    }\n\n    // keep report representation clean\n    delete suite.timer;\n    delete suite.id;\n    delete suite.parentId;\n    delete suite.fullName;\n  };\n\n  JSR.specStarted = function (spec) {\n    spec = this._cacheSpec(spec);\n    spec.timer = new Timer().start();\n    // build up suites->spec tree as we go\n    spec.suiteId = this.suiteStack.slice(this.suiteStack.length -1)[0];\n    this.suites[spec.suiteId].specs.push(spec);\n  };\n\n  JSR.specDone = function (spec) {\n    spec = this._cacheSpec(spec);\n\n    spec.duration = spec.timer.elapsed();\n    spec.durationSec = spec.duration / 1000;\n\n    spec.skipped = spec.status === 'pending';\n    spec.passed = spec.skipped || spec.status === 'passed';\n\n    spec.totalCount = spec.passedExpectations.length + spec.failedExpectations.length;\n    spec.passedCount = spec.passedExpectations.length;\n    spec.failedCount = spec.failedExpectations.length;\n    spec.failures = [];\n\n    for (var i = 0, j = spec.failedExpectations.length; i < j; i++) {\n      var fail = spec.failedExpectations[i];\n      spec.failures.push({\n        type: 'expect',\n        expected: fail.expected,\n        passed: false,\n        message: fail.message,\n        matcherName: fail.matcherName,\n        trace: {\n          stack: fail.stack\n        }\n      });\n    }\n\n    // maintain parent suite state\n    var parent = this.suites[spec.suiteId];\n    if (spec.failed) {\n      parent.failingSpecs.push(spec);\n    }\n    parent.passed = parent.passed && spec.passed;\n\n    // keep report representation clean\n    delete spec.timer;\n    delete spec.totalExpectations;\n    delete spec.passedExpectations;\n    delete spec.suiteId;\n    delete spec.fullName;\n    delete spec.id;\n    delete spec.status;\n    delete spec.failedExpectations;\n  };\n\n  JSR.jasmineDone = function () {\n    this._buildReport();\n  };\n\n  JSR.getJSReport = function () {\n    if (jasmine.jsReport) {\n      return jasmine.jsReport;\n    }\n  };\n\n  JSR.getJSReportAsString = function () {\n    if (jasmine.jsReport) {\n      return JSON.stringify(jasmine.jsReport);\n    }\n  };\n\n  // Private methods\n  // ---------------\n\n  JSR._haveSpec = function (spec) {\n    return this.specs[spec.id] != null;\n  };\n\n  JSR._cacheSpec = function (spec) {\n    var existing = this.specs[spec.id];\n    if (existing == null) {\n      existing = this.specs[spec.id] = _clone(spec);\n    } else {\n      _extend(existing, spec);\n    }\n    return existing;\n  };\n\n  JSR._haveSuite = function (suite) {\n    return this.suites[suite.id] != null;\n  };\n\n  JSR._cacheSuite = function (suite) {\n    var existing = this.suites[suite.id];\n    if (existing == null) {\n      existing = this.suites[suite.id] = _clone(suite);\n    } else {\n      _extend(existing, suite);\n    }\n    return existing;\n  };\n\n  JSR._buildReport = function () {\n    var overallDuration = 0;\n    var overallPassed = true;\n    var overallSuites = [];\n\n    for (var i = 0, j = this.rootSuites.length; i < j; i++) {\n      var suite = this.suites[this.rootSuites[i]];\n      overallDuration += suite.duration;\n      overallPassed = overallPassed && suite.passed;\n      overallSuites.push(suite);\n    }\n\n    jasmine.jsReport = {\n      passed: overallPassed,\n      durationSec: overallDuration / 1000,\n      suites: overallSuites\n    };\n  };\n\n})(jasmine);"
  },
  {
    "path": "spec/version.spec.js",
    "content": "describe('Core MediumEditor', function () {\n\n    it('exists', function () {\n        expect(MediumEditor).toBeTruthy();\n    });\n\n    describe('MediumEditor.version', function () {\n\n        it('exists', function () {\n            expect(MediumEditor.version).toBeTruthy();\n        });\n\n        it('has major/minor/revision ints', function () {\n            expect(MediumEditor.version.major).toBeDefined();\n            expect(MediumEditor.version.minor).toBeDefined();\n            expect(MediumEditor.version.revision).toBeDefined();\n            expect(MediumEditor.version.preRelease).toBeDefined();\n        });\n\n        it('exposes the major/minor/revison as a string', function () {\n            var v = '' + MediumEditor.version;\n            expect(typeof v).toEqual('string');\n        });\n    });\n\n    describe('MediumEditor.parseVersionString', function () {\n\n        it('exists', function () {\n            expect(MediumEditor.parseVersionString).toBeTruthy();\n        });\n\n        it('parses a normal version string', function () {\n            var info = MediumEditor.parseVersionString('1.2.3');\n\n            expect(info.major).toBe(1);\n            expect(info.minor).toBe(2);\n            expect(info.revision).toBe(3);\n            expect(info.preRelease).toBe('');\n            expect(info.toString()).toBe('1.2.3');\n        });\n\n        it('parses pre-release versions', function () {\n            var info = MediumEditor.parseVersionString('5.0.0-alpha.1');\n\n            expect(info.major).toBe(5);\n            expect(info.minor).toBe(0);\n            expect(info.revision).toBe(0);\n            expect(info.preRelease).toBe('alpha.1');\n            expect(info.toString()).toBe('5.0.0-alpha.1');\n        });\n    });\n\n});\n"
  },
  {
    "path": "src/js/core.js",
    "content": "(function () {\n    'use strict';\n\n    var initialContent = {};\n\n    // Event handlers that shouldn't be exposed externally\n\n    function handleDisableExtraSpaces(event) {\n        var node = MediumEditor.selection.getSelectionStart(this.options.ownerDocument),\n            textContent = node.textContent,\n            caretPositions = MediumEditor.selection.getCaretOffsets(node);\n\n        if ((textContent[caretPositions.left - 1] === undefined) || (textContent[caretPositions.left - 1].trim() === '') || (textContent[caretPositions.left] !== undefined && textContent[caretPositions.left].trim() === '')) {\n            event.preventDefault();\n        }\n    }\n\n    function handleDisabledEnterKeydown(event, element) {\n        if (this.options.disableReturn || element.getAttribute('data-disable-return')) {\n            event.preventDefault();\n        } else if (this.options.disableDoubleReturn || element.getAttribute('data-disable-double-return')) {\n            var node = MediumEditor.selection.getSelectionStart(this.options.ownerDocument);\n\n            // if current text selection is empty OR previous sibling text is empty OR it is not a list\n            if ((node && node.textContent.trim() === '' && node.nodeName.toLowerCase() !== 'li') ||\n                (node.previousElementSibling && node.previousElementSibling.nodeName.toLowerCase() !== 'br' &&\n                    node.previousElementSibling.textContent.trim() === '')) {\n                event.preventDefault();\n            }\n        }\n    }\n\n    function handleTabKeydown(event) {\n        // Override tab only for pre nodes\n        var node = MediumEditor.selection.getSelectionStart(this.options.ownerDocument),\n            tag = node && node.nodeName.toLowerCase();\n\n        if (tag === 'pre') {\n            event.preventDefault();\n            MediumEditor.util.insertHTMLCommand(this.options.ownerDocument, '    ');\n        }\n\n        // Tab to indent list structures!\n        if (MediumEditor.util.isListItem(node)) {\n            event.preventDefault();\n\n            // If Shift is down, outdent, otherwise indent\n            if (event.shiftKey) {\n                this.options.ownerDocument.execCommand('outdent', false, null);\n            } else {\n                this.options.ownerDocument.execCommand('indent', false, null);\n            }\n        }\n    }\n\n    function handleBlockDeleteKeydowns(event) {\n        var p, node = MediumEditor.selection.getSelectionStart(this.options.ownerDocument),\n            tagName = node.nodeName.toLowerCase(),\n            isEmpty = /^(\\s+|<br\\/?>)?$/i,\n            isHeader = /h\\d/i;\n\n        if (MediumEditor.util.isKey(event, [MediumEditor.util.keyCode.BACKSPACE, MediumEditor.util.keyCode.ENTER]) &&\n            // has a preceeding sibling\n            node.previousElementSibling &&\n            // in a header\n            isHeader.test(tagName) &&\n            // at the very end of the block\n            MediumEditor.selection.getCaretOffsets(node).left === 0) {\n            if (MediumEditor.util.isKey(event, MediumEditor.util.keyCode.BACKSPACE) && isEmpty.test(node.previousElementSibling.innerHTML)) {\n                // backspacing the begining of a header into an empty previous element will\n                // change the tagName of the current node to prevent one\n                // instead delete previous node and cancel the event.\n                node.previousElementSibling.parentNode.removeChild(node.previousElementSibling);\n                event.preventDefault();\n            } else if (!this.options.disableDoubleReturn && MediumEditor.util.isKey(event, MediumEditor.util.keyCode.ENTER)) {\n                // hitting return in the begining of a header will create empty header elements before the current one\n                // instead, make \"<p><br></p>\" element, which are what happens if you hit return in an empty paragraph\n                p = this.options.ownerDocument.createElement('p');\n                p.innerHTML = '<br>';\n                node.previousElementSibling.parentNode.insertBefore(p, node);\n                event.preventDefault();\n            }\n        } else if (MediumEditor.util.isKey(event, MediumEditor.util.keyCode.DELETE) &&\n            // between two sibling elements\n            node.nextElementSibling &&\n            node.previousElementSibling &&\n            // not in a header\n            !isHeader.test(tagName) &&\n            // in an empty tag\n            isEmpty.test(node.innerHTML) &&\n            // when the next tag *is* a header\n            isHeader.test(node.nextElementSibling.nodeName.toLowerCase())) {\n            // hitting delete in an empty element preceding a header, ex:\n            //  <p>[CURSOR]</p><h1>Header</h1>\n            // Will cause the h1 to become a paragraph.\n            // Instead, delete the paragraph node and move the cursor to the begining of the h1\n\n            // remove node and move cursor to start of header\n            MediumEditor.selection.moveCursor(this.options.ownerDocument, node.nextElementSibling);\n\n            node.previousElementSibling.parentNode.removeChild(node);\n\n            event.preventDefault();\n        } else if (MediumEditor.util.isKey(event, MediumEditor.util.keyCode.BACKSPACE) &&\n            tagName === 'li' &&\n            // hitting backspace inside an empty li\n            isEmpty.test(node.innerHTML) &&\n            // is first element (no preceeding siblings)\n            !node.previousElementSibling &&\n            // parent also does not have a sibling\n            !node.parentElement.previousElementSibling &&\n            // is not the only li in a list\n            node.nextElementSibling &&\n            node.nextElementSibling.nodeName.toLowerCase() === 'li') {\n            // backspacing in an empty first list element in the first list (with more elements) ex:\n            //  <ul><li>[CURSOR]</li><li>List Item 2</li></ul>\n            // will remove the first <li> but add some extra element before (varies based on browser)\n            // Instead, this will:\n            // 1) remove the list element\n            // 2) create a paragraph before the list\n            // 3) move the cursor into the paragraph\n\n            // create a paragraph before the list\n            p = this.options.ownerDocument.createElement('p');\n            p.innerHTML = '<br>';\n            node.parentElement.parentElement.insertBefore(p, node.parentElement);\n\n            // move the cursor into the new paragraph\n            MediumEditor.selection.moveCursor(this.options.ownerDocument, p);\n\n            // remove the list element\n            node.parentElement.removeChild(node);\n\n            event.preventDefault();\n        } else if (MediumEditor.util.isKey(event, MediumEditor.util.keyCode.BACKSPACE) &&\n            (MediumEditor.util.getClosestTag(node, 'blockquote') !== false) &&\n            MediumEditor.selection.getCaretOffsets(node).left === 0) {\n\n            // when cursor is at the begining of the element and the element is <blockquote>\n            // then pressing backspace key should change the <blockquote> to a <p> tag\n            event.preventDefault();\n            MediumEditor.util.execFormatBlock(this.options.ownerDocument, 'p');\n        } else if (MediumEditor.util.isKey(event, MediumEditor.util.keyCode.ENTER) &&\n            (MediumEditor.util.getClosestTag(node, 'blockquote') !== false) &&\n            MediumEditor.selection.getCaretOffsets(node).right === 0) {\n\n            // when cursor is at the end of <blockquote>,\n            // then pressing enter key should create <p> tag, not <blockquote>\n            p = this.options.ownerDocument.createElement('p');\n            p.innerHTML = '<br>';\n            node.parentElement.insertBefore(p, node.nextSibling);\n\n            // move the cursor into the new paragraph\n            MediumEditor.selection.moveCursor(this.options.ownerDocument, p);\n\n            event.preventDefault();\n        } else if (MediumEditor.util.isKey(event, MediumEditor.util.keyCode.BACKSPACE) &&\n            MediumEditor.util.isMediumEditorElement(node.parentElement) &&\n            !node.previousElementSibling &&\n            node.nextElementSibling &&\n            isEmpty.test(node.innerHTML)) {\n\n            // when cursor is in the first element, it's empty and user presses backspace,\n            // do delete action instead to get rid of the first element and move caret to 2nd\n            event.preventDefault();\n            MediumEditor.selection.moveCursor(this.options.ownerDocument, node.nextSibling);\n            node.parentElement.removeChild(node);\n        }\n    }\n\n    function handleKeyup(event) {\n        var node = MediumEditor.selection.getSelectionStart(this.options.ownerDocument),\n            tagName;\n\n        if (!node) {\n            return;\n        }\n\n        // https://github.com/yabwe/medium-editor/issues/994\n        // Firefox thrown an error when calling `formatBlock` on an empty editable blockContainer that's not a <div>\n        if (MediumEditor.util.isMediumEditorElement(node) && node.children.length === 0 && !MediumEditor.util.isBlockContainer(node)) {\n            this.options.ownerDocument.execCommand('formatBlock', false, 'p');\n        }\n\n        // https://github.com/yabwe/medium-editor/issues/1455\n        // if somehow we have the BR as the selected element, typing does nothing, so move the cursor\n        if (node.nodeName === 'BR') {\n            MediumEditor.selection.moveCursor(this.options.ownerDocument, node.parentElement);\n        }\n\n        // https://github.com/yabwe/medium-editor/issues/834\n        // https://github.com/yabwe/medium-editor/pull/382\n        // Don't call format block if this is a block element (ie h1, figCaption, etc.)\n        if (MediumEditor.util.isKey(event, MediumEditor.util.keyCode.ENTER) &&\n            !MediumEditor.util.isListItem(node) &&\n            !MediumEditor.util.isBlockContainer(node)) {\n\n            tagName = node.nodeName.toLowerCase();\n            // For anchor tags, unlink\n            if (tagName === 'a') {\n                this.options.ownerDocument.execCommand('unlink', false, null);\n            } else if (!event.shiftKey && !event.ctrlKey) {\n                this.options.ownerDocument.execCommand('formatBlock', false, 'p');\n                // https://github.com/yabwe/medium-editor/issues/1455\n                // firefox puts the focus on the br - so we need to move the cursor to the newly created p\n                if (MediumEditor.util.isFF) {\n                    var newParagraph = node.querySelector('p');\n                    if (newParagraph) {\n                        MediumEditor.selection.moveCursor(this.options.ownerDocument, newParagraph);\n                    }\n                }\n            }\n        }\n    }\n\n    function handleEditableInput(event, editable) {\n        var textarea = editable.parentNode.querySelector('textarea[medium-editor-textarea-id=\"' + editable.getAttribute('medium-editor-textarea-id') + '\"]');\n        if (textarea) {\n            textarea.value = editable.innerHTML.trim();\n        }\n    }\n\n    // Internal helper methods which shouldn't be exposed externally\n\n    function addToEditors(win) {\n        if (!win._mediumEditors) {\n            // To avoid breaking users who are assuming that the unique id on\n            // medium-editor elements will start at 1, inserting a 'null' in the\n            // array so the unique-id can always map to the index of the editor instance\n            win._mediumEditors = [null];\n        }\n\n        // If this already has a unique id, re-use it\n        if (!this.id) {\n            this.id = win._mediumEditors.length;\n        }\n\n        win._mediumEditors[this.id] = this;\n    }\n\n    function removeFromEditors(win) {\n        if (!win._mediumEditors || !win._mediumEditors[this.id]) {\n            return;\n        }\n\n        /* Setting the instance to null in the array instead of deleting it allows:\n         * 1) Each instance to preserve its own unique-id, even after being destroyed\n         *    and initialized again\n         * 2) The unique-id to always correspond to an index in the array of medium-editor\n         *    instances. Thus, we will be able to look at a contenteditable, and determine\n         *    which instance it belongs to, by indexing into the global array.\n         */\n        win._mediumEditors[this.id] = null;\n    }\n\n    function createElementsArray(selector, doc, filterEditorElements) {\n        var elements = [];\n\n        if (!selector) {\n            selector = [];\n        }\n        // If string, use as query selector\n        if (typeof selector === 'string') {\n            selector = doc.querySelectorAll(selector);\n        }\n        // If element, put into array\n        if (MediumEditor.util.isElement(selector)) {\n            selector = [selector];\n        }\n\n        if (filterEditorElements) {\n            // Remove elements that have already been initialized by the editor\n            // selecotr might not be an array (ie NodeList) so use for loop\n            for (var i = 0; i < selector.length; i++) {\n                var el = selector[i];\n                if (MediumEditor.util.isElement(el) &&\n                    !el.getAttribute('data-medium-editor-element') &&\n                    !el.getAttribute('medium-editor-textarea-id')) {\n                    elements.push(el);\n                }\n            }\n        } else {\n            // Convert NodeList (or other array like object) into an array\n            elements = Array.prototype.slice.apply(selector);\n        }\n\n        return elements;\n    }\n\n    function cleanupTextareaElement(element) {\n        var textarea = element.parentNode.querySelector('textarea[medium-editor-textarea-id=\"' + element.getAttribute('medium-editor-textarea-id') + '\"]');\n        if (textarea) {\n            // Un-hide the textarea\n            textarea.classList.remove('medium-editor-hidden');\n            textarea.removeAttribute('medium-editor-textarea-id');\n        }\n        if (element.parentNode) {\n            element.parentNode.removeChild(element);\n        }\n    }\n\n    function setExtensionDefaults(extension, defaults) {\n        Object.keys(defaults).forEach(function (prop) {\n            if (extension[prop] === undefined) {\n                extension[prop] = defaults[prop];\n            }\n        });\n        return extension;\n    }\n\n    function initExtension(extension, name, instance) {\n        var extensionDefaults = {\n            'window': instance.options.contentWindow,\n            'document': instance.options.ownerDocument,\n            'base': instance\n        };\n\n        // Add default options into the extension\n        extension = setExtensionDefaults(extension, extensionDefaults);\n\n        // Call init on the extension\n        if (typeof extension.init === 'function') {\n            extension.init();\n        }\n\n        // Set extension name (if not already set)\n        if (!extension.name) {\n            extension.name = name;\n        }\n        return extension;\n    }\n\n    function isToolbarEnabled() {\n        // If any of the elements don't have the toolbar disabled\n        // We need a toolbar\n        if (this.elements.every(function (element) {\n            return !!element.getAttribute('data-disable-toolbar');\n        })) {\n            return false;\n        }\n\n        return this.options.toolbar !== false;\n    }\n\n    function isAnchorPreviewEnabled() {\n        // If toolbar is disabled, don't add\n        if (!isToolbarEnabled.call(this)) {\n            return false;\n        }\n\n        return this.options.anchorPreview !== false;\n    }\n\n    function isPlaceholderEnabled() {\n        return this.options.placeholder !== false;\n    }\n\n    function isAutoLinkEnabled() {\n        return this.options.autoLink !== false;\n    }\n\n    function isImageDraggingEnabled() {\n        return this.options.imageDragging !== false;\n    }\n\n    function isKeyboardCommandsEnabled() {\n        return this.options.keyboardCommands !== false;\n    }\n\n    function shouldUseFileDraggingExtension() {\n        // Since the file-dragging extension replaces the image-dragging extension,\n        // we need to check if the user passed an overrided image-dragging extension.\n        // If they have, to avoid breaking users, we won't use file-dragging extension.\n        return !this.options.extensions['imageDragging'];\n    }\n\n    function createContentEditable(textarea) {\n        var div = this.options.ownerDocument.createElement('div'),\n            now = Date.now(),\n            uniqueId = 'medium-editor-' + now,\n            atts = textarea.attributes;\n\n        // Some browsers can move pretty fast, since we're using a timestamp\n        // to make a unique-id, ensure that the id is actually unique on the page\n        while (this.options.ownerDocument.getElementById(uniqueId)) {\n            now++;\n            uniqueId = 'medium-editor-' + now;\n        }\n\n        div.className = textarea.className;\n        div.id = uniqueId;\n        div.innerHTML = textarea.value;\n\n        textarea.setAttribute('medium-editor-textarea-id', uniqueId);\n\n        // re-create all attributes from the textearea to the new created div\n        for (var i = 0, n = atts.length; i < n; i++) {\n            // do not re-create existing attributes\n            if (!div.hasAttribute(atts[i].nodeName)) {\n                div.setAttribute(atts[i].nodeName, atts[i].value);\n            }\n        }\n\n        // If textarea has a form, listen for reset on the form to clear\n        // the content of the created div\n        if (textarea.form) {\n            this.on(textarea.form, 'reset', function (event) {\n                if (!event.defaultPrevented) {\n                    this.resetContent(this.options.ownerDocument.getElementById(uniqueId));\n                }\n            }.bind(this));\n        }\n\n        textarea.classList.add('medium-editor-hidden');\n        textarea.parentNode.insertBefore(\n            div,\n            textarea\n        );\n\n        return div;\n    }\n\n    function initElement(element, editorId) {\n        if (!element.getAttribute('data-medium-editor-element')) {\n            if (element.nodeName.toLowerCase() === 'textarea') {\n                element = createContentEditable.call(this, element);\n\n                // Make sure we only attach to editableInput once for <textarea> elements\n                if (!this.instanceHandleEditableInput) {\n                    this.instanceHandleEditableInput = handleEditableInput.bind(this);\n                    this.subscribe('editableInput', this.instanceHandleEditableInput);\n                }\n            }\n\n            if (!this.options.disableEditing && !element.getAttribute('data-disable-editing')) {\n                element.setAttribute('contentEditable', true);\n                element.setAttribute('spellcheck', this.options.spellcheck);\n            }\n\n            // Make sure we only attach to editableKeydownEnter once for disable-return options\n            if (!this.instanceHandleEditableKeydownEnter) {\n                if (element.getAttribute('data-disable-return') || element.getAttribute('data-disable-double-return')) {\n                    this.instanceHandleEditableKeydownEnter = handleDisabledEnterKeydown.bind(this);\n                    this.subscribe('editableKeydownEnter', this.instanceHandleEditableKeydownEnter);\n                }\n            }\n\n            // if we're not disabling return, add a handler to help handle cleanup\n            // for certain cases when enter is pressed\n            if (!this.options.disableReturn && !element.getAttribute('data-disable-return')) {\n                this.on(element, 'keyup', handleKeyup.bind(this));\n            }\n\n            var elementId = MediumEditor.util.guid();\n\n            element.setAttribute('data-medium-editor-element', true);\n            element.classList.add('medium-editor-element');\n            element.setAttribute('role', 'textbox');\n            element.setAttribute('aria-multiline', true);\n            element.setAttribute('data-medium-editor-editor-index', editorId);\n            // TODO: Merge data-medium-editor-element and medium-editor-index attributes for 6.0.0\n            // medium-editor-index is not named correctly anymore and can be re-purposed to signify\n            // whether the element has been initialized or not\n            element.setAttribute('medium-editor-index', elementId);\n            initialContent[elementId] = element.innerHTML;\n\n            this.events.attachAllEventsToElement(element);\n        }\n\n        return element;\n    }\n\n    function attachHandlers() {\n        // attach to tabs\n        this.subscribe('editableKeydownTab', handleTabKeydown.bind(this));\n\n        // Bind keys which can create or destroy a block element: backspace, delete, return\n        this.subscribe('editableKeydownDelete', handleBlockDeleteKeydowns.bind(this));\n        this.subscribe('editableKeydownEnter', handleBlockDeleteKeydowns.bind(this));\n\n        // Bind double space event\n        if (this.options.disableExtraSpaces) {\n            this.subscribe('editableKeydownSpace', handleDisableExtraSpaces.bind(this));\n        }\n\n        // Make sure we only attach to editableKeydownEnter once for disable-return options\n        if (!this.instanceHandleEditableKeydownEnter) {\n            // disabling return or double return\n            if (this.options.disableReturn || this.options.disableDoubleReturn) {\n                this.instanceHandleEditableKeydownEnter = handleDisabledEnterKeydown.bind(this);\n                this.subscribe('editableKeydownEnter', this.instanceHandleEditableKeydownEnter);\n            }\n        }\n    }\n\n    function initExtensions() {\n\n        this.extensions = [];\n\n        // Passed in extensions\n        Object.keys(this.options.extensions).forEach(function (name) {\n            // Always save the toolbar extension for last\n            if (name !== 'toolbar' && this.options.extensions[name]) {\n                this.extensions.push(initExtension(this.options.extensions[name], name, this));\n            }\n        }, this);\n\n        // 4 Cases for imageDragging + fileDragging extensons:\n        //\n        // 1. ImageDragging ON + No Custom Image Dragging Extension:\n        //    * Use fileDragging extension (default options)\n        // 2. ImageDragging OFF + No Custom Image Dragging Extension:\n        //    * Use fileDragging extension w/ images turned off\n        // 3. ImageDragging ON + Custom Image Dragging Extension:\n        //    * Don't use fileDragging (could interfere with custom image dragging extension)\n        // 4. ImageDragging OFF + Custom Image Dragging:\n        //    * Don't use fileDragging (could interfere with custom image dragging extension)\n        if (shouldUseFileDraggingExtension.call(this)) {\n            var opts = this.options.fileDragging;\n            if (!opts) {\n                opts = {};\n\n                // Image is in the 'allowedTypes' list by default.\n                // If imageDragging is off override the 'allowedTypes' list with an empty one\n                if (!isImageDraggingEnabled.call(this)) {\n                    opts.allowedTypes = [];\n                }\n            }\n            this.addBuiltInExtension('fileDragging', opts);\n        }\n\n        // Built-in extensions\n        var builtIns = {\n            paste: true,\n            'anchor-preview': isAnchorPreviewEnabled.call(this),\n            autoLink: isAutoLinkEnabled.call(this),\n            keyboardCommands: isKeyboardCommandsEnabled.call(this),\n            placeholder: isPlaceholderEnabled.call(this)\n        };\n        Object.keys(builtIns).forEach(function (name) {\n            if (builtIns[name]) {\n                this.addBuiltInExtension(name);\n            }\n        }, this);\n\n        // Users can pass in a custom toolbar extension\n        // so check for that first and if it's not present\n        // just create the default toolbar\n        var toolbarExtension = this.options.extensions['toolbar'];\n        if (!toolbarExtension && isToolbarEnabled.call(this)) {\n            // Backwards compatability\n            var toolbarOptions = MediumEditor.util.extend({}, this.options.toolbar, {\n                allowMultiParagraphSelection: this.options.allowMultiParagraphSelection // deprecated\n            });\n            toolbarExtension = new MediumEditor.extensions.toolbar(toolbarOptions);\n        }\n\n        // If the toolbar is not disabled, so we actually have an extension\n        // initialize it and add it to the extensions array\n        if (toolbarExtension) {\n            this.extensions.push(initExtension(toolbarExtension, 'toolbar', this));\n        }\n    }\n\n    function mergeOptions(defaults, options) {\n        var deprecatedProperties = [\n            ['allowMultiParagraphSelection', 'toolbar.allowMultiParagraphSelection']\n        ];\n        // warn about using deprecated properties\n        if (options) {\n            deprecatedProperties.forEach(function (pair) {\n                if (options.hasOwnProperty(pair[0]) && options[pair[0]] !== undefined) {\n                    MediumEditor.util.deprecated(pair[0], pair[1], 'v6.0.0');\n                }\n            });\n        }\n\n        return MediumEditor.util.defaults({}, options, defaults);\n    }\n\n    function execActionInternal(action, opts) {\n        /*jslint regexp: true*/\n        var appendAction = /^append-(.+)$/gi,\n            justifyAction = /justify([A-Za-z]*)$/g, /* Detecting if is justifyCenter|Right|Left */\n            match,\n            cmdValueArgument;\n        /*jslint regexp: false*/\n\n        // Actions starting with 'append-' should attempt to format a block of text ('formatBlock') using a specific\n        // type of block element (ie append-blockquote, append-h1, append-pre, etc.)\n        match = appendAction.exec(action);\n        if (match) {\n            return MediumEditor.util.execFormatBlock(this.options.ownerDocument, match[1]);\n        }\n\n        if (action === 'fontSize') {\n            // TODO: Deprecate support for opts.size in 6.0.0\n            if (opts.size) {\n                MediumEditor.util.deprecated('.size option for fontSize command', '.value', '6.0.0');\n            }\n            cmdValueArgument = opts.value || opts.size;\n            return this.options.ownerDocument.execCommand('fontSize', false, cmdValueArgument);\n        }\n\n        if (action === 'fontName') {\n            // TODO: Deprecate support for opts.name in 6.0.0\n            if (opts.name) {\n                MediumEditor.util.deprecated('.name option for fontName command', '.value', '6.0.0');\n            }\n            cmdValueArgument = opts.value || opts.name;\n            return this.options.ownerDocument.execCommand('fontName', false, cmdValueArgument);\n        }\n\n        if (action === 'createLink') {\n            return this.createLink(opts);\n        }\n\n        if (action === 'image') {\n            var src = this.options.contentWindow.getSelection().toString().trim();\n            return this.options.ownerDocument.execCommand('insertImage', false, src);\n        }\n\n        if (action === 'html') {\n            var html = this.options.contentWindow.getSelection().toString().trim();\n            return MediumEditor.util.insertHTMLCommand(this.options.ownerDocument, html);\n        }\n\n        /* Issue: https://github.com/yabwe/medium-editor/issues/595\n         * If the action is to justify the text */\n        if (justifyAction.exec(action)) {\n            var result = this.options.ownerDocument.execCommand(action, false, null),\n                parentNode = MediumEditor.selection.getSelectedParentElement(MediumEditor.selection.getSelectionRange(this.options.ownerDocument));\n            if (parentNode) {\n                cleanupJustifyDivFragments.call(this, MediumEditor.util.getTopBlockContainer(parentNode));\n            }\n\n            return result;\n        }\n\n        cmdValueArgument = opts && opts.value;\n        return this.options.ownerDocument.execCommand(action, false, cmdValueArgument);\n    }\n\n    /* If we've just justified text within a container block\n     * Chrome may have removed <br> elements and instead wrapped lines in <div> elements\n     * with a text-align property.  If so, we want to fix this\n     */\n    function cleanupJustifyDivFragments(blockContainer) {\n        if (!blockContainer) {\n            return;\n        }\n\n        var textAlign,\n            childDivs = Array.prototype.slice.call(blockContainer.childNodes).filter(function (element) {\n                var isDiv = element.nodeName.toLowerCase() === 'div';\n                if (isDiv && !textAlign) {\n                    textAlign = element.style.textAlign;\n                }\n                return isDiv;\n            });\n\n        /* If we found child <div> elements with text-align style attributes\n         * we should fix this by:\n         *\n         * 1) Unwrapping each <div> which has a text-align style\n         * 2) Insert a <br> element after each set of 'unwrapped' div children\n         * 3) Set the text-align style of the parent block element\n         */\n        if (childDivs.length) {\n            // Since we're mucking with the HTML, preserve selection\n            this.saveSelection();\n            childDivs.forEach(function (div) {\n                if (div.style.textAlign === textAlign) {\n                    var lastChild = div.lastChild;\n                    if (lastChild) {\n                        // Instead of a div, extract the child elements and add a <br>\n                        MediumEditor.util.unwrap(div, this.options.ownerDocument);\n                        var br = this.options.ownerDocument.createElement('BR');\n                        lastChild.parentNode.insertBefore(br, lastChild.nextSibling);\n                    }\n                }\n            }, this);\n            blockContainer.style.textAlign = textAlign;\n            // We're done, so restore selection\n            this.restoreSelection();\n        }\n    }\n\n    MediumEditor.prototype = {\n        // NOT DOCUMENTED - exposed for backwards compatability\n        init: function (elements, options) {\n            this.options = mergeOptions.call(this, this.defaults, options);\n            this.origElements = elements;\n\n            if (!this.options.elementsContainer) {\n                this.options.elementsContainer = this.options.ownerDocument.body;\n            }\n\n            return this.setup();\n        },\n\n        setup: function () {\n            if (this.isActive) {\n                return;\n            }\n\n            addToEditors.call(this, this.options.contentWindow);\n            this.events = new MediumEditor.Events(this);\n            this.elements = [];\n\n            this.addElements(this.origElements);\n\n            if (this.elements.length === 0) {\n                return;\n            }\n\n            this.isActive = true;\n\n            // Call initialization helpers\n            initExtensions.call(this);\n            attachHandlers.call(this);\n        },\n\n        destroy: function () {\n            if (!this.isActive) {\n                return;\n            }\n\n            this.isActive = false;\n\n            this.extensions.forEach(function (extension) {\n                if (typeof extension.destroy === 'function') {\n                    extension.destroy();\n                }\n            }, this);\n\n            this.events.destroy();\n\n            this.elements.forEach(function (element) {\n                // Reset elements content, fix for issue where after editor destroyed the red underlines on spelling errors are left\n                if (this.options.spellcheck) {\n                    element.innerHTML = element.innerHTML;\n                }\n\n                // cleanup extra added attributes\n                element.removeAttribute('contentEditable');\n                element.removeAttribute('spellcheck');\n                element.removeAttribute('data-medium-editor-element');\n                element.classList.remove('medium-editor-element');\n                element.removeAttribute('role');\n                element.removeAttribute('aria-multiline');\n                element.removeAttribute('medium-editor-index');\n                element.removeAttribute('data-medium-editor-editor-index');\n\n                // Remove any elements created for textareas\n                if (element.getAttribute('medium-editor-textarea-id')) {\n                    cleanupTextareaElement(element);\n                }\n            }, this);\n            this.elements = [];\n            this.instanceHandleEditableKeydownEnter = null;\n            this.instanceHandleEditableInput = null;\n\n            removeFromEditors.call(this, this.options.contentWindow);\n        },\n\n        on: function (target, event, listener, useCapture) {\n            this.events.attachDOMEvent(target, event, listener, useCapture);\n\n            return this;\n        },\n\n        off: function (target, event, listener, useCapture) {\n            this.events.detachDOMEvent(target, event, listener, useCapture);\n\n            return this;\n        },\n\n        subscribe: function (event, listener) {\n            this.events.attachCustomEvent(event, listener);\n\n            return this;\n        },\n\n        unsubscribe: function (event, listener) {\n            this.events.detachCustomEvent(event, listener);\n\n            return this;\n        },\n\n        trigger: function (name, data, editable) {\n            this.events.triggerCustomEvent(name, data, editable);\n\n            return this;\n        },\n\n        delay: function (fn) {\n            var self = this;\n            return setTimeout(function () {\n                if (self.isActive) {\n                    fn();\n                }\n            }, this.options.delay);\n        },\n\n        serialize: function () {\n            var i,\n                elementid,\n                content = {},\n                len = this.elements.length;\n\n            for (i = 0; i < len; i += 1) {\n                elementid = (this.elements[i].id !== '') ? this.elements[i].id : 'element-' + i;\n                content[elementid] = {\n                    value: this.elements[i].innerHTML.trim()\n                };\n            }\n            return content;\n        },\n\n        getExtensionByName: function (name) {\n            var extension;\n            if (this.extensions && this.extensions.length) {\n                this.extensions.some(function (ext) {\n                    if (ext.name === name) {\n                        extension = ext;\n                        return true;\n                    }\n                    return false;\n                });\n            }\n            return extension;\n        },\n\n        /**\n         * NOT DOCUMENTED - exposed as a helper for other extensions to use\n         */\n        addBuiltInExtension: function (name, opts) {\n            var extension = this.getExtensionByName(name),\n                merged;\n            if (extension) {\n                return extension;\n            }\n\n            switch (name) {\n                case 'anchor':\n                    merged = MediumEditor.util.extend({}, this.options.anchor, opts);\n                    extension = new MediumEditor.extensions.anchor(merged);\n                    break;\n                case 'anchor-preview':\n                    extension = new MediumEditor.extensions.anchorPreview(this.options.anchorPreview);\n                    break;\n                case 'autoLink':\n                    extension = new MediumEditor.extensions.autoLink();\n                    break;\n                case 'fileDragging':\n                    extension = new MediumEditor.extensions.fileDragging(opts);\n                    break;\n                case 'fontname':\n                    extension = new MediumEditor.extensions.fontName(this.options.fontName);\n                    break;\n                case 'fontsize':\n                    extension = new MediumEditor.extensions.fontSize(opts);\n                    break;\n                case 'keyboardCommands':\n                    extension = new MediumEditor.extensions.keyboardCommands(this.options.keyboardCommands);\n                    break;\n                case 'paste':\n                    extension = new MediumEditor.extensions.paste(this.options.paste);\n                    break;\n                case 'placeholder':\n                    extension = new MediumEditor.extensions.placeholder(this.options.placeholder);\n                    break;\n                default:\n                    // All of the built-in buttons for MediumEditor are extensions\n                    // so check to see if the extension we're creating is a built-in button\n                    if (MediumEditor.extensions.button.isBuiltInButton(name)) {\n                        if (opts) {\n                            merged = MediumEditor.util.defaults({}, opts, MediumEditor.extensions.button.prototype.defaults[name]);\n                            extension = new MediumEditor.extensions.button(merged);\n                        } else {\n                            extension = new MediumEditor.extensions.button(name);\n                        }\n                    }\n            }\n\n            if (extension) {\n                this.extensions.push(initExtension(extension, name, this));\n            }\n\n            return extension;\n        },\n\n        stopSelectionUpdates: function () {\n            this.preventSelectionUpdates = true;\n        },\n\n        startSelectionUpdates: function () {\n            this.preventSelectionUpdates = false;\n        },\n\n        checkSelection: function () {\n            var toolbar = this.getExtensionByName('toolbar');\n            if (toolbar) {\n                toolbar.checkState();\n            }\n            return this;\n        },\n\n        // Wrapper around document.queryCommandState for checking whether an action has already\n        // been applied to the current selection\n        queryCommandState: function (action) {\n            var fullAction = /^full-(.+)$/gi,\n                match,\n                queryState = null;\n\n            // Actions starting with 'full-' need to be modified since this is a medium-editor concept\n            match = fullAction.exec(action);\n            if (match) {\n                action = match[1];\n            }\n\n            try {\n                queryState = this.options.ownerDocument.queryCommandState(action);\n            } catch (exc) {\n                queryState = null;\n            }\n\n            return queryState;\n        },\n\n        execAction: function (action, opts) {\n            /*jslint regexp: true*/\n            var fullAction = /^full-(.+)$/gi,\n                match,\n                result;\n            /*jslint regexp: false*/\n\n            // Actions starting with 'full-' should be applied to to the entire contents of the editable element\n            // (ie full-bold, full-append-pre, etc.)\n            match = fullAction.exec(action);\n            if (match) {\n                // Store the current selection to be restored after applying the action\n                this.saveSelection();\n                // Select all of the contents before calling the action\n                this.selectAllContents();\n                result = execActionInternal.call(this, match[1], opts);\n                // Restore the previous selection\n                this.restoreSelection();\n            } else {\n                result = execActionInternal.call(this, action, opts);\n            }\n\n            // do some DOM clean-up for known browser issues after the action\n            if (action === 'insertunorderedlist' || action === 'insertorderedlist') {\n                MediumEditor.util.cleanListDOM(this.options.ownerDocument, this.getSelectedParentElement());\n            }\n\n            // https://github.com/yabwe/medium-editor/issues/1496\n            // But only if something is selected i.e, getSelectedParentElement !== {}\n            if (this.getSelectedParentElement().length > 0 && MediumEditor.util.isFF) {\n                MediumEditor.util.getContainerEditorElement(this.getSelectedParentElement()).focus();\n            }\n\n            this.checkSelection();\n            return result;\n        },\n\n        getSelectedParentElement: function (range) {\n            if (range === undefined) {\n                try {\n                    range = this.options.contentWindow.getSelection().getRangeAt(0);\n                } catch (err) {\n                    return {};\n                }\n            }\n            return MediumEditor.selection.getSelectedParentElement(range);\n        },\n\n        selectAllContents: function () {\n            var currNode = MediumEditor.selection.getSelectionElement(this.options.contentWindow);\n\n            if (currNode) {\n                // Move to the lowest descendant node that still selects all of the contents\n                while (currNode.children.length === 1) {\n                    currNode = currNode.children[0];\n                }\n\n                this.selectElement(currNode);\n            }\n        },\n\n        selectElement: function (element) {\n            MediumEditor.selection.selectNode(element, this.options.ownerDocument);\n\n            var selElement = MediumEditor.selection.getSelectionElement(this.options.contentWindow);\n            if (selElement) {\n                this.events.focusElement(selElement);\n            }\n        },\n\n        getFocusedElement: function () {\n            var focused;\n            this.elements.some(function (element) {\n                // Find the element that has focus\n                if (!focused && element.getAttribute('data-medium-focused')) {\n                    focused = element;\n                }\n\n                // bail if we found the element that had focus\n                return !!focused;\n            }, this);\n\n            return focused;\n        },\n\n        // Export the state of the selection in respect to one of this\n        // instance of MediumEditor's elements\n        exportSelection: function () {\n            var selectionElement = MediumEditor.selection.getSelectionElement(this.options.contentWindow),\n                editableElementIndex = this.elements.indexOf(selectionElement),\n                selectionState = null;\n\n            if (editableElementIndex >= 0) {\n                selectionState = MediumEditor.selection.exportSelection(selectionElement, this.options.ownerDocument);\n            }\n\n            if (selectionState !== null && editableElementIndex !== 0) {\n                selectionState.editableElementIndex = editableElementIndex;\n            }\n\n            return selectionState;\n        },\n\n        saveSelection: function () {\n            this.selectionState = this.exportSelection();\n        },\n\n        // Restore a selection based on a selectionState returned by a call\n        // to MediumEditor.exportSelection\n        importSelection: function (selectionState, favorLaterSelectionAnchor) {\n            if (!selectionState) {\n                return;\n            }\n\n            var editableElement = this.elements[selectionState.editableElementIndex || 0];\n            MediumEditor.selection.importSelection(selectionState, editableElement, this.options.ownerDocument, favorLaterSelectionAnchor);\n        },\n\n        restoreSelection: function () {\n            this.importSelection(this.selectionState);\n        },\n\n        createLink: function (opts) {\n            var currentEditor = MediumEditor.selection.getSelectionElement(this.options.contentWindow),\n                customEvent = {},\n                targetUrl;\n\n            // Make sure the selection is within an element this editor is tracking\n            if (this.elements.indexOf(currentEditor) === -1) {\n                return;\n            }\n\n            try {\n                this.events.disableCustomEvent('editableInput');\n                // TODO: Deprecate support for opts.url in 6.0.0\n                if (opts.url) {\n                    MediumEditor.util.deprecated('.url option for createLink', '.value', '6.0.0');\n                }\n                targetUrl = opts.url || opts.value;\n                if (targetUrl && targetUrl.trim().length > 0) {\n                    var currentSelection = this.options.contentWindow.getSelection();\n                    if (currentSelection) {\n                        var currRange = currentSelection.getRangeAt(0),\n                            commonAncestorContainer = currRange.commonAncestorContainer,\n                            exportedSelection,\n                            startContainerParentElement,\n                            endContainerParentElement,\n                            textNodes;\n\n                        // If the selection is contained within a single text node\n                        // and the selection starts at the beginning of the text node,\n                        // MSIE still says the startContainer is the parent of the text node.\n                        // If the selection is contained within a single text node, we\n                        // want to just use the default browser 'createLink', so we need\n                        // to account for this case and adjust the commonAncestorContainer accordingly\n                        if (currRange.endContainer.nodeType === 3 &&\n                            currRange.startContainer.nodeType !== 3 &&\n                            currRange.startOffset === 0 &&\n                            currRange.startContainer.firstChild === currRange.endContainer) {\n                            commonAncestorContainer = currRange.endContainer;\n                        }\n\n                        startContainerParentElement = MediumEditor.util.getClosestBlockContainer(currRange.startContainer);\n                        endContainerParentElement = MediumEditor.util.getClosestBlockContainer(currRange.endContainer);\n\n                        // If the selection is not contained within a single text node\n                        // but the selection is contained within the same block element\n                        // we want to make sure we create a single link, and not multiple links\n                        // which can happen with the built in browser functionality\n                        if (commonAncestorContainer.nodeType !== 3 && commonAncestorContainer.textContent.length !== 0 && startContainerParentElement === endContainerParentElement) {\n                            var parentElement = (startContainerParentElement || currentEditor),\n                                fragment = this.options.ownerDocument.createDocumentFragment();\n\n                            // since we are going to create a link from an extracted text,\n                            // be sure that if we are updating a link, we won't let an empty link behind (see #754)\n                            // (Workaroung for Chrome)\n                            this.execAction('unlink');\n\n                            exportedSelection = this.exportSelection();\n                            fragment.appendChild(parentElement.cloneNode(true));\n\n                            if (currentEditor === parentElement) {\n                                // We have to avoid the editor itself being wiped out when it's the only block element,\n                                // as our reference inside this.elements gets detached from the page when insertHTML runs.\n                                // If we just use [parentElement, 0] and [parentElement, parentElement.childNodes.length]\n                                // as the range boundaries, this happens whenever parentElement === currentEditor.\n                                // The tradeoff to this workaround is that a orphaned tag can sometimes be left behind at\n                                // the end of the editor's content.\n                                // In Gecko:\n                                // as an empty <strong></strong> if parentElement.lastChild is a <strong> tag.\n                                // In WebKit:\n                                // an invented <br /> tag at the end in the same situation\n                                MediumEditor.selection.select(\n                                    this.options.ownerDocument,\n                                    parentElement.firstChild,\n                                    0,\n                                    parentElement.lastChild,\n                                    parentElement.lastChild.nodeType === 3 ?\n                                        parentElement.lastChild.nodeValue.length : parentElement.lastChild.childNodes.length\n                                );\n                            } else {\n                                MediumEditor.selection.select(\n                                    this.options.ownerDocument,\n                                    parentElement,\n                                    0,\n                                    parentElement,\n                                    parentElement.childNodes.length\n                                );\n                            }\n\n                            var modifiedExportedSelection = this.exportSelection();\n\n                            textNodes = MediumEditor.util.findOrCreateMatchingTextNodes(\n                                this.options.ownerDocument,\n                                fragment,\n                                {\n                                    start: exportedSelection.start - modifiedExportedSelection.start,\n                                    end: exportedSelection.end - modifiedExportedSelection.start,\n                                    editableElementIndex: exportedSelection.editableElementIndex\n                                }\n                            );\n                            // If textNodes are not present, when changing link on images\n                            // ex: <a><img src=\"http://image.test.com\"></a>, change fragment to currRange.startContainer\n                            // and set textNodes array to [imageElement, imageElement]\n                            if (textNodes.length === 0) {\n                                fragment = this.options.ownerDocument.createDocumentFragment();\n                                fragment.appendChild(commonAncestorContainer.cloneNode(true));\n                                textNodes = [fragment.firstChild.firstChild, fragment.firstChild.lastChild];\n                            }\n\n                            // Creates the link in the document fragment\n                            MediumEditor.util.createLink(this.options.ownerDocument, textNodes, targetUrl.trim());\n\n                            // Chrome trims the leading whitespaces when inserting HTML, which messes up restoring the selection.\n                            var leadingWhitespacesCount = (fragment.firstChild.innerHTML.match(/^\\s+/) || [''])[0].length;\n\n                            // Now move the created link back into the original document in a way to preserve undo/redo history\n                            MediumEditor.util.insertHTMLCommand(this.options.ownerDocument, fragment.firstChild.innerHTML.replace(/^\\s+/, ''));\n                            exportedSelection.start -= leadingWhitespacesCount;\n                            exportedSelection.end -= leadingWhitespacesCount;\n\n                            this.importSelection(exportedSelection);\n                        } else {\n                            this.options.ownerDocument.execCommand('createLink', false, targetUrl);\n                        }\n\n                        if (this.options.targetBlank || opts.target === '_blank') {\n                            MediumEditor.util.setTargetBlank(MediumEditor.selection.getSelectionStart(this.options.ownerDocument), targetUrl);\n                        } else {\n                            MediumEditor.util.removeTargetBlank(MediumEditor.selection.getSelectionStart(this.options.ownerDocument), targetUrl);\n                        }\n\n                        if (opts.buttonClass) {\n                            MediumEditor.util.addClassToAnchors(MediumEditor.selection.getSelectionStart(this.options.ownerDocument), opts.buttonClass);\n                        }\n                    }\n                }\n                // Fire input event for backwards compatibility if anyone was listening directly to the DOM input event\n                if (this.options.targetBlank || opts.target === '_blank' || opts.buttonClass) {\n                    customEvent = this.options.ownerDocument.createEvent('HTMLEvents');\n                    customEvent.initEvent('input', true, true, this.options.contentWindow);\n                    for (var i = 0, len = this.elements.length; i < len; i += 1) {\n                        this.elements[i].dispatchEvent(customEvent);\n                    }\n                }\n            } finally {\n                this.events.enableCustomEvent('editableInput');\n            }\n            // Fire our custom editableInput event\n            this.events.triggerCustomEvent('editableInput', customEvent, currentEditor);\n        },\n\n        cleanPaste: function (text) {\n            this.getExtensionByName('paste').cleanPaste(text);\n        },\n\n        pasteHTML: function (html, options) {\n            this.getExtensionByName('paste').pasteHTML(html, options);\n        },\n\n        setContent: function (html, index) {\n            index = index || 0;\n\n            if (this.elements[index]) {\n                var target = this.elements[index];\n                target.innerHTML = html;\n                this.checkContentChanged(target);\n            }\n        },\n\n        getContent: function (index) {\n            index = index || 0;\n\n            if (this.elements[index]) {\n                return this.elements[index].innerHTML.trim();\n            }\n            return null;\n        },\n\n        checkContentChanged: function (editable) {\n            editable = editable || MediumEditor.selection.getSelectionElement(this.options.contentWindow);\n            this.events.updateInput(editable, { target: editable, currentTarget: editable });\n        },\n\n        resetContent: function (element) {\n            // For all elements that exist in the this.elements array, we can assume:\n            // - Its initial content has been set in the initialContent object\n            // - It has a medium-editor-index attribute which is the key value in the initialContent object\n\n            if (element) {\n                var index = this.elements.indexOf(element);\n                if (index !== -1) {\n                    this.setContent(initialContent[element.getAttribute('medium-editor-index')], index);\n                }\n                return;\n            }\n\n            this.elements.forEach(function (el, idx) {\n                this.setContent(initialContent[el.getAttribute('medium-editor-index')], idx);\n            }, this);\n        },\n\n        addElements: function (selector) {\n            // Convert elements into an array\n            var elements = createElementsArray(selector, this.options.ownerDocument, true);\n\n            // Do we have elements to add now?\n            if (elements.length === 0) {\n                return false;\n            }\n\n            elements.forEach(function (element) {\n                // Initialize all new elements (we check that in those functions don't worry)\n                element = initElement.call(this, element, this.id);\n\n                // Add new elements to our internal elements array\n                this.elements.push(element);\n\n                // Trigger event so extensions can know when an element has been added\n                this.trigger('addElement', { target: element, currentTarget: element }, element);\n            }, this);\n        },\n\n        removeElements: function (selector) {\n            // Convert elements into an array\n            var elements = createElementsArray(selector, this.options.ownerDocument),\n                toRemove = elements.map(function (el) {\n                    // For textareas, make sure we're looking at the editor div and not the textarea itself\n                    if (el.getAttribute('medium-editor-textarea-id') && el.parentNode) {\n                        return el.parentNode.querySelector('div[medium-editor-textarea-id=\"' + el.getAttribute('medium-editor-textarea-id') + '\"]');\n                    } else {\n                        return el;\n                    }\n                });\n\n            this.elements = this.elements.filter(function (element) {\n                // If this is an element we want to remove\n                if (toRemove.indexOf(element) !== -1) {\n                    this.events.cleanupElement(element);\n                    if (element.getAttribute('medium-editor-textarea-id')) {\n                        cleanupTextareaElement(element);\n                    }\n                    // Trigger event so extensions can clean-up elements that are being removed\n                    this.trigger('removeElement', { target: element, currentTarget: element }, element);\n                    return false;\n                }\n                return true;\n            }, this);\n        }\n    };\n\n    MediumEditor.getEditorFromElement = function (element) {\n        var index = element.getAttribute('data-medium-editor-editor-index'),\n            win = element && element.ownerDocument && (element.ownerDocument.defaultView || element.ownerDocument.parentWindow);\n        if (win && win._mediumEditors && win._mediumEditors[index]) {\n            return win._mediumEditors[index];\n        }\n        return null;\n    };\n}());\n"
  },
  {
    "path": "src/js/defaults/buttons.js",
    "content": "(function () {\n    'use strict';\n\n    /* MediumEditor.extensions.button.defaults: [Object]\n     * Set of default config options for all of the built-in MediumEditor buttons\n     */\n    MediumEditor.extensions.button.prototype.defaults = {\n        'bold': {\n            name: 'bold',\n            action: 'bold',\n            aria: 'bold',\n            tagNames: ['b', 'strong'],\n            style: {\n                prop: 'font-weight',\n                value: '700|bold'\n            },\n            useQueryState: true,\n            contentDefault: '<b>B</b>',\n            contentFA: '<i class=\"fa fa-bold\"></i>'\n        },\n        'italic': {\n            name: 'italic',\n            action: 'italic',\n            aria: 'italic',\n            tagNames: ['i', 'em'],\n            style: {\n                prop: 'font-style',\n                value: 'italic'\n            },\n            useQueryState: true,\n            contentDefault: '<b><i>I</i></b>',\n            contentFA: '<i class=\"fa fa-italic\"></i>'\n        },\n        'underline': {\n            name: 'underline',\n            action: 'underline',\n            aria: 'underline',\n            tagNames: ['u'],\n            style: {\n                prop: 'text-decoration',\n                value: 'underline'\n            },\n            useQueryState: true,\n            contentDefault: '<b><u>U</u></b>',\n            contentFA: '<i class=\"fa fa-underline\"></i>'\n        },\n        'strikethrough': {\n            name: 'strikethrough',\n            action: 'strikethrough',\n            aria: 'strike through',\n            tagNames: ['strike'],\n            style: {\n                prop: 'text-decoration',\n                value: 'line-through'\n            },\n            useQueryState: true,\n            contentDefault: '<s>A</s>',\n            contentFA: '<i class=\"fa fa-strikethrough\"></i>'\n        },\n        'superscript': {\n            name: 'superscript',\n            action: 'superscript',\n            aria: 'superscript',\n            tagNames: ['sup'],\n            /* firefox doesn't behave the way we want it to, so we CAN'T use queryCommandState for superscript\n               https://github.com/guardian/scribe/blob/master/BROWSERINCONSISTENCIES.md#documentquerycommandstate */\n            // useQueryState: true\n            contentDefault: '<b>x<sup>1</sup></b>',\n            contentFA: '<i class=\"fa fa-superscript\"></i>'\n        },\n        'subscript': {\n            name: 'subscript',\n            action: 'subscript',\n            aria: 'subscript',\n            tagNames: ['sub'],\n            /* firefox doesn't behave the way we want it to, so we CAN'T use queryCommandState for subscript\n               https://github.com/guardian/scribe/blob/master/BROWSERINCONSISTENCIES.md#documentquerycommandstate */\n            // useQueryState: true\n            contentDefault: '<b>x<sub>1</sub></b>',\n            contentFA: '<i class=\"fa fa-subscript\"></i>'\n        },\n        'image': {\n            name: 'image',\n            action: 'image',\n            aria: 'image',\n            tagNames: ['img'],\n            contentDefault: '<b>image</b>',\n            contentFA: '<i class=\"fa fa-picture-o\"></i>'\n        },\n        'html': {\n            name: 'html',\n            action: 'html',\n            aria: 'evaluate html',\n            tagNames: ['iframe', 'object'],\n            contentDefault: '<b>html</b>',\n            contentFA: '<i class=\"fa fa-code\"></i>'\n        },\n        'orderedlist': {\n            name: 'orderedlist',\n            action: 'insertorderedlist',\n            aria: 'ordered list',\n            tagNames: ['ol'],\n            useQueryState: true,\n            contentDefault: '<b>1.</b>',\n            contentFA: '<i class=\"fa fa-list-ol\"></i>'\n        },\n        'unorderedlist': {\n            name: 'unorderedlist',\n            action: 'insertunorderedlist',\n            aria: 'unordered list',\n            tagNames: ['ul'],\n            useQueryState: true,\n            contentDefault: '<b>&bull;</b>',\n            contentFA: '<i class=\"fa fa-list-ul\"></i>'\n        },\n        'indent': {\n            name: 'indent',\n            action: 'indent',\n            aria: 'indent',\n            tagNames: [],\n            contentDefault: '<b>&rarr;</b>',\n            contentFA: '<i class=\"fa fa-indent\"></i>'\n        },\n        'outdent': {\n            name: 'outdent',\n            action: 'outdent',\n            aria: 'outdent',\n            tagNames: [],\n            contentDefault: '<b>&larr;</b>',\n            contentFA: '<i class=\"fa fa-outdent\"></i>'\n        },\n        'justifyCenter': {\n            name: 'justifyCenter',\n            action: 'justifyCenter',\n            aria: 'center justify',\n            tagNames: [],\n            style: {\n                prop: 'text-align',\n                value: 'center'\n            },\n            contentDefault: '<b>C</b>',\n            contentFA: '<i class=\"fa fa-align-center\"></i>'\n        },\n        'justifyFull': {\n            name: 'justifyFull',\n            action: 'justifyFull',\n            aria: 'full justify',\n            tagNames: [],\n            style: {\n                prop: 'text-align',\n                value: 'justify'\n            },\n            contentDefault: '<b>J</b>',\n            contentFA: '<i class=\"fa fa-align-justify\"></i>'\n        },\n        'justifyLeft': {\n            name: 'justifyLeft',\n            action: 'justifyLeft',\n            aria: 'left justify',\n            tagNames: [],\n            style: {\n                prop: 'text-align',\n                value: 'left'\n            },\n            contentDefault: '<b>L</b>',\n            contentFA: '<i class=\"fa fa-align-left\"></i>'\n        },\n        'justifyRight': {\n            name: 'justifyRight',\n            action: 'justifyRight',\n            aria: 'right justify',\n            tagNames: [],\n            style: {\n                prop: 'text-align',\n                value: 'right'\n            },\n            contentDefault: '<b>R</b>',\n            contentFA: '<i class=\"fa fa-align-right\"></i>'\n        },\n        // Known inline elements that are not removed, or not removed consistantly across browsers:\n        // <span>, <label>, <br>\n        'removeFormat': {\n            name: 'removeFormat',\n            aria: 'remove formatting',\n            action: 'removeFormat',\n            contentDefault: '<b>X</b>',\n            contentFA: '<i class=\"fa fa-eraser\"></i>'\n        },\n\n        /***** Buttons for appending block elements (append-<element> action) *****/\n\n        'quote': {\n            name: 'quote',\n            action: 'append-blockquote',\n            aria: 'blockquote',\n            tagNames: ['blockquote'],\n            contentDefault: '<b>&ldquo;</b>',\n            contentFA: '<i class=\"fa fa-quote-right\"></i>'\n        },\n        'pre': {\n            name: 'pre',\n            action: 'append-pre',\n            aria: 'preformatted text',\n            tagNames: ['pre'],\n            contentDefault: '<b>0101</b>',\n            contentFA: '<i class=\"fa fa-code fa-lg\"></i>'\n        },\n        'h1': {\n            name: 'h1',\n            action: 'append-h1',\n            aria: 'header type one',\n            tagNames: ['h1'],\n            contentDefault: '<b>H1</b>',\n            contentFA: '<i class=\"fa fa-header\"><sup>1</sup>'\n        },\n        'h2': {\n            name: 'h2',\n            action: 'append-h2',\n            aria: 'header type two',\n            tagNames: ['h2'],\n            contentDefault: '<b>H2</b>',\n            contentFA: '<i class=\"fa fa-header\"><sup>2</sup>'\n        },\n        'h3': {\n            name: 'h3',\n            action: 'append-h3',\n            aria: 'header type three',\n            tagNames: ['h3'],\n            contentDefault: '<b>H3</b>',\n            contentFA: '<i class=\"fa fa-header\"><sup>3</sup>'\n        },\n        'h4': {\n            name: 'h4',\n            action: 'append-h4',\n            aria: 'header type four',\n            tagNames: ['h4'],\n            contentDefault: '<b>H4</b>',\n            contentFA: '<i class=\"fa fa-header\"><sup>4</sup>'\n        },\n        'h5': {\n            name: 'h5',\n            action: 'append-h5',\n            aria: 'header type five',\n            tagNames: ['h5'],\n            contentDefault: '<b>H5</b>',\n            contentFA: '<i class=\"fa fa-header\"><sup>5</sup>'\n        },\n        'h6': {\n            name: 'h6',\n            action: 'append-h6',\n            aria: 'header type six',\n            tagNames: ['h6'],\n            contentDefault: '<b>H6</b>',\n            contentFA: '<i class=\"fa fa-header\"><sup>6</sup>'\n        }\n    };\n\n})();\n"
  },
  {
    "path": "src/js/defaults/options.js",
    "content": "(function () {\n    // summary: The default options hash used by the Editor\n\n    MediumEditor.prototype.defaults = {\n        activeButtonClass: 'medium-editor-button-active',\n        buttonLabels: false,\n        delay: 0,\n        disableReturn: false,\n        disableDoubleReturn: false,\n        disableExtraSpaces: false,\n        disableEditing: false,\n        autoLink: false,\n        elementsContainer: false,\n        contentWindow: window,\n        ownerDocument: document,\n        targetBlank: false,\n        extensions: {},\n        spellcheck: true\n    };\n})();\n"
  },
  {
    "path": "src/js/events.js",
    "content": "(function () {\n    'use strict';\n\n    function isElementDescendantOfExtension(extensions, element) {\n        if (!extensions) {\n            return false;\n        }\n\n        return extensions.some(function (extension) {\n            if (typeof extension.getInteractionElements !== 'function') {\n                return false;\n            }\n\n            var extensionElements = extension.getInteractionElements();\n            if (!extensionElements) {\n                return false;\n            }\n\n            if (!Array.isArray(extensionElements)) {\n                extensionElements = [extensionElements];\n            }\n            return extensionElements.some(function (el) {\n                return MediumEditor.util.isDescendant(el, element, true);\n            });\n        });\n    }\n\n    var Events = function (instance) {\n        this.base = instance;\n        this.options = this.base.options;\n        this.events = [];\n        this.disabledEvents = {};\n        this.customEvents = {};\n        this.listeners = {};\n    };\n\n    Events.prototype = {\n        InputEventOnContenteditableSupported: !MediumEditor.util.isIE && !MediumEditor.util.isEdge,\n\n        // Helpers for event handling\n\n        attachDOMEvent: function (targets, event, listener, useCapture) {\n            var win = this.base.options.contentWindow,\n                doc = this.base.options.ownerDocument;\n\n            targets = MediumEditor.util.isElement(targets) || [win, doc].indexOf(targets) > -1 ? [targets] : targets;\n\n            Array.prototype.forEach.call(targets, function (target) {\n                target.addEventListener(event, listener, useCapture);\n                this.events.push([target, event, listener, useCapture]);\n            }.bind(this));\n        },\n\n        detachDOMEvent: function (targets, event, listener, useCapture) {\n            var index, e,\n                win = this.base.options.contentWindow,\n                doc = this.base.options.ownerDocument;\n\n            if (targets) {\n                targets = MediumEditor.util.isElement(targets) || [win, doc].indexOf(targets) > -1 ? [targets] : targets;\n\n                Array.prototype.forEach.call(targets, function (target) {\n                    index = this.indexOfListener(target, event, listener, useCapture);\n                    if (index !== -1) {\n                        e = this.events.splice(index, 1)[0];\n                        e[0].removeEventListener(e[1], e[2], e[3]);\n                    }\n                }.bind(this));\n            }\n        },\n\n        indexOfListener: function (target, event, listener, useCapture) {\n            var i, n, item;\n            for (i = 0, n = this.events.length; i < n; i = i + 1) {\n                item = this.events[i];\n                if (item[0] === target && item[1] === event && item[2] === listener && item[3] === useCapture) {\n                    return i;\n                }\n            }\n            return -1;\n        },\n\n        detachAllDOMEvents: function () {\n            var e = this.events.pop();\n            while (e) {\n                e[0].removeEventListener(e[1], e[2], e[3]);\n                e = this.events.pop();\n            }\n        },\n\n        detachAllEventsFromElement: function (element) {\n            var filtered = this.events.filter(function (e) {\n                return e && e[0].getAttribute && e[0].getAttribute('medium-editor-index') === element.getAttribute('medium-editor-index');\n            });\n\n            for (var i = 0, len = filtered.length; i < len; i++) {\n                var e = filtered[i];\n                this.detachDOMEvent(e[0], e[1], e[2], e[3]);\n            }\n        },\n\n        // Attach all existing handlers to a new element\n        attachAllEventsToElement: function (element) {\n            if (this.listeners['editableInput']) {\n                this.contentCache[element.getAttribute('medium-editor-index')] = element.innerHTML;\n            }\n\n            if (this.eventsCache) {\n                this.eventsCache.forEach(function (e) {\n                    this.attachDOMEvent(element, e['name'], e['handler'].bind(this));\n                }, this);\n            }\n        },\n\n        enableCustomEvent: function (event) {\n            if (this.disabledEvents[event] !== undefined) {\n                delete this.disabledEvents[event];\n            }\n        },\n\n        disableCustomEvent: function (event) {\n            this.disabledEvents[event] = true;\n        },\n\n        // custom events\n        attachCustomEvent: function (event, listener) {\n            this.setupListener(event);\n            if (!this.customEvents[event]) {\n                this.customEvents[event] = [];\n            }\n            this.customEvents[event].push(listener);\n        },\n\n        detachCustomEvent: function (event, listener) {\n            var index = this.indexOfCustomListener(event, listener);\n            if (index !== -1) {\n                this.customEvents[event].splice(index, 1);\n                // TODO: If array is empty, should detach internal listeners via destroyListener()\n            }\n        },\n\n        indexOfCustomListener: function (event, listener) {\n            if (!this.customEvents[event] || !this.customEvents[event].length) {\n                return -1;\n            }\n\n            return this.customEvents[event].indexOf(listener);\n        },\n\n        detachAllCustomEvents: function () {\n            this.customEvents = {};\n            // TODO: Should detach internal listeners here via destroyListener()\n        },\n\n        triggerCustomEvent: function (name, data, editable) {\n            if (this.customEvents[name] && !this.disabledEvents[name]) {\n                this.customEvents[name].forEach(function (listener) {\n                    listener(data, editable);\n                });\n            }\n        },\n\n        // Cleaning up\n\n        destroy: function () {\n            this.detachAllDOMEvents();\n            this.detachAllCustomEvents();\n            this.detachExecCommand();\n\n            if (this.base.elements) {\n                this.base.elements.forEach(function (element) {\n                    element.removeAttribute('data-medium-focused');\n                });\n            }\n        },\n\n        // Listening to calls to document.execCommand\n\n        // Attach a listener to be notified when document.execCommand is called\n        attachToExecCommand: function () {\n            if (this.execCommandListener) {\n                return;\n            }\n\n            // Store an instance of the listener so:\n            // 1) We only attach to execCommand once\n            // 2) We can remove the listener later\n            this.execCommandListener = function (execInfo) {\n                this.handleDocumentExecCommand(execInfo);\n            }.bind(this);\n\n            // Ensure that execCommand has been wrapped correctly\n            this.wrapExecCommand();\n\n            // Add listener to list of execCommand listeners\n            this.options.ownerDocument.execCommand.listeners.push(this.execCommandListener);\n        },\n\n        // Remove our listener for calls to document.execCommand\n        detachExecCommand: function () {\n            var doc = this.options.ownerDocument;\n            if (!this.execCommandListener || !doc.execCommand.listeners) {\n                return;\n            }\n\n            // Find the index of this listener in the array of listeners so it can be removed\n            var index = doc.execCommand.listeners.indexOf(this.execCommandListener);\n            if (index !== -1) {\n                doc.execCommand.listeners.splice(index, 1);\n            }\n\n            // If the list of listeners is now empty, put execCommand back to its original state\n            if (!doc.execCommand.listeners.length) {\n                this.unwrapExecCommand();\n            }\n        },\n\n        // Wrap document.execCommand in a custom method so we can listen to calls to it\n        wrapExecCommand: function () {\n            var doc = this.options.ownerDocument;\n\n            // Ensure all instance of MediumEditor only wrap execCommand once\n            if (doc.execCommand.listeners) {\n                return;\n            }\n\n            // Helper method to call all listeners to execCommand\n            var callListeners = function (args, result) {\n                if (doc.execCommand.listeners) {\n                    doc.execCommand.listeners.forEach(function (listener) {\n                        listener({\n                            command: args[0],\n                            value: args[2],\n                            args: args,\n                            result: result\n                        });\n                    });\n                }\n            },\n\n                // Create a wrapper method for execCommand which will:\n                // 1) Call document.execCommand with the correct arguments\n                // 2) Loop through any listeners and notify them that execCommand was called\n                //    passing extra info on the call\n                // 3) Return the result\n                wrapper = function () {\n                    var result = doc.execCommand.orig.apply(this, arguments);\n\n                    if (!doc.execCommand.listeners) {\n                        return result;\n                    }\n\n                    var args = Array.prototype.slice.call(arguments);\n                    callListeners(args, result);\n\n                    return result;\n                };\n\n            // Store a reference to the original execCommand\n            wrapper.orig = doc.execCommand;\n\n            // Attach an array for storing listeners\n            wrapper.listeners = [];\n\n            // Helper for notifying listeners\n            wrapper.callListeners = callListeners;\n\n            // Overwrite execCommand\n            doc.execCommand = wrapper;\n        },\n\n        // Revert document.execCommand back to its original self\n        unwrapExecCommand: function () {\n            var doc = this.options.ownerDocument;\n            if (!doc.execCommand.orig) {\n                return;\n            }\n\n            // Use the reference to the original execCommand to revert back\n            doc.execCommand = doc.execCommand.orig;\n        },\n\n        // Listening to browser events to emit events medium-editor cares about\n        setupListener: function (name) {\n            if (this.listeners[name]) {\n                return;\n            }\n\n            switch (name) {\n                case 'externalInteraction':\n                    // Detecting when user has interacted with elements outside of MediumEditor\n                    this.attachDOMEvent(this.options.ownerDocument.body, 'mousedown', this.handleBodyMousedown.bind(this), true);\n                    this.attachDOMEvent(this.options.ownerDocument.body, 'click', this.handleBodyClick.bind(this), true);\n                    this.attachDOMEvent(this.options.ownerDocument.body, 'focus', this.handleBodyFocus.bind(this), true);\n                    break;\n                case 'blur':\n                    // Detecting when focus is lost\n                    this.setupListener('externalInteraction');\n                    break;\n                case 'focus':\n                    // Detecting when focus moves into some part of MediumEditor\n                    this.setupListener('externalInteraction');\n                    break;\n                case 'editableInput':\n                    // setup cache for knowing when the content has changed\n                    this.contentCache = {};\n                    this.base.elements.forEach(function (element) {\n                        this.contentCache[element.getAttribute('medium-editor-index')] = element.innerHTML;\n                    }, this);\n\n                    // Attach to the 'oninput' event, handled correctly by most browsers\n                    if (this.InputEventOnContenteditableSupported) {\n                        this.attachToEachElement('input', this.handleInput);\n                    }\n\n                    // For browsers which don't support the input event on contenteditable (IE)\n                    // we'll attach to 'selectionchange' on the document and 'keypress' on the editables\n                    if (!this.InputEventOnContenteditableSupported) {\n                        this.setupListener('editableKeypress');\n                        this.keypressUpdateInput = true;\n                        this.attachDOMEvent(document, 'selectionchange', this.handleDocumentSelectionChange.bind(this));\n                        // Listen to calls to execCommand\n                        this.attachToExecCommand();\n                    }\n                    break;\n                case 'editableClick':\n                    // Detecting click in the contenteditables\n                    this.attachToEachElement('click', this.handleClick);\n                    break;\n                case 'editableBlur':\n                    // Detecting blur in the contenteditables\n                    this.attachToEachElement('blur', this.handleBlur);\n                    break;\n                case 'editableKeypress':\n                    // Detecting keypress in the contenteditables\n                    this.attachToEachElement('keypress', this.handleKeypress);\n                    break;\n                case 'editableKeyup':\n                    // Detecting keyup in the contenteditables\n                    this.attachToEachElement('keyup', this.handleKeyup);\n                    break;\n                case 'editableKeydown':\n                    // Detecting keydown on the contenteditables\n                    this.attachToEachElement('keydown', this.handleKeydown);\n                    break;\n                case 'editableKeydownSpace':\n                    // Detecting keydown for SPACE on the contenteditables\n                    this.setupListener('editableKeydown');\n                    break;\n                case 'editableKeydownEnter':\n                    // Detecting keydown for ENTER on the contenteditables\n                    this.setupListener('editableKeydown');\n                    break;\n                case 'editableKeydownTab':\n                    // Detecting keydown for TAB on the contenteditable\n                    this.setupListener('editableKeydown');\n                    break;\n                case 'editableKeydownDelete':\n                    // Detecting keydown for DELETE/BACKSPACE on the contenteditables\n                    this.setupListener('editableKeydown');\n                    break;\n                case 'editableMouseover':\n                    // Detecting mouseover on the contenteditables\n                    this.attachToEachElement('mouseover', this.handleMouseover);\n                    break;\n                case 'editableDrag':\n                    // Detecting dragover and dragleave on the contenteditables\n                    this.attachToEachElement('dragover', this.handleDragging);\n                    this.attachToEachElement('dragleave', this.handleDragging);\n                    break;\n                case 'editableDrop':\n                    // Detecting drop on the contenteditables\n                    this.attachToEachElement('drop', this.handleDrop);\n                    break;\n                // TODO: We need to have a custom 'paste' event separate from 'editablePaste'\n                // Need to think about the way to introduce this without breaking folks\n                case 'editablePaste':\n                    // Detecting paste on the contenteditables\n                    this.attachToEachElement('paste', this.handlePaste);\n                    break;\n            }\n            this.listeners[name] = true;\n        },\n\n        attachToEachElement: function (name, handler) {\n            // build our internal cache to know which element got already what handler attached\n            if (!this.eventsCache) {\n                this.eventsCache = [];\n            }\n\n            this.base.elements.forEach(function (element) {\n                this.attachDOMEvent(element, name, handler.bind(this));\n            }, this);\n\n            this.eventsCache.push({ 'name': name, 'handler': handler });\n        },\n\n        cleanupElement: function (element) {\n            var index = element.getAttribute('medium-editor-index');\n            if (index) {\n                this.detachAllEventsFromElement(element);\n                if (this.contentCache) {\n                    delete this.contentCache[index];\n                }\n            }\n        },\n\n        focusElement: function (element) {\n            element.focus();\n            this.updateFocus(element, { target: element, type: 'focus' });\n        },\n\n        updateFocus: function (target, eventObj) {\n            var hadFocus = this.base.getFocusedElement(),\n                toFocus;\n\n            // For clicks, we need to know if the mousedown that caused the click happened inside the existing focused element\n            // or one of the extension elements.  If so, we don't want to focus another element\n            if (hadFocus &&\n                eventObj.type === 'click' &&\n                this.lastMousedownTarget &&\n                (MediumEditor.util.isDescendant(hadFocus, this.lastMousedownTarget, true) ||\n                    isElementDescendantOfExtension(this.base.extensions, this.lastMousedownTarget))) {\n                toFocus = hadFocus;\n            }\n\n            if (!toFocus) {\n                this.base.elements.some(function (element) {\n                    // If the target is part of an editor element, this is the element getting focus\n                    if (!toFocus && (MediumEditor.util.isDescendant(element, target, true))) {\n                        toFocus = element;\n                    }\n\n                    // bail if we found an element that's getting focus\n                    return !!toFocus;\n                }, this);\n            }\n\n            // Check if the target is external (not part of the editor, toolbar, or any other extension)\n            var externalEvent = !MediumEditor.util.isDescendant(hadFocus, target, true) &&\n                !isElementDescendantOfExtension(this.base.extensions, target);\n\n            if (toFocus !== hadFocus) {\n                // If element has focus, and focus is going outside of editor\n                // Don't blur focused element if clicking on editor, toolbar, or anchorpreview\n                if (hadFocus && externalEvent) {\n                    // Trigger blur on the editable that has lost focus\n                    hadFocus.removeAttribute('data-medium-focused');\n                    this.triggerCustomEvent('blur', eventObj, hadFocus);\n                }\n\n                // If focus is going into an editor element\n                if (toFocus) {\n                    // Trigger focus on the editable that now has focus\n                    toFocus.setAttribute('data-medium-focused', true);\n                    this.triggerCustomEvent('focus', eventObj, toFocus);\n                }\n            }\n\n            if (externalEvent) {\n                this.triggerCustomEvent('externalInteraction', eventObj);\n            }\n        },\n\n        updateInput: function (target, eventObj) {\n            if (!this.contentCache) {\n                return;\n            }\n            // An event triggered which signifies that the user may have changed someting\n            // Look in our cache of input for the contenteditables to see if something changed\n            var index = target.getAttribute('medium-editor-index'),\n                html = target.innerHTML;\n\n            if (html !== this.contentCache[index]) {\n                // The content has changed since the last time we checked, fire the event\n                this.triggerCustomEvent('editableInput', eventObj, target);\n            }\n            this.contentCache[index] = html;\n        },\n\n        handleDocumentSelectionChange: function (event) {\n            // When selectionchange fires, target and current target are set\n            // to document, since this is where the event is handled\n            // However, currentTarget will have an 'activeElement' property\n            // which will point to whatever element has focus.\n            if (event.currentTarget && event.currentTarget.activeElement) {\n                var activeElement = event.currentTarget.activeElement,\n                    currentTarget;\n                // We can look at the 'activeElement' to determine if the selectionchange has\n                // happened within a contenteditable owned by this instance of MediumEditor\n                this.base.elements.some(function (element) {\n                    if (MediumEditor.util.isDescendant(element, activeElement, true)) {\n                        currentTarget = element;\n                        return true;\n                    }\n                    return false;\n                }, this);\n\n                // We know selectionchange fired within one of our contenteditables\n                if (currentTarget) {\n                    this.updateInput(currentTarget, { target: activeElement, currentTarget: currentTarget });\n                }\n            }\n        },\n\n        handleDocumentExecCommand: function () {\n            // document.execCommand has been called\n            // If one of our contenteditables currently has focus, we should\n            // attempt to trigger the 'editableInput' event\n            var target = this.base.getFocusedElement();\n            if (target) {\n                this.updateInput(target, { target: target, currentTarget: target });\n            }\n        },\n\n        handleBodyClick: function (event) {\n            this.updateFocus(event.target, event);\n        },\n\n        handleBodyFocus: function (event) {\n            this.updateFocus(event.target, event);\n        },\n\n        handleBodyMousedown: function (event) {\n            this.lastMousedownTarget = event.target;\n        },\n\n        handleInput: function (event) {\n            this.updateInput(event.currentTarget, event);\n        },\n\n        handleClick: function (event) {\n            this.triggerCustomEvent('editableClick', event, event.currentTarget);\n        },\n\n        handleBlur: function (event) {\n            this.triggerCustomEvent('editableBlur', event, event.currentTarget);\n        },\n\n        handleKeypress: function (event) {\n            this.triggerCustomEvent('editableKeypress', event, event.currentTarget);\n\n            // If we're doing manual detection of the editableInput event we need\n            // to check for input changes during 'keypress'\n            if (this.keypressUpdateInput) {\n                var eventObj = { target: event.target, currentTarget: event.currentTarget };\n\n                // In IE, we need to let the rest of the event stack complete before we detect\n                // changes to input, so using setTimeout here\n                setTimeout(function () {\n                    this.updateInput(eventObj.currentTarget, eventObj);\n                }.bind(this), 0);\n            }\n        },\n\n        handleKeyup: function (event) {\n            this.triggerCustomEvent('editableKeyup', event, event.currentTarget);\n        },\n\n        handleMouseover: function (event) {\n            this.triggerCustomEvent('editableMouseover', event, event.currentTarget);\n        },\n\n        handleDragging: function (event) {\n            this.triggerCustomEvent('editableDrag', event, event.currentTarget);\n        },\n\n        handleDrop: function (event) {\n            this.triggerCustomEvent('editableDrop', event, event.currentTarget);\n        },\n\n        handlePaste: function (event) {\n            this.triggerCustomEvent('editablePaste', event, event.currentTarget);\n        },\n\n        handleKeydown: function (event) {\n\n            this.triggerCustomEvent('editableKeydown', event, event.currentTarget);\n\n            if (MediumEditor.util.isKey(event, MediumEditor.util.keyCode.SPACE)) {\n                return this.triggerCustomEvent('editableKeydownSpace', event, event.currentTarget);\n            }\n\n            if (MediumEditor.util.isKey(event, MediumEditor.util.keyCode.ENTER) || (event.ctrlKey && MediumEditor.util.isKey(event, MediumEditor.util.keyCode.M))) {\n                return this.triggerCustomEvent('editableKeydownEnter', event, event.currentTarget);\n            }\n\n            if (MediumEditor.util.isKey(event, MediumEditor.util.keyCode.TAB)) {\n                return this.triggerCustomEvent('editableKeydownTab', event, event.currentTarget);\n            }\n\n            if (MediumEditor.util.isKey(event, [MediumEditor.util.keyCode.DELETE, MediumEditor.util.keyCode.BACKSPACE])) {\n                return this.triggerCustomEvent('editableKeydownDelete', event, event.currentTarget);\n            }\n        }\n    };\n\n    MediumEditor.Events = Events;\n}());\n"
  },
  {
    "path": "src/js/extension.js",
    "content": "(function () {\n    'use strict';\n\n    var Extension = function (options) {\n        MediumEditor.util.extend(this, options);\n    };\n\n    Extension.extend = function (protoProps) {\n        // magic extender thinger. mostly borrowed from backbone/goog.inherits\n        // place this function on some thing you want extend-able.\n        //\n        // example:\n        //\n        //      function Thing(args){\n        //          this.options = args;\n        //      }\n        //\n        //      Thing.prototype = { foo: \"bar\" };\n        //      Thing.extend = extenderify;\n        //\n        //      var ThingTwo = Thing.extend({ foo: \"baz\" });\n        //\n        //      var thingOne = new Thing(); // foo === \"bar\"\n        //      var thingTwo = new ThingTwo(); // foo === \"baz\"\n        //\n        //      which seems like some simply shallow copy nonsense\n        //      at first, but a lot more is going on there.\n        //\n        //      passing a `constructor` to the extend props\n        //      will cause the instance to instantiate through that\n        //      instead of the parent's constructor.\n\n        var parent = this,\n            child;\n\n        // The constructor function for the new subclass is either defined by you\n        // (the \"constructor\" property in your `extend` definition), or defaulted\n        // by us to simply call the parent's constructor.\n\n        if (protoProps && protoProps.hasOwnProperty('constructor')) {\n            child = protoProps.constructor;\n        } else {\n            child = function () {\n                return parent.apply(this, arguments);\n            };\n        }\n\n        // das statics (.extend comes over, so your subclass can have subclasses too)\n        MediumEditor.util.extend(child, parent);\n\n        // Set the prototype chain to inherit from `parent`, without calling\n        // `parent`'s constructor function.\n        var Surrogate = function () {\n            this.constructor = child;\n        };\n        Surrogate.prototype = parent.prototype;\n        child.prototype = new Surrogate();\n\n        if (protoProps) {\n            MediumEditor.util.extend(child.prototype, protoProps);\n        }\n\n        // todo: $super?\n\n        return child;\n    };\n\n    Extension.prototype = {\n        /* init: [function]\n         *\n         * Called by MediumEditor during initialization.\n         * The .base property will already have been set to\n         * current instance of MediumEditor when this is called.\n         * All helper methods will exist as well\n         */\n        init: function () {},\n\n        /* base: [MediumEditor instance]\n         *\n         * If not overriden, this will be set to the current instance\n         * of MediumEditor, before the init method is called\n         */\n        base: undefined,\n\n        /* name: [string]\n         *\n         * 'name' of the extension, used for retrieving the extension.\n         * If not set, MediumEditor will set this to be the key\n         * used when passing the extension into MediumEditor via the\n         * 'extensions' option\n         */\n        name: undefined,\n\n        /* checkState: [function (node)]\n         *\n         * If implemented, this function will be called one or more times\n         * the state of the editor & toolbar are updated.\n         * When the state is updated, the editor does the following:\n         *\n         * 1) Find the parent node containing the current selection\n         * 2) Call checkState on the extension, passing the node as an argument\n         * 3) Get the parent node of the previous node\n         * 4) Repeat steps #2 and #3 until we move outside the parent contenteditable\n         */\n        checkState: undefined,\n\n        /* destroy: [function ()]\n         *\n         * This method should remove any created html, custom event handlers\n         * or any other cleanup tasks that should be performed.\n         * If implemented, this function will be called when MediumEditor's\n         * destroy method has been called.\n         */\n        destroy: undefined,\n\n        /* As alternatives to checkState, these functions provide a more structured\n         * path to updating the state of an extension (usually a button) whenever\n         * the state of the editor & toolbar are updated.\n         */\n\n        /* queryCommandState: [function ()]\n         *\n         * If implemented, this function will be called once on each extension\n         * when the state of the editor/toolbar is being updated.\n         *\n         * If this function returns a non-null value, the extension will\n         * be ignored as the code climbs the dom tree.\n         *\n         * If this function returns true, and the setActive() function is defined\n         * setActive() will be called\n         */\n        queryCommandState: undefined,\n\n        /* isActive: [function ()]\n         *\n         * If implemented, this function will be called when MediumEditor\n         * has determined that this extension is 'active' for the current selection.\n         * This may be called when the editor & toolbar are being updated,\n         * but only if queryCommandState() or isAlreadyApplied() functions\n         * are implemented, and when called, return true.\n         */\n        isActive: undefined,\n\n        /* isAlreadyApplied: [function (node)]\n         *\n         * If implemented, this function is similar to checkState() in\n         * that it will be called repeatedly as MediumEditor moves up\n         * the DOM to update the editor & toolbar after a state change.\n         *\n         * NOTE: This function will NOT be called if checkState() has\n         * been implemented. This function will NOT be called if\n         * queryCommandState() is implemented and returns a non-null\n         * value when called\n         */\n        isAlreadyApplied: undefined,\n\n        /* setActive: [function ()]\n         *\n         * If implemented, this function is called when MediumEditor knows\n         * that this extension is currently enabled.  Currently, this\n         * function is called when updating the editor & toolbar, and\n         * only if queryCommandState() or isAlreadyApplied(node) return\n         * true when called\n         */\n        setActive: undefined,\n\n        /* setInactive: [function ()]\n         *\n         * If implemented, this function is called when MediumEditor knows\n         * that this extension is currently disabled.  Curently, this\n         * is called at the beginning of each state change for\n         * the editor & toolbar. After calling this, MediumEditor\n         * will attempt to update the extension, either via checkState()\n         * or the combination of queryCommandState(), isAlreadyApplied(node),\n         * isActive(), and setActive()\n         */\n        setInactive: undefined,\n\n        /* getInteractionElements: [function ()]\n         *\n         * If the extension renders any elements that the user can interact with,\n         * this method should be implemented and return the root element or an array\n         * containing all of the root elements. MediumEditor will call this function\n         * during interaction to see if the user clicked on something outside of the editor.\n         * The elements are used to check if the target element of a click or\n         * other user event is a descendant of any extension elements.\n         * This way, the editor can also count user interaction within editor elements as\n         * interactions with the editor, and thus not trigger 'blur'\n         */\n        getInteractionElements: undefined,\n\n        /************************ Helpers ************************\n         * The following are helpers that are either set by MediumEditor\n         * during initialization, or are helper methods which either\n         * route calls to the MediumEditor instance or provide common\n         * functionality for all extensions\n         *********************************************************/\n\n        /* window: [Window]\n         *\n         * If not overriden, this will be set to the window object\n         * to be used by MediumEditor and its extensions.  This is\n         * passed via the 'contentWindow' option to MediumEditor\n         * and is the global 'window' object by default\n         */\n        'window': undefined,\n\n        /* document: [Document]\n         *\n         * If not overriden, this will be set to the document object\n         * to be used by MediumEditor and its extensions. This is\n         * passed via the 'ownerDocument' optin to MediumEditor\n         * and is the global 'document' object by default\n         */\n        'document': undefined,\n\n        /* getEditorElements: [function ()]\n         *\n         * Helper function which returns an array containing\n         * all the contenteditable elements for this instance\n         * of MediumEditor\n         */\n        getEditorElements: function () {\n            return this.base.elements;\n        },\n\n        /* getEditorId: [function ()]\n         *\n         * Helper function which returns a unique identifier\n         * for this instance of MediumEditor\n         */\n        getEditorId: function () {\n            return this.base.id;\n        },\n\n        /* getEditorOptions: [function (option)]\n         *\n         * Helper function which returns the value of an option\n         * used to initialize this instance of MediumEditor\n         */\n        getEditorOption: function (option) {\n            return this.base.options[option];\n        }\n    };\n\n    /* List of method names to add to the prototype of Extension\n     * Each of these methods will be defined as helpers that\n     * just call directly into the MediumEditor instance.\n     *\n     * example for 'on' method:\n     * Extension.prototype.on = function () {\n     *     return this.base.on.apply(this.base, arguments);\n     * }\n     */\n    [\n        // general helpers\n        'execAction',\n\n        // event handling\n        'on',\n        'off',\n        'subscribe',\n        'trigger'\n\n    ].forEach(function (helper) {\n        Extension.prototype[helper] = function () {\n            return this.base[helper].apply(this.base, arguments);\n        };\n    });\n\n    MediumEditor.Extension = Extension;\n})();\n"
  },
  {
    "path": "src/js/extensions/README.md",
    "content": "# Extensions\n\n* [Building An Extension (Walkthrough)](./WALKTHROUGH-EXTENSION.md)\n* [Building A Button (Walkthrough)](./WALKTHROUGH-BUTTON.md)\n\n<!-- START doctoc generated TOC please keep comment here to allow auto update -->\n<!-- DON'T EDIT THIS SECTION, INSTEAD RE-RUN doctoc TO UPDATE -->\n\n* [What is an Extension?](#what-is-an-extension)\n  * [Examples of functionality that are implemented via built-in extensions:](#examples-of-functionality-that-are-implemented-via-built-in-extensions)\n  * [Examples of custom built external extensions:](#examples-of-custom-built-external-extensions)\n* [What is a Button?](#what-is-a-button)\n  * [All of the built-in MediumEditor buttons are just Button Extensions with different configuration](#all-of-the-built-in-mediumeditor-buttons-are-just-button-extensions-with-different-configuration)\n  * [Examples of custom built external buttons:](#examples-of-custom-built-external-buttons)\n* [What is a Form Extension?](#what-is-a-form-extension)\n  * [Built-in Form Extensions](#built-in-form-extensions)\n* [Extension](#extension)\n  * [Extension Interface](#extension-interface)\n    * [`name` _(string)_](#name-_string_)\n    * [`init()`](#init)\n    * [`checkState(node)`](#checkstatenode)\n    * [`destroy()`](#destroy)\n    * [`queryCommandState()`](#querycommandstate)\n    * [`getInteractionElements()`](#getinteractionelements)\n    * [`isActive()`](#isactive)\n    * [`isAlreadyApplied(node)`](#isalreadyappliednode)\n    * [`setActive()`](#setactive)\n    * [`setInactive()`](#setinactive)\n  * [Extension Helpers](#extension-helpers)\n    * [`base` _(MediumEditor)_](#base-_mediumeditor_)\n    * [`window` _(Window)_](#window-_window_)\n    * [`document` _(Document)_](#document-_document_)\n    * [`getEditorElements()`](#geteditorelements)\n    * [`getEditorId()`](#geteditorid)\n    * [`getEditorOption(option)`](#geteditoroptionoption)\n  * [Extension Proxy Methods](#extension-proxy-methods)\n    * [`execAction(action, opts)`](#execactionaction-opts)\n    * [`on(target, event, listener, useCapture)`](#ontarget-event-listener-usecapture)\n    * [`off(target, event, listener, useCapture)`](#offtarget-event-listener-usecapture)\n    * [`subscribe(name, listener)`](#subscribename-listener)\n    * [`trigger(name, data, editable)`](#triggername-data-editable)\n* [Button](#button)\n  * [Button Interface](#button-interface)\n    * [`getButton()`](#getbutton)\n  * [Button Helpers](#button-helpers)\n    * [`action` _(string)_](#action-_string_)\n    * [`aria` _(string)_](#aria-_string_)\n    * [`tagNames` _(Array)_](#tagnames-_array_)\n    * [`style` _(Object)_](#style-_object_)\n    * [`useQueryState` _(boolean)_](#usequerystate-_boolean_)\n    * [`contentDefault` _(string)_](#contentdefault-_string_)\n    * [`contentFA` _(string)_](#contentfa-_string_)\n    * [`classList` _(Array)_](#classlist-_array_)\n    * [`attrs` _(Object)_](#attrs-_object_)\n    * [`handleClick(event)` _(function)_](#handleclickevent-_function_)\n* [Form Button](#form-button)\n  * [Form Button Interface](#form-button-interface)\n  * [Form Button Helpers](#form-button-helpers)\n\n<!-- END doctoc generated TOC please keep comment here to allow auto update -->\n\n## What is an Extension?\n\n**Extensions** are custom actions or commands that can be passed in via the `extensions` option to medium-editor.  They can replace existing buttons if they share the same name, or can add additional custom functionality into the editor.  Extensions can be implemented in any way, and just provide a way to hook into medium-editor.  New extensions can be created by extending the exposed `MediumEditor.Extension` object via `MediumEditor.Extension.extend()`.\n\n\n#### Examples of functionality that are implemented via built-in extensions:\n* [The MediumEditor Toolbar](https://github.com/yabwe/medium-editor/blob/toolbar-extension/src/js/extensions/toolbar.js)\n  * The entire toolbar which contains all of the MediumEditor buttons is implemented as an extension!\n* [Auto-Link Detection](https://github.com/yabwe/medium-editor/blob/master/src/js/extensions/auto-link.js)\n  * Automatically detecting when a url has been added and converting it into an anchor tag\n* [Anchor Preview Tooltip](https://github.com/yabwe/medium-editor/blob/master/src/js/extensions/anchor-preview.js)\n  * When a user hovers over a link a tooltip is displayed showing the href of the anchor tag\n* [Image Drag & Drop](https://github.com/yabwe/medium-editor/blob/master/src/js/extensions/image-dragging.js)\n  * Allows users to drag and drop images into the editor\n* [Keyboard Commands](https://github.com/yabwe/medium-editor/blob/master/src/js/extensions/keyboard-commands.js)\n  * Mapping keyboard shortcuts to various commands\n* [Placeholder Text](https://github.com/yabwe/medium-editor/blob/master/src/js/extensions/placeholder.js)\n  * Shows placeholder text when the editor is empty\n* [Paste Handling](https://github.com/yabwe/medium-editor/blob/master/src/js/extensions/paste.js)\n  * Handles filtering and modifying text that is pasted into the editor\n\n#### Examples of custom built external extensions:\n* [MediumEditor markdown](https://github.com/IonicaBizau/medium-editor-markdown)\n  * A Medium Editor extension to add markdown support.\n* [MediumEditor Insert Plugin](https://github.com/orthes/medium-editor-insert-plugin)\n  * Enables users to insert into the editor various types of content like images or embeds.\n* [MediumEditor Multi-placeholder Plugin](https://github.com/smiled0g/medium-editor-multi-placeholders-plugin)\n  * Enables users to add multiple placeholders with specific HTML tags.\n* [MediumEditor TCMention Plugin](https://github.com/tomchentw/medium-editor-tc-mention)\n  * A Medium Editor extension for adding custom 'mention' support, circa Medium 2.0.\n* [MediumEditor AutoList](https://github.com/varun-raj/medium-editor-autolist)\n  * A Medium Editor extension for converting `1.` and `*` into ordered lists and unordered lists.\n* [MediumEditor Toolbar States](https://github.com/davideanderson/medium-editor-toolbar-states)\n  * An Extension for the medium-editor which allows different toolbar configurations based on the selected element(s).\n* [MediumEditor AutoFocus](https://github.com/dazorni/medium-editor-autofocus)\n  * Autofocus plugin for medium-editor\n* [MediumEditor Thaana Keyboard](https://github.com/jawish/medium-editor-thaanakbd)\n  * Thaana Keyboard extension for medium-editor\n* [MediumEditor Merge Fields Plugin](https://github.com/epascarello/merge-fields-plugin-for-medium-editor)\n  * Add merge fields for medium-editor\n* [MediumEditor Google Docs Anchor Preview](https://github.com/patternhq/MediumTools/tree/master/GdocMediumAnchorPreview)\n  * Google Doc style link preview for medium-editor\n\n## What is a Button?\n**Buttons** are a specific type of Extension which have a contract with the MediumEditor toolbar.  Buttons have specific lifecycle methods that MediumEditor and the toolbar use to interact with these specific types of Extensions.  These contract create easy hooks, allowing custom buttons to:\n* Display an element in the toolbar _(ie a clickable button/link)_\n* Execute an action on the editor text when clicked _(ie bold, underline, blockquote, etc.)_\n* Update the appearance of the element based on the user selection _(ie the bold button looks 'active' if the selected text is already bold, 'inactive' if the text is not bold)_\n\n#### All of the built-in MediumEditor buttons are just Button Extensions with different [configuration](https://github.com/yabwe/medium-editor/blob/master/src/js/defaults/buttons.js):\n* bold, italic, underline, strikethrough\n* subscript, superscript\n* image\n* quote, pre\n* orderedlist, unorderedlist\n* indent, outdent\n* justifyLeft, justifyCenter, justifyRight, justifyFull\n* h1, h2, h3, h4, h5, h6\n* removeFormat\n* html\n\n#### Examples of custom built external buttons:\n* [MediumEditor tables](https://github.com/yabwe/medium-editor-tables)\n  * Extension to add a table button/behavior to MediumEditor\n* [MediumEditor Custom HTML](https://github.com/jillix/medium-editor-custom-html)\n  * An extension that inserts custom HTML using a new button in the MediumEditor toolbar\n* [MediumButton](https://github.com/arcs-/MediumButton)\n  * Extends your Medium Editor with the possibility add buttons.\n* [MediumEditor Phrase](https://github.com/nymag/medium-editor-phrase)\n  * Adds a configurable button to the MediumEditor toolbar which adds phrasing content tags (e.g. `span` tags) to selected text.\n* [MediumEditor Handsontable](https://github.com/asselinpaul/medium-editor-handsontable)\n  * Supports adding [handsontable](https://handsontable.com/) spreadsheets to MediumEditor.\n* [MediumEditor Lists](https://github.com/mkawczynski07/medium-editor-list)\n  * Adds a \"Add Paragraph\" button which allows for inserting customized paragraphs to MediumEditor\n* [MediumEditor Embed Button](https://github.com/orhanveli/medium-editor-embed-button)\n  * oEmbed based embedding button extension to add rich embeds to your document.\n\n## What is a Form Extension?\n**Form Extensions** are a specific type of Button Extension which collect input from the user via the toolbar.  Form Extensions extend from Button, and thus inherit all of the lifecycle methods of a Button.  In addition, Form Extensions have some additional methods exposed to interact with MediumEditor and provide some common functionality.\n\n#### Built-in Form Extensions\n* [Anchor Button](https://github.com/yabwe/medium-editor/blob/master/src/js/extensions/anchor.js)\n  * The `'anchor'` Button is actually a form extension, which when clicked, prompts the the user for a url (as well as some optional checkboxes) via a control in the toolbar and converts the selected text into a link.  If the selection is already a link, clicking the button unwraps text within the anchor tag.\n* [FontSize Button (beta)](https://github.com/yabwe/medium-editor/blob/master/src/js/extensions/fontsize.js)\n  * The `'fontsize'` Button is a form extension, which when clicked, allows the user to modify the size of the existing text via a control in the toolbar.\n\n\n# Extension\n\n## Extension Interface\n\nThe following are properties and method that MediumEditor will attempt to use / call to interact with the extension internally.\n\n### `name` _(string)_\n\nThe name to identify the extension by.  This is used for calls to [`MediumEditor.getExtensionByName(name)`](../../../API.md#getextensionbynamename) to retrieve the extension.  If not defined, this will be set to whatever identifier was used when passing the extension into MediumEditor via the `extensions` option.\n\n```javascript\nvar MyExtension = MediumEditor.Extension.extend({\n  name: 'myextension'\n});\n\nvar myExt = new MyExtension();\n\nvar editor = new MediumEditor('.editor', {\n  extensions: {\n    'myextension': myExt\n  }\n});\n\neditor.getExtensionByName(`myextension`) === myExt //true\n```\n\n***\n### `init()`\n\nCalled by MediumEditor during initialization.  The `.base` property will already have been set to current instance of MediumEditor when this is called. All helper methods will exist as well.\n\n***\n### `checkState(node)`\n\nIf implemented, this method will be called one or more times after the state of the editor & toolbar are updated. When the state is updated, the editor does the following:\n\n1. Find the parent node containing the current selection\n2. Call `checkState(node)` on each extension, passing the node as an argument\n3. Get the parent node of the previous node\n4. Repeat steps #2 and #3 until we move outside the parent contenteditable\n\n**Arguments**\n\n1. _**node** (`Node`)_:\n\n  * Current node, within the ancestors of the selection, that is being checked whenever a selection change occurred.\n\nHere's an example of an extension that will add/remove a class to editor elements depending on whether or the current selection is within an element with a custom data attribute:\n\n```javascript\nvar EditedExtension = MediumEditor.Extension.extend({\n  name: 'edited',\n  checkState: function (node) {\n    // checkState is called multiple times for each selection change\n    // so only store a value if the attribute was found\n    if (!this.foundAttribute && node.getAttribute('data-edited')) {\n      this.foundAttribute = true;\n    }\n\n    // Once we've moved up the ancestors to the container element\n    // we know we're done iterating up and can add/remove the css class\n    if (MediumEditor.util.isMediumEditorElement(node)) {\n      if (this.foundAttribute) {\n        node.classList.add('edited-text');\n      } else {\n        node.classList.remove('edited-text');\n      }\n      // Make sure the property is not persisted for the next time\n      // selection is updated\n      delete this.foundAttribute;\n    }\n  }\n});\n\nvar editedExt = new EditedExtension();\n\nvar editor = new MediumEditor('.editor', {\n  extensions: {\n    'edited': editedExt\n  }\n});\n```\n\n***\n### `destroy()`\n\nIf implemented, this method will be called whenever the MediumEditor is being destroyed (via a call to [`MediumEditor.destroy()`](../../../API.md#destroy)).\n\nThis gives the extensions the chance to remove any created html, custom event handlers or execute any other cleanup tasks that should be performed.\n\n***\n### `queryCommandState()`\n\nIf implemented, this method will be called once on each extension when the state of the editor/toolbar is being updated.\n\nIf this method returns a non-null value, the extension will be ignored as the code climbs the dom tree.\n\nIf this method returns `true`, and the `setActive()` method is defined on the extension, the `setActive()` method will be called by MediumEditor.\n\n**Returns:** `boolean` OR `null`\n\n***\n### `getInteractionElements()`\n\nIf 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.\n\nMediumEditor 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'\n\n***\n### `isActive()`\n\nIf implemented, this method will be called from MediumEditor to determine whether the button has already been set as 'active'.\n\nIf it returns `true`, this extension/button will be skipped for checking its active state as MediumEditor responds to the change in selection.\n\nIf it returns `false`, `isAlreadyApplied()` will still be passed each ancestor element as the MediumEditor code climbs the DOM hierarchy to respond to the change in selection.\n\n\n**Returns:** `boolean`\n\n***\n### `isAlreadyApplied(node)`\n\nIf implemented, this method is similar to `checkState()` in that it will be called repeatedly as MediumEditor moves up the DOM to update the editor & toolbar after a state change.\n\n**NOTE:**\n\n* This method will NOT be called if `checkState()` has been implemented.\n* This method will NOT be called if `queryCommandState()` is implemented and returns a non-null value when called.\n\n**Arguments**\n\n1. _**node** (`Node`)_:\n\n  * Node to check for whether the current extension has already been applied.\n\n**Returns:** `boolean`\n\n***\n### `setActive()`\n\nIf implemented, this method is called when MediumEditor knows that this extension is currently enabled.\n\nCurrently, this method is called when updating the editor & toolbar, and if `queryCommandState()` or `isAlreadyApplied(node)` return true when called.\n\n***\n### `setInactive()`\n\nIf implemented, this method is called when MediumEditor knows that this extension has not been applied to the current selection. Curently, this is called at the beginning of each state change for the editor & toolbar.\n\nAfter calling this, MediumEditor will attempt to update the extension, either via `checkState()` or the combination of `queryCommandState()`, `isAlreadyApplied(node)`, `isActive()`, and `setActive()`\n\n## Extension Helpers\n\nThe following are helpers that are either set by MediumEditor during initialization, or are helper methods which either route calls to the MediumEditor instance or provide common functionality for all extensions.\n\n### `base` _(MediumEditor)_\n\nA reference to the instance of MediumEditor that this extension is part of.\n\nFor example, if you wanted to save the current selection within MediumEditor to be used later, you could call the following within your extension:\n\n```javascript\nthis.base.saveSelection();\n```\n\n***\n### `window` _(Window)_\n\nA reference to the content window to be used by this instance of MediumEditor.  This maps to the value of the [`contentWindow`](../../../OPTIONS.md#contentwindow) option that is passed into MediumEditor.\n\nFor example, if you wanted to get the width of the window that contains this instance of MediumEditor, you could call the following within your extension:\n\n```javascript\nvar windowWidth = this.window.innerWidth;\n```\n\n***\n### `document` _(Document)_\n\nA reference to the owner document to be used by this instance of MediumEditor.  This maps to the value of the [`ownerDocument`](../../../OPTIONS.md#ownerdocument) option that is passed into MediumEditor.\n\nFor example, to create an element in the current document corresponding to this instance of MediumEditor, you would call the following within your extension:\n\n```javascript\nvar button = this.document.createElement('button');\n```\n\n***\n### `getEditorElements()`\n\nReturns a reference to the array of **elements** monitored by this instance of MediumEditor.\n\n**Returns:** `Array` of `HTMLElement`s\n\nFor example, the following is the destroy method of the Placeholder Extension, which removes an attribute from all editor **elements**:\n\n```javascript\nMediumEditor.extensions.placeholder = MediumEditor.Extension.extend({\n  // ...\n  destroy: function () {\n    this.getEditorElements().forEach(function (el) {\n      if (el.getAttribute('data-placeholder') === this.text) {\n        el.removeAttribute('data-placeholder');\n      }\n    }, this);\n  },\n  // ...\n});\n```\n\n***\n### `getEditorId()`\n\nReturns the unique identifier for this instance of MediumEditor\n\n**Returns:** `Number`\n\nFor example, the following is an excerpt from the `createToolbar()` method of the Toolbar extension, which creates the toolbar element and gives it a unique id tied to the editor's unique id:\n\n```javascript\nMediumEditor.extensions.placeholder = MediumEditor.Extension.extend({\n  // ...\n  createToolbar: function () {\n    var toolbar = this.document.createElement('div');\n\n    toolbar.id = 'medium-editor-toolbar-' + this.getEditorId();\n    toolbar.className = 'medium-editor-toolbar';\n\n    // ...\n\n    return toolbar;\n  },\n  // ...\n});\n```\n\n***\n### `getEditorOption(option)`\n\nReturns the value of a specific option used to initialize the MediumEditor object.\n\n**Arguments**\n\n1. _**option** ('String')_\n\n  * Name of the MediumEditor option to retrieve.\n\n**Returns:** Value of the MediumEditor option\n\nFor example, the following is an excerpt from the `getTemplate()` method of the Anchor extension, which checks the [`buttonLabels`](../../../OPTIONS.md#buttonlabels) option MediumEditor to decide the appearance of the 'save' button in the form:\n\n```javascript\nMediumEditor.extensions.anchor = MediumEditor.extensions.form.extend({\n  // ...\n  getTemplate: function () {\n    var template = [\n      '<input type=\"text\" class=\"medium-editor-toolbar-input\" placeholder=\"', this.placeholderText, '\">'\n    ];\n\n    template.push(\n      '<a href=\"#\" class=\"medium-editor-toolbar-save\">',\n      this.getEditorOption('buttonLabels') === 'fontawesome' ? '<i class=\"fa fa-check\"></i>' : this.formSaveLabel,\n      '</a>'\n    );\n\n    // ...\n  },\n  // ...\n});\n```\n\n## Extension Proxy Methods\n\n* These are methods that are just proxied calls into existing MediumEditor functions:\n\n### `execAction(action, opts)`\n\nCalls [`MediumEditor.execAction(action, opts)`](../../../API.md#execactionaction-opts)\n\nFor example, the Button Extension will - by default - call `execAction()` each time a button is clicked, to trigger a command:\n\n```javascript\nMediumEditor.extensions.button = MediumEditor.Extension.extend({\n  // ...\n  handleClick: function (event) {\n    event.preventDefault();\n    event.stopPropagation();\n\n    var action = this.getAction(); // 'bold', 'italic', etc.\n\n    if (action) {\n      this.execAction(action);\n    }\n  },\n  // ...\n});\n```\n\n***\n### `on(target, event, listener, useCapture)`\n\nCalls [`MediumEditor.on(target, event, listener, useCapture)`](../../../API.md#ontarget-event-listener-usecapture)\n\nThis allows extensions to easily attach event handlers to the DOM which will automatically be detached when MediumEditor is destroyed.\n\nFor example, when the Anchor Preview Extension detects a `mouseover` event for a link, it will attach to the `mouseout` event for the same link so it can hide the anchor preview:\n\n```javascript\nMediumEditor.extensions.anchorPreview = MediumEditor.Extension.extend({\n  // ...\n  handleEditableMouseover: function (event) {\n    // ...\n    this.instanceHandleAnchorMouseout = this.handleAnchorMouseout.bind(this);\n    this.on(this.anchorToPreview, 'mouseout', this.instanceHandleAnchorMouseout);\n    // ...\n  },\n  // ...\n});\n```\n\n***\n### `off(target, event, listener, useCapture)`\n\nCalls [`MediumEditor.off(target, event, listener, useCapture)`](../../../API.md#offtarget-event-listener-usecapture)\n\nTo compliment the above example for `on(target, event, listener, useCapture)`, when the Anchor Preview Extension detects a `mouseout` event for a link, it will detach the the event handler for `mouseout` until the next time the mouse hovers over the link:\n\n```javascript\nMediumEditor.extensions.anchorPreview = MediumEditor.Extension.extend({\n  // ...\n  handleAnchorMouseout: function () {\n    this.anchorToPreview = null;\n    this.off(this.activeAnchor, 'mouseout', this.instanceHandleAnchorMouseout);\n    this.instanceHandleAnchorMouseout = null;\n  },\n  // ...\n});\n```\n\n***\n### `subscribe(name, listener)`\n\nCalls [`MediumEditor.subscribe(name, listener)`](../../../API.md#subscribename-listener)\n\nFor example, the Keyboard Commands Extension will subscribe to the `editableKeydown` custom event during `init()`, to monitor when keys are pressed while any of the editor **elements** are focused:\n\n```javascript\nMediumEditor.extensions.keyboardCommands = MediumEditor.Extension.extend({\n  // ...\n  init: function () {\n    MediumEditor.Extension.prototype.init.apply(this, arguments);\n\n    this.subscribe('editableKeydown', this.handleKeydown.bind(this));\n    // ...\n  },\n  // ...\n});\n```\n\n***\n### `trigger(name, data, editable)`\n\nCalls [`MediumEditor.trigger(name, data, editable)`](../../../API.md#triggername-data-editable)\n\nFor example, the Toolbar Extension triggers the `hideToolbar` custom event whenever the toolbar is being hidden:\n\n```javascript\nMediumEditor.extensions.toolbar = MediumEditor.Extension.extend({\n  // ...\n  hideToolbar: function () {\n    if (this.isDisplayed()) {\n      this.getToolbarElement().classList.remove('medium-editor-toolbar-active');\n      this.trigger('hideToolbar', {}, this.base.getFocusedElement());\n    }\n  },\n  // ...\n});\n```\n\n# Button\n\nButtons are a specific type of extension which will render a button into the toolbar, allowing for custom logic to run whenever the button is clicked.  The extension framework also allows the button extension to respond to the user's selection each time the selection changes to do things such as 'activate/deactivate' the button.  This allows for things like having the Bold button be 'activated' whenever the user's selection includes already bold text, or 'inactive' if the selection is not already bold.\n\n***\n## Button Interface\n\nThe only method that defines an extension as a **Button Extension** is the `getButton()` method. As long as the name of the **Button Extension** is passed via the `toolbar.buttons` option, and the `getButton()` method is implemented, then the toolbar will treat the extension as a **Button Extension**\n\n### `getButton()`\n\nIf the name of an extension appears in the `toolbar.buttons` option, the MediumEditor toolbar will attempt to call this `getButton()` method on the extension. The `HTMLElement` returned by this method will be appended to the toolbar.\n\nThe `getButton()` method on each button will be called and appended to the toolbar in the order that they were specified in the `toolbar.buttons` option.\n\n***\n## Button Helpers\n\nThe following are properties and methods of the built-in button extension implementation (`MediumEditor.extensions.button`) that can be reused and/or overriden to make custom button extensions easier to create.\n\n### `action` _(string)_\n\nBy default, the action argument to pass to MediumEditor.execAction() when the button is clicked.\n\nThe value of this will also be set as the value of the `data-action` attribute which will be set on the button.\n\n**Example:**\n\nThe following would create a custom button extension which would 'bold' text via the built-in 'bold' support in the browser:\n\n```js\nvar CustomButtonExtension = MediumEditor.extensions.button.extend({\n  name: 'custom-button-extension',\n\n  action: 'bold'\n\n  // ... other properties/methods ...\n})\n```\n\n***\n### `aria` _(string)_\n\nThe value to add as both the `aria-label` and `title` attributes of the button.\n\n**Example:**\n\nThe following would create a custom button extension which would have `'bold text'` as both the `aria-label` and the `title` attribute of the button in the toolbar:\n\n```js\nvar CustomButtonExtension = MediumEditor.extensions.button.extend({\n  name: 'custom-button-extension',\n\n  aria: 'bold text'\n\n  // ... other properties/methods ...\n})\n```\n\n***\n### `tagNames` _(Array)_\n\nArray 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.\n\n**NOTE:**\n\n`tagNames` is not used if `useQueryState` is set to `true`.\n\n**Example:**\n\nThe following would create a custom button extension which would be 'active' in the toolbar if the selection is within a `<b>` or a `<strong>` tag:\n\n```js\nvar CustomButtonExtension = MediumEditor.extensions.button.extend({\n  name: 'custom-button-extension',\n\n  useQueryState: false,\n\n  tagNames: ['b', 'strong'],\n\n  // ... other properties/methods ...\n})\n```\n\n***\n### `style` _(Object)_\n\nA 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.\n\n**Properties of this object:**\n\n* **prop** *[String]*: name of the css property\n* **value** *[String]*: value(s) of the css property (multiple values can be separated by a '|')\n\n**NOTE:**\n\n`style` is not used if `useQueryState` is set to `true`.\n\n**Example:**\n\nThe following would create a custom button extension which would be 'active' in the toolbar if the `font-weight` was either `700` of `'bold'`:\n\n```js\nvar CustomButtonExtension = MediumEditor.extensions.button.extend({\n  name: 'custom-button-extension',\n\n  useQueryState: false,\n\n  style: {\n    prop: 'font-weight',\n    value: '700|bold'\n  },\n\n  // ... other properties/methods ...\n})\n```\n\n***\n### `useQueryState` _(boolean)_\n\nEnables/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.\n\n**Example:**\n\nThe following would create a custom button extension which would be enabled if the browser decided the text was 'bold':\n\n```js\nvar CustomButtonExtension = MediumEditor.extensions.button.extend({\n  name: 'custom-button-extension',\n\n  action: 'bold',\n  useQueryState: true,\n\n  // ... other properties/methods ...\n})\n```\n\nFor 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.\n\n***\n### `contentDefault` _(string)_\n\nDefault innerHTML to put inside the button\n\n***\n### `contentFA` _(string)_\n\nThe innerHTML to use for the content of the button if the `buttonLabels` option for MediumEditor is set to `'fontawesome'`\n\n**Example:**\n\nThe following is pulled from the HighlightButton example, which defines a button for both the default case, and when `fontawesome` icons are being used.\n\n```js\nvar CustomButtonExtension = MediumEditor.extensions.button.extend({\n  name: 'custom-button-extension',\n\n  contentDefault: '<b>H</b>',\n  contentFA: '<i class=\"fa fa-paint-brush\"></i>',\n\n  // ... other properties/methods ...\n})\n```\n\n***\n### `classList` _(Array)_\n\nAn array of classNames (strings) to be added to the button.\n\n**Example:**\n\nThe following would create a custom button extension where the button element in the toolbar would have both a `custom-button` and a `custom-extension` class:\n\n```js\nvar CustomButtonExtension = MediumEditor.extensions.button.extend({\n  name: 'custom-button-extension',\n\n  classList: ['custom-button', 'custom-extension'],\n\n  // ... other properties/methods ...\n})\n```\n\n***\n### `attrs` _(Object)_\n\nA set of key-value pairs to add to the button as custom attributes.\n\n**Example:**\n\nThe following would create a custom button extension where the button element in the toolbar would have a `data-is-custom` attribute set to `true`:\n\n```js\nvar CustomButtonExtension = MediumEditor.extensions.button.extend({\n  name: 'custom-button-extension',\n\n  attrs: {\n    'data-is-custom': 'true'\n  },\n\n  // ... other properties/methods ...\n})\n```\n\n***\n### `handleClick(event)` _(function)_\n\nThe event listener called when the button is clicked.  The default built-in button will call `this.execAction(action)` when the button is clicked.\n\n**Example:**\n\nThe following would create a custom button extension where the button would prompt the user for an action to execute:\n\n```js\nvar CustomButtonExtension = MediumEditor.extensions.button.extend({\n  name: 'custom-button-extension',\n\n  handleClick: function (event) {\n    var action = prompt(\"Please enter an action\", \"bold\");\n    if (action) {\n      this.execAction(action);\n    }\n  },\n\n  // ... other properties/methods ...\n})\n```\n\n# Form Button\n\n## Form Button Interface\n\n## Form Button Helpers\n"
  },
  {
    "path": "src/js/extensions/WALKTHROUGH-BUTTON.md",
    "content": "# Walkthrough - Building a Button\n\n<!-- START doctoc generated TOC please keep comment here to allow auto update -->\n<!-- DON'T EDIT THIS SECTION, INSTEAD RE-RUN doctoc TO UPDATE -->\n\n- [HighlighterButton](#highlighterbutton)\n  - [1. Define the Extension](#1-define-the-extension)\n  - [2. Create and Display a Button](#2-create-and-display-a-button)\n  - [3. Improve Appearance](#3-improve-appearance)\n  - [4. Handle Button Click](#4-handle-button-click)\n  - [5. Respond to Selection](#5-respond-to-selection)\n  - [6. Leverage Existing Button Code](#6-leverage-existing-button-code)\n\n<!-- END doctoc generated TOC please keep comment here to allow auto update -->\n\n## HighlighterButton\n\nYou can find a demo of this example in the source code via [button-example.html](../../../demo/button-example.html).\n\nTo interact with the demo, load the page from your fork in a browser via:\n\n`file://[Medium Editor Source Root]/demo/button-example.html`\n\n### 1. Define the Extension\n\nAs a simple example, let's create a button extension that will highlight the selected text by wrapping the selection in a `<mark>` element.  Whenever the user selects text which is already within a `<mark>` tag, the button should appear as 'active'.  If the text is already wrapped within a `<mark>` element, clicking the 'active' button will 'un-highlight' the text by removing the wrapping `<mark>` tag.\n\nTo start, we need to define the new extension and create a button which can appear in the toolbar:\n\n```js\nvar HighlighterButton = MediumEditor.Extension.extend({\n  name: 'highlighter'\n});\n```\n\nWe now have an extension named `'highlighter'` which we can pass into MediumEditor like this.\n\n```js\nvar editor = new MediumEditor('.editable', {\n  toolbar: {\n    buttons: ['bold', 'italic', 'underline', 'highlighter']\n  },\n  extensions: {\n    'highlighter': new HighlighterButton()\n  }\n});\n```\n\n**NOTE:**\n\nIn order for the toolbar to look for a button to add to the toolbar, the name of the extension must be passed in the `toolbar.buttons` array.\n\n***\n### 2. Create and Display a Button\n\nNow that we have a base extension, we need to create an actual button element which can appear in the toolbar.\n\n**CODE:**\n```js\nvar HighlighterButton = MediumEditor.Extension.extend({\n  name: 'highlighter',\n\n  init: function () {\n    this.button = this.document.createElement('button');\n    this.button.classList.add('medium-editor-action');\n    this.button.innerHTML = '<b>H</b>';\n  },\n\n  getButton: function () {\n    return this.button;\n  }\n});\n```\n\n**PREVIEW:**\n<p align=\"center\"><img src=\"http://yabwe.github.io/medium-editor/img/button-example-01.png\" /></p>\n\nHere, we're implementing the the `init()` method to create our button, and then implementing the `getButton()` method as an accessor for our created button element.\n\nAfter all the extensions are created, the toolbar will loop through the list of the `buttons` passed in via the `toolbar.buttons` option.  For each button name in that list, it will retrieve the extension with that name and see if it has implemented a `getButton()` method.  If it has, it will take the element returned by this and append it to the toolbar.\n\nAs a result, whenever we run our code and highlight some text, we now have 4 buttons: Bold, Italic, Underline, and our custom Highlighter button.\n\n***\n### 3. Improve Appearance\n\nSince all of MediumEditor's built-in buttons have font-awesome icons, let's enable font-awesome icons and update our button to use a font-awesome icon as well as have a tooltip on hover.\n\n**CODE:**\n```js\nvar HighlighterButton = MediumEditor.Extension.extend({\n  name: 'highlighter',\n\n  init: function () {\n    this.button = this.document.createElement('button');\n    this.button.classList.add('medium-editor-action');\n    this.button.innerHTML = '<i class=\"fa fa-paint-brush\"></i>';\n    this.button.title = 'Highlight';\n  },\n\n  getButton: function () {\n    return this.button;\n  }\n});\n\n// Code for initializing MediumEditor\nvar editor = new MediumEditor('.editable', {\n  toolbar: {\n    buttons: ['bold', 'italic', 'underline', 'highlighter']\n  },\n  buttonLabels: 'fontawesome', // use font-awesome icons for other buttons\n  extensions: {\n    'highlighter': new HighlighterButton()\n  }\n});\n```\n\n**PREVIEW:**\n<p align=\"center\"><img src=\"http://yabwe.github.io/medium-editor/img/button-example-02.png\" /></p>\n\nTo change the apperances, we have:\n\n1. Changed the `innerHTML` of our button to be `<i class=\"fa fa-paint-brush\"></i>` in the `init()` method\n1. Added 'Highlight' as a title attribute to enable the tooltip\n1. Passed the `buttonLabels: 'fontawesome'` option when initializing MediumEditor to enable font-awesome icons for all buttons in the toolbar\n\n***\n### 4. Handle Button Click\n\nNow let's make the button actually do something.  To do this, we'll be using a great open source library called [rangy](https://github.com/timdown/rangy) created by [Tim Down](https://github.com/timdown).  We'll be using the [CSS Class Applier Module](https://github.com/timdown/rangy/wiki/Class-Applier-Module) which allows us to wrap the selection in a specific element type.\n\n**CODE:**\n```js\nrangy.init();\n\nvar HighlighterButton = MediumEditor.Extension.extend({\n  name: 'highlighter',\n\n  init: function () {\n    this.classApplier = rangy.createClassApplier('highlight', {\n        elementTagName: 'mark',\n        normalize: true\n    });\n\n    this.button = this.document.createElement('button');\n    this.button.classList.add('medium-editor-action');\n    this.button.innerHTML = '<i class=\"fa fa-paint-brush\"></i>';\n    this.button.title = 'Highlight';\n\n    this.on(this.button, 'click', this.handleClick.bind(this));\n  },\n\n  getButton: function () {\n    return this.button;\n  },\n\n  handleClick: function (event) {\n    this.classApplier.toggleSelection();\n\n    // Ensure the editor knows about an html change so watchers are notified\n    // ie: <textarea> elements depend on the editableInput event to stay synchronized\n    this.base.checkContentChanged();\n  }\n});\n```\n\n**BEFORE:**\n<p align=\"center\"><img src=\"http://yabwe.github.io/medium-editor/img/button-example-02.png\" /></p>\n\n**AFTER:**\n<p align=\"center\"><img src=\"http://yabwe.github.io/medium-editor/img/button-example-03.png\" /></p>\n\nIn order to initialize **rangy**, we've added a call to `rangy.init()` before the definition of our button extension, and we've also created an instance of the **CSS Class Applier** in the `init()` method.  We're creating a **CSS Class Applier** which will create a `<mark>` element with a `'highlight'` class on it.\n\nIn addition to initializing **rangy**, we've attached an event listener for the 'click' event of the button via the `this.on()` helper which we get by extending `MediumEditor.Extension`.  Our 'click' handler will then call the `toggleSelection()` method of the **CSS Class Applier**, which will then wrap the selection in a `<mark>` element.\n\nAfter highlighting the text and clicking the button, the text now appears highlighted (see above).  You can see the resulting HTML below:\n\n**HTML:**\n<p align=\"center\"><img src=\"http://yabwe.github.io/medium-editor/img/button-example-04.png\" /></p>\n\n**NOTE:**\n\nA great convienience of using the `toggleSelection()` method of the **CSS Class Applier** is that it will also unwrap the selection.  So, since we're always calling `toggleSelection()` when the button is clicked, if you highlight the same text and click the button again, the text will go back to normal and the `<mark>` element will be removed.\n\n**NOTE:**\n\nIn `handleClick` the extra call to `this.base.checkContentChanged` is calling the core editor directly to notify it that the html may have changed.  This is needed here so that any external dependencies that are watching for changes the HTML are notified.  For example, you can pass a `<textarea>` as a editor element when initializing MediumEditor and the editor relies on knowing about changes to the html so it can keep the `<textarea>` synchronized with changes to the generated `<div>` that is displayed as the actual editor element.\n\n***\n### 5. Respond to Selection\n\nThe last piece of functionality we want is to have the button's appearance respond to what the user has selected.  We want the button to appear as 'active' if the selection occurs inside of a `<mark>` element, and we want to the button to appear as `inactive` if the selection is outside of a `<mark>` element.\n\n**CODE:**\n```js\nrangy.init();\n\nvar HighlighterButton = MediumEditor.Extension.extend({\n  name: 'highlighter',\n\n  init: function () {\n    this.classApplier = rangy.createClassApplier('highlight', {\n        elementTagName: 'mark',\n        normalize: true\n    });\n\n    this.button = this.document.createElement('button');\n    this.button.classList.add('medium-editor-action');\n    this.button.innerHTML = '<i class=\"fa fa-paint-brush\"></i>';\n    this.button.title = 'Highlight';\n\n    this.on(this.button, 'click', this.handleClick.bind(this));\n  },\n\n  getButton: function () {\n    return this.button;\n  },\n\n  handleClick: function (event) {\n    this.classApplier.toggleSelection();\n    this.base.checkContentChanged();\n  },\n\n  isAlreadyApplied: function (node) {\n    return node.nodeName.toLowerCase() === 'mark';\n  },\n\n  isActive: function () {\n    return this.button.classList.contains('medium-editor-button-active');\n  },\n\n  setInactive: function () {\n    this.button.classList.remove('medium-editor-button-active');\n  },\n\n  setActive: function () {\n    this.button.classList.add('medium-editor-button-active');\n  }\n});\n```\n\n**SELECTION IS HIGHLIGHTED:**\n<p align=\"center\"><img src=\"http://yabwe.github.io/medium-editor/img/button-example-05.png\" /></p>\n\n**SELECTION IS NOT HIGHLIGHTED:**\n<p align=\"center\"><img src=\"http://yabwe.github.io/medium-editor/img/button-example-06.png\" /></p>\n\nAs shown above, now our button responds to what the user has selected.  To make this final piece work, we've implemented 4 extension methods:\n\n1. **isAlreadyApplied(node)**\n  * This will be called on each element which contains the user's selection, starting with the lowest element and climbing its ancestors.  If any of these elements are a `<mark>` element, we return `true` since that means the selection is higlighted.\n1. **isActive()**\n  * This should return whether the button is already active.  We check this by seeing if the `'medium-editor-button-active'` class already exists on the toolbar button.\n1. **setActive()**\n  * This is called when we should make our button active (add the '`medium-editor-button-active'` class)\n1. **setInactive()**\n  * This is called when we should make our button inactive (remove the `'medium-editor-button-active'` class)\n\n***\n### 6. Leverage Existing Button Code\n\nSince a lot of the built-in buttons for MediumEditor do very similar things, we can leverage a large portion of the existing button code to help reduce the amount of code we need for our extension.  To take advantage of this, we can extend from the `MediumEditor.extensions.button` extension and re-use much of the functionality.  You can find this code in [`button.js`](button.js).\n\nThe result is a HighlighterButton extension which requires significantly less custom code:\n\n```js\nrangy.init();\n\nvar HighlighterButton = MediumEditor.extensions.button.extend({\n  name: 'highlighter',\n\n  tagNames: ['mark'], // nodeName which indicates the button should be 'active' when isAlreadyApplied() is called\n  contentDefault: '<b>H</b>', // default innerHTML of the button\n  contentFA: '<i class=\"fa fa-paint-brush\"></i>', // innerHTML of button when 'fontawesome' is being used\n  aria: 'Highlight', // used as both aria-label and title attributes\n  action: 'highlight', // used as the data-action attribute of the button\n\n  init: function () {\n    MediumEditor.extensions.button.prototype.init.call(this);\n\n    this.classApplier = rangy.createClassApplier('highlight', {\n      elementTagName: 'mark',\n      normalize: true\n    });\n  },\n\n  handleClick: function (event) {\n    this.classApplier.toggleSelection();\n    this.base.checkContentChanged();\n  }\n});\n```\n\nThe built-in functionality we were able to take advantage of includes:\n\n1. **getButton()**\n  * The default button implementation will ensure the button is created using configurable custom options\n1. **Button element properties**\n  * `contentDefault`: default innerHTML of the button\n  * `contentFA`: innerHTML of the button when 'fontawesome' is being used\n  * `aria`: used as both the aria-label and title attributes of the button\n  * `action`: the value of the `data-action` attribute of the button\n1. **handleClick**\n  * The default button implementation will attach the `handleClick` method as an event listener to the 'click' event of the button.  We've overridden `handleClick()` to do our own custom logic using the **CSS Class Applier**.\n1. **isAlreadyApplied()** and **tagNames**\n  * The default implementation of `isAlreadyApplied()` will use the `tagNames` array of element names to decide whether the button is implemented or not.  If a node with one of these `tagNames` is found, the button will be activated.\n1. **isActive()**, **setActive()**, and **setInactive()**\n  * The default button extension implements each of these methods, using whatever css class is configured as the `activeButtonClass` in MediumEditor (`'medium-editor-button-active'` by default)\n"
  },
  {
    "path": "src/js/extensions/WALKTHROUGH-EXTENSION.md",
    "content": "# Walkthrough - Building an Extension\n\n<!-- START doctoc generated TOC please keep comment here to allow auto update -->\n<!-- DON'T EDIT THIS SECTION, INSTEAD RE-RUN doctoc TO UPDATE -->\n\n- [DisableContextMenuExtension](#disablecontextmenuextension)\n  - [1. Define the Extension](#1-define-the-extension)\n  - [2. Attaching To Context Menu Event](#2-attaching-to-context-menu-event)\n  - [3. Adding Functionality](#3-adding-functionality)\n  - [4. Leveraging Custom Event Listeners](#4-leveraging-custom-event-listeners)\n\n<!-- END doctoc generated TOC please keep comment here to allow auto update -->\n\n## DisableContextMenuExtension\n\nYou can find a demo of this example in the source code via [extension-example.html](../../../demo/extension-example.html).\n\nTo interact with the demo, load the page from your fork in a browser via: \n\n`file://[Medium Editor Source Root]/demo/extension-example.html`\n\n### 1. Define the Extension\n\nAs a simple example, let's create an extension that disables the context menu from appearing when the user right-clicks on the editor.\n\nDefining this extension is as simple as calling `MediumEditor.Extension.extend()` and passing in the methods/properties we want to override.\n\n```js\nvar DisableContextMenuExtension = MediumEditor.Extension.extend({\n  name: 'disable-context-menu'\n});\n```\n\nWe now have an extension named `'disable-context-menu'` which we can pass into MediumEditor like this:\n\n```js\nvar editor = new MediumEditor('.editable', {\n  extensions: {\n    'disable-context-menu': new DisableContextMenuExtension()\n  }\n});\n```\n\n***\n### 2. Attaching To Context Menu Event\n\nTo make the extension actually do something, we'll want to attach to the `contextmenu` event on all **elements** of the editor.  We can set this up by implementing the `init()` method, which is called on every Extension during setup of MediumEditor:\n\n```js\nvar DisableContextMenuExtension = MediumEditor.Extension.extend({\n  name: 'disable-context-menu',\n\n  init: function () {\n    this.getEditorElements().forEach(function (element) {\n      this.base.on(element, 'contextmenu', this.handleContextmenu.bind(this));\n    }, this);\n  },\n\n  handleContextmenu: function (event) { }\n});\n```\n\nHere, we're leveraging some of the helpers that are available to all Extensions.\n\n* We're using `this.getEditorElements()`, which is a helper function to give us an array containing all **elements** maintained by this editor.\n* We're using `this.base`, which is a reference to the MediumEditor instance.\n* We're using `this.base.on()`, which is a [method of MediumEditor](../../../API.md#ontarget-event-listener-usecapture) for attaching to DOM Events. Using this method ensures our event handlers will be detached when MediumEditor is destroyed.\n\n**NOTE:**\n\n* There are a few helper methods that allow us to make calls directly into the MediumEditor instance without having to reference `this.base`. One of them is a reference to the `on()` method, so instead of the above code we can just use `this.on(element, 'contextmenu', this.handleContextmenu.bind(this))` which is what we'll use in the rest of the example.\n\n***\n### 3. Adding Functionality\n\nSo, the last piece we need is to handle the `contextmenu` event and prevent the default action:\n\n```js\nvar DisableContextMenuExtension = MediumEditor.Extension.extend({\n  name: 'disable-context-menu',\n\n  init: function () {\n    this.getEditorElements().forEach(function (element) {\n      this.base.on(element, 'contextmenu', this.handleContextmenu.bind(this));\n    }, this);\n  },\n\n  handleContextmenu: function (event) {\n    event.preventDefault();\n  }\n});\n```\n\nNow we have a working extension which prevents the context menu from showing up for any of the **elements**.  Let's add some more functionality to allow for toggling this feature on and off.\n\n***\n### 4. Leveraging Custom Event Listeners\n\nLet's say we wanted to support toggling on/off the disable-context-menu extension, for a specific **element**, whenever the user presses ESCAPE.  To do this, we'll need to add 2 pieces of functionality:\n\n1. Listen to the `keydown` event on each **element**. For this, we can leverage the built-in [`editableKeyDown` custom event](../../../CUSTOM-EVENTS.md#editablekeydown).  This allows us to use the 2nd argument of custom event listeners (the active editor **element**) to toggle on/off a `data-allow-context-menu` attribute on the **element**.\n\n2. When the `contextmenu` event fires, we only want to prevent the context menu from appearing if the `data-allow-context-menu` attribute is not present.\n\n```js\nvar DisableContextMenuExtension = MediumEditor.Extension.extend({\n  name: 'disable-context-menu',\n\n  init: function () {\n    this.getEditorElements().forEach(function (element) {\n      this.on(element, 'contextmenu', this.handleContextmenu.bind(this));\n    }, this);\n    this.subscribe('editableKeydown', this.handleKeydown.bind(this));\n  },\n\n  handleContextmenu: function (event) {\n    if (!event.currentTarget.getAttribute('data-allow-context-menu')) {\n      event.preventDefault();\n    }\n  },\n\n  handleKeydown: function (event, editable) {\n    // If the user hits escape, toggle the data-allow-context-menu attribute\n    if (MediumEditor.util.isKey(event, MediumEditor.util.keyCode.ESCAPE)) {\n      if (editable.hasAttribute('data-allow-context-menu')) {\n        editable.removeAttribute('data-allow-context-menu');\n      } else {\n        editable.setAttribute('data-allow-context-menu', true);\n      }\n    }\n  }\n});\n```\n\n**NOTE:**\n\nFor events like `keydown`, we could always use `currentTarget` and not need to use the reference to the editable element (like how we use the `currentTarget` when handling the `contextmenu` event).  However, there may be times when we want to trigger one of these events manually, and this allows us to specify exactly which editable element we want to trigger the event for.  It's also a handy standardization for events which are more complicated, like the custom `focus` and `blur` events."
  },
  {
    "path": "src/js/extensions/anchor-preview.js",
    "content": "(function () {\n    'use strict';\n\n    var AnchorPreview = MediumEditor.Extension.extend({\n        name: 'anchor-preview',\n\n        // Anchor Preview Options\n\n        /* hideDelay: [number]  (previously options.anchorPreviewHideDelay)\n         * time in milliseconds to show the anchor tag preview after the mouse has left the anchor tag.\n         */\n        hideDelay: 500,\n\n        /* previewValueSelector: [string]\n         * the default selector to locate where to put the activeAnchor value in the preview\n         */\n        previewValueSelector: 'a',\n\n        /* showWhenToolbarIsVisible: [boolean]\n         * determines whether the anchor tag preview shows up when the toolbar is visible\n         */\n        showWhenToolbarIsVisible: false,\n\n        /* showOnEmptyLinks: [boolean]\n        * determines whether the anchor tag preview shows up on links with href=\"\" or href=\"#something\"\n        */\n        showOnEmptyLinks: true,\n\n        init: function () {\n            this.anchorPreview = this.createPreview();\n\n            this.getEditorOption('elementsContainer').appendChild(this.anchorPreview);\n\n            this.attachToEditables();\n        },\n\n        getInteractionElements: function () {\n            return this.getPreviewElement();\n        },\n\n        // TODO: Remove this function in 6.0.0\n        getPreviewElement: function () {\n            return this.anchorPreview;\n        },\n\n        createPreview: function () {\n            var el = this.document.createElement('div');\n\n            el.id = 'medium-editor-anchor-preview-' + this.getEditorId();\n            el.className = 'medium-editor-anchor-preview';\n            el.innerHTML = this.getTemplate();\n\n            this.on(el, 'click', this.handleClick.bind(this));\n\n            return el;\n        },\n\n        getTemplate: function () {\n            return '<div class=\"medium-editor-toolbar-anchor-preview\" id=\"medium-editor-toolbar-anchor-preview\">' +\n                '    <a class=\"medium-editor-toolbar-anchor-preview-inner\"></a>' +\n                '</div>';\n        },\n\n        destroy: function () {\n            if (this.anchorPreview) {\n                if (this.anchorPreview.parentNode) {\n                    this.anchorPreview.parentNode.removeChild(this.anchorPreview);\n                }\n                delete this.anchorPreview;\n            }\n        },\n\n        hidePreview: function () {\n            if (this.anchorPreview) {\n                this.anchorPreview.classList.remove('medium-editor-anchor-preview-active');\n            }\n            this.activeAnchor = null;\n        },\n\n        showPreview: function (anchorEl) {\n            if (this.anchorPreview.classList.contains('medium-editor-anchor-preview-active') ||\n                    anchorEl.getAttribute('data-disable-preview')) {\n                return true;\n            }\n\n            if (this.previewValueSelector) {\n                this.anchorPreview.querySelector(this.previewValueSelector).textContent = anchorEl.attributes.href.value;\n                this.anchorPreview.querySelector(this.previewValueSelector).href = anchorEl.attributes.href.value;\n            }\n\n            this.anchorPreview.classList.add('medium-toolbar-arrow-over');\n            this.anchorPreview.classList.remove('medium-toolbar-arrow-under');\n\n            if (!this.anchorPreview.classList.contains('medium-editor-anchor-preview-active')) {\n                this.anchorPreview.classList.add('medium-editor-anchor-preview-active');\n            }\n\n            this.activeAnchor = anchorEl;\n\n            this.positionPreview();\n            this.attachPreviewHandlers();\n\n            return this;\n        },\n\n        positionPreview: function (activeAnchor) {\n            activeAnchor = activeAnchor || this.activeAnchor;\n            var containerWidth = this.window.innerWidth,\n                buttonHeight = this.anchorPreview.offsetHeight,\n                boundary = activeAnchor.getBoundingClientRect(),\n                diffLeft = this.diffLeft,\n                diffTop = this.diffTop,\n                elementsContainer = this.getEditorOption('elementsContainer'),\n                elementsContainerAbsolute = ['absolute', 'fixed'].indexOf(window.getComputedStyle(elementsContainer).getPropertyValue('position')) > -1,\n                relativeBoundary = {},\n                halfOffsetWidth, defaultLeft, middleBoundary, elementsContainerBoundary, top;\n\n            halfOffsetWidth = this.anchorPreview.offsetWidth / 2;\n            var toolbarExtension = this.base.getExtensionByName('toolbar');\n            if (toolbarExtension) {\n                diffLeft = toolbarExtension.diffLeft;\n                diffTop = toolbarExtension.diffTop;\n            }\n            defaultLeft = diffLeft - halfOffsetWidth;\n\n            // If container element is absolute / fixed, recalculate boundaries to be relative to the container\n            if (elementsContainerAbsolute) {\n                elementsContainerBoundary = elementsContainer.getBoundingClientRect();\n                ['top', 'left'].forEach(function (key) {\n                    relativeBoundary[key] = boundary[key] - elementsContainerBoundary[key];\n                });\n\n                relativeBoundary.width = boundary.width;\n                relativeBoundary.height = boundary.height;\n                boundary = relativeBoundary;\n\n                containerWidth = elementsContainerBoundary.width;\n\n                // Adjust top position according to container scroll position\n                top = elementsContainer.scrollTop;\n            } else {\n                // Adjust top position according to window scroll position\n                top = this.window.pageYOffset;\n            }\n\n            middleBoundary = boundary.left + boundary.width / 2;\n            top += buttonHeight + boundary.top + boundary.height - diffTop - this.anchorPreview.offsetHeight;\n\n            this.anchorPreview.style.top = Math.round(top) + 'px';\n            this.anchorPreview.style.right = 'initial';\n            if (middleBoundary < halfOffsetWidth) {\n                this.anchorPreview.style.left = defaultLeft + halfOffsetWidth + 'px';\n                this.anchorPreview.style.right = 'initial';\n            } else if ((containerWidth - middleBoundary) < halfOffsetWidth) {\n                this.anchorPreview.style.left = 'auto';\n                this.anchorPreview.style.right = 0;\n            } else {\n                this.anchorPreview.style.left = defaultLeft + middleBoundary + 'px';\n                this.anchorPreview.style.right = 'initial';\n            }\n        },\n\n        attachToEditables: function () {\n            this.subscribe('editableMouseover', this.handleEditableMouseover.bind(this));\n            this.subscribe('positionedToolbar', this.handlePositionedToolbar.bind(this));\n        },\n\n        handlePositionedToolbar: function () {\n            // If the toolbar is visible and positioned, we don't need to hide the preview\n            // when showWhenToolbarIsVisible is true\n            if (!this.showWhenToolbarIsVisible) {\n                this.hidePreview();\n            }\n        },\n\n        handleClick: function (event) {\n            var anchorExtension = this.base.getExtensionByName('anchor'),\n                activeAnchor = this.activeAnchor;\n\n            if (anchorExtension && activeAnchor) {\n                event.preventDefault();\n\n                this.base.selectElement(this.activeAnchor);\n\n                // Using setTimeout + delay because:\n                // We may actually be displaying the anchor form, which should be controlled by delay\n                this.base.delay(function () {\n                    if (activeAnchor) {\n                        var opts = {\n                            value: activeAnchor.attributes.href.value,\n                            target: activeAnchor.getAttribute('target'),\n                            buttonClass: activeAnchor.getAttribute('class')\n                        };\n                        anchorExtension.showForm(opts);\n                        activeAnchor = null;\n                    }\n                }.bind(this));\n            }\n\n            this.hidePreview();\n        },\n\n        handleAnchorMouseout: function () {\n            this.anchorToPreview = null;\n            this.off(this.activeAnchor, 'mouseout', this.instanceHandleAnchorMouseout);\n            this.instanceHandleAnchorMouseout = null;\n        },\n\n        handleEditableMouseover: function (event) {\n            var target = MediumEditor.util.getClosestTag(event.target, 'a');\n\n            if (false === target) {\n                return;\n            }\n\n            // Detect empty href attributes\n            // The browser will make href=\"\" or href=\"#top\"\n            // into absolute urls when accessed as event.target.href, so check the html\n            if (!this.showOnEmptyLinks &&\n                (!/href=[\"']\\S+[\"']/.test(target.outerHTML) || /href=[\"']#\\S+[\"']/.test(target.outerHTML))) {\n                return true;\n            }\n\n            // only show when toolbar is not present\n            var toolbar = this.base.getExtensionByName('toolbar');\n            if (!this.showWhenToolbarIsVisible && toolbar && toolbar.isDisplayed && toolbar.isDisplayed()) {\n                return true;\n            }\n\n            // detach handler for other anchor in case we hovered multiple anchors quickly\n            if (this.activeAnchor && this.activeAnchor !== target) {\n                this.detachPreviewHandlers();\n            }\n\n            this.anchorToPreview = target;\n\n            this.instanceHandleAnchorMouseout = this.handleAnchorMouseout.bind(this);\n            this.on(this.anchorToPreview, 'mouseout', this.instanceHandleAnchorMouseout);\n            // Using setTimeout + delay because:\n            // - We're going to show the anchor preview according to the configured delay\n            //   if the mouse has not left the anchor tag in that time\n            this.base.delay(function () {\n                if (this.anchorToPreview) {\n                    this.showPreview(this.anchorToPreview);\n                }\n            }.bind(this));\n        },\n\n        handlePreviewMouseover: function () {\n            this.lastOver = (new Date()).getTime();\n            this.hovering = true;\n        },\n\n        handlePreviewMouseout: function (event) {\n            if (!event.relatedTarget || !/anchor-preview/.test(event.relatedTarget.className)) {\n                this.hovering = false;\n            }\n        },\n\n        updatePreview: function () {\n            if (this.hovering) {\n                return true;\n            }\n            var durr = (new Date()).getTime() - this.lastOver;\n            if (durr > this.hideDelay) {\n                // hide the preview 1/2 second after mouse leaves the link\n                this.detachPreviewHandlers();\n            }\n        },\n\n        detachPreviewHandlers: function () {\n            // cleanup\n            clearInterval(this.intervalTimer);\n            if (this.instanceHandlePreviewMouseover) {\n                this.off(this.anchorPreview, 'mouseover', this.instanceHandlePreviewMouseover);\n                this.off(this.anchorPreview, 'mouseout', this.instanceHandlePreviewMouseout);\n                if (this.activeAnchor) {\n                    this.off(this.activeAnchor, 'mouseover', this.instanceHandlePreviewMouseover);\n                    this.off(this.activeAnchor, 'mouseout', this.instanceHandlePreviewMouseout);\n                }\n            }\n\n            this.hidePreview();\n\n            this.hovering = this.instanceHandlePreviewMouseover = this.instanceHandlePreviewMouseout = null;\n        },\n\n        // TODO: break up method and extract out handlers\n        attachPreviewHandlers: function () {\n            this.lastOver = (new Date()).getTime();\n            this.hovering = true;\n\n            this.instanceHandlePreviewMouseover = this.handlePreviewMouseover.bind(this);\n            this.instanceHandlePreviewMouseout = this.handlePreviewMouseout.bind(this);\n\n            this.intervalTimer = setInterval(this.updatePreview.bind(this), 200);\n\n            this.on(this.anchorPreview, 'mouseover', this.instanceHandlePreviewMouseover);\n            this.on(this.anchorPreview, 'mouseout', this.instanceHandlePreviewMouseout);\n            this.on(this.activeAnchor, 'mouseover', this.instanceHandlePreviewMouseover);\n            this.on(this.activeAnchor, 'mouseout', this.instanceHandlePreviewMouseout);\n        }\n    });\n\n    MediumEditor.extensions.anchorPreview = AnchorPreview;\n}());\n"
  },
  {
    "path": "src/js/extensions/anchor.js",
    "content": "(function () {\n    'use strict';\n\n    var AnchorForm = MediumEditor.extensions.form.extend({\n        /* Anchor Form Options */\n\n        /* customClassOption: [string]  (previously options.anchorButton + options.anchorButtonClass)\n         * Custom class name the user can optionally have added to their created links (ie 'button').\n         * If passed as a non-empty string, a checkbox will be displayed allowing the user to choose\n         * whether to have the class added to the created link or not.\n         */\n        customClassOption: null,\n\n        /* customClassOptionText: [string]\n         * text to be shown in the checkbox when the __customClassOption__ is being used.\n         */\n        customClassOptionText: 'Button',\n\n        /* linkValidation: [boolean]  (previously options.checkLinkFormat)\n         * enables/disables check for common URL protocols on anchor links.\n         */\n        linkValidation: false,\n\n        /* placeholderText: [string]  (previously options.anchorInputPlaceholder)\n         * text to be shown as placeholder of the anchor input.\n         */\n        placeholderText: 'Paste or type a link',\n\n        /* targetCheckbox: [boolean]  (previously options.anchorTarget)\n         * enables/disables displaying a \"Open in new window\" checkbox, which when checked\n         * changes the `target` attribute of the created link.\n         */\n        targetCheckbox: false,\n\n        /* targetCheckboxText: [string]  (previously options.anchorInputCheckboxLabel)\n         * text to be shown in the checkbox enabled via the __targetCheckbox__ option.\n         */\n        targetCheckboxText: 'Open in new window',\n\n        // Options for the Button base class\n        name: 'anchor',\n        action: 'createLink',\n        aria: 'link',\n        tagNames: ['a'],\n        contentDefault: '<b>#</b>',\n        contentFA: '<i class=\"fa fa-link\"></i>',\n\n        init: function () {\n            MediumEditor.extensions.form.prototype.init.apply(this, arguments);\n\n            this.subscribe('editableKeydown', this.handleKeydown.bind(this));\n        },\n\n        // Called when the button the toolbar is clicked\n        // Overrides ButtonExtension.handleClick\n        handleClick: function (event) {\n            event.preventDefault();\n            event.stopPropagation();\n\n            var range = MediumEditor.selection.getSelectionRange(this.document);\n\n            if (range.startContainer.nodeName.toLowerCase() === 'a' ||\n                range.endContainer.nodeName.toLowerCase() === 'a' ||\n                MediumEditor.util.getClosestTag(MediumEditor.selection.getSelectedParentElement(range), 'a')) {\n                return this.execAction('unlink');\n            }\n\n            if (!this.isDisplayed()) {\n                this.showForm();\n            }\n\n            return false;\n        },\n\n        // Called when user hits the defined shortcut (CTRL / COMMAND + K)\n        handleKeydown: function (event) {\n            if (MediumEditor.util.isKey(event, MediumEditor.util.keyCode.K) && MediumEditor.util.isMetaCtrlKey(event) && !event.shiftKey) {\n                this.handleClick(event);\n            }\n        },\n\n        // Called by medium-editor to append form to the toolbar\n        getForm: function () {\n            if (!this.form) {\n                this.form = this.createForm();\n            }\n            return this.form;\n        },\n\n        getTemplate: function () {\n            var template = [\n                '<input type=\"text\" class=\"medium-editor-toolbar-input\" placeholder=\"', this.placeholderText, '\">'\n            ];\n\n            template.push(\n                '<a href=\"#\" class=\"medium-editor-toolbar-save\">',\n                this.getEditorOption('buttonLabels') === 'fontawesome' ? '<i class=\"fa fa-check\"></i>' : this.formSaveLabel,\n                '</a>'\n            );\n\n            template.push('<a href=\"#\" class=\"medium-editor-toolbar-close\">',\n                this.getEditorOption('buttonLabels') === 'fontawesome' ? '<i class=\"fa fa-times\"></i>' : this.formCloseLabel,\n                '</a>');\n\n            // both of these options are slightly moot with the ability to\n            // override the various form buildup/serialize functions.\n\n            if (this.targetCheckbox) {\n                // fixme: ideally, this targetCheckboxText would be a formLabel too,\n                // figure out how to deprecate? also consider `fa-` icon default implcations.\n                template.push(\n                    '<div class=\"medium-editor-toolbar-form-row\">',\n                    '<input type=\"checkbox\" class=\"medium-editor-toolbar-anchor-target\" id=\"medium-editor-toolbar-anchor-target-field-' + this.getEditorId() + '\">',\n                    '<label for=\"medium-editor-toolbar-anchor-target-field-' + this.getEditorId() + '\">',\n                    this.targetCheckboxText,\n                    '</label>',\n                    '</div>'\n                );\n            }\n\n            if (this.customClassOption) {\n                // fixme: expose this `Button` text as a formLabel property, too\n                // and provide similar access to a `fa-` icon default.\n                template.push(\n                    '<div class=\"medium-editor-toolbar-form-row\">',\n                    '<input type=\"checkbox\" class=\"medium-editor-toolbar-anchor-button\" id=\"medium-editor-toolbar-anchor-button-field-' + this.getEditorId() + '\">',\n                    '<label for=\"medium-editor-toolbar-anchor-button-field-' + this.getEditorId() + '\">',\n                    this.customClassOptionText,\n                    '</label>',\n                    '</div>'\n                );\n            }\n\n            return template.join('');\n\n        },\n\n        // Used by medium-editor when the default toolbar is to be displayed\n        isDisplayed: function () {\n            return MediumEditor.extensions.form.prototype.isDisplayed.apply(this);\n        },\n\n        hideForm: function () {\n            MediumEditor.extensions.form.prototype.hideForm.apply(this);\n            this.getInput().value = '';\n        },\n\n        showForm: function (opts) {\n            var input = this.getInput(),\n                targetCheckbox = this.getAnchorTargetCheckbox(),\n                buttonCheckbox = this.getAnchorButtonCheckbox();\n\n            opts = opts || { value: '' };\n            // TODO: This is for backwards compatability\n            // We don't need to support the 'string' argument in 6.0.0\n            if (typeof opts === 'string') {\n                opts = {\n                    value: opts\n                };\n            }\n\n            this.base.saveSelection();\n            this.hideToolbarDefaultActions();\n            MediumEditor.extensions.form.prototype.showForm.apply(this);\n            this.setToolbarPosition();\n\n            input.value = opts.value;\n            input.focus();\n\n            // If we have a target checkbox, we want it to be checked/unchecked\n            // based on whether the existing link has target=_blank\n            if (targetCheckbox) {\n                targetCheckbox.checked = opts.target === '_blank';\n            }\n\n            // If we have a custom class checkbox, we want it to be checked/unchecked\n            // based on whether an existing link already has the class\n            if (buttonCheckbox) {\n                var classList = opts.buttonClass ? opts.buttonClass.split(' ') : [];\n                buttonCheckbox.checked = (classList.indexOf(this.customClassOption) !== -1);\n            }\n        },\n\n        // Called by core when tearing down medium-editor (destroy)\n        destroy: function () {\n            if (!this.form) {\n                return false;\n            }\n\n            if (this.form.parentNode) {\n                this.form.parentNode.removeChild(this.form);\n            }\n\n            delete this.form;\n        },\n\n        // core methods\n\n        getFormOpts: function () {\n            // no notion of private functions? wanted `_getFormOpts`\n            var targetCheckbox = this.getAnchorTargetCheckbox(),\n                buttonCheckbox = this.getAnchorButtonCheckbox(),\n                opts = {\n                    value: this.getInput().value.trim()\n                };\n\n            if (this.linkValidation) {\n                opts.value = this.checkLinkFormat(opts.value);\n            }\n\n            opts.target = '_self';\n            if (targetCheckbox && targetCheckbox.checked) {\n                opts.target = '_blank';\n            }\n\n            if (buttonCheckbox && buttonCheckbox.checked) {\n                opts.buttonClass = this.customClassOption;\n            }\n\n            return opts;\n        },\n\n        doFormSave: function () {\n            var opts = this.getFormOpts();\n            this.completeFormSave(opts);\n        },\n\n        completeFormSave: function (opts) {\n            this.base.restoreSelection();\n            this.execAction(this.action, opts);\n            this.base.checkSelection();\n        },\n\n        ensureEncodedUri: function (str) {\n            return str === decodeURI(str) ? encodeURI(str) : str;\n        },\n\n        ensureEncodedUriComponent: function (str) {\n            return str === decodeURIComponent(str) ? encodeURIComponent(str) : str;\n        },\n\n        ensureEncodedParam: function (param) {\n            var split = param.split('='),\n                key = split[0],\n                val = split[1];\n\n            return key + (val === undefined ? '' : '=' + this.ensureEncodedUriComponent(val));\n        },\n\n        ensureEncodedQuery: function (queryString) {\n            return queryString.split('&').map(this.ensureEncodedParam.bind(this)).join('&');\n        },\n\n        checkLinkFormat: function (value) {\n            // Matches any alphabetical characters followed by ://\n            // Matches protocol relative \"//\"\n            // Matches common external protocols \"mailto:\" \"tel:\" \"maps:\"\n            // Matches relative hash link, begins with \"#\"\n            var urlSchemeRegex = /^([a-z]+:)?\\/\\/|^(mailto|tel|maps):|^\\#/i,\n                hasScheme = urlSchemeRegex.test(value),\n                scheme = '',\n                // telRegex is a regex for checking if the string is a telephone number\n                telRegex = /^\\+?\\s?\\(?(?:\\d\\s?\\-?\\)?){3,20}$/,\n                urlParts = value.match(/^(.*?)(?:\\?(.*?))?(?:#(.*))?$/),\n                path = urlParts[1],\n                query = urlParts[2],\n                fragment = urlParts[3];\n\n            if (telRegex.test(value)) {\n                return 'tel:' + value;\n            }\n\n            if (!hasScheme) {\n                var host = path.split('/')[0];\n                // if the host part of the path looks like a hostname\n                if (host.match(/.+(\\.|:).+/) || host === 'localhost') {\n                    scheme = 'http://';\n                }\n            }\n\n            return scheme +\n                // Ensure path is encoded\n                this.ensureEncodedUri(path) +\n                // Ensure query is encoded\n                (query === undefined ? '' : '?' + this.ensureEncodedQuery(query)) +\n                // Include fragment unencoded as encodeUriComponent is too\n                // heavy handed for the many characters allowed in a fragment\n                (fragment === undefined ? '' : '#' + fragment);\n        },\n\n        doFormCancel: function () {\n            this.base.restoreSelection();\n            this.base.checkSelection();\n        },\n\n        // form creation and event handling\n        attachFormEvents: function (form) {\n            var close = form.querySelector('.medium-editor-toolbar-close'),\n                save = form.querySelector('.medium-editor-toolbar-save'),\n                input = form.querySelector('.medium-editor-toolbar-input');\n\n            // Handle clicks on the form itself\n            this.on(form, 'click', this.handleFormClick.bind(this));\n\n            // Handle typing in the textbox\n            this.on(input, 'keyup', this.handleTextboxKeyup.bind(this));\n\n            // Handle close button clicks\n            this.on(close, 'click', this.handleCloseClick.bind(this));\n\n            // Handle save button clicks (capture)\n            this.on(save, 'click', this.handleSaveClick.bind(this), true);\n\n        },\n\n        createForm: function () {\n            var doc = this.document,\n                form = doc.createElement('div');\n\n            // Anchor Form (div)\n            form.className = 'medium-editor-toolbar-form';\n            form.id = 'medium-editor-toolbar-form-anchor-' + this.getEditorId();\n            form.innerHTML = this.getTemplate();\n            this.attachFormEvents(form);\n\n            return form;\n        },\n\n        getInput: function () {\n            return this.getForm().querySelector('input.medium-editor-toolbar-input');\n        },\n\n        getAnchorTargetCheckbox: function () {\n            return this.getForm().querySelector('.medium-editor-toolbar-anchor-target');\n        },\n\n        getAnchorButtonCheckbox: function () {\n            return this.getForm().querySelector('.medium-editor-toolbar-anchor-button');\n        },\n\n        handleTextboxKeyup: function (event) {\n            // For ENTER -> create the anchor\n            if (event.keyCode === MediumEditor.util.keyCode.ENTER) {\n                event.preventDefault();\n                this.doFormSave();\n                return;\n            }\n\n            // For ESCAPE -> close the form\n            if (event.keyCode === MediumEditor.util.keyCode.ESCAPE) {\n                event.preventDefault();\n                this.doFormCancel();\n            }\n        },\n\n        handleFormClick: function (event) {\n            // make sure not to hide form when clicking inside the form\n            event.stopPropagation();\n        },\n\n        handleSaveClick: function (event) {\n            // Clicking Save -> create the anchor\n            event.preventDefault();\n            this.doFormSave();\n        },\n\n        handleCloseClick: function (event) {\n            // Click Close -> close the form\n            event.preventDefault();\n            this.doFormCancel();\n        }\n    });\n\n    MediumEditor.extensions.anchor = AnchorForm;\n}());\n"
  },
  {
    "path": "src/js/extensions/auto-link.js",
    "content": "(function () {\n    'use strict';\n\n    var WHITESPACE_CHARS,\n        KNOWN_TLDS_FRAGMENT,\n        LINK_REGEXP_TEXT,\n        KNOWN_TLDS_REGEXP,\n        LINK_REGEXP;\n\n    WHITESPACE_CHARS = [' ', '\\t', '\\n', '\\r', '\\u00A0', '\\u2000', '\\u2001', '\\u2002', '\\u2003',\n                                    '\\u2028', '\\u2029'];\n    KNOWN_TLDS_FRAGMENT = 'com|net|org|edu|gov|mil|aero|asia|biz|cat|coop|info|int|jobs|mobi|museum|name|post|pro|tel|travel|' +\n        '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|' +\n        '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|' +\n        '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|' +\n        '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|' +\n        '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|' +\n        '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|' +\n        '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';\n\n    LINK_REGEXP_TEXT =\n        '(' +\n        // Version of Gruber URL Regexp optimized for JS: http://stackoverflow.com/a/17733640\n        '((?:(https?://|ftps?://|nntp://)|www\\\\d{0,3}[.]|[a-z0-9.\\\\-]+[.](' + KNOWN_TLDS_FRAGMENT + ')\\\\\\/)\\\\S+(?:[^\\\\s`!\\\\[\\\\]{};:\\'\\\".,?\\u00AB\\u00BB\\u201C\\u201D\\u2018\\u2019]))' +\n        // Addition to above Regexp to support bare domains/one level subdomains with common non-i18n TLDs and without www prefix:\n        ')|(([a-z0-9\\\\-]+\\\\.)?[a-z0-9\\\\-]+\\\\.(' + KNOWN_TLDS_FRAGMENT + '))';\n\n    KNOWN_TLDS_REGEXP = new RegExp('^(' + KNOWN_TLDS_FRAGMENT + ')$', 'i');\n\n    LINK_REGEXP = new RegExp(LINK_REGEXP_TEXT, 'gi');\n\n    function nodeIsNotInsideAnchorTag(node) {\n        return !MediumEditor.util.getClosestTag(node, 'a');\n    }\n\n    var AutoLink = MediumEditor.Extension.extend({\n        init: function () {\n            MediumEditor.Extension.prototype.init.apply(this, arguments);\n\n            this.disableEventHandling = false;\n            this.subscribe('editableKeypress', this.onKeypress.bind(this));\n            this.subscribe('editableBlur', this.onBlur.bind(this));\n            // MS IE has it's own auto-URL detect feature but ours is better in some ways. Be consistent.\n            this.document.execCommand('AutoUrlDetect', false, false);\n        },\n\n        isLastInstance: function () {\n            var activeInstances = 0;\n            for (var i = 0; i < this.window._mediumEditors.length; i++) {\n                var editor = this.window._mediumEditors[i];\n                if (editor !== null && editor.getExtensionByName('autoLink') !== undefined) {\n                    activeInstances++;\n                }\n            }\n            return activeInstances === 1;\n        },\n\n        destroy: function () {\n            // Turn AutoUrlDetect back on\n            if (this.document.queryCommandSupported('AutoUrlDetect') && this.isLastInstance()) {\n                this.document.execCommand('AutoUrlDetect', false, true);\n            }\n        },\n\n        onBlur: function (blurEvent, editable) {\n            this.performLinking(editable);\n        },\n\n        onKeypress: function (keyPressEvent) {\n            if (this.disableEventHandling) {\n                return;\n            }\n\n            if (MediumEditor.util.isKey(keyPressEvent, [MediumEditor.util.keyCode.SPACE, MediumEditor.util.keyCode.ENTER])) {\n                clearTimeout(this.performLinkingTimeout);\n                // Saving/restoring the selection in the middle of a keypress doesn't work well...\n                this.performLinkingTimeout = setTimeout(function () {\n                    try {\n                        var sel = this.base.exportSelection();\n                        if (this.performLinking(keyPressEvent.target)) {\n                            // pass true for favorLaterSelectionAnchor - this is needed for links at the end of a\n                            // paragraph in MS IE, or MS IE causes the link to be deleted right after adding it.\n                            this.base.importSelection(sel, true);\n                        }\n                    } catch (e) {\n                        if (window.console) {\n                            window.console.error('Failed to perform linking', e);\n                        }\n                        this.disableEventHandling = true;\n                    }\n                }.bind(this), 0);\n            }\n        },\n\n        performLinking: function (contenteditable) {\n            /*\n            Perform linking on blockElement basis, blockElements are HTML elements with text content and without\n            child element.\n\n            Example:\n            - HTML content\n            <blockquote>\n              <p>link.</p>\n              <p>my</p>\n            </blockquote>\n\n            - blockElements\n            [<p>link.</p>, <p>my</p>]\n\n            otherwise the detection can wrongly find the end of one paragraph and the beginning of another paragraph\n            to constitute a link, such as a paragraph ending \"link.\" and the next paragraph beginning with \"my\" is\n            interpreted into \"link.my\" and the code tries to create a link across blockElements - which doesn't work\n            and is terrible.\n            (Medium deletes the spaces/returns between P tags so the textContent ends up without paragraph spacing)\n            */\n            var blockElements = MediumEditor.util.splitByBlockElements(contenteditable),\n                documentModified = false;\n            if (blockElements.length === 0) {\n                blockElements = [contenteditable];\n            }\n            for (var i = 0; i < blockElements.length; i++) {\n                documentModified = this.removeObsoleteAutoLinkSpans(blockElements[i]) || documentModified;\n                documentModified = this.performLinkingWithinElement(blockElements[i]) || documentModified;\n            }\n            this.base.events.updateInput(contenteditable, { target: contenteditable, currentTarget: contenteditable });\n            return documentModified;\n        },\n\n        removeObsoleteAutoLinkSpans: function (element) {\n            if (!element || element.nodeType === 3) {\n                return false;\n            }\n\n            var spans = element.querySelectorAll('span[data-auto-link=\"true\"]'),\n                documentModified = false;\n\n            for (var i = 0; i < spans.length; i++) {\n                var textContent = spans[i].textContent;\n                if (textContent.indexOf('://') === -1) {\n                    textContent = MediumEditor.util.ensureUrlHasProtocol(textContent);\n                }\n                if (spans[i].getAttribute('data-href') !== textContent && nodeIsNotInsideAnchorTag(spans[i])) {\n                    documentModified = true;\n                    var trimmedTextContent = textContent.replace(/\\s+$/, '');\n                    if (spans[i].getAttribute('data-href') === trimmedTextContent) {\n                        var charactersTrimmed = textContent.length - trimmedTextContent.length,\n                            subtree = MediumEditor.util.splitOffDOMTree(spans[i], this.splitTextBeforeEnd(spans[i], charactersTrimmed));\n                        spans[i].parentNode.insertBefore(subtree, spans[i].nextSibling);\n                    } else {\n                        // Some editing has happened to the span, so just remove it entirely. The user can put it back\n                        // around just the href content if they need to prevent it from linking\n                        MediumEditor.util.unwrap(spans[i], this.document);\n                    }\n                }\n            }\n            return documentModified;\n        },\n\n        splitTextBeforeEnd: function (element, characterCount) {\n            var treeWalker = this.document.createTreeWalker(element, NodeFilter.SHOW_TEXT, null, false),\n                lastChildNotExhausted = true;\n\n            // Start the tree walker at the last descendant of the span\n            while (lastChildNotExhausted) {\n                lastChildNotExhausted = treeWalker.lastChild() !== null;\n            }\n\n            var currentNode,\n                currentNodeValue,\n                previousNode;\n            while (characterCount > 0 && previousNode !== null) {\n                currentNode = treeWalker.currentNode;\n                currentNodeValue = currentNode.nodeValue;\n                if (currentNodeValue.length > characterCount) {\n                    previousNode = currentNode.splitText(currentNodeValue.length - characterCount);\n                    characterCount = 0;\n                } else {\n                    previousNode = treeWalker.previousNode();\n                    characterCount -= currentNodeValue.length;\n                }\n            }\n            return previousNode;\n        },\n\n        performLinkingWithinElement: function (element) {\n            var matches = this.findLinkableText(element),\n                linkCreated = false;\n\n            for (var matchIndex = 0; matchIndex < matches.length; matchIndex++) {\n                var matchingTextNodes = MediumEditor.util.findOrCreateMatchingTextNodes(this.document, element,\n                        matches[matchIndex]);\n                if (this.shouldNotLink(matchingTextNodes)) {\n                    continue;\n                }\n                this.createAutoLink(matchingTextNodes, matches[matchIndex].href);\n            }\n            return linkCreated;\n        },\n\n        shouldNotLink: function (textNodes) {\n            var shouldNotLink = false;\n            for (var i = 0; i < textNodes.length && shouldNotLink === false; i++) {\n                // Do not link if the text node is either inside an anchor or inside span[data-auto-link]\n                shouldNotLink = !!MediumEditor.util.traverseUp(textNodes[i], function (node) {\n                    return node.nodeName.toLowerCase() === 'a' ||\n                        (node.getAttribute && node.getAttribute('data-auto-link') === 'true');\n                });\n            }\n            return shouldNotLink;\n        },\n\n        findLinkableText: function (contenteditable) {\n            var textContent = contenteditable.textContent,\n                match = null,\n                matches = [];\n\n            while ((match = LINK_REGEXP.exec(textContent)) !== null) {\n                var matchOk = true,\n                    matchEnd = match.index + match[0].length;\n                // If the regexp detected something as a link that has text immediately preceding/following it, bail out.\n                matchOk = (match.index === 0 || WHITESPACE_CHARS.indexOf(textContent[match.index - 1]) !== -1) &&\n                    (matchEnd === textContent.length || WHITESPACE_CHARS.indexOf(textContent[matchEnd]) !== -1);\n                // If the regexp detected a bare domain that doesn't use one of our expected TLDs, bail out.\n                matchOk = matchOk && (match[0].indexOf('/') !== -1 ||\n                    KNOWN_TLDS_REGEXP.test(match[0].split('.').pop().split('?').shift()));\n\n                if (matchOk) {\n                    matches.push({\n                        href: match[0],\n                        start: match.index,\n                        end: matchEnd\n                    });\n                }\n            }\n            return matches;\n        },\n\n        createAutoLink: function (textNodes, href) {\n            href = MediumEditor.util.ensureUrlHasProtocol(href);\n            var anchor = MediumEditor.util.createLink(this.document, textNodes, href, this.getEditorOption('targetBlank') ? '_blank' : null),\n                span = this.document.createElement('span');\n            span.setAttribute('data-auto-link', 'true');\n            span.setAttribute('data-href', href);\n            anchor.insertBefore(span, anchor.firstChild);\n            while (anchor.childNodes.length > 1) {\n                span.appendChild(anchor.childNodes[1]);\n            }\n        }\n\n    });\n\n    MediumEditor.extensions.autoLink = AutoLink;\n}());\n"
  },
  {
    "path": "src/js/extensions/button.js",
    "content": "(function () {\n    'use strict';\n\n    var Button = MediumEditor.Extension.extend({\n\n        /* Button Options */\n\n        /* action: [string]\n         * The action argument to pass to MediumEditor.execAction()\n         * when the button is clicked\n         */\n        action: undefined,\n\n        /* aria: [string]\n         * The value to add as the aria-label attribute of the button\n         * element displayed in the toolbar.\n         * This is also used as the tooltip for the button\n         */\n        aria: undefined,\n\n        /* tagNames: [Array]\n         * NOTE: This is not used if useQueryState is set to true.\n         *\n         * Array of element tag names that would indicate that this\n         * button has already been applied. If this action has already\n         * been applied, the button will be displayed as 'active' in the toolbar\n         *\n         * Example:\n         * For 'bold', if the text is ever within a <b> or <strong>\n         * tag that indicates the text is already bold. So the array\n         * of tagNames for bold would be: ['b', 'strong']\n         */\n        tagNames: undefined,\n\n        /* style: [Object]\n         * NOTE: This is not used if useQueryState is set to true.\n         *\n         * A pair of css property & value(s) that indicate that this\n         * button has already been applied. If this action has already\n         * been applied, the button will be displayed as 'active' in the toolbar\n         * Properties of the object:\n         *   prop [String]: name of the css property\n         *   value [String]: value(s) of the css property\n         *                   multiple values can be separated by a '|'\n         *\n         * Example:\n         * For 'bold', if the text is ever within an element with a 'font-weight'\n         * style property set to '700' or 'bold', that indicates the text\n         * is already bold.  So the style object for bold would be:\n         * { prop: 'font-weight', value: '700|bold' }\n         */\n        style: undefined,\n\n        /* useQueryState: [boolean]\n         * Enables/disables whether this button should use the built-in\n         * document.queryCommandState() method to determine whether\n         * the action has already been applied.  If the action has already\n         * been applied, the button will be displayed as 'active' in the toolbar\n         *\n         * Example:\n         * For 'bold', if this is set to true, the code will call:\n         * document.queryCommandState('bold') which will return true if the\n         * browser thinks the text is already bold, and false otherwise\n         */\n        useQueryState: undefined,\n\n        /* contentDefault: [string]\n         * Default innerHTML to put inside the button\n         */\n        contentDefault: undefined,\n\n        /* contentFA: [string]\n         * The innerHTML to use for the content of the button\n         * if the `buttonLabels` option for MediumEditor is set to 'fontawesome'\n         */\n        contentFA: undefined,\n\n        /* classList: [Array]\n         * An array of classNames (strings) to be added to the button\n         */\n        classList: undefined,\n\n        /* attrs: [object]\n         * A set of key-value pairs to add to the button as custom attributes\n         */\n        attrs: undefined,\n\n        // The button constructor can optionally accept the name of a built-in button\n        // (ie 'bold', 'italic', etc.)\n        // When the name of a button is passed, it will initialize itself with the\n        // configuration for that button\n        constructor: function (options) {\n            if (Button.isBuiltInButton(options)) {\n                MediumEditor.Extension.call(this, this.defaults[options]);\n            } else {\n                MediumEditor.Extension.call(this, options);\n            }\n        },\n\n        init: function () {\n            MediumEditor.Extension.prototype.init.apply(this, arguments);\n\n            this.button = this.createButton();\n            this.on(this.button, 'click', this.handleClick.bind(this));\n        },\n\n        /* getButton: [function ()]\n         *\n         * If implemented, this function will be called when\n         * the toolbar is being created.  The DOM Element returned\n         * by this function will be appended to the toolbar along\n         * with any other buttons.\n         */\n        getButton: function () {\n            return this.button;\n        },\n\n        getAction: function () {\n            return (typeof this.action === 'function') ? this.action(this.base.options) : this.action;\n        },\n\n        getAria: function () {\n            return (typeof this.aria === 'function') ? this.aria(this.base.options) : this.aria;\n        },\n\n        getTagNames: function () {\n            return (typeof this.tagNames === 'function') ? this.tagNames(this.base.options) : this.tagNames;\n        },\n\n        createButton: function () {\n            var button = this.document.createElement('button'),\n                content = this.contentDefault,\n                ariaLabel = this.getAria(),\n                buttonLabels = this.getEditorOption('buttonLabels');\n            // Add class names\n            button.classList.add('medium-editor-action');\n            button.classList.add('medium-editor-action-' + this.name);\n            if (this.classList) {\n                this.classList.forEach(function (className) {\n                    button.classList.add(className);\n                });\n            }\n\n            // Add attributes\n            button.setAttribute('data-action', this.getAction());\n            if (ariaLabel) {\n                button.setAttribute('title', ariaLabel);\n                button.setAttribute('aria-label', ariaLabel);\n            }\n            if (this.attrs) {\n                Object.keys(this.attrs).forEach(function (attr) {\n                    button.setAttribute(attr, this.attrs[attr]);\n                }, this);\n            }\n\n            if (buttonLabels === 'fontawesome' && this.contentFA) {\n                content = this.contentFA;\n            }\n            button.innerHTML = content;\n            return button;\n        },\n\n        handleClick: function (event) {\n            event.preventDefault();\n            event.stopPropagation();\n\n            var action = this.getAction();\n\n            if (action) {\n                this.execAction(action);\n            }\n        },\n\n        isActive: function () {\n            return this.button.classList.contains(this.getEditorOption('activeButtonClass'));\n        },\n\n        setInactive: function () {\n            this.button.classList.remove(this.getEditorOption('activeButtonClass'));\n            delete this.knownState;\n        },\n\n        setActive: function () {\n            this.button.classList.add(this.getEditorOption('activeButtonClass'));\n            delete this.knownState;\n        },\n\n        queryCommandState: function () {\n            var queryState = null;\n            if (this.useQueryState) {\n                queryState = this.base.queryCommandState(this.getAction());\n            }\n            return queryState;\n        },\n\n        isAlreadyApplied: function (node) {\n            var isMatch = false,\n                tagNames = this.getTagNames(),\n                styleVals,\n                computedStyle;\n\n            if (this.knownState === false || this.knownState === true) {\n                return this.knownState;\n            }\n\n            if (tagNames && tagNames.length > 0) {\n                isMatch = tagNames.indexOf(node.nodeName.toLowerCase()) !== -1;\n            }\n\n            if (!isMatch && this.style) {\n                styleVals = this.style.value.split('|');\n                computedStyle = this.window.getComputedStyle(node, null).getPropertyValue(this.style.prop);\n                styleVals.forEach(function (val) {\n                    if (!this.knownState) {\n                        isMatch = (computedStyle.indexOf(val) !== -1);\n                        // text-decoration is not inherited by default\n                        // so if the computed style for text-decoration doesn't match\n                        // don't write to knownState so we can fallback to other checks\n                        if (isMatch || this.style.prop !== 'text-decoration') {\n                            this.knownState = isMatch;\n                        }\n                    }\n                }, this);\n            }\n\n            return isMatch;\n        }\n    });\n\n    Button.isBuiltInButton = function (name) {\n        return (typeof name === 'string') && MediumEditor.extensions.button.prototype.defaults.hasOwnProperty(name);\n    };\n\n    MediumEditor.extensions.button = Button;\n}());\n"
  },
  {
    "path": "src/js/extensions/deprecated/image-dragging.js",
    "content": "(function () {\n    'use strict';\n\n    var ImageDragging = MediumEditor.Extension.extend({\n        init: function () {\n            MediumEditor.Extension.prototype.init.apply(this, arguments);\n\n            this.subscribe('editableDrag', this.handleDrag.bind(this));\n            this.subscribe('editableDrop', this.handleDrop.bind(this));\n        },\n\n        handleDrag: function (event) {\n            var className = 'medium-editor-dragover';\n            event.preventDefault();\n            event.dataTransfer.dropEffect = 'copy';\n\n            if (event.type === 'dragover') {\n                event.target.classList.add(className);\n            } else if (event.type === 'dragleave') {\n                event.target.classList.remove(className);\n            }\n        },\n\n        handleDrop: function (event) {\n            var className = 'medium-editor-dragover',\n                files;\n            event.preventDefault();\n            event.stopPropagation();\n\n            // IE9 does not support the File API, so prevent file from opening in a new window\n            // but also don't try to actually get the file\n            if (event.dataTransfer.files) {\n                files = Array.prototype.slice.call(event.dataTransfer.files, 0);\n                files.some(function (file) {\n                    if (file.type.match('image')) {\n                        var fileReader, id;\n                        fileReader = new FileReader();\n                        fileReader.readAsDataURL(file);\n\n                        id = 'medium-img-' + (+new Date());\n                        MediumEditor.util.insertHTMLCommand(this.document, '<img class=\"medium-editor-image-loading\" id=\"' + id + '\" />');\n\n                        fileReader.onload = function () {\n                            var img = this.document.getElementById(id);\n                            if (img) {\n                                img.removeAttribute('id');\n                                img.removeAttribute('class');\n                                img.src = fileReader.result;\n                            }\n                        }.bind(this);\n                    }\n                }.bind(this));\n            }\n            event.target.classList.remove(className);\n        }\n    });\n\n    MediumEditor.extensions.imageDragging = ImageDragging;\n}());\n"
  },
  {
    "path": "src/js/extensions/file-dragging.js",
    "content": "(function () {\n    'use strict';\n\n    var CLASS_DRAG_OVER = 'medium-editor-dragover';\n\n    function clearClassNames(element) {\n        var editable = MediumEditor.util.getContainerEditorElement(element),\n            existing = Array.prototype.slice.call(editable.parentElement.querySelectorAll('.' + CLASS_DRAG_OVER));\n\n        existing.forEach(function (el) {\n            el.classList.remove(CLASS_DRAG_OVER);\n        });\n    }\n\n    var FileDragging = MediumEditor.Extension.extend({\n        name: 'fileDragging',\n\n        allowedTypes: ['image'],\n\n        init: function () {\n            MediumEditor.Extension.prototype.init.apply(this, arguments);\n\n            this.subscribe('editableDrag', this.handleDrag.bind(this));\n            this.subscribe('editableDrop', this.handleDrop.bind(this));\n        },\n\n        handleDrag: function (event) {\n            event.preventDefault();\n            event.dataTransfer.dropEffect = 'copy';\n\n            var target = event.target.classList ? event.target : event.target.parentElement;\n\n            // Ensure the class gets removed from anything that had it before\n            clearClassNames(target);\n\n            if (event.type === 'dragover') {\n                target.classList.add(CLASS_DRAG_OVER);\n            }\n        },\n\n        handleDrop: function (event) {\n            // Prevent file from opening in the current window\n            event.preventDefault();\n            event.stopPropagation();\n            // Select the dropping target, and set the selection to the end of the target\n            // https://github.com/yabwe/medium-editor/issues/980\n            this.base.selectElement(event.target);\n            var selection = this.base.exportSelection();\n            selection.start = selection.end;\n            this.base.importSelection(selection);\n            // IE9 does not support the File API, so prevent file from opening in the window\n            // but also don't try to actually get the file\n            if (event.dataTransfer.files) {\n                Array.prototype.slice.call(event.dataTransfer.files).forEach(function (file) {\n                    if (this.isAllowedFile(file)) {\n                        if (file.type.match('image')) {\n                            this.insertImageFile(file);\n                        }\n                    }\n                }, this);\n            }\n\n            // Make sure we remove our class from everything\n            clearClassNames(event.target);\n        },\n\n        isAllowedFile: function (file) {\n            return this.allowedTypes.some(function (fileType) {\n                return !!file.type.match(fileType);\n            });\n        },\n\n        insertImageFile: function (file) {\n            if (typeof FileReader !== 'function') {\n                return;\n            }\n            var fileReader = new FileReader();\n            fileReader.readAsDataURL(file);\n\n            // attach the onload event handler, makes it easier to listen in with jasmine\n            fileReader.addEventListener('load', function (e) {\n                var addImageElement = this.document.createElement('img');\n                addImageElement.src = e.target.result;\n                MediumEditor.util.insertHTMLCommand(this.document, addImageElement.outerHTML);\n            }.bind(this));\n        }\n    });\n\n    MediumEditor.extensions.fileDragging = FileDragging;\n}());\n"
  },
  {
    "path": "src/js/extensions/fontname.js",
    "content": "(function () {\n    'use strict';\n\n    var FontNameForm = MediumEditor.extensions.form.extend({\n\n        name: 'fontname',\n        action: 'fontName',\n        aria: 'change font name',\n        contentDefault: '&#xB1;', // ±\n        contentFA: '<i class=\"fa fa-font\"></i>',\n\n        fonts: ['', 'Arial', 'Verdana', 'Times New Roman'],\n\n        init: function () {\n            MediumEditor.extensions.form.prototype.init.apply(this, arguments);\n        },\n\n        // Called when the button the toolbar is clicked\n        // Overrides ButtonExtension.handleClick\n        handleClick: function (event) {\n            event.preventDefault();\n            event.stopPropagation();\n\n            if (!this.isDisplayed()) {\n                // Get FontName of current selection (convert to string since IE returns this as number)\n                var fontName = this.document.queryCommandValue('fontName') + '';\n                this.showForm(fontName);\n            }\n\n            return false;\n        },\n\n        // Called by medium-editor to append form to the toolbar\n        getForm: function () {\n            if (!this.form) {\n                this.form = this.createForm();\n            }\n            return this.form;\n        },\n\n        // Used by medium-editor when the default toolbar is to be displayed\n        isDisplayed: function () {\n            return this.getForm().style.display === 'block';\n        },\n\n        hideForm: function () {\n            this.getForm().style.display = 'none';\n            this.getSelect().value = '';\n        },\n\n        showForm: function (fontName) {\n            var select = this.getSelect();\n\n            this.base.saveSelection();\n            this.hideToolbarDefaultActions();\n            this.getForm().style.display = 'block';\n            this.setToolbarPosition();\n\n            select.value = fontName || '';\n            select.focus();\n        },\n\n        // Called by core when tearing down medium-editor (destroy)\n        destroy: function () {\n            if (!this.form) {\n                return false;\n            }\n\n            if (this.form.parentNode) {\n                this.form.parentNode.removeChild(this.form);\n            }\n\n            delete this.form;\n        },\n\n        // core methods\n\n        doFormSave: function () {\n            this.base.restoreSelection();\n            this.base.checkSelection();\n        },\n\n        doFormCancel: function () {\n            this.base.restoreSelection();\n            this.clearFontName();\n            this.base.checkSelection();\n        },\n\n        // form creation and event handling\n        createForm: function () {\n            var doc = this.document,\n                form = doc.createElement('div'),\n                select = doc.createElement('select'),\n                close = doc.createElement('a'),\n                save = doc.createElement('a'),\n                option;\n\n            // Font Name Form (div)\n            form.className = 'medium-editor-toolbar-form';\n            form.id = 'medium-editor-toolbar-form-fontname-' + this.getEditorId();\n\n            // Handle clicks on the form itself\n            this.on(form, 'click', this.handleFormClick.bind(this));\n\n            // Add font names\n            for (var i = 0; i<this.fonts.length; i++) {\n                option = doc.createElement('option');\n                option.innerHTML = this.fonts[i];\n                option.value = this.fonts[i];\n                select.appendChild(option);\n            }\n\n            select.className = 'medium-editor-toolbar-select';\n            form.appendChild(select);\n\n            // Handle typing in the textbox\n            this.on(select, 'change', this.handleFontChange.bind(this));\n\n            // Add save buton\n            save.setAttribute('href', '#');\n            save.className = 'medium-editor-toobar-save';\n            save.innerHTML = this.getEditorOption('buttonLabels') === 'fontawesome' ?\n                             '<i class=\"fa fa-check\"></i>' :\n                             '&#10003;';\n            form.appendChild(save);\n\n            // Handle save button clicks (capture)\n            this.on(save, 'click', this.handleSaveClick.bind(this), true);\n\n            // Add close button\n            close.setAttribute('href', '#');\n            close.className = 'medium-editor-toobar-close';\n            close.innerHTML = this.getEditorOption('buttonLabels') === 'fontawesome' ?\n                              '<i class=\"fa fa-times\"></i>' :\n                              '&times;';\n            form.appendChild(close);\n\n            // Handle close button clicks\n            this.on(close, 'click', this.handleCloseClick.bind(this));\n\n            return form;\n        },\n\n        getSelect: function () {\n            return this.getForm().querySelector('select.medium-editor-toolbar-select');\n        },\n\n        clearFontName: function () {\n            MediumEditor.selection.getSelectedElements(this.document).forEach(function (el) {\n                if (el.nodeName.toLowerCase() === 'font' && el.hasAttribute('face')) {\n                    el.removeAttribute('face');\n                }\n            });\n        },\n\n        handleFontChange: function () {\n            var font = this.getSelect().value;\n            if (font === '') {\n                this.clearFontName();\n            } else {\n                this.execAction('fontName', { value: font });\n            }\n        },\n\n        handleFormClick: function (event) {\n            // make sure not to hide form when clicking inside the form\n            event.stopPropagation();\n        },\n\n        handleSaveClick: function (event) {\n            // Clicking Save -> create the font size\n            event.preventDefault();\n            this.doFormSave();\n        },\n\n        handleCloseClick: function (event) {\n            // Click Close -> close the form\n            event.preventDefault();\n            this.doFormCancel();\n        }\n    });\n\n    MediumEditor.extensions.fontName = FontNameForm;\n}());\n"
  },
  {
    "path": "src/js/extensions/fontsize.js",
    "content": "(function () {\n    'use strict';\n\n    var FontSizeForm = MediumEditor.extensions.form.extend({\n\n        name: 'fontsize',\n        action: 'fontSize',\n        aria: 'increase/decrease font size',\n        contentDefault: '&#xB1;', // ±\n        contentFA: '<i class=\"fa fa-text-height\"></i>',\n\n        init: function () {\n            MediumEditor.extensions.form.prototype.init.apply(this, arguments);\n        },\n\n        // Called when the button the toolbar is clicked\n        // Overrides ButtonExtension.handleClick\n        handleClick: function (event) {\n            event.preventDefault();\n            event.stopPropagation();\n\n            if (!this.isDisplayed()) {\n                // Get fontsize of current selection (convert to string since IE returns this as number)\n                var fontSize = this.document.queryCommandValue('fontSize') + '';\n                this.showForm(fontSize);\n            }\n\n            return false;\n        },\n\n        // Called by medium-editor to append form to the toolbar\n        getForm: function () {\n            if (!this.form) {\n                this.form = this.createForm();\n            }\n            return this.form;\n        },\n\n        // Used by medium-editor when the default toolbar is to be displayed\n        isDisplayed: function () {\n            return this.getForm().style.display === 'block';\n        },\n\n        hideForm: function () {\n            this.getForm().style.display = 'none';\n            this.getInput().value = '';\n        },\n\n        showForm: function (fontSize) {\n            var input = this.getInput();\n\n            this.base.saveSelection();\n            this.hideToolbarDefaultActions();\n            this.getForm().style.display = 'block';\n            this.setToolbarPosition();\n\n            input.value = fontSize || '';\n            input.focus();\n        },\n\n        // Called by core when tearing down medium-editor (destroy)\n        destroy: function () {\n            if (!this.form) {\n                return false;\n            }\n\n            if (this.form.parentNode) {\n                this.form.parentNode.removeChild(this.form);\n            }\n\n            delete this.form;\n        },\n\n        // core methods\n\n        doFormSave: function () {\n            this.base.restoreSelection();\n            this.base.checkSelection();\n        },\n\n        doFormCancel: function () {\n            this.base.restoreSelection();\n            this.clearFontSize();\n            this.base.checkSelection();\n        },\n\n        // form creation and event handling\n        createForm: function () {\n            var doc = this.document,\n                form = doc.createElement('div'),\n                input = doc.createElement('input'),\n                close = doc.createElement('a'),\n                save = doc.createElement('a');\n\n            // Font Size Form (div)\n            form.className = 'medium-editor-toolbar-form';\n            form.id = 'medium-editor-toolbar-form-fontsize-' + this.getEditorId();\n\n            // Handle clicks on the form itself\n            this.on(form, 'click', this.handleFormClick.bind(this));\n\n            // Add font size slider\n            input.setAttribute('type', 'range');\n            input.setAttribute('min', '1');\n            input.setAttribute('max', '7');\n            input.className = 'medium-editor-toolbar-input';\n            form.appendChild(input);\n\n            // Handle typing in the textbox\n            this.on(input, 'change', this.handleSliderChange.bind(this));\n\n            // Add save buton\n            save.setAttribute('href', '#');\n            save.className = 'medium-editor-toobar-save';\n            save.innerHTML = this.getEditorOption('buttonLabels') === 'fontawesome' ?\n                             '<i class=\"fa fa-check\"></i>' :\n                             '&#10003;';\n            form.appendChild(save);\n\n            // Handle save button clicks (capture)\n            this.on(save, 'click', this.handleSaveClick.bind(this), true);\n\n            // Add close button\n            close.setAttribute('href', '#');\n            close.className = 'medium-editor-toobar-close';\n            close.innerHTML = this.getEditorOption('buttonLabels') === 'fontawesome' ?\n                              '<i class=\"fa fa-times\"></i>' :\n                              '&times;';\n            form.appendChild(close);\n\n            // Handle close button clicks\n            this.on(close, 'click', this.handleCloseClick.bind(this));\n\n            return form;\n        },\n\n        getInput: function () {\n            return this.getForm().querySelector('input.medium-editor-toolbar-input');\n        },\n\n        clearFontSize: function () {\n            MediumEditor.selection.getSelectedElements(this.document).forEach(function (el) {\n                if (el.nodeName.toLowerCase() === 'font' && el.hasAttribute('size')) {\n                    el.removeAttribute('size');\n                }\n            });\n        },\n\n        handleSliderChange: function () {\n            var size = this.getInput().value;\n            if (size === '4') {\n                this.clearFontSize();\n            } else {\n                this.execAction('fontSize', { value: size });\n            }\n        },\n\n        handleFormClick: function (event) {\n            // make sure not to hide form when clicking inside the form\n            event.stopPropagation();\n        },\n\n        handleSaveClick: function (event) {\n            // Clicking Save -> create the font size\n            event.preventDefault();\n            this.doFormSave();\n        },\n\n        handleCloseClick: function (event) {\n            // Click Close -> close the form\n            event.preventDefault();\n            this.doFormCancel();\n        }\n    });\n\n    MediumEditor.extensions.fontSize = FontSizeForm;\n}());"
  },
  {
    "path": "src/js/extensions/form.js",
    "content": "(function () {\n    'use strict';\n\n    /* Base functionality for an extension which will display\n     * a 'form' inside the toolbar\n     */\n    var FormExtension = MediumEditor.extensions.button.extend({\n\n        init: function () {\n            MediumEditor.extensions.button.prototype.init.apply(this, arguments);\n        },\n\n        // default labels for the form buttons\n        formSaveLabel: '&#10003;',\n        formCloseLabel: '&times;',\n\n        /* activeClass: [string]\n         * set class which added to shown form\n         */\n        activeClass: 'medium-editor-toolbar-form-active',\n\n        /* hasForm: [boolean]\n         *\n         * Setting this to true will cause getForm() to be called\n         * when the toolbar is created, so the form can be appended\n         * inside the toolbar container\n         */\n        hasForm: true,\n\n        /* getForm: [function ()]\n         *\n         * When hasForm is true, this function must be implemented\n         * and return a DOM Element which will be appended to\n         * the toolbar container. The form should start hidden, and\n         * the extension can choose when to hide/show it\n         */\n        getForm: function () {},\n\n        /* isDisplayed: [function ()]\n         *\n         * This function should return true/false reflecting\n         * whether the form is currently displayed\n         */\n        isDisplayed: function () {\n            if (this.hasForm) {\n                return this.getForm().classList.contains(this.activeClass);\n            }\n            return false;\n        },\n\n        /* showForm: [function ()]\n         *\n         * This function should show the form element inside\n         * the toolbar container\n         */\n        showForm: function () {\n            if (this.hasForm) {\n                this.getForm().classList.add(this.activeClass);\n            }\n        },\n\n        /* hideForm: [function ()]\n         *\n         * This function should hide the form element inside\n         * the toolbar container\n         */\n        hideForm: function () {\n            if (this.hasForm) {\n                this.getForm().classList.remove(this.activeClass);\n            }\n        },\n\n        /************************ Helpers ************************\n         * The following are helpers that are either set by MediumEditor\n         * during initialization, or are helper methods which either\n         * route calls to the MediumEditor instance or provide common\n         * functionality for all form extensions\n         *********************************************************/\n\n        /* showToolbarDefaultActions: [function ()]\n         *\n         * Helper method which will turn back the toolbar after canceling\n         * the customized form\n         */\n        showToolbarDefaultActions: function () {\n            var toolbar = this.base.getExtensionByName('toolbar');\n            if (toolbar) {\n                toolbar.showToolbarDefaultActions();\n            }\n        },\n\n        /* hideToolbarDefaultActions: [function ()]\n         *\n         * Helper function which will hide the default contents of the\n         * toolbar, but leave the toolbar container in the same state\n         * to allow a form to display its custom contents inside the toolbar\n         */\n        hideToolbarDefaultActions: function () {\n            var toolbar = this.base.getExtensionByName('toolbar');\n            if (toolbar) {\n                toolbar.hideToolbarDefaultActions();\n            }\n        },\n\n        /* setToolbarPosition: [function ()]\n         *\n         * Helper function which will update the size and position\n         * of the toolbar based on the toolbar content and the current\n         * position of the user's selection\n         */\n        setToolbarPosition: function () {\n            var toolbar = this.base.getExtensionByName('toolbar');\n            if (toolbar) {\n                toolbar.setToolbarPosition();\n            }\n        }\n    });\n\n    MediumEditor.extensions.form = FormExtension;\n})();"
  },
  {
    "path": "src/js/extensions/keyboard-commands.js",
    "content": "(function () {\n    'use strict';\n\n    var KeyboardCommands = MediumEditor.Extension.extend({\n        name: 'keyboard-commands',\n\n        /* KeyboardCommands Options */\n\n        /* commands: [Array]\n         * Array of objects describing each command and the combination of keys that will trigger it\n         * Required for each object:\n         *   command [String] (argument passed to editor.execAction())\n         *   key [String] (keyboard character that triggers this command)\n         *   meta [boolean] (whether the ctrl/meta key has to be active or inactive)\n         *   shift [boolean] (whether the shift key has to be active or inactive)\n         *   alt [boolean] (whether the alt key has to be active or inactive)\n         */\n        commands: [\n            {\n                command: 'bold',\n                key: 'B',\n                meta: true,\n                shift: false,\n                alt: false\n            },\n            {\n                command: 'italic',\n                key: 'I',\n                meta: true,\n                shift: false,\n                alt: false\n            },\n            {\n                command: 'underline',\n                key: 'U',\n                meta: true,\n                shift: false,\n                alt: false\n            }\n        ],\n\n        init: function () {\n            MediumEditor.Extension.prototype.init.apply(this, arguments);\n\n            this.subscribe('editableKeydown', this.handleKeydown.bind(this));\n            this.keys = {};\n            this.commands.forEach(function (command) {\n                var keyCode = command.key.charCodeAt(0);\n                if (!this.keys[keyCode]) {\n                    this.keys[keyCode] = [];\n                }\n                this.keys[keyCode].push(command);\n            }, this);\n        },\n\n        handleKeydown: function (event) {\n            var keyCode = MediumEditor.util.getKeyCode(event);\n            if (!this.keys[keyCode]) {\n                return;\n            }\n\n            var isMeta = MediumEditor.util.isMetaCtrlKey(event),\n                isShift = !!event.shiftKey,\n                isAlt = !!event.altKey;\n\n            this.keys[keyCode].forEach(function (data) {\n                if (data.meta === isMeta &&\n                    data.shift === isShift &&\n                    (data.alt === isAlt ||\n                     undefined === data.alt)) { // TODO deprecated: remove check for undefined === data.alt when jumping to 6.0.0\n                    event.preventDefault();\n                    event.stopPropagation();\n\n                    // command can be a function to execute\n                    if (typeof data.command === 'function') {\n                        data.command.apply(this);\n                    }\n                    // command can be false so the shortcut is just disabled\n                    else if (false !== data.command) {\n                        this.execAction(data.command);\n                    }\n                }\n            }, this);\n        }\n    });\n\n    MediumEditor.extensions.keyboardCommands = KeyboardCommands;\n}());\n"
  },
  {
    "path": "src/js/extensions/paste.js",
    "content": "(function () {\n    'use strict';\n\n    /* Helpers and internal variables that don't need to be members of actual paste handler */\n\n    var pasteBinDefaultContent = '%ME_PASTEBIN%',\n        lastRange = null,\n        keyboardPasteEditable = null,\n        stopProp = function (event) {\n            event.stopPropagation();\n        };\n\n    /*jslint regexp: true*/\n    /**\n        jslint does not allow character negation, because the negation\n        will not match any unicode characters. In the regexes in this\n        block, negation is used specifically to match the end of an html\n        tag, and in fact unicode characters *should* be allowed.\n    */\n    function createReplacements() {\n        return [\n            // Remove anything but the contents within the BODY element\n            [new RegExp(/^[\\s\\S]*<body[^>]*>\\s*|\\s*<\\/body[^>]*>[\\s\\S]*$/g), ''],\n\n            // cleanup comments added by Chrome when pasting html\n            [new RegExp(/<!--StartFragment-->|<!--EndFragment-->/g), ''],\n\n            // Trailing BR elements\n            [new RegExp(/<br>$/i), ''],\n\n            // replace two bogus tags that begin pastes from google docs\n            [new RegExp(/<[^>]*docs-internal-guid[^>]*>/gi), ''],\n            [new RegExp(/<\\/b>(<br[^>]*>)?$/gi), ''],\n\n             // un-html spaces and newlines inserted by OS X\n            [new RegExp(/<span class=\"Apple-converted-space\">\\s+<\\/span>/g), ' '],\n            [new RegExp(/<br class=\"Apple-interchange-newline\">/g), '<br>'],\n\n            // replace google docs italics+bold with a span to be replaced once the html is inserted\n            [new RegExp(/<span[^>]*(font-style:italic;font-weight:(bold|700)|font-weight:(bold|700);font-style:italic)[^>]*>/gi), '<span class=\"replace-with italic bold\">'],\n\n            // replace google docs italics with a span to be replaced once the html is inserted\n            [new RegExp(/<span[^>]*font-style:italic[^>]*>/gi), '<span class=\"replace-with italic\">'],\n\n            //[replace google docs bolds with a span to be replaced once the html is inserted\n            [new RegExp(/<span[^>]*font-weight:(bold|700)[^>]*>/gi), '<span class=\"replace-with bold\">'],\n\n             // replace manually entered b/i/a tags with real ones\n            [new RegExp(/&lt;(\\/?)(i|b|a)&gt;/gi), '<$1$2>'],\n\n             // replace manually a tags with real ones, converting smart-quotes from google docs\n            [new RegExp(/&lt;a(?:(?!href).)+href=(?:&quot;|&rdquo;|&ldquo;|\"|“|”)(((?!&quot;|&rdquo;|&ldquo;|\"|“|”).)*)(?:&quot;|&rdquo;|&ldquo;|\"|“|”)(?:(?!&gt;).)*&gt;/gi), '<a href=\"$1\">'],\n\n            // Newlines between paragraphs in html have no syntactic value,\n            // but then have a tendency to accidentally become additional paragraphs down the line\n            [new RegExp(/<\\/p>\\n+/gi), '</p>'],\n            [new RegExp(/\\n+<p/gi), '<p'],\n\n            // Microsoft Word makes these odd tags, like <o:p></o:p>\n            [new RegExp(/<\\/?o:[a-z]*>/gi), ''],\n\n            // Microsoft Word adds some special elements around list items\n            [new RegExp(/<!\\[if !supportLists\\]>(((?!<!).)*)<!\\[endif]\\>/gi), '$1']\n        ];\n    }\n    /*jslint regexp: false*/\n\n    /**\n     * Gets various content types out of the Clipboard API. It will also get the\n     * plain text using older IE and WebKit API.\n     *\n     * @param {event} event Event fired on paste.\n     * @param {win} reference to window\n     * @param {doc} reference to document\n     * @return {Object} Object with mime types and data for those mime types.\n     */\n    function getClipboardContent(event, win, doc) {\n        var dataTransfer = event.clipboardData || win.clipboardData || doc.dataTransfer,\n            data = {};\n\n        if (!dataTransfer) {\n            return data;\n        }\n\n        // Use old WebKit/IE API\n        if (dataTransfer.getData) {\n            var legacyText = dataTransfer.getData('Text');\n            if (legacyText && legacyText.length > 0) {\n                data['text/plain'] = legacyText;\n            }\n        }\n\n        if (dataTransfer.types) {\n            for (var i = 0; i < dataTransfer.types.length; i++) {\n                var contentType = dataTransfer.types[i];\n                data[contentType] = dataTransfer.getData(contentType);\n            }\n        }\n\n        return data;\n    }\n\n    var PasteHandler = MediumEditor.Extension.extend({\n        /* Paste Options */\n\n        /* forcePlainText: [boolean]\n         * Forces pasting as plain text.\n         */\n        forcePlainText: true,\n\n        /* cleanPastedHTML: [boolean]\n         * cleans pasted content from different sources, like google docs etc.\n         */\n        cleanPastedHTML: false,\n\n        /* preCleanReplacements: [Array]\n         * custom pairs (2 element arrays) of RegExp and replacement text to use during past when\n         * __forcePlainText__ or __cleanPastedHTML__ are `true` OR when calling `cleanPaste(text)` helper method.\n         * These replacements are executed before any medium editor defined replacements.\n         */\n        preCleanReplacements: [],\n\n        /* cleanReplacements: [Array]\n         * custom pairs (2 element arrays) of RegExp and replacement text to use during paste when\n         * __forcePlainText__ or __cleanPastedHTML__ are `true` OR when calling `cleanPaste(text)` helper method.\n         * These replacements are executed after any medium editor defined replacements.\n         */\n        cleanReplacements: [],\n\n        /* cleanAttrs:: [Array]\n         * list of element attributes to remove during paste when __cleanPastedHTML__ is `true` or when\n         * calling `cleanPaste(text)` or `pasteHTML(html, options)` helper methods.\n         */\n        cleanAttrs: ['class', 'style', 'dir'],\n\n        /* cleanTags: [Array]\n         * list of element tag names to remove during paste when __cleanPastedHTML__ is `true` or when\n         * calling `cleanPaste(text)` or `pasteHTML(html, options)` helper methods.\n         */\n        cleanTags: ['meta'],\n\n        /* unwrapTags: [Array]\n         * list of element tag names to unwrap (remove the element tag but retain its child elements)\n         * during paste when __cleanPastedHTML__ is `true` or when\n         * calling `cleanPaste(text)` or `pasteHTML(html, options)` helper methods.\n         */\n        unwrapTags: [],\n\n        init: function () {\n            MediumEditor.Extension.prototype.init.apply(this, arguments);\n\n            if (this.forcePlainText || this.cleanPastedHTML) {\n                this.subscribe('editableKeydown', this.handleKeydown.bind(this));\n                // We need access to the full event data in paste\n                // so we can't use the editablePaste event here\n                this.getEditorElements().forEach(function (element) {\n                    this.on(element, 'paste', this.handlePaste.bind(this));\n                }, this);\n                this.subscribe('addElement', this.handleAddElement.bind(this));\n            }\n        },\n\n        handleAddElement: function (event, editable) {\n            this.on(editable, 'paste', this.handlePaste.bind(this));\n        },\n\n        destroy: function () {\n            // Make sure pastebin is destroyed in case it's still around for some reason\n            if (this.forcePlainText || this.cleanPastedHTML) {\n                this.removePasteBin();\n            }\n        },\n\n        handlePaste: function (event, editable) {\n            if (event.defaultPrevented) {\n                return;\n            }\n\n            var clipboardContent = getClipboardContent(event, this.window, this.document),\n                pastedHTML = clipboardContent['text/html'],\n                pastedPlain = clipboardContent['text/plain'];\n\n            if (this.window.clipboardData && event.clipboardData === undefined && !pastedHTML) {\n                // If window.clipboardData exists, but event.clipboardData doesn't exist,\n                // we're probably in IE. IE only has two possibilities for clipboard\n                // data format: 'Text' and 'URL'.\n                //\n                // For IE, we'll fallback to 'Text' for text/html\n                pastedHTML = pastedPlain;\n            }\n\n            if (pastedHTML || pastedPlain) {\n                event.preventDefault();\n\n                this.doPaste(pastedHTML, pastedPlain, editable);\n            }\n        },\n\n        doPaste: function (pastedHTML, pastedPlain, editable) {\n            var paragraphs,\n                html = '',\n                p;\n\n            if (this.cleanPastedHTML && pastedHTML) {\n                return this.cleanPaste(pastedHTML);\n            }\n\n            if (!pastedPlain) {\n                return;\n            }\n\n            if (!(this.getEditorOption('disableReturn') || (editable && editable.getAttribute('data-disable-return')))) {\n                paragraphs = pastedPlain.split(/[\\r\\n]+/g);\n                // If there are no \\r\\n in data, don't wrap in <p>\n                if (paragraphs.length > 1) {\n                    for (p = 0; p < paragraphs.length; p += 1) {\n                        if (paragraphs[p] !== '') {\n                            html += '<p>' + MediumEditor.util.htmlEntities(paragraphs[p]) + '</p>';\n                        }\n                    }\n                } else {\n                    html = MediumEditor.util.htmlEntities(paragraphs[0]);\n                }\n            } else {\n                html = MediumEditor.util.htmlEntities(pastedPlain);\n            }\n            MediumEditor.util.insertHTMLCommand(this.document, html);\n        },\n\n        handlePasteBinPaste: function (event) {\n            if (event.defaultPrevented) {\n                this.removePasteBin();\n                return;\n            }\n\n            var clipboardContent = getClipboardContent(event, this.window, this.document),\n                pastedHTML = clipboardContent['text/html'],\n                pastedPlain = clipboardContent['text/plain'],\n                editable = keyboardPasteEditable;\n\n            // If we have valid html already, or we're not in cleanPastedHTML mode\n            // we can ignore the paste bin and just paste now\n            if (!this.cleanPastedHTML || pastedHTML) {\n                event.preventDefault();\n                this.removePasteBin();\n                this.doPaste(pastedHTML, pastedPlain, editable);\n\n                // The event handling code listens for paste on the editable element\n                // in order to trigger the editablePaste event.  Since this paste event\n                // is happening on the pastebin, the event handling code never knows about it\n                // So, we have to trigger editablePaste manually\n                this.trigger('editablePaste', { currentTarget: editable, target: editable }, editable);\n                return;\n            }\n\n            // We need to look at the paste bin, so do a setTimeout to let the paste\n            // fall through into the paste bin\n            setTimeout(function () {\n                // Only look for HTML if we're in cleanPastedHTML mode\n                if (this.cleanPastedHTML) {\n                    // If clipboard didn't have HTML, try the paste bin\n                    pastedHTML = this.getPasteBinHtml();\n                }\n\n                // If we needed the paste bin, we're done with it now, remove it\n                this.removePasteBin();\n\n                // Handle the paste with the html from the paste bin\n                this.doPaste(pastedHTML, pastedPlain, editable);\n\n                // The event handling code listens for paste on the editable element\n                // in order to trigger the editablePaste event.  Since this paste event\n                // is happening on the pastebin, the event handling code never knows about it\n                // So, we have to trigger editablePaste manually\n                this.trigger('editablePaste', { currentTarget: editable, target: editable }, editable);\n            }.bind(this), 0);\n        },\n\n        handleKeydown: function (event, editable) {\n            // if it's not Ctrl+V, do nothing\n            if (!(MediumEditor.util.isKey(event, MediumEditor.util.keyCode.V) && MediumEditor.util.isMetaCtrlKey(event))) {\n                return;\n            }\n\n            event.stopImmediatePropagation();\n\n            this.removePasteBin();\n            this.createPasteBin(editable);\n        },\n\n        createPasteBin: function (editable) {\n            var rects,\n                range = MediumEditor.selection.getSelectionRange(this.document),\n                top = this.window.pageYOffset;\n\n            keyboardPasteEditable = editable;\n\n            if (range) {\n                rects = range.getClientRects();\n\n                // on empty line, rects is empty so we grab information from the first container of the range\n                if (rects.length) {\n                    top += rects[0].top;\n                } else if (range.startContainer.getBoundingClientRect !== undefined) {\n                    top += range.startContainer.getBoundingClientRect().top;\n                } else {\n                    top += range.getBoundingClientRect().top;\n                }\n            }\n\n            lastRange = range;\n\n            var pasteBinElm = this.document.createElement('div');\n            pasteBinElm.id = this.pasteBinId = 'medium-editor-pastebin-' + (+Date.now());\n            pasteBinElm.setAttribute('style', 'border: 1px red solid; position: absolute; top: ' + top + 'px; width: 10px; height: 10px; overflow: hidden; opacity: 0');\n            pasteBinElm.setAttribute('contentEditable', true);\n            pasteBinElm.innerHTML = pasteBinDefaultContent;\n\n            this.document.body.appendChild(pasteBinElm);\n\n            // avoid .focus() to stop other event (actually the paste event)\n            this.on(pasteBinElm, 'focus', stopProp);\n            this.on(pasteBinElm, 'focusin', stopProp);\n            this.on(pasteBinElm, 'focusout', stopProp);\n\n            pasteBinElm.focus();\n\n            MediumEditor.selection.selectNode(pasteBinElm, this.document);\n\n            if (!this.boundHandlePaste) {\n                this.boundHandlePaste = this.handlePasteBinPaste.bind(this);\n            }\n\n            this.on(pasteBinElm, 'paste', this.boundHandlePaste);\n        },\n\n        removePasteBin: function () {\n            if (null !== lastRange) {\n                MediumEditor.selection.selectRange(this.document, lastRange);\n                lastRange = null;\n            }\n\n            if (null !== keyboardPasteEditable) {\n                keyboardPasteEditable = null;\n            }\n\n            var pasteBinElm = this.getPasteBin();\n            if (!pasteBinElm) {\n                return;\n            }\n\n            if (pasteBinElm) {\n                this.off(pasteBinElm, 'focus', stopProp);\n                this.off(pasteBinElm, 'focusin', stopProp);\n                this.off(pasteBinElm, 'focusout', stopProp);\n                this.off(pasteBinElm, 'paste', this.boundHandlePaste);\n                pasteBinElm.parentElement.removeChild(pasteBinElm);\n            }\n        },\n\n        getPasteBin: function () {\n            return this.document.getElementById(this.pasteBinId);\n        },\n\n        getPasteBinHtml: function () {\n            var pasteBinElm = this.getPasteBin();\n\n            if (!pasteBinElm) {\n                return false;\n            }\n\n            // WebKit has a nice bug where it clones the paste bin if you paste from for example notepad\n            // so we need to force plain text mode in this case\n            if (pasteBinElm.firstChild && pasteBinElm.firstChild.id === 'mcepastebin') {\n                return false;\n            }\n\n            var pasteBinHtml = pasteBinElm.innerHTML;\n\n            // If paste bin is empty try using plain text mode\n            // since that is better than nothing right\n            if (!pasteBinHtml || pasteBinHtml === pasteBinDefaultContent) {\n                return false;\n            }\n\n            return pasteBinHtml;\n        },\n\n        cleanPaste: function (text) {\n            var i, elList, tmp, workEl,\n                multiline = /<p|<br|<div/.test(text),\n                replacements = [].concat(\n                    this.preCleanReplacements || [],\n                    createReplacements(),\n                    this.cleanReplacements || []);\n\n            for (i = 0; i < replacements.length; i += 1) {\n                text = text.replace(replacements[i][0], replacements[i][1]);\n            }\n\n            if (!multiline) {\n                return this.pasteHTML(text);\n            }\n\n            // create a temporary div to cleanup block elements\n            tmp = this.document.createElement('div');\n\n            // double br's aren't converted to p tags, but we want paragraphs.\n            tmp.innerHTML = '<p>' + text.split('<br><br>').join('</p><p>') + '</p>';\n\n            // block element cleanup\n            elList = tmp.querySelectorAll('a,p,div,br');\n            for (i = 0; i < elList.length; i += 1) {\n                workEl = elList[i];\n\n                // Microsoft Word replaces some spaces with newlines.\n                // While newlines between block elements are meaningless, newlines within\n                // elements are sometimes actually spaces.\n                workEl.innerHTML = workEl.innerHTML.replace(/\\n/gi, ' ');\n\n                switch (workEl.nodeName.toLowerCase()) {\n                    case 'p':\n                    case 'div':\n                        this.filterCommonBlocks(workEl);\n                        break;\n                    case 'br':\n                        this.filterLineBreak(workEl);\n                        break;\n                }\n            }\n\n            this.pasteHTML(tmp.innerHTML);\n        },\n\n        pasteHTML: function (html, options) {\n            options = MediumEditor.util.defaults({}, options, {\n                cleanAttrs: this.cleanAttrs,\n                cleanTags: this.cleanTags,\n                unwrapTags: this.unwrapTags\n            });\n\n            var elList, workEl, i, fragmentBody, pasteBlock = this.document.createDocumentFragment();\n\n            pasteBlock.appendChild(this.document.createElement('body'));\n\n            fragmentBody = pasteBlock.querySelector('body');\n            fragmentBody.innerHTML = html;\n\n            this.cleanupSpans(fragmentBody);\n\n            elList = fragmentBody.querySelectorAll('*');\n            for (i = 0; i < elList.length; i += 1) {\n                workEl = elList[i];\n\n                if ('a' === workEl.nodeName.toLowerCase() && this.getEditorOption('targetBlank')) {\n                    MediumEditor.util.setTargetBlank(workEl);\n                }\n\n                MediumEditor.util.cleanupAttrs(workEl, options.cleanAttrs);\n                MediumEditor.util.cleanupTags(workEl, options.cleanTags);\n                MediumEditor.util.unwrapTags(workEl, options.unwrapTags);\n            }\n\n            MediumEditor.util.insertHTMLCommand(this.document, fragmentBody.innerHTML.replace(/&nbsp;/g, ' '));\n        },\n\n        // TODO (6.0): Make this an internal helper instead of member of paste handler\n        isCommonBlock: function (el) {\n            return (el && (el.nodeName.toLowerCase() === 'p' || el.nodeName.toLowerCase() === 'div'));\n        },\n\n        // TODO (6.0): Make this an internal helper instead of member of paste handler\n        filterCommonBlocks: function (el) {\n            if (/^\\s*$/.test(el.textContent) && el.parentNode) {\n                el.parentNode.removeChild(el);\n            }\n        },\n\n        // TODO (6.0): Make this an internal helper instead of member of paste handler\n        filterLineBreak: function (el) {\n            if (this.isCommonBlock(el.previousElementSibling)) {\n                // remove stray br's following common block elements\n                this.removeWithParent(el);\n            } else if (this.isCommonBlock(el.parentNode) && (el.parentNode.firstChild === el || el.parentNode.lastChild === el)) {\n                // remove br's just inside open or close tags of a div/p\n                this.removeWithParent(el);\n            } else if (el.parentNode && el.parentNode.childElementCount === 1 && el.parentNode.textContent === '') {\n                // and br's that are the only child of elements other than div/p\n                this.removeWithParent(el);\n            }\n        },\n\n        // TODO (6.0): Make this an internal helper instead of member of paste handler\n        // remove an element, including its parent, if it is the only element within its parent\n        removeWithParent: function (el) {\n            if (el && el.parentNode) {\n                if (el.parentNode.parentNode && el.parentNode.childElementCount === 1) {\n                    el.parentNode.parentNode.removeChild(el.parentNode);\n                } else {\n                    el.parentNode.removeChild(el);\n                }\n            }\n        },\n\n        // TODO (6.0): Make this an internal helper instead of member of paste handler\n        cleanupSpans: function (containerEl) {\n            var i,\n                el,\n                newEl,\n                spans = containerEl.querySelectorAll('.replace-with'),\n                isCEF = function (el) {\n                    return (el && el.nodeName !== '#text' && el.getAttribute('contenteditable') === 'false');\n                };\n\n            for (i = 0; i < spans.length; i += 1) {\n                el = spans[i];\n                newEl = this.document.createElement(el.classList.contains('bold') ? 'b' : 'i');\n\n                if (el.classList.contains('bold') && el.classList.contains('italic')) {\n                    // add an i tag as well if this has both italics and bold\n                    newEl.innerHTML = '<i>' + el.innerHTML + '</i>';\n                } else {\n                    newEl.innerHTML = el.innerHTML;\n                }\n                el.parentNode.replaceChild(newEl, el);\n            }\n\n            spans = containerEl.querySelectorAll('span');\n            for (i = 0; i < spans.length; i += 1) {\n                el = spans[i];\n\n                // bail if span is in contenteditable = false\n                if (MediumEditor.util.traverseUp(el, isCEF)) {\n                    return false;\n                }\n\n                // remove empty spans, replace others with their contents\n                MediumEditor.util.unwrap(el, this.document);\n            }\n        }\n    });\n\n    MediumEditor.extensions.paste = PasteHandler;\n}());\n"
  },
  {
    "path": "src/js/extensions/placeholder.js",
    "content": "(function () {\n    'use strict';\n\n    var Placeholder = MediumEditor.Extension.extend({\n        name: 'placeholder',\n\n        /* Placeholder Options */\n\n        /* text: [string]\n         * Text to display in the placeholder\n         */\n        text: 'Type your text',\n\n        /* hideOnClick: [boolean]\n         * Should we hide the placeholder on click (true) or when user starts typing (false)\n         */\n        hideOnClick: true,\n\n        init: function () {\n            MediumEditor.Extension.prototype.init.apply(this, arguments);\n\n            this.initPlaceholders();\n            this.attachEventHandlers();\n        },\n\n        initPlaceholders: function () {\n            this.getEditorElements().forEach(this.initElement, this);\n        },\n\n        handleAddElement: function (event, editable) {\n            this.initElement(editable);\n        },\n\n        initElement: function (el) {\n            if (!el.getAttribute('data-placeholder')) {\n                el.setAttribute('data-placeholder', this.text);\n            }\n            this.updatePlaceholder(el);\n        },\n\n        destroy: function () {\n            this.getEditorElements().forEach(this.cleanupElement, this);\n        },\n\n        handleRemoveElement: function (event, editable) {\n            this.cleanupElement(editable);\n        },\n\n        cleanupElement: function (el) {\n            if (el.getAttribute('data-placeholder') === this.text) {\n                el.removeAttribute('data-placeholder');\n            }\n        },\n\n        showPlaceholder: function (el) {\n            if (el) {\n                // https://github.com/yabwe/medium-editor/issues/234\n                // In firefox, styling the placeholder with an absolutely positioned\n                // pseudo element causes the cursor to appear in a bad location\n                // when the element is completely empty, so apply a different class to\n                // style it with a relatively positioned pseudo element\n                if (MediumEditor.util.isFF && el.childNodes.length === 0) {\n                    el.classList.add('medium-editor-placeholder-relative');\n                    el.classList.remove('medium-editor-placeholder');\n                } else {\n                    el.classList.add('medium-editor-placeholder');\n                    el.classList.remove('medium-editor-placeholder-relative');\n                }\n            }\n        },\n\n        hidePlaceholder: function (el) {\n            if (el) {\n                el.classList.remove('medium-editor-placeholder');\n                el.classList.remove('medium-editor-placeholder-relative');\n            }\n        },\n\n        updatePlaceholder: function (el, dontShow) {\n            // If the element has content, hide the placeholder\n            if (el.querySelector('img, blockquote, ul, ol, table') || (el.textContent.replace(/^\\s+|\\s+$/g, '') !== '')) {\n                return this.hidePlaceholder(el);\n            }\n\n            if (!dontShow) {\n                this.showPlaceholder(el);\n            }\n        },\n\n        attachEventHandlers: function () {\n            if (this.hideOnClick) {\n                // For the 'hideOnClick' option, the placeholder should always be hidden on focus\n                this.subscribe('focus', this.handleFocus.bind(this));\n            }\n\n            // If the editor has content, it should always hide the placeholder\n            this.subscribe('editableInput', this.handleInput.bind(this));\n\n            // When the editor loses focus, check if the placeholder should be visible\n            this.subscribe('blur', this.handleBlur.bind(this));\n\n            // Need to know when elements are added/removed from the editor\n            this.subscribe('addElement', this.handleAddElement.bind(this));\n            this.subscribe('removeElement', this.handleRemoveElement.bind(this));\n        },\n\n        handleInput: function (event, element) {\n            // If the placeholder should be hidden on focus and the\n            // element has focus, don't show the placeholder\n            var dontShow = this.hideOnClick && (element === this.base.getFocusedElement());\n\n            // Editor's content has changed, check if the placeholder should be hidden\n            this.updatePlaceholder(element, dontShow);\n        },\n\n        handleFocus: function (event, element) {\n            // Editor has focus, hide the placeholder\n            this.hidePlaceholder(element);\n        },\n\n        handleBlur: function (event, element) {\n            // Editor has lost focus, check if the placeholder should be shown\n            this.updatePlaceholder(element);\n        }\n    });\n\n    MediumEditor.extensions.placeholder = Placeholder;\n}());\n"
  },
  {
    "path": "src/js/extensions/toolbar.js",
    "content": "(function () {\n    'use strict';\n\n    var Toolbar = MediumEditor.Extension.extend({\n        name: 'toolbar',\n\n        /* Toolbar Options */\n\n        /* align: ['left'|'center'|'right']\n         * When the __static__ option is true, this aligns the static toolbar\n         * relative to the medium-editor element.\n         */\n        align: 'center',\n\n        /* allowMultiParagraphSelection: [boolean]\n         * enables/disables whether the toolbar should be displayed when\n         * selecting multiple paragraphs/block elements\n         */\n        allowMultiParagraphSelection: true,\n\n        /* buttons: [Array]\n         * the names of the set of buttons to display on the toolbar.\n         */\n        buttons: ['bold', 'italic', 'underline', 'anchor', 'h2', 'h3', 'quote'],\n\n        /* diffLeft: [Number]\n         * value in pixels to be added to the X axis positioning of the toolbar.\n         */\n        diffLeft: 0,\n\n        /* diffTop: [Number]\n         * value in pixels to be added to the Y axis positioning of the toolbar.\n         */\n        diffTop: -10,\n\n        /* firstButtonClass: [string]\n         * CSS class added to the first button in the toolbar.\n         */\n        firstButtonClass: 'medium-editor-button-first',\n\n        /* lastButtonClass: [string]\n         * CSS class added to the last button in the toolbar.\n         */\n        lastButtonClass: 'medium-editor-button-last',\n\n        /* standardizeSelectionStart: [boolean]\n         * enables/disables standardizing how the beginning of a range is decided\n         * between browsers whenever the selected text is analyzed for updating toolbar buttons status.\n         */\n        standardizeSelectionStart: false,\n\n        /* static: [boolean]\n         * enable/disable the toolbar always displaying in the same location\n         * relative to the medium-editor element.\n         */\n        static: false,\n\n        /* sticky: [boolean]\n         * When the __static__ option is true, this enables/disables the toolbar\n         * \"sticking\" to the viewport and staying visible on the screen while\n         * the page scrolls.\n         */\n        sticky: false,\n\n        /* stickyTopOffset: [Number]\n         * Value in pixel of the top offset above the toolbar\n         */\n        stickyTopOffset: 0,\n\n        /* updateOnEmptySelection: [boolean]\n         * When the __static__ option is true, this enables/disables updating\n         * the state of the toolbar buttons even when the selection is collapsed\n         * (there is no selection, just a cursor).\n         */\n        updateOnEmptySelection: false,\n\n        /* relativeContainer: [node]\n         * appending the toolbar to a given node instead of body\n         */\n        relativeContainer: null,\n\n        init: function () {\n            MediumEditor.Extension.prototype.init.apply(this, arguments);\n\n            this.initThrottledMethods();\n\n            if (!this.relativeContainer) {\n                this.getEditorOption('elementsContainer').appendChild(this.getToolbarElement());\n            } else {\n                this.relativeContainer.appendChild(this.getToolbarElement());\n            }\n        },\n\n        // Helper method to execute method for every extension, but ignoring the toolbar extension\n        forEachExtension: function (iterator, context) {\n            return this.base.extensions.forEach(function (command) {\n                if (command === this) {\n                    return;\n                }\n                return iterator.apply(context || this, arguments);\n            }, this);\n        },\n\n        // Toolbar creation/deletion\n\n        createToolbar: function () {\n            var toolbar = this.document.createElement('div');\n\n            toolbar.id = 'medium-editor-toolbar-' + this.getEditorId();\n            toolbar.className = 'medium-editor-toolbar';\n\n            if (this.static) {\n                toolbar.className += ' static-toolbar';\n            } else if (this.relativeContainer) {\n                toolbar.className += ' medium-editor-relative-toolbar';\n            } else {\n                toolbar.className += ' medium-editor-stalker-toolbar';\n            }\n\n            toolbar.appendChild(this.createToolbarButtons());\n\n            // Add any forms that extensions may have\n            this.forEachExtension(function (extension) {\n                if (extension.hasForm) {\n                    toolbar.appendChild(extension.getForm());\n                }\n            });\n\n            this.attachEventHandlers();\n\n            return toolbar;\n        },\n\n        createToolbarButtons: function () {\n            var ul = this.document.createElement('ul'),\n                li,\n                btn,\n                buttons,\n                extension,\n                buttonName,\n                buttonOpts;\n\n            ul.id = 'medium-editor-toolbar-actions' + this.getEditorId();\n            ul.className = 'medium-editor-toolbar-actions';\n            ul.style.display = 'block';\n\n            this.buttons.forEach(function (button) {\n                if (typeof button === 'string') {\n                    buttonName = button;\n                    buttonOpts = null;\n                } else {\n                    buttonName = button.name;\n                    buttonOpts = button;\n                }\n\n                // If the button already exists as an extension, it'll be returned\n                // othwerise it'll create the default built-in button\n                extension = this.base.addBuiltInExtension(buttonName, buttonOpts);\n\n                if (extension && typeof extension.getButton === 'function') {\n                    btn = extension.getButton(this.base);\n                    li = this.document.createElement('li');\n                    if (MediumEditor.util.isElement(btn)) {\n                        li.appendChild(btn);\n                    } else {\n                        li.innerHTML = btn;\n                    }\n                    ul.appendChild(li);\n                }\n            }, this);\n\n            buttons = ul.querySelectorAll('button');\n            if (buttons.length > 0) {\n                buttons[0].classList.add(this.firstButtonClass);\n                buttons[buttons.length - 1].classList.add(this.lastButtonClass);\n            }\n\n            return ul;\n        },\n\n        destroy: function () {\n            if (this.toolbar) {\n                if (this.toolbar.parentNode) {\n                    this.toolbar.parentNode.removeChild(this.toolbar);\n                }\n                delete this.toolbar;\n            }\n        },\n\n        // Toolbar accessors\n\n        getInteractionElements: function () {\n            return this.getToolbarElement();\n        },\n\n        getToolbarElement: function () {\n            if (!this.toolbar) {\n                this.toolbar = this.createToolbar();\n            }\n\n            return this.toolbar;\n        },\n\n        getToolbarActionsElement: function () {\n            return this.getToolbarElement().querySelector('.medium-editor-toolbar-actions');\n        },\n\n        // Toolbar event handlers\n\n        initThrottledMethods: function () {\n            // throttledPositionToolbar is throttled because:\n            // - It will be called when the browser is resizing, which can fire many times very quickly\n            // - For some event (like resize) a slight lag in UI responsiveness is OK and provides performance benefits\n            this.throttledPositionToolbar = MediumEditor.util.throttle(function () {\n                if (this.base.isActive) {\n                    this.positionToolbarIfShown();\n                }\n            }.bind(this));\n        },\n\n        attachEventHandlers: function () {\n            // MediumEditor custom events for when user beings and ends interaction with a contenteditable and its elements\n            this.subscribe('blur', this.handleBlur.bind(this));\n            this.subscribe('focus', this.handleFocus.bind(this));\n\n            // Updating the state of the toolbar as things change\n            this.subscribe('editableClick', this.handleEditableClick.bind(this));\n            this.subscribe('editableKeyup', this.handleEditableKeyup.bind(this));\n\n            // Handle mouseup on document for updating the selection in the toolbar\n            this.on(this.document.documentElement, 'mouseup', this.handleDocumentMouseup.bind(this));\n\n            // Add a scroll event for sticky toolbar\n            if (this.static && this.sticky) {\n                // On scroll (capture), re-position the toolbar\n                this.on(this.window, 'scroll', this.handleWindowScroll.bind(this), true);\n            }\n\n            // On resize, re-position the toolbar\n            this.on(this.window, 'resize', this.handleWindowResize.bind(this));\n        },\n\n        handleWindowScroll: function () {\n            this.positionToolbarIfShown();\n        },\n\n        handleWindowResize: function () {\n            this.throttledPositionToolbar();\n        },\n\n        handleDocumentMouseup: function (event) {\n            // Do not trigger checkState when mouseup fires over the toolbar\n            if (event &&\n                    event.target &&\n                    MediumEditor.util.isDescendant(this.getToolbarElement(), event.target)) {\n                return false;\n            }\n            this.checkState();\n        },\n\n        handleEditableClick: function () {\n            // Delay the call to checkState to handle bug where selection is empty\n            // immediately after clicking inside a pre-existing selection\n            setTimeout(function () {\n                this.checkState();\n            }.bind(this), 0);\n        },\n\n        handleEditableKeyup: function () {\n            this.checkState();\n        },\n\n        handleBlur: function () {\n            // Kill any previously delayed calls to hide the toolbar\n            clearTimeout(this.hideTimeout);\n\n            // Blur may fire even if we have a selection, so we want to prevent any delayed showToolbar\n            // calls from happening in this specific case\n            clearTimeout(this.delayShowTimeout);\n\n            // Delay the call to hideToolbar to handle bug with multiple editors on the page at once\n            this.hideTimeout = setTimeout(function () {\n                this.hideToolbar();\n            }.bind(this), 1);\n        },\n\n        handleFocus: function () {\n            this.checkState();\n        },\n\n        // Hiding/showing toolbar\n\n        isDisplayed: function () {\n            return this.getToolbarElement().classList.contains('medium-editor-toolbar-active');\n        },\n\n        showToolbar: function () {\n            clearTimeout(this.hideTimeout);\n            if (!this.isDisplayed()) {\n                this.getToolbarElement().classList.add('medium-editor-toolbar-active');\n                this.trigger('showToolbar', {}, this.base.getFocusedElement());\n            }\n        },\n\n        hideToolbar: function () {\n            if (this.isDisplayed()) {\n                this.getToolbarElement().classList.remove('medium-editor-toolbar-active');\n                this.trigger('hideToolbar', {}, this.base.getFocusedElement());\n            }\n        },\n\n        isToolbarDefaultActionsDisplayed: function () {\n            return this.getToolbarActionsElement().style.display === 'block';\n        },\n\n        hideToolbarDefaultActions: function () {\n            if (this.isToolbarDefaultActionsDisplayed()) {\n                this.getToolbarActionsElement().style.display = 'none';\n            }\n        },\n\n        showToolbarDefaultActions: function () {\n            this.hideExtensionForms();\n\n            if (!this.isToolbarDefaultActionsDisplayed()) {\n                this.getToolbarActionsElement().style.display = 'block';\n            }\n\n            // Using setTimeout + options.delay because:\n            // We will actually be displaying the toolbar, which should be controlled by options.delay\n            this.delayShowTimeout = this.base.delay(function () {\n                this.showToolbar();\n            }.bind(this));\n        },\n\n        hideExtensionForms: function () {\n            // Hide all extension forms\n            this.forEachExtension(function (extension) {\n                if (extension.hasForm && extension.isDisplayed()) {\n                    extension.hideForm();\n                }\n            });\n        },\n\n        // Responding to changes in user selection\n\n        // Checks for existance of multiple block elements in the current selection\n        multipleBlockElementsSelected: function () {\n            var regexEmptyHTMLTags = /<[^\\/>][^>]*><\\/[^>]+>/gim, // http://stackoverflow.com/questions/3129738/remove-empty-tags-using-regex\n                regexBlockElements = new RegExp('<(' + MediumEditor.util.blockContainerElementNames.join('|') + ')[^>]*>', 'g'),\n                selectionHTML = MediumEditor.selection.getSelectionHtml(this.document).replace(regexEmptyHTMLTags, ''), // Filter out empty blocks from selection\n                hasMultiParagraphs = selectionHTML.match(regexBlockElements); // Find how many block elements are within the html\n\n            return !!hasMultiParagraphs && hasMultiParagraphs.length > 1;\n        },\n\n        modifySelection: function () {\n            var selection = this.window.getSelection(),\n                selectionRange = selection.getRangeAt(0);\n\n            /*\n            * In firefox, there are cases (ie doubleclick of a word) where the selectionRange start\n            * will be at the very end of an element.  In other browsers, the selectionRange start\n            * would instead be at the very beginning of an element that actually has content.\n            * example:\n            *   <span>foo</span><span>bar</span>\n            *\n            * If the text 'bar' is selected, most browsers will have the selectionRange start at the beginning\n            * of the 'bar' span.  However, there are cases where firefox will have the selectionRange start\n            * at the end of the 'foo' span.  The contenteditable behavior will be ok, but if there are any\n            * properties on the 'bar' span, they won't be reflected accurately in the toolbar\n            * (ie 'Bold' button wouldn't be active)\n            *\n            * So, for cases where the selectionRange start is at the end of an element/node, find the next\n            * adjacent text node that actually has content in it, and move the selectionRange start there.\n            */\n            if (this.standardizeSelectionStart &&\n                    selectionRange.startContainer.nodeValue &&\n                    (selectionRange.startOffset === selectionRange.startContainer.nodeValue.length)) {\n                var adjacentNode = MediumEditor.util.findAdjacentTextNodeWithContent(MediumEditor.selection.getSelectionElement(this.window), selectionRange.startContainer, this.document);\n                if (adjacentNode) {\n                    var offset = 0;\n                    while (adjacentNode.nodeValue.substr(offset, 1).trim().length === 0) {\n                        offset = offset + 1;\n                    }\n                    selectionRange = MediumEditor.selection.select(this.document, adjacentNode, offset,\n                        selectionRange.endContainer, selectionRange.endOffset);\n                }\n            }\n        },\n\n        checkState: function () {\n            if (this.base.preventSelectionUpdates) {\n                return;\n            }\n\n            // If no editable has focus OR selection is inside contenteditable = false\n            // hide toolbar\n            if (!this.base.getFocusedElement() ||\n                    MediumEditor.selection.selectionInContentEditableFalse(this.window)) {\n                return this.hideToolbar();\n            }\n\n            // If there's no selection element, selection element doesn't belong to this editor\n            // or toolbar is disabled for this selection element\n            // hide toolbar\n            var selectionElement = MediumEditor.selection.getSelectionElement(this.window);\n            if (!selectionElement ||\n                    this.getEditorElements().indexOf(selectionElement) === -1 ||\n                    selectionElement.getAttribute('data-disable-toolbar')) {\n                return this.hideToolbar();\n            }\n\n            // Now we know there's a focused editable with a selection\n\n            // If the updateOnEmptySelection option is true, show the toolbar\n            if (this.updateOnEmptySelection && this.static) {\n                return this.showAndUpdateToolbar();\n            }\n\n            // If we don't have a 'valid' selection -> hide toolbar\n            if (!MediumEditor.selection.selectionContainsContent(this.document) ||\n                (this.allowMultiParagraphSelection === false && this.multipleBlockElementsSelected())) {\n                return this.hideToolbar();\n            }\n\n            this.showAndUpdateToolbar();\n        },\n\n        // Updating the toolbar\n\n        showAndUpdateToolbar: function () {\n            this.modifySelection();\n            this.setToolbarButtonStates();\n            this.trigger('positionToolbar', {}, this.base.getFocusedElement());\n            this.showToolbarDefaultActions();\n            this.setToolbarPosition();\n        },\n\n        setToolbarButtonStates: function () {\n            this.forEachExtension(function (extension) {\n                if (typeof extension.isActive === 'function' &&\n                    typeof extension.setInactive === 'function') {\n                    extension.setInactive();\n                }\n            });\n\n            this.checkActiveButtons();\n        },\n\n        checkActiveButtons: function () {\n            var manualStateChecks = [],\n                queryState = null,\n                selectionRange = MediumEditor.selection.getSelectionRange(this.document),\n                parentNode,\n                updateExtensionState = function (extension) {\n                    if (typeof extension.checkState === 'function') {\n                        extension.checkState(parentNode);\n                    } else if (typeof extension.isActive === 'function' &&\n                               typeof extension.isAlreadyApplied === 'function' &&\n                               typeof extension.setActive === 'function') {\n                        if (!extension.isActive() && extension.isAlreadyApplied(parentNode)) {\n                            extension.setActive();\n                        }\n                    }\n                };\n\n            if (!selectionRange) {\n                return;\n            }\n\n            // Loop through all extensions\n            this.forEachExtension(function (extension) {\n                // For those extensions where we can use document.queryCommandState(), do so\n                if (typeof extension.queryCommandState === 'function') {\n                    queryState = extension.queryCommandState();\n                    // If queryCommandState returns a valid value, we can trust the browser\n                    // and don't need to do our manual checks\n                    if (queryState !== null) {\n                        if (queryState && typeof extension.setActive === 'function') {\n                            extension.setActive();\n                        }\n                        return;\n                    }\n                }\n                // We can't use queryCommandState for this extension, so add to manualStateChecks\n                manualStateChecks.push(extension);\n            });\n\n            parentNode = MediumEditor.selection.getSelectedParentElement(selectionRange);\n\n            // Make sure the selection parent isn't outside of the contenteditable\n            if (!this.getEditorElements().some(function (element) {\n                    return MediumEditor.util.isDescendant(element, parentNode, true);\n                })) {\n                return;\n            }\n\n            // Climb up the DOM and do manual checks for whether a certain extension is currently enabled for this node\n            while (parentNode) {\n                manualStateChecks.forEach(updateExtensionState);\n\n                // we can abort the search upwards if we leave the contentEditable element\n                if (MediumEditor.util.isMediumEditorElement(parentNode)) {\n                    break;\n                }\n                parentNode = parentNode.parentNode;\n            }\n        },\n\n        // Positioning toolbar\n\n        positionToolbarIfShown: function () {\n            if (this.isDisplayed()) {\n                this.setToolbarPosition();\n            }\n        },\n\n        setToolbarPosition: function () {\n            var container = this.base.getFocusedElement(),\n                selection = this.window.getSelection();\n\n            // If there isn't a valid selection, bail\n            if (!container) {\n                return this;\n            }\n\n            if (this.static || !selection.isCollapsed) {\n                this.showToolbar();\n\n                // we don't need any absolute positioning if relativeContainer is set\n                if (!this.relativeContainer) {\n                    if (this.static) {\n                        this.positionStaticToolbar(container);\n                    } else {\n                        this.positionToolbar(selection);\n                    }\n                }\n\n                this.trigger('positionedToolbar', {}, this.base.getFocusedElement());\n            }\n        },\n\n        positionStaticToolbar: function (container) {\n            // position the toolbar at left 0, so we can get the real width of the toolbar\n            this.getToolbarElement().style.left = '0';\n\n            // document.documentElement for IE 9\n            var scrollTop = (this.document.documentElement && this.document.documentElement.scrollTop) || this.document.body.scrollTop,\n                windowWidth = this.window.innerWidth,\n                toolbarElement = this.getToolbarElement(),\n                containerRect = container.getBoundingClientRect(),\n                containerTop = containerRect.top + scrollTop,\n                containerCenter = (containerRect.left + (containerRect.width / 2)),\n                toolbarHeight = toolbarElement.offsetHeight,\n                toolbarWidth = toolbarElement.offsetWidth,\n                halfOffsetWidth = toolbarWidth / 2,\n                targetLeft;\n\n            if (this.sticky) {\n                // If it's beyond the height of the editor, position it at the bottom of the editor\n                if (scrollTop > (containerTop + container.offsetHeight - toolbarHeight - this.stickyTopOffset)) {\n                    toolbarElement.style.top = (containerTop + container.offsetHeight - toolbarHeight) + 'px';\n                    toolbarElement.classList.remove('medium-editor-sticky-toolbar');\n                // Stick the toolbar to the top of the window\n                } else if (scrollTop > (containerTop - toolbarHeight - this.stickyTopOffset)) {\n                    toolbarElement.classList.add('medium-editor-sticky-toolbar');\n                    toolbarElement.style.top = this.stickyTopOffset + 'px';\n                // Normal static toolbar position\n                } else {\n                    toolbarElement.classList.remove('medium-editor-sticky-toolbar');\n                    toolbarElement.style.top = containerTop - toolbarHeight + 'px';\n                }\n            } else {\n                toolbarElement.style.top = containerTop - toolbarHeight + 'px';\n            }\n\n            switch (this.align) {\n                case 'left':\n                    targetLeft = containerRect.left;\n                    break;\n\n                case 'right':\n                    targetLeft = containerRect.right - toolbarWidth;\n                    break;\n\n                case 'center':\n                    targetLeft = containerCenter - halfOffsetWidth;\n                    break;\n            }\n\n            if (targetLeft < 0) {\n                targetLeft = 0;\n            } else if ((targetLeft + toolbarWidth) > windowWidth) {\n                targetLeft = (windowWidth - Math.ceil(toolbarWidth) - 1);\n            }\n\n            toolbarElement.style.left = targetLeft + 'px';\n        },\n\n        positionToolbar: function (selection) {\n            // position the toolbar at left 0, so we can get the real width of the toolbar\n            this.getToolbarElement().style.left = '0';\n            this.getToolbarElement().style.right = 'initial';\n\n            var range = selection.getRangeAt(0),\n                boundary = range.getBoundingClientRect();\n\n            // Handle selections with just images\n            if (!boundary || ((boundary.height === 0 && boundary.width === 0) && range.startContainer === range.endContainer)) {\n                // If there's a nested image, use that for the bounding rectangle\n                if (range.startContainer.nodeType === 1 && range.startContainer.querySelector('img')) {\n                    boundary = range.startContainer.querySelector('img').getBoundingClientRect();\n                } else {\n                    boundary = range.startContainer.getBoundingClientRect();\n                }\n            }\n\n            var containerWidth = this.window.innerWidth,\n                toolbarElement = this.getToolbarElement(),\n                toolbarHeight = toolbarElement.offsetHeight,\n                toolbarWidth = toolbarElement.offsetWidth,\n                halfOffsetWidth = toolbarWidth / 2,\n                buttonHeight = 50,\n                defaultLeft = this.diffLeft - halfOffsetWidth,\n                elementsContainer = this.getEditorOption('elementsContainer'),\n                elementsContainerAbsolute = ['absolute', 'fixed'].indexOf(window.getComputedStyle(elementsContainer).getPropertyValue('position')) > -1,\n                positions = {},\n                relativeBoundary = {},\n                middleBoundary, elementsContainerBoundary;\n\n            // If container element is absolute / fixed, recalculate boundaries to be relative to the container\n            if (elementsContainerAbsolute) {\n                elementsContainerBoundary = elementsContainer.getBoundingClientRect();\n                ['top', 'left'].forEach(function (key) {\n                    relativeBoundary[key] = boundary[key] - elementsContainerBoundary[key];\n                });\n\n                relativeBoundary.width = boundary.width;\n                relativeBoundary.height = boundary.height;\n                boundary = relativeBoundary;\n\n                containerWidth = elementsContainerBoundary.width;\n\n                // Adjust top position according to container scroll position\n                positions.top = elementsContainer.scrollTop;\n            } else {\n                // Adjust top position according to window scroll position\n                positions.top = this.window.pageYOffset;\n            }\n\n            middleBoundary = boundary.left + boundary.width / 2;\n            positions.top += boundary.top - toolbarHeight;\n\n            if (boundary.top < buttonHeight) {\n                toolbarElement.classList.add('medium-toolbar-arrow-over');\n                toolbarElement.classList.remove('medium-toolbar-arrow-under');\n                positions.top += buttonHeight + boundary.height - this.diffTop;\n            } else {\n                toolbarElement.classList.add('medium-toolbar-arrow-under');\n                toolbarElement.classList.remove('medium-toolbar-arrow-over');\n                positions.top += this.diffTop;\n            }\n\n            if (middleBoundary < halfOffsetWidth) {\n                positions.left = defaultLeft + halfOffsetWidth;\n                positions.right = 'initial';\n            } else if ((containerWidth - middleBoundary) < halfOffsetWidth) {\n                positions.left = 'auto';\n                positions.right = 0;\n            } else {\n                positions.left = defaultLeft + middleBoundary;\n                positions.right = 'initial';\n            }\n\n            ['top', 'left', 'right'].forEach(function (key) {\n                toolbarElement.style[key] = positions[key] + (isNaN(positions[key]) ? '' : 'px');\n            });\n        }\n    });\n\n    MediumEditor.extensions.toolbar = Toolbar;\n}());\n"
  },
  {
    "path": "src/js/globals.js",
    "content": "/*jshint unused: false */\nfunction MediumEditor(elements, options) {\n    'use strict';\n    return this.init(elements, options);\n}\n\nMediumEditor.extensions = {};\n/*jshint unused: true */"
  },
  {
    "path": "src/js/polyfills.js",
    "content": "/*\n * classList.js: Cross-browser full element.classList implementation.\n * 2014-12-13\n *\n * By Eli Grey, http://eligrey.com\n * Public Domain.\n * NO WARRANTY EXPRESSED OR IMPLIED. USE AT YOUR OWN RISK.\n */\n\n/*global self, document, DOMException */\n\n/*! @source http://purl.eligrey.com/github/classList.js/blob/master/classList.js */\n\n// Full polyfill for browsers with no classList support\nif (!(\"classList\" in document.createElement(\"_\"))) {\n  (function (view) {\n\n  \"use strict\";\n\n  if (!('Element' in view)) return;\n\n  var\n      classListProp = \"classList\"\n    , protoProp = \"prototype\"\n    , elemCtrProto = view.Element[protoProp]\n    , objCtr = Object\n    , strTrim = String[protoProp].trim || function () {\n      return this.replace(/^\\s+|\\s+$/g, \"\");\n    }\n    , arrIndexOf = Array[protoProp].indexOf || function (item) {\n      var\n          i = 0\n        , len = this.length\n      ;\n      for (; i < len; i++) {\n        if (i in this && this[i] === item) {\n          return i;\n        }\n      }\n      return -1;\n    }\n    // Vendors: please allow content code to instantiate DOMExceptions\n    , DOMEx = function (type, message) {\n      this.name = type;\n      this.code = DOMException[type];\n      this.message = message;\n    }\n    , checkTokenAndGetIndex = function (classList, token) {\n      if (token === \"\") {\n        throw new DOMEx(\n            \"SYNTAX_ERR\"\n          , \"An invalid or illegal string was specified\"\n        );\n      }\n      if (/\\s/.test(token)) {\n        throw new DOMEx(\n            \"INVALID_CHARACTER_ERR\"\n          , \"String contains an invalid character\"\n        );\n      }\n      return arrIndexOf.call(classList, token);\n    }\n    , ClassList = function (elem) {\n      var\n          trimmedClasses = strTrim.call(elem.getAttribute(\"class\") || \"\")\n        , classes = trimmedClasses ? trimmedClasses.split(/\\s+/) : []\n        , i = 0\n        , len = classes.length\n      ;\n      for (; i < len; i++) {\n        this.push(classes[i]);\n      }\n      this._updateClassName = function () {\n        elem.setAttribute(\"class\", this.toString());\n      };\n    }\n    , classListProto = ClassList[protoProp] = []\n    , classListGetter = function () {\n      return new ClassList(this);\n    }\n  ;\n  // Most DOMException implementations don't allow calling DOMException's toString()\n  // on non-DOMExceptions. Error's toString() is sufficient here.\n  DOMEx[protoProp] = Error[protoProp];\n  classListProto.item = function (i) {\n    return this[i] || null;\n  };\n  classListProto.contains = function (token) {\n    token += \"\";\n    return checkTokenAndGetIndex(this, token) !== -1;\n  };\n  classListProto.add = function () {\n    var\n        tokens = arguments\n      , i = 0\n      , l = tokens.length\n      , token\n      , updated = false\n    ;\n    do {\n      token = tokens[i] + \"\";\n      if (checkTokenAndGetIndex(this, token) === -1) {\n        this.push(token);\n        updated = true;\n      }\n    }\n    while (++i < l);\n\n    if (updated) {\n      this._updateClassName();\n    }\n  };\n  classListProto.remove = function () {\n    var\n        tokens = arguments\n      , i = 0\n      , l = tokens.length\n      , token\n      , updated = false\n      , index\n    ;\n    do {\n      token = tokens[i] + \"\";\n      index = checkTokenAndGetIndex(this, token);\n      while (index !== -1) {\n        this.splice(index, 1);\n        updated = true;\n        index = checkTokenAndGetIndex(this, token);\n      }\n    }\n    while (++i < l);\n\n    if (updated) {\n      this._updateClassName();\n    }\n  };\n  classListProto.toggle = function (token, force) {\n    token += \"\";\n\n    var\n        result = this.contains(token)\n      , method = result ?\n        force !== true && \"remove\"\n      :\n        force !== false && \"add\"\n    ;\n\n    if (method) {\n      this[method](token);\n    }\n\n    if (force === true || force === false) {\n      return force;\n    } else {\n      return !result;\n    }\n  };\n  classListProto.toString = function () {\n    return this.join(\" \");\n  };\n\n  if (objCtr.defineProperty) {\n    var classListPropDesc = {\n        get: classListGetter\n      , enumerable: true\n      , configurable: true\n    };\n    try {\n      objCtr.defineProperty(elemCtrProto, classListProp, classListPropDesc);\n    } catch (ex) { // IE 8 doesn't support enumerable:true\n      if (ex.number === -0x7FF5EC54) {\n        classListPropDesc.enumerable = false;\n        objCtr.defineProperty(elemCtrProto, classListProp, classListPropDesc);\n      }\n    }\n  } else if (objCtr[protoProp].__defineGetter__) {\n    elemCtrProto.__defineGetter__(classListProp, classListGetter);\n  }\n\n  }(self));\n}\n\n/* Blob.js\n * A Blob implementation.\n * 2014-07-24\n *\n * By Eli Grey, http://eligrey.com\n * By Devin Samarin, https://github.com/dsamarin\n * License: X11/MIT\n *   See https://github.com/eligrey/Blob.js/blob/master/LICENSE.md\n */\n\n/*global self, unescape */\n/*jslint bitwise: true, regexp: true, confusion: true, es5: true, vars: true, white: true,\n  plusplus: true */\n\n/*! @source http://purl.eligrey.com/github/Blob.js/blob/master/Blob.js */\n\n(function (view) {\n  \"use strict\";\n\n  view.URL = view.URL || view.webkitURL;\n\n  if (view.Blob && view.URL) {\n    try {\n      new Blob;\n      return;\n    } catch (e) {}\n  }\n\n  // Internally we use a BlobBuilder implementation to base Blob off of\n  // in order to support older browsers that only have BlobBuilder\n  var BlobBuilder = view.BlobBuilder || view.WebKitBlobBuilder || view.MozBlobBuilder || (function(view) {\n    var\n        get_class = function(object) {\n        return Object.prototype.toString.call(object).match(/^\\[object\\s(.*)\\]$/)[1];\n      }\n      , FakeBlobBuilder = function BlobBuilder() {\n        this.data = [];\n      }\n      , FakeBlob = function Blob(data, type, encoding) {\n        this.data = data;\n        this.size = data.length;\n        this.type = type;\n        this.encoding = encoding;\n      }\n      , FBB_proto = FakeBlobBuilder.prototype\n      , FB_proto = FakeBlob.prototype\n      , FileReaderSync = view.FileReaderSync\n      , FileException = function(type) {\n        this.code = this[this.name = type];\n      }\n      , file_ex_codes = (\n          \"NOT_FOUND_ERR SECURITY_ERR ABORT_ERR NOT_READABLE_ERR ENCODING_ERR \"\n        + \"NO_MODIFICATION_ALLOWED_ERR INVALID_STATE_ERR SYNTAX_ERR\"\n      ).split(\" \")\n      , file_ex_code = file_ex_codes.length\n      , real_URL = view.URL || view.webkitURL || view\n      , real_create_object_URL = real_URL.createObjectURL\n      , real_revoke_object_URL = real_URL.revokeObjectURL\n      , URL = real_URL\n      , btoa = view.btoa\n      , atob = view.atob\n\n      , ArrayBuffer = view.ArrayBuffer\n      , Uint8Array = view.Uint8Array\n\n      , origin = /^[\\w-]+:\\/*\\[?[\\w\\.:-]+\\]?(?::[0-9]+)?/\n    ;\n    FakeBlob.fake = FB_proto.fake = true;\n    while (file_ex_code--) {\n      FileException.prototype[file_ex_codes[file_ex_code]] = file_ex_code + 1;\n    }\n    // Polyfill URL\n    if (!real_URL.createObjectURL) {\n      URL = view.URL = function(uri) {\n        var\n            uri_info = document.createElementNS(\"http://www.w3.org/1999/xhtml\", \"a\")\n          , uri_origin\n        ;\n        uri_info.href = uri;\n        if (!(\"origin\" in uri_info)) {\n          if (uri_info.protocol.toLowerCase() === \"data:\") {\n            uri_info.origin = null;\n          } else {\n            uri_origin = uri.match(origin);\n            uri_info.origin = uri_origin && uri_origin[1];\n          }\n        }\n        return uri_info;\n      };\n    }\n    URL.createObjectURL = function(blob) {\n      var\n          type = blob.type\n        , data_URI_header\n      ;\n      if (type === null) {\n        type = \"application/octet-stream\";\n      }\n      if (blob instanceof FakeBlob) {\n        data_URI_header = \"data:\" + type;\n        if (blob.encoding === \"base64\") {\n          return data_URI_header + \";base64,\" + blob.data;\n        } else if (blob.encoding === \"URI\") {\n          return data_URI_header + \",\" + decodeURIComponent(blob.data);\n        } if (btoa) {\n          return data_URI_header + \";base64,\" + btoa(blob.data);\n        } else {\n          return data_URI_header + \",\" + encodeURIComponent(blob.data);\n        }\n      } else if (real_create_object_URL) {\n        return real_create_object_URL.call(real_URL, blob);\n      }\n    };\n    URL.revokeObjectURL = function(object_URL) {\n      if (object_URL.substring(0, 5) !== \"data:\" && real_revoke_object_URL) {\n        real_revoke_object_URL.call(real_URL, object_URL);\n      }\n    };\n    FBB_proto.append = function(data/*, endings*/) {\n      var bb = this.data;\n      // decode data to a binary string\n      if (Uint8Array && (data instanceof ArrayBuffer || data instanceof Uint8Array)) {\n        var\n            str = \"\"\n          , buf = new Uint8Array(data)\n          , i = 0\n          , buf_len = buf.length\n        ;\n        for (; i < buf_len; i++) {\n          str += String.fromCharCode(buf[i]);\n        }\n        bb.push(str);\n      } else if (get_class(data) === \"Blob\" || get_class(data) === \"File\") {\n        if (FileReaderSync) {\n          var fr = new FileReaderSync;\n          bb.push(fr.readAsBinaryString(data));\n        } else {\n          // async FileReader won't work as BlobBuilder is sync\n          throw new FileException(\"NOT_READABLE_ERR\");\n        }\n      } else if (data instanceof FakeBlob) {\n        if (data.encoding === \"base64\" && atob) {\n          bb.push(atob(data.data));\n        } else if (data.encoding === \"URI\") {\n          bb.push(decodeURIComponent(data.data));\n        } else if (data.encoding === \"raw\") {\n          bb.push(data.data);\n        }\n      } else {\n        if (typeof data !== \"string\") {\n          data += \"\"; // convert unsupported types to strings\n        }\n        // decode UTF-16 to binary string\n        bb.push(unescape(encodeURIComponent(data)));\n      }\n    };\n    FBB_proto.getBlob = function(type) {\n      if (!arguments.length) {\n        type = null;\n      }\n      return new FakeBlob(this.data.join(\"\"), type, \"raw\");\n    };\n    FBB_proto.toString = function() {\n      return \"[object BlobBuilder]\";\n    };\n    FB_proto.slice = function(start, end, type) {\n      var args = arguments.length;\n      if (args < 3) {\n        type = null;\n      }\n      return new FakeBlob(\n          this.data.slice(start, args > 1 ? end : this.data.length)\n        , type\n        , this.encoding\n      );\n    };\n    FB_proto.toString = function() {\n      return \"[object Blob]\";\n    };\n    FB_proto.close = function() {\n      this.size = 0;\n      delete this.data;\n    };\n    return FakeBlobBuilder;\n  }(view));\n\n  view.Blob = function(blobParts, options) {\n    var type = options ? (options.type || \"\") : \"\";\n    var builder = new BlobBuilder();\n    if (blobParts) {\n      for (var i = 0, len = blobParts.length; i < len; i++) {\n        if (Uint8Array && blobParts[i] instanceof Uint8Array) {\n          builder.append(blobParts[i].buffer);\n        }\n        else {\n          builder.append(blobParts[i]);\n        }\n      }\n    }\n    var blob = builder.getBlob(type);\n    if (!blob.slice && blob.webkitSlice) {\n      blob.slice = blob.webkitSlice;\n    }\n    return blob;\n  };\n\n  var getPrototypeOf = Object.getPrototypeOf || function(object) {\n    return object.__proto__;\n  };\n  view.Blob.prototype = getPrototypeOf(new view.Blob());\n}(typeof self !== \"undefined\" && self || typeof window !== \"undefined\" && window || this.content || this));\n"
  },
  {
    "path": "src/js/selection.js",
    "content": "(function () {\n    'use strict';\n\n    function filterOnlyParentElements(node) {\n        if (MediumEditor.util.isBlockContainer(node)) {\n            return NodeFilter.FILTER_ACCEPT;\n        } else {\n            return NodeFilter.FILTER_SKIP;\n        }\n    }\n\n    var Selection = {\n        findMatchingSelectionParent: function (testElementFunction, contentWindow) {\n            var selection = contentWindow.getSelection(),\n                range,\n                current;\n\n            if (selection.rangeCount === 0) {\n                return false;\n            }\n\n            range = selection.getRangeAt(0);\n            current = range.commonAncestorContainer;\n\n            return MediumEditor.util.traverseUp(current, testElementFunction);\n        },\n\n        getSelectionElement: function (contentWindow) {\n            return this.findMatchingSelectionParent(function (el) {\n                return MediumEditor.util.isMediumEditorElement(el);\n            }, contentWindow);\n        },\n\n        // http://stackoverflow.com/questions/17678843/cant-restore-selection-after-html-modify-even-if-its-the-same-html\n        // Tim Down\n        exportSelection: function (root, doc) {\n            if (!root) {\n                return null;\n            }\n\n            var selectionState = null,\n                selection = doc.getSelection();\n\n            if (selection.rangeCount > 0) {\n                var range = selection.getRangeAt(0),\n                    preSelectionRange = range.cloneRange(),\n                    start;\n\n                preSelectionRange.selectNodeContents(root);\n                preSelectionRange.setEnd(range.startContainer, range.startOffset);\n                start = preSelectionRange.toString().length;\n\n                selectionState = {\n                    start: start,\n                    end: start + range.toString().length\n                };\n\n                // Check to see if the selection starts with any images\n                // if so we need to make sure the the beginning of the selection is\n                // set correctly when importing selection\n                if (this.doesRangeStartWithImages(range, doc)) {\n                    selectionState.startsWithImage = true;\n                }\n\n                // Check to see if the selection has any trailing images\n                // if so, this this means we need to look for them when we import selection\n                var trailingImageCount = this.getTrailingImageCount(root, selectionState, range.endContainer, range.endOffset);\n                if (trailingImageCount) {\n                    selectionState.trailingImageCount = trailingImageCount;\n                }\n\n                // If start = 0 there may still be an empty paragraph before it, but we don't care.\n                if (start !== 0) {\n                    var emptyBlocksIndex = this.getIndexRelativeToAdjacentEmptyBlocks(doc, root, range.startContainer, range.startOffset);\n                    if (emptyBlocksIndex !== -1) {\n                        selectionState.emptyBlocksIndex = emptyBlocksIndex;\n                    }\n                }\n            }\n\n            return selectionState;\n        },\n\n        // http://stackoverflow.com/questions/17678843/cant-restore-selection-after-html-modify-even-if-its-the-same-html\n        // Tim Down\n        //\n        // {object} selectionState - the selection to import\n        // {DOMElement} root - the root element the selection is being restored inside of\n        // {Document} doc - the document to use for managing selection\n        // {boolean} [favorLaterSelectionAnchor] - defaults to false. If true, import the cursor immediately\n        //      subsequent to an anchor tag if it would otherwise be placed right at the trailing edge inside the\n        //      anchor. This cursor positioning, even though visually equivalent to the user, can affect behavior\n        //      in MS IE.\n        importSelection: function (selectionState, root, doc, favorLaterSelectionAnchor) {\n            if (!selectionState || !root) {\n                return;\n            }\n\n            var range = doc.createRange();\n            range.setStart(root, 0);\n            range.collapse(true);\n\n            var node = root,\n                nodeStack = [],\n                charIndex = 0,\n                foundStart = false,\n                foundEnd = false,\n                trailingImageCount = 0,\n                stop = false,\n                nextCharIndex,\n                allowRangeToStartAtEndOfNode = false,\n                lastTextNode = null;\n\n            // When importing selection, the start of the selection may lie at the end of an element\n            // or at the beginning of an element.  Since visually there is no difference between these 2\n            // we will try to move the selection to the beginning of an element since this is generally\n            // what users will expect and it's a more predictable behavior.\n            //\n            // However, there are some specific cases when we don't want to do this:\n            //  1) We're attempting to move the cursor outside of the end of an anchor [favorLaterSelectionAnchor = true]\n            //  2) The selection starts with an image, which is special since an image doesn't have any 'content'\n            //     as far as selection and ranges are concerned\n            //  3) The selection starts after a specified number of empty block elements (selectionState.emptyBlocksIndex)\n            //\n            // For these cases, we want the selection to start at a very specific location, so we should NOT\n            // automatically move the cursor to the beginning of the first actual chunk of text\n            if (favorLaterSelectionAnchor || selectionState.startsWithImage || typeof selectionState.emptyBlocksIndex !== 'undefined') {\n                allowRangeToStartAtEndOfNode = true;\n            }\n\n            while (!stop && node) {\n                // Only iterate over elements and text nodes\n                if (node.nodeType > 3) {\n                    node = nodeStack.pop();\n                    continue;\n                }\n\n                // If we hit a text node, we need to add the amount of characters to the overall count\n                if (node.nodeType === 3 && !foundEnd) {\n                    nextCharIndex = charIndex + node.length;\n                    // Check if we're at or beyond the start of the selection we're importing\n                    if (!foundStart && selectionState.start >= charIndex && selectionState.start <= nextCharIndex) {\n                        // NOTE: We only want to allow a selection to start at the END of an element if\n                        //  allowRangeToStartAtEndOfNode is true\n                        if (allowRangeToStartAtEndOfNode || selectionState.start < nextCharIndex) {\n                            range.setStart(node, selectionState.start - charIndex);\n                            foundStart = true;\n                        }\n                        // We're at the end of a text node where the selection could start but we shouldn't\n                        // make the selection start here because allowRangeToStartAtEndOfNode is false.\n                        // However, we should keep a reference to this node in case there aren't any more\n                        // text nodes after this, so that we have somewhere to import the selection to\n                        else {\n                            lastTextNode = node;\n                        }\n                    }\n                    // We've found the start of the selection, check if we're at or beyond the end of the selection we're importing\n                    if (foundStart && selectionState.end >= charIndex && selectionState.end <= nextCharIndex) {\n                        if (!selectionState.trailingImageCount) {\n                            range.setEnd(node, selectionState.end - charIndex);\n                            stop = true;\n                        } else {\n                            foundEnd = true;\n                        }\n                    }\n                    charIndex = nextCharIndex;\n                } else {\n                    if (selectionState.trailingImageCount && foundEnd) {\n                        if (node.nodeName.toLowerCase() === 'img') {\n                            trailingImageCount++;\n                        }\n                        if (trailingImageCount === selectionState.trailingImageCount) {\n                            // Find which index the image is in its parent's children\n                            var endIndex = 0;\n                            while (node.parentNode.childNodes[endIndex] !== node) {\n                                endIndex++;\n                            }\n                            range.setEnd(node.parentNode, endIndex + 1);\n                            stop = true;\n                        }\n                    }\n\n                    if (!stop && node.nodeType === 1) {\n                        // this is an element\n                        // add all its children to the stack\n                        var i = node.childNodes.length - 1;\n                        while (i >= 0) {\n                            nodeStack.push(node.childNodes[i]);\n                            i -= 1;\n                        }\n                    }\n                }\n\n                if (!stop) {\n                    node = nodeStack.pop();\n                }\n            }\n\n            // If we've gone through the entire text but didn't find the beginning of a text node\n            // to make the selection start at, we should fall back to starting the selection\n            // at the END of the last text node we found\n            if (!foundStart && lastTextNode) {\n                range.setStart(lastTextNode, lastTextNode.length);\n                range.setEnd(lastTextNode, lastTextNode.length);\n            }\n\n            if (typeof selectionState.emptyBlocksIndex !== 'undefined') {\n                range = this.importSelectionMoveCursorPastBlocks(doc, root, selectionState.emptyBlocksIndex, range);\n            }\n\n            // If the selection is right at the ending edge of a link, put it outside the anchor tag instead of inside.\n            if (favorLaterSelectionAnchor) {\n                range = this.importSelectionMoveCursorPastAnchor(selectionState, range);\n            }\n\n            this.selectRange(doc, range);\n        },\n\n        // Utility method called from importSelection only\n        importSelectionMoveCursorPastAnchor: function (selectionState, range) {\n            var nodeInsideAnchorTagFunction = function (node) {\n                return node.nodeName.toLowerCase() === 'a';\n            };\n            if (selectionState.start === selectionState.end &&\n                    range.startContainer.nodeType === 3 &&\n                    range.startOffset === range.startContainer.nodeValue.length &&\n                    MediumEditor.util.traverseUp(range.startContainer, nodeInsideAnchorTagFunction)) {\n                var prevNode = range.startContainer,\n                    currentNode = range.startContainer.parentNode;\n                while (currentNode !== null && currentNode.nodeName.toLowerCase() !== 'a') {\n                    if (currentNode.childNodes[currentNode.childNodes.length - 1] !== prevNode) {\n                        currentNode = null;\n                    } else {\n                        prevNode = currentNode;\n                        currentNode = currentNode.parentNode;\n                    }\n                }\n                if (currentNode !== null && currentNode.nodeName.toLowerCase() === 'a') {\n                    var currentNodeIndex = null;\n                    for (var i = 0; currentNodeIndex === null && i < currentNode.parentNode.childNodes.length; i++) {\n                        if (currentNode.parentNode.childNodes[i] === currentNode) {\n                            currentNodeIndex = i;\n                        }\n                    }\n                    range.setStart(currentNode.parentNode, currentNodeIndex + 1);\n                    range.collapse(true);\n                }\n            }\n            return range;\n        },\n\n        // Uses the emptyBlocksIndex calculated by getIndexRelativeToAdjacentEmptyBlocks\n        // to move the cursor back to the start of the correct paragraph\n        importSelectionMoveCursorPastBlocks: function (doc, root, index, range) {\n            var treeWalker = doc.createTreeWalker(root, NodeFilter.SHOW_ELEMENT, filterOnlyParentElements, false),\n                startContainer = range.startContainer,\n                startBlock,\n                targetNode,\n                currIndex = 0;\n            index = index || 1; // If index is 0, we still want to move to the next block\n\n            // Chrome counts newlines and spaces that separate block elements as actual elements.\n            // If the selection is inside one of these text nodes, and it has a previous sibling\n            // which is a block element, we want the treewalker to start at the previous sibling\n            // and NOT at the parent of the textnode\n            if (startContainer.nodeType === 3 && MediumEditor.util.isBlockContainer(startContainer.previousSibling)) {\n                startBlock = startContainer.previousSibling;\n            } else {\n                startBlock = MediumEditor.util.getClosestBlockContainer(startContainer);\n            }\n\n            // Skip over empty blocks until we hit the block we want the selection to be in\n            while (treeWalker.nextNode()) {\n                if (!targetNode) {\n                    // Loop through all blocks until we hit the starting block element\n                    if (startBlock === treeWalker.currentNode) {\n                        targetNode = treeWalker.currentNode;\n                    }\n                } else {\n                    targetNode = treeWalker.currentNode;\n                    currIndex++;\n                    // We hit the target index, bail\n                    if (currIndex === index) {\n                        break;\n                    }\n                    // If we find a non-empty block, ignore the emptyBlocksIndex and just put selection here\n                    if (targetNode.textContent.length > 0) {\n                        break;\n                    }\n                }\n            }\n\n            if (!targetNode) {\n                targetNode = startBlock;\n            }\n\n            // We're selecting a high-level block node, so make sure the cursor gets moved into the deepest\n            // element at the beginning of the block\n            range.setStart(MediumEditor.util.getFirstSelectableLeafNode(targetNode), 0);\n\n            return range;\n        },\n\n        // Returns -1 unless the cursor is at the beginning of a paragraph/block\n        // If the paragraph/block is preceeded by empty paragraphs/block (with no text)\n        // it will return the number of empty paragraphs before the cursor.\n        // Otherwise, it will return 0, which indicates the cursor is at the beginning\n        // of a paragraph/block, and not at the end of the paragraph/block before it\n        getIndexRelativeToAdjacentEmptyBlocks: function (doc, root, cursorContainer, cursorOffset) {\n            // If there is text in front of the cursor, that means there isn't only empty blocks before it\n            if (cursorContainer.textContent.length > 0 && cursorOffset > 0) {\n                return -1;\n            }\n\n            // Check if the block that contains the cursor has any other text in front of the cursor\n            var node = cursorContainer;\n            if (node.nodeType !== 3) {\n                node = cursorContainer.childNodes[cursorOffset];\n            }\n            if (node) {\n                // The element isn't at the beginning of a block, so it has content before it\n                if (!MediumEditor.util.isElementAtBeginningOfBlock(node)) {\n                    return -1;\n                }\n\n                var previousSibling = MediumEditor.util.findPreviousSibling(node);\n                // If there is no previous sibling, this is the first text element in the editor\n                if (!previousSibling) {\n                    return -1;\n                }\n                // If the previous sibling has text, then there are no empty blocks before this\n                else if (previousSibling.nodeValue) {\n                    return -1;\n                }\n            }\n\n            // Walk over block elements, counting number of empty blocks between last piece of text\n            // and the block the cursor is in\n            var closestBlock = MediumEditor.util.getClosestBlockContainer(cursorContainer),\n                treeWalker = doc.createTreeWalker(root, NodeFilter.SHOW_ELEMENT, filterOnlyParentElements, false),\n                emptyBlocksCount = 0;\n            while (treeWalker.nextNode()) {\n                var blockIsEmpty = treeWalker.currentNode.textContent === '';\n                if (blockIsEmpty || emptyBlocksCount > 0) {\n                    emptyBlocksCount += 1;\n                }\n                if (treeWalker.currentNode === closestBlock) {\n                    return emptyBlocksCount;\n                }\n                if (!blockIsEmpty) {\n                    emptyBlocksCount = 0;\n                }\n            }\n\n            return emptyBlocksCount;\n        },\n\n        // Returns true if the selection range begins with an image tag\n        // Returns false if the range starts with any non empty text nodes\n        doesRangeStartWithImages: function (range, doc) {\n            if (range.startOffset !== 0 || range.startContainer.nodeType !== 1) {\n                return false;\n            }\n\n            if (range.startContainer.nodeName.toLowerCase() === 'img') {\n                return true;\n            }\n\n            var img = range.startContainer.querySelector('img');\n            if (!img) {\n                return false;\n            }\n\n            var treeWalker = doc.createTreeWalker(range.startContainer, NodeFilter.SHOW_ALL, null, false);\n            while (treeWalker.nextNode()) {\n                var next = treeWalker.currentNode;\n                // If we hit the image, then there isn't any text before the image so\n                // the image is at the beginning of the range\n                if (next === img) {\n                    break;\n                }\n                // If we haven't hit the iamge, but found text that contains content\n                // then the range doesn't start with an image\n                if (next.nodeValue) {\n                    return false;\n                }\n            }\n\n            return true;\n        },\n\n        getTrailingImageCount: function (root, selectionState, endContainer, endOffset) {\n            // If the endOffset of a range is 0, the endContainer doesn't contain images\n            // If the endContainer is a text node, there are no trailing images\n            if (endOffset === 0 || endContainer.nodeType !== 1) {\n                return 0;\n            }\n\n            // If the endContainer isn't an image, and doesn't have an image descendants\n            // there are no trailing images\n            if (endContainer.nodeName.toLowerCase() !== 'img' && !endContainer.querySelector('img')) {\n                return 0;\n            }\n\n            var lastNode = endContainer.childNodes[endOffset - 1];\n            while (lastNode.hasChildNodes()) {\n                lastNode = lastNode.lastChild;\n            }\n\n            var node = root,\n                nodeStack = [],\n                charIndex = 0,\n                foundStart = false,\n                foundEnd = false,\n                stop = false,\n                nextCharIndex,\n                trailingImages = 0;\n\n            while (!stop && node) {\n                // Only iterate over elements and text nodes\n                if (node.nodeType > 3) {\n                    node = nodeStack.pop();\n                    continue;\n                }\n\n                if (node.nodeType === 3 && !foundEnd) {\n                    trailingImages = 0;\n                    nextCharIndex = charIndex + node.length;\n                    if (!foundStart && selectionState.start >= charIndex && selectionState.start <= nextCharIndex) {\n                        foundStart = true;\n                    }\n                    if (foundStart && selectionState.end >= charIndex && selectionState.end <= nextCharIndex) {\n                        foundEnd = true;\n                    }\n                    charIndex = nextCharIndex;\n                } else {\n                    if (node.nodeName.toLowerCase() === 'img') {\n                        trailingImages++;\n                    }\n\n                    if (node === lastNode) {\n                        stop = true;\n                    } else if (node.nodeType === 1) {\n                        // this is an element\n                        // add all its children to the stack\n                        var i = node.childNodes.length - 1;\n                        while (i >= 0) {\n                            nodeStack.push(node.childNodes[i]);\n                            i -= 1;\n                        }\n                    }\n                }\n\n                if (!stop) {\n                    node = nodeStack.pop();\n                }\n            }\n\n            return trailingImages;\n        },\n\n        // determine if the current selection contains any 'content'\n        // content being any non-white space text or an image\n        selectionContainsContent: function (doc) {\n            var sel = doc.getSelection();\n\n            // collapsed selection or selection withour range doesn't contain content\n            if (!sel || sel.isCollapsed || !sel.rangeCount) {\n                return false;\n            }\n\n            // if toString() contains any text, the selection contains some content\n            if (sel.toString().trim() !== '') {\n                return true;\n            }\n\n            // if selection contains only image(s), it will return empty for toString()\n            // so check for an image manually\n            var selectionNode = this.getSelectedParentElement(sel.getRangeAt(0));\n            if (selectionNode) {\n                if (selectionNode.nodeName.toLowerCase() === 'img' ||\n                    (selectionNode.nodeType === 1 && selectionNode.querySelector('img'))) {\n                    return true;\n                }\n            }\n\n            return false;\n        },\n\n        selectionInContentEditableFalse: function (contentWindow) {\n            // determine if the current selection is exclusively inside\n            // a contenteditable=\"false\", though treat the case of an\n            // explicit contenteditable=\"true\" inside a \"false\" as false.\n            var sawtrue,\n                sawfalse = this.findMatchingSelectionParent(function (el) {\n                    var ce = el && el.getAttribute('contenteditable');\n                    if (ce === 'true') {\n                        sawtrue = true;\n                    }\n                    return el.nodeName !== '#text' && ce === 'false';\n                }, contentWindow);\n\n            return !sawtrue && sawfalse;\n        },\n\n        // http://stackoverflow.com/questions/4176923/html-of-selected-text\n        // by Tim Down\n        getSelectionHtml: function getSelectionHtml(doc) {\n            var i,\n                html = '',\n                sel = doc.getSelection(),\n                len,\n                container;\n            if (sel.rangeCount) {\n                container = doc.createElement('div');\n                for (i = 0, len = sel.rangeCount; i < len; i += 1) {\n                    container.appendChild(sel.getRangeAt(i).cloneContents());\n                }\n                html = container.innerHTML;\n            }\n            return html;\n        },\n\n        /**\n         *  Find the caret position within an element irrespective of any inline tags it may contain.\n         *\n         *  @param {DOMElement} An element containing the cursor to find offsets relative to.\n         *  @param {Range} A Range representing cursor position. Will window.getSelection if none is passed.\n         *  @return {Object} 'left' and 'right' attributes contain offsets from begining and end of Element\n         */\n        getCaretOffsets: function getCaretOffsets(element, range) {\n            var preCaretRange, postCaretRange;\n\n            if (!range) {\n                range = window.getSelection().getRangeAt(0);\n            }\n\n            preCaretRange = range.cloneRange();\n            postCaretRange = range.cloneRange();\n\n            preCaretRange.selectNodeContents(element);\n            preCaretRange.setEnd(range.endContainer, range.endOffset);\n\n            postCaretRange.selectNodeContents(element);\n            postCaretRange.setStart(range.endContainer, range.endOffset);\n\n            return {\n                left: preCaretRange.toString().length,\n                right: postCaretRange.toString().length\n            };\n        },\n\n        // http://stackoverflow.com/questions/15867542/range-object-get-selection-parent-node-chrome-vs-firefox\n        rangeSelectsSingleNode: function (range) {\n            var startNode = range.startContainer;\n            return startNode === range.endContainer &&\n                startNode.hasChildNodes() &&\n                range.endOffset === range.startOffset + 1;\n        },\n\n        getSelectedParentElement: function (range) {\n            if (!range) {\n                return null;\n            }\n\n            // Selection encompasses a single element\n            if (this.rangeSelectsSingleNode(range) && range.startContainer.childNodes[range.startOffset].nodeType !== 3) {\n                return range.startContainer.childNodes[range.startOffset];\n            }\n\n            // Selection range starts inside a text node, so get its parent\n            if (range.startContainer.nodeType === 3) {\n                return range.startContainer.parentNode;\n            }\n\n            // Selection starts inside an element\n            return range.startContainer;\n        },\n\n        getSelectedElements: function (doc) {\n            var selection = doc.getSelection(),\n                range,\n                toRet,\n                currNode;\n\n            if (!selection.rangeCount || selection.isCollapsed || !selection.getRangeAt(0).commonAncestorContainer) {\n                return [];\n            }\n\n            range = selection.getRangeAt(0);\n\n            if (range.commonAncestorContainer.nodeType === 3) {\n                toRet = [];\n                currNode = range.commonAncestorContainer;\n                while (currNode.parentNode && currNode.parentNode.childNodes.length === 1) {\n                    toRet.push(currNode.parentNode);\n                    currNode = currNode.parentNode;\n                }\n\n                return toRet;\n            }\n\n            return [].filter.call(range.commonAncestorContainer.getElementsByTagName('*'), function (el) {\n                return (typeof selection.containsNode === 'function') ? selection.containsNode(el, true) : true;\n            });\n        },\n\n        selectNode: function (node, doc) {\n            var range = doc.createRange();\n            range.selectNodeContents(node);\n            this.selectRange(doc, range);\n        },\n\n        select: function (doc, startNode, startOffset, endNode, endOffset) {\n            var range = doc.createRange();\n            range.setStart(startNode, startOffset);\n            if (endNode) {\n                range.setEnd(endNode, endOffset);\n            } else {\n                range.collapse(true);\n            }\n            this.selectRange(doc, range);\n            return range;\n        },\n\n        /**\n         *  Clear the current highlighted selection and set the caret to the start or the end of that prior selection, defaults to end.\n         *\n         *  @param {DomDocument} doc            Current document\n         *  @param {boolean} moveCursorToStart  A boolean representing whether or not to set the caret to the beginning of the prior selection.\n         */\n        clearSelection: function (doc, moveCursorToStart) {\n            if (moveCursorToStart) {\n                doc.getSelection().collapseToStart();\n            } else {\n                doc.getSelection().collapseToEnd();\n            }\n        },\n\n        /**\n         * Move cursor to the given node with the given offset.\n         *\n         * @param  {DomDocument} doc     Current document\n         * @param  {DomElement}  node    Element where to jump\n         * @param  {integer}     offset  Where in the element should we jump, 0 by default\n         */\n        moveCursor: function (doc, node, offset) {\n            this.select(doc, node, offset);\n        },\n\n        getSelectionRange: function (ownerDocument) {\n            var selection = ownerDocument.getSelection();\n            if (selection.rangeCount === 0) {\n                return null;\n            }\n            return selection.getRangeAt(0);\n        },\n\n        selectRange: function (ownerDocument, range) {\n            var selection = ownerDocument.getSelection();\n\n            selection.removeAllRanges();\n            selection.addRange(range);\n        },\n\n        // http://stackoverflow.com/questions/1197401/how-can-i-get-the-element-the-caret-is-in-with-javascript-when-using-contentedi\n        // by You\n        getSelectionStart: function (ownerDocument) {\n            var node = ownerDocument.getSelection().anchorNode,\n                startNode = (node && node.nodeType === 3 ? node.parentNode : node);\n\n            return startNode;\n        }\n    };\n\n    MediumEditor.selection = Selection;\n}());\n"
  },
  {
    "path": "src/js/util.js",
    "content": "/*global NodeFilter*/\n\n(function (window) {\n    'use strict';\n\n    function copyInto(overwrite, dest) {\n        var prop,\n            sources = Array.prototype.slice.call(arguments, 2);\n        dest = dest || {};\n        for (var i = 0; i < sources.length; i++) {\n            var source = sources[i];\n            if (source) {\n                for (prop in source) {\n                    if (source.hasOwnProperty(prop) &&\n                        typeof source[prop] !== 'undefined' &&\n                        (overwrite || dest.hasOwnProperty(prop) === false)) {\n                        dest[prop] = source[prop];\n                    }\n                }\n            }\n        }\n        return dest;\n    }\n\n    // https://developer.mozilla.org/en-US/docs/Web/API/Node/contains\n    // Some browsers (including phantom) don't return true for Node.contains(child)\n    // if child is a text node.  Detect these cases here and use a fallback\n    // for calls to Util.isDescendant()\n    var nodeContainsWorksWithTextNodes = false;\n    try {\n        var testParent = document.createElement('div'),\n            testText = document.createTextNode(' ');\n        testParent.appendChild(testText);\n        nodeContainsWorksWithTextNodes = testParent.contains(testText);\n    } catch (exc) {}\n\n    var Util = {\n\n        // http://stackoverflow.com/questions/17907445/how-to-detect-ie11#comment30165888_17907562\n        // by rg89\n        isIE: ((navigator.appName === 'Microsoft Internet Explorer') || ((navigator.appName === 'Netscape') && (new RegExp('Trident/.*rv:([0-9]{1,}[.0-9]{0,})').exec(navigator.userAgent) !== null))),\n\n        isEdge: (/Edge\\/\\d+/).exec(navigator.userAgent) !== null,\n\n        // if firefox\n        isFF: (navigator.userAgent.toLowerCase().indexOf('firefox') > -1),\n\n        // http://stackoverflow.com/a/11752084/569101\n        isMac: (window.navigator.platform.toUpperCase().indexOf('MAC') >= 0),\n\n        // https://github.com/jashkenas/underscore\n        // Lonely letter MUST USE the uppercase code\n        keyCode: {\n            BACKSPACE: 8,\n            TAB: 9,\n            ENTER: 13,\n            ESCAPE: 27,\n            SPACE: 32,\n            DELETE: 46,\n            K: 75, // K keycode, and not k\n            M: 77,\n            V: 86\n        },\n\n        /**\n         * Returns true if it's metaKey on Mac, or ctrlKey on non-Mac.\n         * See #591\n         */\n        isMetaCtrlKey: function (event) {\n            if ((Util.isMac && event.metaKey) || (!Util.isMac && event.ctrlKey)) {\n                return true;\n            }\n\n            return false;\n        },\n\n        /**\n         * Returns true if the key associated to the event is inside keys array\n         *\n         * @see : https://github.com/jquery/jquery/blob/0705be475092aede1eddae01319ec931fb9c65fc/src/event.js#L473-L484\n         * @see : http://stackoverflow.com/q/4471582/569101\n         */\n        isKey: function (event, keys) {\n            var keyCode = Util.getKeyCode(event);\n\n            // it's not an array let's just compare strings!\n            if (false === Array.isArray(keys)) {\n                return keyCode === keys;\n            }\n\n            if (-1 === keys.indexOf(keyCode)) {\n                return false;\n            }\n\n            return true;\n        },\n\n        getKeyCode: function (event) {\n            var keyCode = event.which;\n\n            // getting the key code from event\n            if (null === keyCode) {\n                keyCode = event.charCode !== null ? event.charCode : event.keyCode;\n            }\n\n            return keyCode;\n        },\n\n        blockContainerElementNames: [\n            // elements our editor generates\n            'p', 'h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'blockquote', 'pre', 'ul', 'li', 'ol',\n            // all other known block elements\n            'address', 'article', 'aside', 'audio', 'canvas', 'dd', 'dl', 'dt', 'fieldset',\n            'figcaption', 'figure', 'footer', 'form', 'header', 'hgroup', 'main', 'nav',\n            'noscript', 'output', 'section', 'video',\n            'table', 'thead', 'tbody', 'tfoot', 'tr', 'th', 'td'\n        ],\n\n        emptyElementNames: ['br', 'col', 'colgroup', 'hr', 'img', 'input', 'source', 'wbr'],\n\n        extend: function extend(/* dest, source1, source2, ...*/) {\n            var args = [true].concat(Array.prototype.slice.call(arguments));\n            return copyInto.apply(this, args);\n        },\n\n        defaults: function defaults(/*dest, source1, source2, ...*/) {\n            var args = [false].concat(Array.prototype.slice.call(arguments));\n            return copyInto.apply(this, args);\n        },\n\n        /*\n         * Create a link around the provided text nodes which must be adjacent to each other and all be\n         * descendants of the same closest block container. If the preconditions are not met, unexpected\n         * behavior will result.\n         */\n        createLink: function (document, textNodes, href, target) {\n            var anchor = document.createElement('a');\n            Util.moveTextRangeIntoElement(textNodes[0], textNodes[textNodes.length - 1], anchor);\n            anchor.setAttribute('href', href);\n            if (target) {\n                if (target === '_blank') {\n                    anchor.setAttribute('rel', 'noopener noreferrer');\n                }\n                anchor.setAttribute('target', target);\n            }\n            return anchor;\n        },\n\n        /*\n         * Given the provided match in the format {start: 1, end: 2} where start and end are indices into the\n         * textContent of the provided element argument, modify the DOM inside element to ensure that the text\n         * identified by the provided match can be returned as text nodes that contain exactly that text, without\n         * any additional text at the beginning or end of the returned array of adjacent text nodes.\n         *\n         * The only DOM manipulation performed by this function is splitting the text nodes, non-text nodes are\n         * not affected in any way.\n         */\n        findOrCreateMatchingTextNodes: function (document, element, match) {\n            var treeWalker = document.createTreeWalker(element, NodeFilter.SHOW_ALL, null, false),\n                matchedNodes = [],\n                currentTextIndex = 0,\n                startReached = false,\n                currentNode = null,\n                newNode = null;\n\n            while ((currentNode = treeWalker.nextNode()) !== null) {\n                if (currentNode.nodeType > 3) {\n                    continue;\n                } else if (currentNode.nodeType === 3) {\n                    if (!startReached && match.start < (currentTextIndex + currentNode.nodeValue.length)) {\n                        startReached = true;\n                        newNode = Util.splitStartNodeIfNeeded(currentNode, match.start, currentTextIndex);\n                    }\n                    if (startReached) {\n                        Util.splitEndNodeIfNeeded(currentNode, newNode, match.end, currentTextIndex);\n                    }\n                    if (startReached && currentTextIndex === match.end) {\n                        break; // Found the node(s) corresponding to the link. Break out and move on to the next.\n                    } else if (startReached && currentTextIndex > (match.end + 1)) {\n                        throw new Error('PerformLinking overshot the target!'); // should never happen...\n                    }\n\n                    if (startReached) {\n                        matchedNodes.push(newNode || currentNode);\n                    }\n\n                    currentTextIndex += currentNode.nodeValue.length;\n                    if (newNode !== null) {\n                        currentTextIndex += newNode.nodeValue.length;\n                        // Skip the newNode as we'll already have pushed it to the matches\n                        treeWalker.nextNode();\n                    }\n                    newNode = null;\n                } else if (currentNode.tagName.toLowerCase() === 'img') {\n                    if (!startReached && (match.start <= currentTextIndex)) {\n                        startReached = true;\n                    }\n                    if (startReached) {\n                        matchedNodes.push(currentNode);\n                    }\n                }\n            }\n            return matchedNodes;\n        },\n\n        /*\n         * Given the provided text node and text coordinates, split the text node if needed to make it align\n         * precisely with the coordinates.\n         *\n         * This function is intended to be called from Util.findOrCreateMatchingTextNodes.\n         */\n        splitStartNodeIfNeeded: function (currentNode, matchStartIndex, currentTextIndex) {\n            if (matchStartIndex !== currentTextIndex) {\n                return currentNode.splitText(matchStartIndex - currentTextIndex);\n            }\n            return null;\n        },\n\n        /*\n         * Given the provided text node and text coordinates, split the text node if needed to make it align\n         * precisely with the coordinates. The newNode argument should from the result of Util.splitStartNodeIfNeeded,\n         * if that function has been called on the same currentNode.\n         *\n         * This function is intended to be called from Util.findOrCreateMatchingTextNodes.\n         */\n        splitEndNodeIfNeeded: function (currentNode, newNode, matchEndIndex, currentTextIndex) {\n            var textIndexOfEndOfFarthestNode,\n                endSplitPoint;\n            textIndexOfEndOfFarthestNode = currentTextIndex + currentNode.nodeValue.length +\n                    (newNode ? newNode.nodeValue.length : 0) - 1;\n            endSplitPoint = matchEndIndex - currentTextIndex -\n                    (newNode ? currentNode.nodeValue.length : 0);\n            if (textIndexOfEndOfFarthestNode >= matchEndIndex &&\n                    currentTextIndex !== textIndexOfEndOfFarthestNode &&\n                    endSplitPoint !== 0) {\n                (newNode || currentNode).splitText(endSplitPoint);\n            }\n        },\n\n        /*\n        * Take an element, and break up all of its text content into unique pieces such that:\n         * 1) All text content of the elements are in separate blocks. No piece of text content should span\n         *    across multiple blocks. This means no element return by this function should have\n         *    any blocks as children.\n         * 2) The union of the textcontent of all of the elements returned here covers all\n         *    of the text within the element.\n         *\n         *\n         * EXAMPLE:\n         * In the event that we have something like:\n         *\n         * <blockquote>\n         *   <p>Some Text</p>\n         *   <ol>\n         *     <li>List Item 1</li>\n         *     <li>List Item 2</li>\n         *   </ol>\n         * </blockquote>\n         *\n         * This function would return these elements as an array:\n         *   [ <p>Some Text</p>, <li>List Item 1</li>, <li>List Item 2</li> ]\n         *\n         * Since the <blockquote> and <ol> elements contain blocks within them they are not returned.\n         * Since the <p> and <li>'s don't contain block elements and cover all the text content of the\n         * <blockquote> container, they are the elements returned.\n         */\n        splitByBlockElements: function (element) {\n            if (element.nodeType !== 3 && element.nodeType !== 1) {\n                return [];\n            }\n\n            var toRet = [],\n                blockElementQuery = MediumEditor.util.blockContainerElementNames.join(',');\n\n            if (element.nodeType === 3 || element.querySelectorAll(blockElementQuery).length === 0) {\n                return [element];\n            }\n\n            for (var i = 0; i < element.childNodes.length; i++) {\n                var child = element.childNodes[i];\n                if (child.nodeType === 3) {\n                    toRet.push(child);\n                } else if (child.nodeType === 1) {\n                    var blockElements = child.querySelectorAll(blockElementQuery);\n                    if (blockElements.length === 0) {\n                        toRet.push(child);\n                    } else {\n                        toRet = toRet.concat(MediumEditor.util.splitByBlockElements(child));\n                    }\n                }\n            }\n\n            return toRet;\n        },\n\n        // Find the next node in the DOM tree that represents any text that is being\n        // displayed directly next to the targetNode (passed as an argument)\n        // Text that appears directly next to the current node can be:\n        //  - A sibling text node\n        //  - A descendant of a sibling element\n        //  - A sibling text node of an ancestor\n        //  - A descendant of a sibling element of an ancestor\n        findAdjacentTextNodeWithContent: function findAdjacentTextNodeWithContent(rootNode, targetNode, ownerDocument) {\n            var pastTarget = false,\n                nextNode,\n                nodeIterator = ownerDocument.createNodeIterator(rootNode, NodeFilter.SHOW_TEXT, null, false);\n\n            // Use a native NodeIterator to iterate over all the text nodes that are descendants\n            // of the rootNode.  Once past the targetNode, choose the first non-empty text node\n            nextNode = nodeIterator.nextNode();\n            while (nextNode) {\n                if (nextNode === targetNode) {\n                    pastTarget = true;\n                } else if (pastTarget) {\n                    if (nextNode.nodeType === 3 && nextNode.nodeValue && nextNode.nodeValue.trim().length > 0) {\n                        break;\n                    }\n                }\n                nextNode = nodeIterator.nextNode();\n            }\n\n            return nextNode;\n        },\n\n        // Find an element's previous sibling within a medium-editor element\n        // If one doesn't exist, find the closest ancestor's previous sibling\n        findPreviousSibling: function (node) {\n            if (!node || Util.isMediumEditorElement(node)) {\n                return false;\n            }\n\n            var previousSibling = node.previousSibling;\n            while (!previousSibling && !Util.isMediumEditorElement(node.parentNode)) {\n                node = node.parentNode;\n                previousSibling = node.previousSibling;\n            }\n\n            return previousSibling;\n        },\n\n        isDescendant: function isDescendant(parent, child, checkEquality) {\n            if (!parent || !child) {\n                return false;\n            }\n            if (parent === child) {\n                return !!checkEquality;\n            }\n            // If parent is not an element, it can't have any descendants\n            if (parent.nodeType !== 1) {\n                return false;\n            }\n            if (nodeContainsWorksWithTextNodes || child.nodeType !== 3) {\n                return parent.contains(child);\n            }\n            var node = child.parentNode;\n            while (node !== null) {\n                if (node === parent) {\n                    return true;\n                }\n                node = node.parentNode;\n            }\n            return false;\n        },\n\n        // https://github.com/jashkenas/underscore\n        isElement: function isElement(obj) {\n            return !!(obj && obj.nodeType === 1);\n        },\n\n        // https://github.com/jashkenas/underscore\n        throttle: function (func, wait) {\n            var THROTTLE_INTERVAL = 50,\n                context,\n                args,\n                result,\n                timeout = null,\n                previous = 0,\n                later = function () {\n                    previous = Date.now();\n                    timeout = null;\n                    result = func.apply(context, args);\n                    if (!timeout) {\n                        context = args = null;\n                    }\n                };\n\n            if (!wait && wait !== 0) {\n                wait = THROTTLE_INTERVAL;\n            }\n\n            return function () {\n                var now = Date.now(),\n                    remaining = wait - (now - previous);\n\n                context = this;\n                args = arguments;\n                if (remaining <= 0 || remaining > wait) {\n                    if (timeout) {\n                        clearTimeout(timeout);\n                        timeout = null;\n                    }\n                    previous = now;\n                    result = func.apply(context, args);\n                    if (!timeout) {\n                        context = args = null;\n                    }\n                } else if (!timeout) {\n                    timeout = setTimeout(later, remaining);\n                }\n                return result;\n            };\n        },\n\n        traverseUp: function (current, testElementFunction) {\n            if (!current) {\n                return false;\n            }\n\n            do {\n                if (current.nodeType === 1) {\n                    if (testElementFunction(current)) {\n                        return current;\n                    }\n                    // do not traverse upwards past the nearest containing editor\n                    if (Util.isMediumEditorElement(current)) {\n                        return false;\n                    }\n                }\n\n                current = current.parentNode;\n            } while (current);\n\n            return false;\n        },\n\n        htmlEntities: function (str) {\n            // converts special characters (like <) into their escaped/encoded values (like &lt;).\n            // This allows you to show to display the string without the browser reading it as HTML.\n            return String(str).replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;').replace(/\"/g, '&quot;');\n        },\n\n        // http://stackoverflow.com/questions/6690752/insert-html-at-caret-in-a-contenteditable-div\n        insertHTMLCommand: function (doc, html) {\n            var selection, range, el, fragment, node, lastNode, toReplace,\n                res = false,\n                ecArgs = ['insertHTML', false, html];\n\n            /* Edge's implementation of insertHTML is just buggy right now:\n             * - Doesn't allow leading white space at the beginning of an element\n             * - Found a case when a <font size=\"2\"> tag was inserted when calling alignCenter inside a blockquote\n             *\n             * There are likely other bugs, these are just the ones we found so far.\n             * For now, let's just use the same fallback we did for IE\n             */\n            if (!MediumEditor.util.isEdge && doc.queryCommandSupported('insertHTML')) {\n                try {\n                    return doc.execCommand.apply(doc, ecArgs);\n                } catch (ignore) {}\n            }\n\n            selection = doc.getSelection();\n            if (selection.rangeCount) {\n                range = selection.getRangeAt(0);\n                toReplace = range.commonAncestorContainer;\n\n                // https://github.com/yabwe/medium-editor/issues/748\n                // If the selection is an empty editor element, create a temporary text node inside of the editor\n                // and select it so that we don't delete the editor element\n                if (Util.isMediumEditorElement(toReplace) && !toReplace.firstChild) {\n                    range.selectNode(toReplace.appendChild(doc.createTextNode('')));\n                } else if ((toReplace.nodeType === 3 && range.startOffset === 0 && range.endOffset === toReplace.nodeValue.length) ||\n                        (toReplace.nodeType !== 3 && toReplace.innerHTML === range.toString())) {\n                    // Ensure range covers maximum amount of nodes as possible\n                    // By moving up the DOM and selecting ancestors whose only child is the range\n                    while (!Util.isMediumEditorElement(toReplace) &&\n                            toReplace.parentNode &&\n                            toReplace.parentNode.childNodes.length === 1 &&\n                            !Util.isMediumEditorElement(toReplace.parentNode)) {\n                        toReplace = toReplace.parentNode;\n                    }\n                    range.selectNode(toReplace);\n                }\n                range.deleteContents();\n\n                el = doc.createElement('div');\n                el.innerHTML = html;\n                fragment = doc.createDocumentFragment();\n                while (el.firstChild) {\n                    node = el.firstChild;\n                    lastNode = fragment.appendChild(node);\n                }\n                range.insertNode(fragment);\n\n                // Preserve the selection:\n                if (lastNode) {\n                    range = range.cloneRange();\n                    range.setStartAfter(lastNode);\n                    range.collapse(true);\n                    MediumEditor.selection.selectRange(doc, range);\n                }\n                res = true;\n            }\n\n            // https://github.com/yabwe/medium-editor/issues/992\n            // If we're monitoring calls to execCommand, notify listeners as if a real call had happened\n            if (doc.execCommand.callListeners) {\n                doc.execCommand.callListeners(ecArgs, res);\n            }\n            return res;\n        },\n\n        execFormatBlock: function (doc, tagName) {\n            // Get the top level block element that contains the selection\n            var blockContainer = Util.getTopBlockContainer(MediumEditor.selection.getSelectionStart(doc)),\n                childNodes;\n\n            // Special handling for blockquote\n            if (tagName === 'blockquote') {\n                if (blockContainer) {\n                    childNodes = Array.prototype.slice.call(blockContainer.childNodes);\n                    // Check if the blockquote has a block element as a child (nested blocks)\n                    if (childNodes.some(function (childNode) {\n                        return Util.isBlockContainer(childNode);\n                    })) {\n                        // FF handles blockquote differently on formatBlock\n                        // allowing nesting, we need to use outdent\n                        // https://developer.mozilla.org/en-US/docs/Rich-Text_Editing_in_Mozilla\n                        return doc.execCommand('outdent', false, null);\n                    }\n                }\n\n                // When IE blockquote needs to be called as indent\n                // http://stackoverflow.com/questions/1816223/rich-text-editor-with-blockquote-function/1821777#1821777\n                if (Util.isIE) {\n                    return doc.execCommand('indent', false, tagName);\n                }\n            }\n\n            // If the blockContainer is already the element type being passed in\n            // treat it as 'undo' formatting and just convert it to a <p>\n            if (blockContainer && tagName === blockContainer.nodeName.toLowerCase()) {\n                tagName = 'p';\n            }\n\n            // When IE we need to add <> to heading elements\n            // http://stackoverflow.com/questions/10741831/execcommand-formatblock-headings-in-ie\n            if (Util.isIE) {\n                tagName = '<' + tagName + '>';\n            }\n\n            // When FF, IE and Edge, we have to handle blockquote node seperately as 'formatblock' does not work.\n            // https://developer.mozilla.org/en-US/docs/Web/API/Document/execCommand#Commands\n            if (blockContainer && blockContainer.nodeName.toLowerCase() === 'blockquote') {\n                // For IE, just use outdent\n                if (Util.isIE && tagName === '<p>') {\n                    return doc.execCommand('outdent', false, tagName);\n                }\n\n                // For Firefox and Edge, make sure there's a nested block element before calling outdent\n                if ((Util.isFF || Util.isEdge) && tagName === 'p') {\n                    childNodes = Array.prototype.slice.call(blockContainer.childNodes);\n                    // If there are some non-block elements we need to wrap everything in a <p> before we outdent\n                    if (childNodes.some(function (childNode) {\n                        return !Util.isBlockContainer(childNode);\n                    })) {\n                        doc.execCommand('formatBlock', false, tagName);\n                    }\n                    return doc.execCommand('outdent', false, tagName);\n                }\n            }\n\n            return doc.execCommand('formatBlock', false, tagName);\n        },\n\n        /**\n         * Set target to blank on the given el element\n         *\n         * TODO: not sure if this should be here\n         *\n         * When creating a link (using core -> createLink) the selection returned by Firefox will be the parent of the created link\n         * 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\n         * using `anchorUrl` to ensure that we are adding target=\"_blank\" on the good one.\n         * This isn't a bulletproof solution anyway ..\n         */\n        setTargetBlank: function (el, anchorUrl) {\n            var i, url = anchorUrl || false;\n            if (el.nodeName.toLowerCase() === 'a') {\n                el.target = '_blank';\n                el.rel = 'noopener noreferrer';\n            } else {\n                el = el.getElementsByTagName('a');\n\n                for (i = 0; i < el.length; i += 1) {\n                    if (false === url || url === el[i].attributes.href.value) {\n                        el[i].target = '_blank';\n                        el[i].rel = 'noopener noreferrer';\n                    }\n                }\n            }\n        },\n\n        /*\n         * this function is called to explicitly remove the target='_blank' as FF holds on to _blank value even\n         * after unchecking the checkbox on anchor form\n         */\n        removeTargetBlank: function (el, anchorUrl) {\n            var i;\n            if (el.nodeName.toLowerCase() === 'a') {\n                el.removeAttribute('target');\n                el.removeAttribute('rel');\n            } else {\n                el = el.getElementsByTagName('a');\n\n                for (i = 0; i < el.length; i += 1) {\n                    if (anchorUrl === el[i].attributes.href.value) {\n                        el[i].removeAttribute('target');\n                        el[i].removeAttribute('rel');\n                    }\n                }\n            }\n        },\n\n        /*\n         * this function adds one or several classes on an a element.\n         * if el parameter is not an a, it will look for a children of el.\n         * if no a children are found, it will look for the a parent.\n         */\n        addClassToAnchors: function (el, buttonClass) {\n            var classes = buttonClass.split(' '),\n                i,\n                j;\n            if (el.nodeName.toLowerCase() === 'a') {\n                for (j = 0; j < classes.length; j += 1) {\n                    el.classList.add(classes[j]);\n                }\n            } else {\n                var aChildren = el.getElementsByTagName('a');\n                if (aChildren.length === 0) {\n                    var parentAnchor = Util.getClosestTag(el, 'a');\n                    el = parentAnchor ? [parentAnchor] : [];\n                } else {\n                    el = aChildren;\n                }\n                for (i = 0; i < el.length; i += 1) {\n                    for (j = 0; j < classes.length; j += 1) {\n                        el[i].classList.add(classes[j]);\n                    }\n                }\n            }\n        },\n\n        isListItem: function (node) {\n            if (!node) {\n                return false;\n            }\n            if (node.nodeName.toLowerCase() === 'li') {\n                return true;\n            }\n\n            var parentNode = node.parentNode,\n                tagName = parentNode.nodeName.toLowerCase();\n            while (tagName === 'li' || (!Util.isBlockContainer(parentNode) && tagName !== 'div')) {\n                if (tagName === 'li') {\n                    return true;\n                }\n                parentNode = parentNode.parentNode;\n                if (parentNode) {\n                    tagName = parentNode.nodeName.toLowerCase();\n                } else {\n                    return false;\n                }\n            }\n            return false;\n        },\n\n        findFirstTextNodeInSelection: function (selection) {\n            if (selection.anchorNode.nodeType === 3) {\n                return selection.anchorNode;\n            }\n\n            var node = selection.anchorNode.firstChild;\n\n            while (node) {\n                if (selection.containsNode(node, true)) {\n                    if (node.nodeType === 3) {\n                        return node;\n                    } else {\n                        node = node.firstChild;\n                    }\n                } else {\n                    node = node.nextSibling;\n                }\n            }\n\n            return null;\n        },\n\n        cleanListDOM: function (ownerDocument, element) {\n            if (element.nodeName.toLowerCase() !== 'li') {\n                if (this.isIE || this.isEdge) {\n                    return;\n                }\n\n                var selection = ownerDocument.getSelection(),\n                    newRange = ownerDocument.createRange(),\n                    oldRange = selection.getRangeAt(0),\n                    startContainer = oldRange.startContainer,\n                    startOffset = oldRange.startOffset,\n                    endContainer = oldRange.endContainer,\n                    endOffset = oldRange.endOffset,\n                    node, newNode, nextNode, moveEndOffset;\n\n                if (element.nodeName.toLowerCase() === 'span') {\n                    // Chrome & Safari unwraps removed li elements into a span\n                    node = element;\n                    moveEndOffset = false;\n                } else {\n                    // FF leaves them as text nodes\n                    node = this.findFirstTextNodeInSelection(selection);\n                    moveEndOffset = startContainer.nodeType !== 3;\n                }\n\n                while (node) {\n                    if (node.nodeName.toLowerCase() !== 'span' && node.nodeType !== 3) {\n                        break;\n                    }\n\n                    if (node.nextSibling && node.nextSibling.nodeName.toLowerCase() === 'br') {\n                        node.nextSibling.remove();\n\n                        if (moveEndOffset) {\n                            endOffset--;\n                        }\n                    }\n\n                    nextNode = node.nextSibling;\n\n                    newNode = ownerDocument.createElement('p');\n                    node.parentNode.replaceChild(newNode, node);\n                    newNode.appendChild(node);\n\n                    node = nextNode;\n                }\n\n                // Restore selection\n                newRange.setStart(startContainer, startOffset);\n                newRange.setEnd(endContainer, endOffset);\n                selection.removeAllRanges();\n                selection.addRange(newRange);\n            } else {\n                var list = element.parentElement;\n\n                if (list.parentElement.nodeName.toLowerCase() === 'p') { // yes we need to clean up\n                    Util.unwrap(list.parentElement, ownerDocument);\n\n                    // move cursor at the end of the text inside the list\n                    // for some unknown reason, the cursor is moved to end of the \"visual\" line\n                    MediumEditor.selection.moveCursor(ownerDocument, element.firstChild, element.firstChild.textContent.length);\n                }\n            }\n        },\n\n        /* splitDOMTree\n         *\n         * Given a root element some descendant element, split the root element\n         * into its own element containing the descendant element and all elements\n         * on the left or right side of the descendant ('right' is default)\n         *\n         * example:\n         *\n         *         <div>\n         *      /    |   \\\n         *  <span> <span> <span>\n         *   / \\    / \\    / \\\n         *  1   2  3   4  5   6\n         *\n         *  If I wanted to split this tree given the <div> as the root and \"4\" as the leaf\n         *  the result would be (the prime ' marks indicates nodes that are created as clones):\n         *\n         *   SPLITTING OFF 'RIGHT' TREE       SPLITTING OFF 'LEFT' TREE\n         *\n         *     <div>            <div>'              <div>'      <div>\n         *      / \\              / \\                 / \\          |\n         * <span> <span>   <span>' <span>       <span> <span>   <span>\n         *   / \\    |        |      / \\           /\\     /\\       /\\\n         *  1   2   3        4     5   6         1  2   3  4     5  6\n         *\n         *  The above example represents splitting off the 'right' or 'left' part of a tree, where\n         *  the <div>' would be returned as an element not appended to the DOM, and the <div>\n         *  would remain in place where it was\n         *\n        */\n        splitOffDOMTree: function (rootNode, leafNode, splitLeft) {\n            var splitOnNode = leafNode,\n                createdNode = null,\n                splitRight = !splitLeft;\n\n            // loop until we hit the root\n            while (splitOnNode !== rootNode) {\n                var currParent = splitOnNode.parentNode,\n                    newParent = currParent.cloneNode(false),\n                    targetNode = (splitRight ? splitOnNode : currParent.firstChild),\n                    appendLast;\n\n                // Create a new parent element which is a clone of the current parent\n                if (createdNode) {\n                    if (splitRight) {\n                        // If we're splitting right, add previous created element before siblings\n                        newParent.appendChild(createdNode);\n                    } else {\n                        // If we're splitting left, add previous created element last\n                        appendLast = createdNode;\n                    }\n                }\n                createdNode = newParent;\n\n                while (targetNode) {\n                    var sibling = targetNode.nextSibling;\n                    // Special handling for the 'splitNode'\n                    if (targetNode === splitOnNode) {\n                        if (!targetNode.hasChildNodes()) {\n                            targetNode.parentNode.removeChild(targetNode);\n                        } else {\n                            // For the node we're splitting on, if it has children, we need to clone it\n                            // and not just move it\n                            targetNode = targetNode.cloneNode(false);\n                        }\n                        // If the resulting split node has content, add it\n                        if (targetNode.textContent) {\n                            createdNode.appendChild(targetNode);\n                        }\n\n                        targetNode = (splitRight ? sibling : null);\n                    } else {\n                        // For general case, just remove the element and only\n                        // add it to the split tree if it contains something\n                        targetNode.parentNode.removeChild(targetNode);\n                        if (targetNode.hasChildNodes() || targetNode.textContent) {\n                            createdNode.appendChild(targetNode);\n                        }\n\n                        targetNode = sibling;\n                    }\n                }\n\n                // If we had an element we wanted to append at the end, do that now\n                if (appendLast) {\n                    createdNode.appendChild(appendLast);\n                }\n\n                splitOnNode = currParent;\n            }\n\n            return createdNode;\n        },\n\n        moveTextRangeIntoElement: function (startNode, endNode, newElement) {\n            if (!startNode || !endNode) {\n                return false;\n            }\n\n            var rootNode = Util.findCommonRoot(startNode, endNode);\n            if (!rootNode) {\n                return false;\n            }\n\n            if (endNode === startNode) {\n                var temp = startNode.parentNode,\n                    sibling = startNode.nextSibling;\n                temp.removeChild(startNode);\n                newElement.appendChild(startNode);\n                if (sibling) {\n                    temp.insertBefore(newElement, sibling);\n                } else {\n                    temp.appendChild(newElement);\n                }\n                return newElement.hasChildNodes();\n            }\n\n            // create rootChildren array which includes all the children\n            // we care about\n            var rootChildren = [],\n                firstChild,\n                lastChild,\n                nextNode;\n            for (var i = 0; i < rootNode.childNodes.length; i++) {\n                nextNode = rootNode.childNodes[i];\n                if (!firstChild) {\n                    if (Util.isDescendant(nextNode, startNode, true)) {\n                        firstChild = nextNode;\n                    }\n                } else {\n                    if (Util.isDescendant(nextNode, endNode, true)) {\n                        lastChild = nextNode;\n                        break;\n                    } else {\n                        rootChildren.push(nextNode);\n                    }\n                }\n            }\n\n            var afterLast = lastChild.nextSibling,\n                fragment = rootNode.ownerDocument.createDocumentFragment();\n\n            // build up fragment on startNode side of tree\n            if (firstChild === startNode) {\n                firstChild.parentNode.removeChild(firstChild);\n                fragment.appendChild(firstChild);\n            } else {\n                fragment.appendChild(Util.splitOffDOMTree(firstChild, startNode));\n            }\n\n            // add any elements between firstChild & lastChild\n            rootChildren.forEach(function (element) {\n                element.parentNode.removeChild(element);\n                fragment.appendChild(element);\n            });\n\n            // build up fragment on endNode side of the tree\n            if (lastChild === endNode) {\n                lastChild.parentNode.removeChild(lastChild);\n                fragment.appendChild(lastChild);\n            } else {\n                fragment.appendChild(Util.splitOffDOMTree(lastChild, endNode, true));\n            }\n\n            // Add fragment into passed in element\n            newElement.appendChild(fragment);\n\n            if (lastChild.parentNode === rootNode) {\n                // If last child is in the root, insert newElement in front of it\n                rootNode.insertBefore(newElement, lastChild);\n            } else if (afterLast) {\n                // If last child was removed, but it had a sibling, insert in front of it\n                rootNode.insertBefore(newElement, afterLast);\n            } else {\n                // lastChild was removed and was the last actual element just append\n                rootNode.appendChild(newElement);\n            }\n\n            return newElement.hasChildNodes();\n        },\n\n        /* based on http://stackoverflow.com/a/6183069 */\n        depthOfNode: function (inNode) {\n            var theDepth = 0,\n                node = inNode;\n            while (node.parentNode !== null) {\n                node = node.parentNode;\n                theDepth++;\n            }\n            return theDepth;\n        },\n\n        findCommonRoot: function (inNode1, inNode2) {\n            var depth1 = Util.depthOfNode(inNode1),\n                depth2 = Util.depthOfNode(inNode2),\n                node1 = inNode1,\n                node2 = inNode2;\n\n            while (depth1 !== depth2) {\n                if (depth1 > depth2) {\n                    node1 = node1.parentNode;\n                    depth1 -= 1;\n                } else {\n                    node2 = node2.parentNode;\n                    depth2 -= 1;\n                }\n            }\n\n            while (node1 !== node2) {\n                node1 = node1.parentNode;\n                node2 = node2.parentNode;\n            }\n\n            return node1;\n        },\n        /* END - based on http://stackoverflow.com/a/6183069 */\n\n        isElementAtBeginningOfBlock: function (node) {\n            var textVal,\n                sibling;\n            while (!Util.isBlockContainer(node) && !Util.isMediumEditorElement(node)) {\n                sibling = node;\n                while (sibling = sibling.previousSibling) {\n                    textVal = sibling.nodeType === 3 ? sibling.nodeValue : sibling.textContent;\n                    if (textVal.length > 0) {\n                        return false;\n                    }\n                }\n                node = node.parentNode;\n            }\n            return true;\n        },\n\n        isMediumEditorElement: function (element) {\n            return element && element.getAttribute && !!element.getAttribute('data-medium-editor-element');\n        },\n\n        getContainerEditorElement: function (element) {\n            return Util.traverseUp(element, function (node) {\n                return Util.isMediumEditorElement(node);\n            });\n        },\n\n        isBlockContainer: function (element) {\n            return element && element.nodeType !== 3 && Util.blockContainerElementNames.indexOf(element.nodeName.toLowerCase()) !== -1;\n        },\n\n        /* Finds the closest ancestor which is a block container element\n         * If element is within editor element but not within any other block element,\n         * the editor element is returned\n         */\n        getClosestBlockContainer: function (node) {\n            return Util.traverseUp(node, function (node) {\n                return Util.isBlockContainer(node) || Util.isMediumEditorElement(node);\n            });\n        },\n\n        /* Finds highest level ancestor element which is a block container element\n         * If element is within editor element but not within any other block element,\n         * the editor element is returned\n         */\n        getTopBlockContainer: function (element) {\n            var topBlock = Util.isBlockContainer(element) ? element : false;\n            Util.traverseUp(element, function (el) {\n                if (Util.isBlockContainer(el)) {\n                    topBlock = el;\n                }\n                if (!topBlock && Util.isMediumEditorElement(el)) {\n                    topBlock = el;\n                    return true;\n                }\n                return false;\n            });\n            return topBlock;\n        },\n\n        getFirstSelectableLeafNode: function (element) {\n            while (element && element.firstChild) {\n                element = element.firstChild;\n            }\n\n            // We don't want to set the selection to an element that can't have children, this messes up Gecko.\n            element = Util.traverseUp(element, function (el) {\n                return Util.emptyElementNames.indexOf(el.nodeName.toLowerCase()) === -1;\n            });\n            // Selecting at the beginning of a table doesn't work in PhantomJS.\n            if (element.nodeName.toLowerCase() === 'table') {\n                var firstCell = element.querySelector('th, td');\n                if (firstCell) {\n                    element = firstCell;\n                }\n            }\n            return element;\n        },\n\n        // TODO: remove getFirstTextNode AND _getFirstTextNode when jumping in 6.0.0 (no code references)\n        getFirstTextNode: function (element) {\n            Util.warn('getFirstTextNode is deprecated and will be removed in version 6.0.0');\n            return Util._getFirstTextNode(element);\n        },\n\n        _getFirstTextNode: function (element) {\n            if (element.nodeType === 3) {\n                return element;\n            }\n\n            for (var i = 0; i < element.childNodes.length; i++) {\n                var textNode = Util._getFirstTextNode(element.childNodes[i]);\n                if (textNode !== null) {\n                    return textNode;\n                }\n            }\n            return null;\n        },\n\n        ensureUrlHasProtocol: function (url) {\n            if (url.indexOf('://') === -1) {\n                return 'http://' + url;\n            }\n            return url;\n        },\n\n        warn: function () {\n            if (window.console !== undefined && typeof window.console.warn === 'function') {\n                window.console.warn.apply(window.console, arguments);\n            }\n        },\n\n        deprecated: function (oldName, newName, version) {\n            // simple deprecation warning mechanism.\n            var m = oldName + ' is deprecated, please use ' + newName + ' instead.';\n            if (version) {\n                m += ' Will be removed in ' + version;\n            }\n            Util.warn(m);\n        },\n\n        deprecatedMethod: function (oldName, newName, args, version) {\n            // run the replacement and warn when someone calls a deprecated method\n            Util.deprecated(oldName, newName, version);\n            if (typeof this[newName] === 'function') {\n                this[newName].apply(this, args);\n            }\n        },\n\n        cleanupAttrs: function (el, attrs) {\n            attrs.forEach(function (attr) {\n                el.removeAttribute(attr);\n            });\n        },\n\n        cleanupTags: function (el, tags) {\n            if (tags.indexOf(el.nodeName.toLowerCase()) !== -1) {\n                el.parentNode.removeChild(el);\n            }\n        },\n\n        unwrapTags: function (el, tags) {\n            if (tags.indexOf(el.nodeName.toLowerCase()) !== -1) {\n                MediumEditor.util.unwrap(el, document);\n            }\n        },\n\n        // get the closest parent\n        getClosestTag: function (el, tag) {\n            return Util.traverseUp(el, function (element) {\n                return element.nodeName.toLowerCase() === tag.toLowerCase();\n            });\n        },\n\n        unwrap: function (el, doc) {\n            var fragment = doc.createDocumentFragment(),\n                nodes = Array.prototype.slice.call(el.childNodes);\n\n            // cast nodeList to array since appending child\n            // to a different node will alter length of el.childNodes\n            for (var i = 0; i < nodes.length; i++) {\n                fragment.appendChild(nodes[i]);\n            }\n\n            if (fragment.childNodes.length) {\n                el.parentNode.replaceChild(fragment, el);\n            } else {\n                el.parentNode.removeChild(el);\n            }\n        },\n\n        guid: function () {\n            function _s4() {\n                return Math\n                    .floor((1 + Math.random()) * 0x10000)\n                    .toString(16)\n                    .substring(1);\n            }\n\n            return _s4() + _s4() + '-' + _s4() + '-' + _s4() + '-' + _s4() + '-' + _s4() + _s4() + _s4();\n        }\n    };\n\n    MediumEditor.util = Util;\n}(window));\n"
  },
  {
    "path": "src/js/version.js",
    "content": "MediumEditor.parseVersionString = function (release) {\n    var split = release.split('-'),\n        version = split[0].split('.'),\n        preRelease = (split.length > 1) ? split[1] : '';\n    return {\n        major: parseInt(version[0], 10),\n        minor: parseInt(version[1], 10),\n        revision: parseInt(version[2], 10),\n        preRelease: preRelease,\n        toString: function () {\n            return [version[0], version[1], version[2]].join('.') + (preRelease ? '-' + preRelease : '');\n        }\n    };\n};\n\nMediumEditor.version = MediumEditor.parseVersionString.call(this, ({\n    // grunt-bump looks for this:\n    'version': '5.23.3'\n}).version);\n"
  },
  {
    "path": "src/sass/_settings.scss",
    "content": "// typography\n$font-fixed: Consolas, \"Liberation Mono\", Menlo, Courier, monospace !default;\n$font-sans-serif: \"Helvetica Neue\", Helvetica, Arial, sans-serif !default;\n\n// ui / positioning\n$z-toolbar: 2000 !default;\n"
  },
  {
    "path": "src/sass/animations/_image-loading.scss",
    "content": "@keyframes medium-editor-image-loading {\n    0% {\n        transform: scale(0)\n    }\n    100% {\n        transform: scale(1);\n    }\n}\n"
  },
  {
    "path": "src/sass/animations/_pop-upwards.scss",
    "content": "@keyframes medium-editor-pop-upwards {\n    0% {\n        opacity: 0;\n        transform: matrix(.97, 0, 0, 1, 0, 12);\n    }\n\n    20% {\n        opacity: .7;\n        transform: matrix(.99, 0, 0, 1, 0, 2);\n    }\n\n    40% {\n        opacity: 1;\n        transform: matrix(1, 0, 0, 1, 0, -1);\n    }\n\n    100% {\n        transform: matrix(1, 0, 0, 1, 0, 0);\n    }\n}\n"
  },
  {
    "path": "src/sass/components/_anchor-preview.scss",
    "content": ".medium-editor-anchor-preview {\n    font-family: $font-sans-serif;\n    font-size: 16px;\n    left: 0;\n    line-height: 1.4;\n    max-width: 280px;\n    position: absolute;\n    text-align: center;\n    top: 0;\n    word-break: break-all;\n    word-wrap: break-word;\n    visibility: hidden;\n    z-index: $z-toolbar;\n\n    a {\n        color: #fff;\n        display: inline-block;\n        margin: 5px 5px 10px;\n    }\n}\n\n.medium-editor-anchor-preview-active {\n    visibility: visible;\n}\n"
  },
  {
    "path": "src/sass/components/_file-dragging.scss",
    "content": ".medium-editor-dragover {\n    background: #ddd;\n}\n\n.medium-editor-image-loading {\n    animation: medium-editor-image-loading 1s infinite ease-in-out;\n    background-color: #333;\n    border-radius: 100%;\n    display: inline-block;\n    height: 40px;\n    width: 40px;\n}\n"
  },
  {
    "path": "src/sass/components/_placeholder.scss",
    "content": ".medium-editor-placeholder {\n    position: relative;\n\n    &:after {\n        content: attr(data-placeholder) !important;\n        font-style: italic;\n        position: absolute;\n        left: 0;\n        top: 0;\n        white-space: pre;\n        padding: inherit;\n        margin: inherit;\n    }\n}\n\n.medium-editor-placeholder-relative {\n    position: relative;\n\n    &:after {\n        content: attr(data-placeholder) !important;\n        font-style: italic;\n        position: relative;\n        white-space: pre;\n        padding: inherit;\n        margin: inherit;\n    }\n}\n"
  },
  {
    "path": "src/sass/components/_toolbar-form.scss",
    "content": ".medium-editor-toolbar-form {\n    display: none;\n\n    input,\n    a {\n        font-family: $font-sans-serif;\n    }\n\n    .medium-editor-toolbar-form-row {\n        line-height: 14px;\n        margin-left: 5px;\n        padding-bottom: 5px;\n    }\n\n    .medium-editor-toolbar-input,\n    label {\n        border: none;\n        box-sizing: border-box;\n        font-size: 14px;\n        margin: 0;\n        padding: 6px;\n        width: 316px;\n        display: inline-block;\n\n        &:focus {\n            appearance: none;\n            border: none;\n            box-shadow: none;\n            outline: 0;\n        }\n    }\n\n    a {\n        display: inline-block;\n        font-size: 24px;\n        font-weight: bolder;\n        margin: 0 10px;\n        text-decoration: none;\n    }\n}\n\n.medium-editor-toolbar-form-active {\n    display: block;\n}\n"
  },
  {
    "path": "src/sass/components/_toolbar.scss",
    "content": "%medium-toolbar-arrow {\n    border-style: solid;\n    content: '';\n    display: block;\n    height: 0;\n    left: 50%;\n    margin-left: -8px;\n    position: absolute;\n    width: 0;\n}\n\n.medium-toolbar-arrow-under:after {\n    @extend %medium-toolbar-arrow;\n    border-width: 8px 8px 0 8px;\n}\n\n.medium-toolbar-arrow-over:before {\n    @extend %medium-toolbar-arrow;\n    border-width: 0 8px 8px 8px;\n    top: -8px;\n}\n\n.medium-editor-toolbar {\n    font-family: $font-sans-serif;\n    font-size: 16px;\n    left: 0;\n    position: absolute;\n    top: 0;\n    visibility: hidden;\n    z-index: $z-toolbar;\n\n    ul {\n        margin: 0;\n        padding: 0;\n    }\n\n    li {\n        float: left;\n        list-style: none;\n        margin: 0;\n        padding: 0;\n\n        button {\n            box-sizing: border-box;\n            cursor: pointer;\n            display: block;\n            font-size: 14px;\n            line-height: 1.33;\n            margin: 0;\n            padding: 15px;\n            text-decoration: none;\n\n            &:focus {\n                outline: none;\n            }\n        }\n\n        .medium-editor-action-underline {\n            text-decoration: underline;\n        }\n\n        .medium-editor-action-pre {\n            font-family: $font-fixed;\n            font-size: 12px;\n            font-weight: 100;\n            padding: 15px 0;\n        }\n    }\n}\n\n.medium-editor-toolbar-active {\n    visibility: visible;\n}\n\n.medium-editor-sticky-toolbar {\n    position: fixed;\n    top: 1px;\n}\n\n.medium-editor-relative-toolbar {\n  position: relative;\n}\n\n.medium-editor-toolbar-active.medium-editor-stalker-toolbar {\n    animation: medium-editor-pop-upwards 160ms forwards linear;\n}\n\n.medium-editor-toolbar-actions {\n    @extend %clearfix;\n}\n\n.medium-editor-action-bold {\n    font-weight: bolder;\n}\n\n.medium-editor-action-italic {\n    font-style: italic;\n}\n"
  },
  {
    "path": "src/sass/medium-editor.scss",
    "content": "@import \"settings\";\n@import \"animations/image-loading\";\n@import \"animations/pop-upwards\";\n@import \"components/anchor-preview\";\n@import \"components/file-dragging\";\n@import \"components/placeholder\";\n@import \"components/toolbar\";\n@import \"components/toolbar-form\";\n@import \"util/clearfix\";\n\n// contenteditable rules\n.medium-editor-element {\n    word-wrap: break-word;\n    min-height: 30px;\n\n    img {\n        max-width: 100%;\n    }\n\n    sub {\n        vertical-align: sub;\n    }\n\n    sup {\n        vertical-align: super;\n    }\n}\n\n.medium-editor-hidden {\n    display: none;\n}\n"
  },
  {
    "path": "src/sass/themes/beagle.scss",
    "content": "// theme settings\n$medium-editor-bgcolor: #000;\n$medium-editor-button-size: 40px;\n$medium-editor-button-active-text-color: #a2d7c7;\n$medium-editor-hover-color: $medium-editor-bgcolor;\n$medium-editor-link-color: #ccc;\n$medium-editor-border-radius: 50px;\n$medium-editor-placeholder-color: #f8f5f3;\n\n// theme rules\n.medium-toolbar-arrow-under:after {\n    border-color: $medium-editor-bgcolor transparent transparent transparent;\n    top: $medium-editor-button-size;\n}\n\n.medium-toolbar-arrow-over:before {\n    border-color: transparent transparent $medium-editor-bgcolor transparent;\n}\n\n.medium-editor-toolbar {\n    background-color: $medium-editor-bgcolor;\n    border: none;\n    border-radius: $medium-editor-border-radius;\n\n    li {\n        button {\n            background-color: transparent;\n            border: none;\n            box-sizing: border-box;\n            color: $medium-editor-link-color;\n            height: $medium-editor-button-size;\n            min-width: $medium-editor-button-size;\n            padding: 5px 12px;\n            transition: background-color .2s ease-in, color .2s ease-in;\n            &:hover {\n                background-color: $medium-editor-hover-color;\n                color: $medium-editor-button-active-text-color;\n            }\n        }\n\n        .medium-editor-button-first {\n            border-bottom-left-radius: $medium-editor-border-radius;\n            border-top-left-radius: $medium-editor-border-radius;\n            padding-left: 24px;\n        }\n\n        .medium-editor-button-last {\n            border-bottom-right-radius: $medium-editor-border-radius;\n            border-right: none;\n            border-top-right-radius: $medium-editor-border-radius;\n            padding-right: 24px\n        }\n\n        .medium-editor-button-active {\n            background-color: $medium-editor-hover-color;\n            color: $medium-editor-button-active-text-color;\n        }\n    }\n}\n\n.medium-editor-toolbar-form {\n    background: $medium-editor-bgcolor;\n    border-radius: $medium-editor-border-radius;\n    color: $medium-editor-link-color;\n    overflow: hidden;\n\n    .medium-editor-toolbar-input {\n        background: $medium-editor-bgcolor;\n        box-sizing: border-box;\n        color: $medium-editor-link-color;\n        height: $medium-editor-button-size;\n        padding-left: 16px;\n        width: 220px;\n\n        &::-webkit-input-placeholder {\n           color: $medium-editor-placeholder-color;\n           color: rgba($medium-editor-placeholder-color, .8);\n        }\n        &:-moz-placeholder { /* Firefox 18- */\n           color: $medium-editor-placeholder-color;\n           color: rgba($medium-editor-placeholder-color, .8);\n        }\n        &::-moz-placeholder {  /* Firefox 19+ */\n           color: $medium-editor-placeholder-color;\n           color: rgba($medium-editor-placeholder-color, .8);\n        }\n        &:-ms-input-placeholder {\n           color: $medium-editor-placeholder-color;\n           color: rgba($medium-editor-placeholder-color, .8);\n        }\n    }\n\n    a {\n        color: $medium-editor-link-color;\n        transform: translateY(2px);\n    }\n\n    .medium-editor-toolbar-close {\n      margin-right: 16px;\n    }\n}\n\n.medium-editor-toolbar-anchor-preview {\n    background: $medium-editor-bgcolor;\n    border-radius: $medium-editor-border-radius;\n    padding: 5px 12px;\n}\n\n.medium-editor-anchor-preview {\n  a {\n    color: $medium-editor-link-color;\n    text-decoration: none;\n  }\n}\n\n.medium-editor-toolbar-actions {\n  li, button {\n    border-radius: $medium-editor-border-radius;\n  }\n}\n"
  },
  {
    "path": "src/sass/themes/bootstrap.scss",
    "content": "// theme settings\n$medium-editor-bgcolor: #428bca;\n$medium-editor-border-color: #357ebd;\n$medium-editor-button-size: 60px;\n$medium-editor-button-active-text-color: #fff;\n$medium-editor-hover-color: #3276b1;\n$medium-editor-link-color: #fff;\n$medium-editor-border-radius: 4px;\n$medium-editor-placeholder-color: #fff;\n\n// theme rules\n.medium-toolbar-arrow-under:after {\n    border-color: $medium-editor-bgcolor transparent transparent transparent;\n    top: $medium-editor-button-size;\n}\n\n.medium-toolbar-arrow-over:before {\n    border-color: transparent transparent $medium-editor-bgcolor transparent;\n}\n\n.medium-editor-toolbar {\n    background-color: $medium-editor-bgcolor;\n    border: 1px solid $medium-editor-border-color;\n    border-radius: $medium-editor-border-radius;\n\n    li {\n        button {\n            background-color: transparent;\n            border: none;\n            border-right: 1px solid $medium-editor-border-color;\n            box-sizing: border-box;\n            color: $medium-editor-link-color;\n            height: $medium-editor-button-size;\n            min-width: $medium-editor-button-size;\n            transition: background-color .2s ease-in, color .2s ease-in;\n            &:hover {\n                background-color: $medium-editor-hover-color;\n                color: $medium-editor-button-active-text-color;\n            }\n        }\n\n        .medium-editor-button-first {\n            border-bottom-left-radius: $medium-editor-border-radius;\n            border-top-left-radius: $medium-editor-border-radius;\n        }\n\n        .medium-editor-button-last {\n            border-bottom-right-radius: $medium-editor-border-radius;\n            border-right: none;\n            border-top-right-radius: $medium-editor-border-radius;\n        }\n\n        .medium-editor-button-active {\n            background-color: $medium-editor-hover-color;\n            color: $medium-editor-button-active-text-color;\n        }\n    }\n}\n\n.medium-editor-toolbar-form {\n    background: $medium-editor-bgcolor;\n    border-radius: $medium-editor-border-radius;\n    color: #fff;\n\n    .medium-editor-toolbar-input {\n        background: $medium-editor-bgcolor;\n        color: $medium-editor-link-color;\n        height: $medium-editor-button-size;\n\n        &::-webkit-input-placeholder {\n           color: $medium-editor-placeholder-color;\n           color: rgba($medium-editor-placeholder-color, .8);\n        }\n        &:-moz-placeholder { /* Firefox 18- */\n           color: $medium-editor-placeholder-color;\n           color: rgba($medium-editor-placeholder-color, .8);\n        }\n        &::-moz-placeholder {  /* Firefox 19+ */\n           color: $medium-editor-placeholder-color;\n           color: rgba($medium-editor-placeholder-color, .8);\n        }\n        &:-ms-input-placeholder {\n           color: $medium-editor-placeholder-color;\n           color: rgba($medium-editor-placeholder-color, .8);\n        }\n    }\n\n    a {\n        color: $medium-editor-link-color;\n    }\n}\n\n.medium-editor-toolbar-anchor-preview {\n    background: $medium-editor-bgcolor;\n    border-radius: $medium-editor-border-radius;\n    color: $medium-editor-link-color;\n}\n\n.medium-editor-placeholder:after {\n    color: $medium-editor-border-color;\n}\n"
  },
  {
    "path": "src/sass/themes/default.scss",
    "content": "// theme settings\n$medium-editor-bgcolor: #242424;\n$medium-editor-button-size: 50px;\n$medium-editor-border-radius: 5px;\n\n// theme rules\n.medium-toolbar-arrow-under:after {\n    border-color: $medium-editor-bgcolor transparent transparent transparent;\n    top: $medium-editor-button-size;\n}\n\n.medium-toolbar-arrow-over:before {\n    border-color: transparent transparent $medium-editor-bgcolor transparent;\n    top: -8px;\n}\n\n.medium-editor-toolbar {\n    background-color: $medium-editor-bgcolor;\n    background: linear-gradient(to bottom, $medium-editor-bgcolor, rgba($medium-editor-bgcolor, 0.75));\n    border: 1px solid #000;\n    border-radius: $medium-editor-border-radius;\n    box-shadow: 0 0 3px #000;\n\n    li {\n        button {\n            background-color: $medium-editor-bgcolor;\n            background: linear-gradient(to bottom, $medium-editor-bgcolor, rgba($medium-editor-bgcolor, 0.89));\n            border: 0;\n            border-right: 1px solid #000;\n            border-left: 1px solid #333;\n            border-left: 1px solid rgba(#fff, .1);\n            box-shadow: 0 2px 2px rgba(0,0,0,0.3);\n            color: #fff;\n            height: $medium-editor-button-size;\n            min-width: $medium-editor-button-size;\n            transition: background-color .2s ease-in;\n\n            &:hover {\n                background-color: #000;\n                color: yellow;\n            }\n        }\n\n        .medium-editor-button-first {\n            border-bottom-left-radius: $medium-editor-border-radius;\n            border-top-left-radius: $medium-editor-border-radius;\n        }\n\n        .medium-editor-button-last {\n            border-bottom-right-radius: $medium-editor-border-radius;\n            border-top-right-radius: $medium-editor-border-radius;\n        }\n\n        .medium-editor-button-active {\n            background-color: #000;\n            background: linear-gradient(to bottom, $medium-editor-bgcolor, rgba(#000, 0.89));\n            color: #fff;\n        }\n    }\n}\n\n.medium-editor-toolbar-form {\n    background: $medium-editor-bgcolor;\n    border-radius: $medium-editor-border-radius;\n    color: #999;\n\n    .medium-editor-toolbar-input {\n        background: $medium-editor-bgcolor;\n        box-sizing: border-box;\n        color: #ccc;\n        height: $medium-editor-button-size;\n    }\n\n    a {\n        color: #fff;\n    }\n}\n\n.medium-editor-toolbar-anchor-preview {\n    background: $medium-editor-bgcolor;\n    border-radius: $medium-editor-border-radius;\n    color: #fff;\n}\n\n.medium-editor-placeholder:after {\n    color: #b3b3b1;\n}\n"
  },
  {
    "path": "src/sass/themes/flat.scss",
    "content": "// theme settings\n$medium-editor-bgcolor: #57ad68;\n$medium-editor-border-color: #fff;\n$medium-editor-button-size: 60px;\n$medium-editor-button-active-text-color: #fff;\n$medium-editor-link-color: #fff;\n$medium-editor-placeholder-color: #fff;\n\n// theme rules\n.medium-toolbar-arrow-under:after {\n    top: $medium-editor-button-size;\n    border-color: $medium-editor-bgcolor transparent transparent transparent;\n}\n\n.medium-toolbar-arrow-over:before {\n    top: -8px;\n    border-color: transparent transparent $medium-editor-bgcolor transparent;\n}\n\n.medium-editor-toolbar {\n    background-color: $medium-editor-bgcolor;\n\n    li {\n        padding: 0;\n\n        button {\n            min-width: $medium-editor-button-size;\n            height: $medium-editor-button-size;\n            border: none;\n            border-right: 1px solid lighten($medium-editor-bgcolor, 20);\n            background-color: transparent;\n            color: $medium-editor-link-color;\n            transition: background-color .2s ease-in, color .2s ease-in;\n            &:hover {\n                background-color: darken($medium-editor-bgcolor, 20);\n                color: $medium-editor-button-active-text-color;\n            }\n        }\n\n        .medium-editor-button-active {\n            background-color: darken($medium-editor-bgcolor, 30);\n            color: $medium-editor-button-active-text-color;\n        }\n\n        .medium-editor-button-last {\n            border-right: none;\n        }\n    }\n}\n\n.medium-editor-toolbar-form {\n    .medium-editor-toolbar-input {\n        height: $medium-editor-button-size;\n        background: $medium-editor-bgcolor;\n        color: $medium-editor-link-color;\n\n        &::-webkit-input-placeholder {\n           color: $medium-editor-placeholder-color;\n           color: rgba($medium-editor-placeholder-color, .8);\n        }\n\n        &:-moz-placeholder { /* Firefox 18- */\n           color: $medium-editor-placeholder-color;\n           color: rgba($medium-editor-placeholder-color, .8);\n        }\n\n        &::-moz-placeholder {  /* Firefox 19+ */\n           color: $medium-editor-placeholder-color;\n           color: rgba($medium-editor-placeholder-color, .8);\n        }\n\n        &:-ms-input-placeholder {\n           color: $medium-editor-placeholder-color;\n           color: rgba($medium-editor-placeholder-color, .8);\n        }\n    }\n\n    a {\n        color: $medium-editor-link-color;\n    }\n}\n\n.medium-editor-toolbar-anchor-preview {\n    background: $medium-editor-bgcolor;\n    color: $medium-editor-link-color;\n}\n\n.medium-editor-placeholder:after {\n    color: lighten($medium-editor-bgcolor, 20);\n}\n"
  },
  {
    "path": "src/sass/themes/mani.scss",
    "content": "// inspired by http://dribbble.com/shots/857472-Toolbar\n\n// theme settings\n$medium-editor-bgcolor: #dee7f0;\n$medium-editor-bgcolor-alt: #5c90c7;\n$medium-editor-border-color: #cdd6e0;\n$medium-editor-button-size: 50px;\n$medium-editor-button-hover-text-color: #fff;\n$medium-editor-button-active-text-color: #000;\n$medium-editor-link-color: #40648a;\n$medium-editor-border-radius: 2px;\n\n// theme rules\n.medium-toolbar-arrow-under:after,\n.medium-toolbar-arrow-over:before {\n    display: none;\n}\n\n.medium-editor-toolbar {\n    border: 1px solid $medium-editor-border-color;\n    background-color: $medium-editor-bgcolor;\n    background-color: rgba($medium-editor-bgcolor, .95);\n    background: linear-gradient(to top, $medium-editor-bgcolor, rgba(#fff, 1));\n    border-radius: $medium-editor-border-radius;\n    box-shadow: 0 2px 6px rgba(#000, .45);\n\n    li {\n        button {\n            min-width: $medium-editor-button-size;\n            height: $medium-editor-button-size;\n            border: none;\n            border-right: 1px solid $medium-editor-border-color;\n            background-color: transparent;\n            color: $medium-editor-link-color;\n            transition: background-color .2s ease-in, color .2s ease-in;\n            &:hover {\n                background-color: $medium-editor-bgcolor-alt;\n                background-color: rgba($medium-editor-bgcolor-alt, .45);\n                color: $medium-editor-button-hover-text-color;\n            }\n        }\n\n        .medium-editor-button-first {\n            border-top-left-radius: $medium-editor-border-radius;\n            border-bottom-left-radius: $medium-editor-border-radius;\n        }\n\n        .medium-editor-button-last {\n            border-top-right-radius: $medium-editor-border-radius;\n            border-bottom-right-radius: $medium-editor-border-radius;\n        }\n\n        .medium-editor-button-active {\n            background-color: $medium-editor-bgcolor-alt;\n            background-color: rgba($medium-editor-bgcolor-alt, .45);\n            color: $medium-editor-button-active-text-color;\n            background: linear-gradient(to bottom, $medium-editor-bgcolor, rgba(#000, .1));\n        }\n    }\n}\n\n.medium-editor-toolbar-form {\n    background: $medium-editor-bgcolor;\n    color: #999;\n    border-radius: $medium-editor-border-radius;\n\n    .medium-editor-toolbar-input {\n        height: $medium-editor-button-size;\n        background: $medium-editor-bgcolor;\n        color: $medium-editor-link-color;\n        box-sizing: border-box;\n    }\n\n    a {\n        color: $medium-editor-link-color;\n    }\n}\n\n.medium-editor-toolbar-anchor-preview {\n    background: $medium-editor-bgcolor;\n    color: $medium-editor-link-color;\n    border-radius: $medium-editor-border-radius;\n}\n\n.medium-editor-placeholder:after {\n    color: $medium-editor-border-color;\n}\n"
  },
  {
    "path": "src/sass/themes/roman.scss",
    "content": "// inspired by http://dribbble.com/shots/848100-Toolbar-Psd\n\n// theme settings\n$medium-editor-bgcolor: #fff;\n$medium-editor-border-color: #a8a8a8;\n$medium-editor-button-size: 50px;\n$medium-editor-button-hover-text-color: #fff;\n$medium-editor-button-active-text-color: #000;\n$medium-editor-link-color: #889aac;\n$medium-editor-border-radius: 5px;\n\n// theme rules\n.medium-toolbar-arrow-under:after,\n.medium-toolbar-arrow-over:before {\n    display: none;\n}\n\n.medium-editor-toolbar {\n    background-color: $medium-editor-bgcolor;\n    background-color: rgba($medium-editor-bgcolor, .95);\n    border-radius: $medium-editor-border-radius;\n    box-shadow: 0 2px 6px rgba(#000, .45);\n\n    li {\n        button {\n            min-width: $medium-editor-button-size;\n            height: $medium-editor-button-size;\n            border: none;\n            border-right: 1px solid $medium-editor-border-color;\n            background-color: transparent;\n            color: $medium-editor-link-color;\n            box-shadow: inset 0 0 3px #f8f8e6;\n            background: linear-gradient(to bottom, $medium-editor-bgcolor, rgba(#000, .2));\n            text-shadow: 1px 4px 6px #def, 0 0 0 #000, 1px 4px 6px #def;\n            transition: background-color .2s ease-in;\n            &:hover {\n                background-color: #fff;\n                color: $medium-editor-button-hover-text-color;\n                color: rgba(#000, .8);\n            }\n        }\n\n        .medium-editor-button-first {\n            border-top-left-radius: $medium-editor-border-radius;\n            border-bottom-left-radius: $medium-editor-border-radius;\n        }\n\n        .medium-editor-button-last {\n            border-top-right-radius: $medium-editor-border-radius;\n            border-bottom-right-radius: $medium-editor-border-radius;\n        }\n\n        .medium-editor-button-active {\n            background-color: #ccc;\n            color: $medium-editor-button-active-text-color;\n            color: rgba(#000, .8);\n            background: linear-gradient(to top, $medium-editor-bgcolor, rgba(#000, .1));\n        }\n    }\n}\n\n.medium-editor-toolbar-form {\n    background: $medium-editor-bgcolor;\n    color: #999;\n    border-radius: $medium-editor-border-radius;\n\n    .medium-editor-toolbar-input {\n        margin: 0;\n        height: $medium-editor-button-size;\n        background: $medium-editor-bgcolor;\n        color: $medium-editor-border-color;\n    }\n\n    a {\n        color: $medium-editor-link-color;\n    }\n}\n\n.medium-editor-toolbar-anchor-preview {\n    background: $medium-editor-bgcolor;\n    color: $medium-editor-link-color;\n    border-radius: $medium-editor-border-radius;\n}\n\n.medium-editor-placeholder:after {\n    color: $medium-editor-border-color;\n}\n"
  },
  {
    "path": "src/sass/themes/tim.scss",
    "content": "// theme settings\n$medium-editor-bgcolor: #2f1e07;\n$medium-editor-border-color: lighten($medium-editor-bgcolor, 10);\n$medium-editor-button-size: 60px;\n$medium-editor-button-active-text-color: #ffedd5;\n$medium-editor-hover-color: darken($medium-editor-bgcolor, 10);\n$medium-editor-link-color: #ffedd5;\n$medium-editor-border-radius: 6px;\n$medium-editor-placeholder-color: #ffedd5;\n\n// theme rules\n.medium-toolbar-arrow-under:after {\n    border-color: $medium-editor-bgcolor transparent transparent transparent;\n    top: $medium-editor-button-size;\n}\n\n.medium-toolbar-arrow-over:before {\n    border-color: transparent transparent $medium-editor-bgcolor transparent;\n}\n\n.medium-editor-toolbar {\n    background-color: $medium-editor-bgcolor;\n    border: 1px solid $medium-editor-border-color;\n    border-radius: $medium-editor-border-radius;\n\n    li {\n        button {\n            background-color: transparent;\n            border: none;\n            border-right: 1px solid $medium-editor-border-color;\n            box-sizing: border-box;\n            color: $medium-editor-link-color;\n            height: $medium-editor-button-size;\n            min-width: $medium-editor-button-size;\n            transition: background-color .2s ease-in, color .2s ease-in;\n            &:hover {\n                background-color: $medium-editor-hover-color;\n                color: $medium-editor-button-active-text-color;\n            }\n        }\n\n        .medium-editor-button-first {\n            border-bottom-left-radius: $medium-editor-border-radius;\n            border-top-left-radius: $medium-editor-border-radius;\n        }\n\n        .medium-editor-button-last {\n            border-bottom-right-radius: $medium-editor-border-radius;\n            border-right: none;\n            border-top-right-radius: $medium-editor-border-radius;\n        }\n\n        .medium-editor-button-active {\n            background-color: $medium-editor-hover-color;\n            color: $medium-editor-button-active-text-color;\n        }\n    }\n}\n\n.medium-editor-toolbar-form {\n    background: $medium-editor-bgcolor;\n    border-radius: $medium-editor-border-radius;\n    color: #ffedd5;\n\n    .medium-editor-toolbar-input {\n        background: $medium-editor-bgcolor;\n        color: $medium-editor-link-color;\n        height: $medium-editor-button-size;\n\n        &::-webkit-input-placeholder {\n           color: $medium-editor-placeholder-color;\n           color: rgba($medium-editor-placeholder-color, .8);\n        }\n        &:-moz-placeholder { /* Firefox 18- */\n           color: $medium-editor-placeholder-color;\n           color: rgba($medium-editor-placeholder-color, .8);\n        }\n        &::-moz-placeholder {  /* Firefox 19+ */\n           color: $medium-editor-placeholder-color;\n           color: rgba($medium-editor-placeholder-color, .8);\n        }\n        &:-ms-input-placeholder {\n           color: $medium-editor-placeholder-color;\n           color: rgba($medium-editor-placeholder-color, .8);\n        }\n    }\n\n    a {\n        color: $medium-editor-link-color;\n    }\n}\n\n.medium-editor-toolbar-anchor-preview {\n    background: $medium-editor-bgcolor;\n    border-radius: $medium-editor-border-radius;\n    color: $medium-editor-link-color;\n}\n\n.medium-editor-placeholder:after {\n    color: $medium-editor-border-color;\n}\n"
  },
  {
    "path": "src/sass/util/_clearfix.scss",
    "content": "%clearfix {\n    &:after {\n        clear: both;\n        content: \"\";\n        display: table;\n    }\n}\n"
  },
  {
    "path": "src/wrappers/end.js",
    "content": "    return MediumEditor;\n}()));\n"
  },
  {
    "path": "src/wrappers/start.js",
    "content": "(function (root, factory) {\n    'use strict';\n    var isElectron = typeof module === 'object' && typeof process !== 'undefined' && process && process.versions && process.versions.electron;\n    if (!isElectron && typeof module === 'object') {\n        module.exports = factory;\n    } else if (typeof define === 'function' && define.amd) {\n        define(function () {\n            return factory;\n        });\n    } else {\n        root.MediumEditor = factory;\n    }\n}(this, function () {\n\n    'use strict';\n"
  }
]