\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
or \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',\n contentFA: ''\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: 'I',\n contentFA: ''\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: 'U',\n contentFA: ''\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: 'A',\n contentFA: ''\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: 'x1',\n contentFA: ''\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: 'x1',\n contentFA: ''\n },\n 'image': {\n name: 'image',\n action: 'image',\n aria: 'image',\n tagNames: ['img'],\n contentDefault: 'image',\n contentFA: ''\n },\n 'html': {\n name: 'html',\n action: 'html',\n aria: 'evaluate html',\n tagNames: ['iframe', 'object'],\n contentDefault: 'html',\n contentFA: ''\n },\n 'orderedlist': {\n name: 'orderedlist',\n action: 'insertorderedlist',\n aria: 'ordered list',\n tagNames: ['ol'],\n useQueryState: true,\n contentDefault: '1.',\n contentFA: ''\n },\n 'unorderedlist': {\n name: 'unorderedlist',\n action: 'insertunorderedlist',\n aria: 'unordered list',\n tagNames: ['ul'],\n useQueryState: true,\n contentDefault: '•',\n contentFA: ''\n },\n 'indent': {\n name: 'indent',\n action: 'indent',\n aria: 'indent',\n tagNames: [],\n contentDefault: '→',\n contentFA: ''\n },\n 'outdent': {\n name: 'outdent',\n action: 'outdent',\n aria: 'outdent',\n tagNames: [],\n contentDefault: '←',\n contentFA: ''\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: 'C',\n contentFA: ''\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: 'J',\n contentFA: ''\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: 'L',\n contentFA: ''\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: 'R',\n contentFA: ''\n },\n // Known inline elements that are not removed, or not removed consistantly across browsers:\n // ,