PageSourceSearch

https://levaa.app/dmxAppConnect/dmxMediumEditor/medium-editor.js

js levaa.app collected 2026-09-29 01:24:16 UTC 322,401 bytes, 7,903 lines download raw bytes

vendor: 23,006 bytes, lines 1-656
1/*global self, document, DOMException */
2
3/*! @source http://purl.eligrey.com/github/classList.js/blob/master/classList.js */
4
5// Full polyfill for browsers with no classList support
6if (!("classList" in document.createElement("_"))) {
7  (function (view) {
8
9  "use strict";
10
11  if (!('Element' in view)) return;
12
13  var
14      classListProp = "classList"
15    , protoProp = "prototype"
16    , elemCtrProto = view.Element[protoProp]
17    , objCtr = Object
18    , strTrim = String[protoProp].trim || function () {
19      return this.replace(/^\s+|\s+$/g, "");
20    }
21    , arrIndexOf = Array[protoProp].indexOf || function (item) {
22      var
23          i = 0
24        , len = this.length
25      ;
26      for (; i < len; i++) {
27        if (i in this && this[i] === item) {
28          return i;
29        }
30      }
31      return -1;
32    }
33    // Vendors: please allow content code to instantiate DOMExceptions
34    , DOMEx = function (type, message) {
35      this.name = type;
36      this.code = DOMException[type];
37      this.message = message;
38    }
39    , checkTokenAndGetIndex = function (classList, token) {
40      if (token === "") {
41        throw new DOMEx(
42            "SYNTAX_ERR"
43          , "An invalid or illegal string was specified"
44        );
45      }
46      if (/\s/.test(token)) {
47        throw new DOMEx(
48            "INVALID_CHARACTER_ERR"
49          , "String contains an invalid character"
50        );
51      }
52      return arrIndexOf.call(classList, token);
53    }
54    , ClassList = function (elem) {
55      var
56          trimmedClasses = strTrim.call(elem.getAttribute("class") || "")
57        , classes = trimmedClasses ? trimmedClasses.split(/\s+/) : []
58        , i = 0
59        , len = classes.length
60      ;
61      for (; i < len; i++) {
62        this.push(classes[i]);
63      }
64      this._updateClassName = function () {
65        elem.setAttribute("class", this.toString());
66      };
67    }
68    , classListProto = ClassList[protoProp] = []
69    , classListGetter = function () {
70      return new ClassList(this);
71    }
72  ;
73  // Most DOMException implementations don't allow calling DOMException's toString()
74  // on non-DOMExceptions. Error's toString() is sufficient here.
75  DOMEx[protoProp] = Error[protoProp];
76  classListProto.item = function (i) {
77    return this[i] || null;
78  };
79  classListProto.contains = function (token) {
80    token += "";
81    return checkTokenAndGetIndex(this, token) !== -1;
82  };
83  classListProto.add = function () {
84    var
85        tokens = arguments
86      , i = 0
87      , l = tokens.length
88      , token
89      , updated = false
90    ;
91    do {
92      token = tokens[i] + "";
93      if (checkTokenAndGetIndex(this, token) === -1) {
94        this.push(token);
95        updated = true;
96      }
97    }
98    while (++i < l);
99
100    if (updated) {
101      this._updateClassName();
102    }
103  };
104  classListProto.remove = function () {
105    var
106        tokens = arguments
107      , i = 0
108      , l = tokens.length
109      , token
110      , updated = false
111      , index
112    ;
113    do {
114      token = tokens[i] + "";
115      index = checkTokenAndGetIndex(this, token);
116      while (index !== -1) {
117        this.splice(index, 1);
118        updated = true;
119        index = checkTokenAndGetIndex(this, token);
120      }
121    }
122    while (++i < l);
123
124    if (updated) {
125      this._updateClassName();
126    }
127  };
128  classListProto.toggle = function (token, force) {
129    token += "";
130
131    var
132        result = this.contains(token)
133      , method = result ?
134        force !== true && "remove"
135      :
136        force !== false && "add"
137    ;
138
139    if (method) {
140      this[method](token);
141    }
142
143    if (force === true || force === false) {
144      return force;
145    } else {
146      return !result;
147    }
148  };
149  classListProto.toString = function () {
150    return this.join(" ");
151  };
152
153  if (objCtr.defineProperty) {
154    var classListPropDesc = {
155        get: classListGetter
156      , enumerable: true
157      , configurable: true
158    };
159    try {
160      objCtr.defineProperty(elemCtrProto, classListProp, classListPropDesc);
161    } catch (ex) { // IE 8 doesn't support enumerable:true
162      if (ex.number === -0x7FF5EC54) {
163        classListPropDesc.enumerable = false;
164        objCtr.defineProperty(elemCtrProto, classListProp, classListPropDesc);
165      }
166    }
167  } else if (objCtr[protoProp].__defineGetter__) {
168    elemCtrProto.__defineGetter__(classListProp, classListGetter);
169  }
170
171  }(self));
172}
173
174/* Blob.js
175 * A Blob implementation.
176 * 2014-07-24
177 *
178 * By Eli Grey, http://eligrey.com
179 * By Devin Samarin, https://github.com/dsamarin
180 * License: X11/MIT
181 *   See https://github.com/eligrey/Blob.js/blob/master/LICENSE.md
182 */
183
184/*global self, unescape */
185/*jslint bitwise: true, regexp: true, confusion: true, es5: true, vars: true, white: true,
186  plusplus: true */
187
188/*! @source http://purl.eligrey.com/github/Blob.js/blob/master/Blob.js */
189
190(function (view) {
191  "use strict";
192
193  view.URL = view.URL || view.webkitURL;
194
195  if (view.Blob && view.URL) {
196    try {
197      new Blob;
198      return;
199    } catch (e) {}
200  }
201
202  // Internally we use a BlobBuilder implementation to base Blob off of
203  // in order to support older browsers that only have BlobBuilder
204  var BlobBuilder = view.BlobBuilder || view.WebKitBlobBuilder || view.MozBlobBuilder || (function(view) {
205    var
206        get_class = function(object) {
207        return Object.prototype.toString.call(object).match(/^\[object\s(.*)\]$/)[1];
208      }
209      , FakeBlobBuilder = function BlobBuilder() {
210        this.data = [];
211      }
212      , FakeBlob = function Blob(data, type, encoding) {
213        this.data = data;
214        this.size = data.length;
215        this.type = type;
216        this.encoding = encoding;
217      }
218      , FBB_proto = FakeBlobBuilder.prototype
219      , FB_proto = FakeBlob.prototype
220      , FileReaderSync = view.FileReaderSync
221      , FileException = function(type) {
222        this.code = this[this.name = type];
223      }
224      , file_ex_codes = (
225          "NOT_FOUND_ERR SECURITY_ERR ABORT_ERR NOT_READABLE_ERR ENCODING_ERR "
226        + "NO_MODIFICATION_ALLOWED_ERR INVALID_STATE_ERR SYNTAX_ERR"
227      ).split(" ")
228      , file_ex_code = file_ex_codes.length
229      , real_URL = view.URL || view.webkitURL || view
230      , real_create_object_URL = real_URL.createObjectURL
231      , real_revoke_object_URL = real_URL.revokeObjectURL
232      , URL = real_URL
233      , btoa = view.btoa
234      , atob = view.atob
235
236      , ArrayBuffer = view.ArrayBuffer
237      , Uint8Array = view.Uint8Array
238
239      , origin = /^[\w-]+:\/*\[?[\w\.:-]+\]?(?::[0-9]+)?/
240    ;
241    FakeBlob.fake = FB_proto.fake = true;
242    while (file_ex_code--) {
243      FileException.prototype[file_ex_codes[file_ex_code]] = file_ex_code + 1;
244    }
245    // Polyfill URL
246    if (!real_URL.createObjectURL) {
247      URL = view.URL = function(uri) {
248        var
249            uri_info = document.createElementNS("http://www.w3.org/1999/xhtml", "a")
250          , uri_origin
251        ;
252        uri_info.href = uri;
253        if (!("origin" in uri_info)) {
254          if (uri_info.protocol.toLowerCase() === "data:") {
255            uri_info.origin = null;
256          } else {
257            uri_origin = uri.match(origin);
258            uri_info.origin = uri_origin && uri_origin[1];
259          }
260        }
261        return uri_info;
262      };
263    }
264    URL.createObjectURL = function(blob) {
265      var
266          type = blob.type
267        , data_URI_header
268      ;
269      if (type === null) {
270        type = "application/octet-stream";
271      }
272      if (blob instanceof FakeBlob) {
273        data_URI_header = "data:" + type;
274        if (blob.encoding === "base64") {
275          return data_URI_header + ";base64," + blob.data;
276        } else if (blob.encoding === "URI") {
277          return data_URI_header + "," + decodeURIComponent(blob.data);
278        } if (btoa) {
279          return data_URI_header + ";base64," + btoa(blob.data);
280        } else {
281          return data_URI_header + "," + encodeURIComponent(blob.data);
282        }
283      } else if (real_create_object_URL) {
284        return real_create_object_URL.call(real_URL, blob);
285      }
286    };
287    URL.revokeObjectURL = function(object_URL) {
288      if (object_URL.substring(0, 5) !== "data:" && real_revoke_object_URL) {
289        real_revoke_object_URL.call(real_URL, object_URL);
290      }
291    };
292    FBB_proto.append = function(data/*, endings*/) {
293      var bb = this.data;
294      // decode data to a binary string
295      if (Uint8Array && (data instanceof ArrayBuffer || data instanceof Uint8Array)) {
296        var
297            str = ""
298          , buf = new Uint8Array(data)
299          , i = 0
300          , buf_len = buf.length
301        ;
302        for (; i < buf_len; i++) {
303          str += String.fromCharCode(buf[i]);
304        }
305        bb.push(str);
306      } else if (get_class(data) === "Blob" || get_class(data) === "File") {
307        if (FileReaderSync) {
308          var fr = new FileReaderSync;
309          bb.push(fr.readAsBinaryString(data));
310        } else {
311          // async FileReader won't work as BlobBuilder is sync
312          throw new FileException("NOT_READABLE_ERR");
313        }
314      } else if (data instanceof FakeBlob) {
315        if (data.encoding === "base64" && atob) {
316          bb.push(atob(data.data));
317        } else if (data.encoding === "URI") {
318          bb.push(decodeURIComponent(data.data));
319        } else if (data.encoding === "raw") {
320          bb.push(data.data);
321        }
322      } else {
323        if (typeof data !== "string") {
324          data += ""; // convert unsupported types to strings
325        }
326        // decode UTF-16 to binary string
327        bb.push(unescape(encodeURIComponent(data)));
328      }
329    };
330    FBB_proto.getBlob = function(type) {
331      if (!arguments.length) {
332        type = null;
333      }
334      return new FakeBlob(this.data.join(""), type, "raw");
335    };
336    FBB_proto.toString = function() {
337      return "[object BlobBuilder]";
338    };
339    FB_proto.slice = function(start, end, type) {
340      var args = arguments.length;
341      if (args < 3) {
342        type = null;
343      }
344      return new FakeBlob(
345          this.data.slice(start, args > 1 ? end : this.data.length)
346        , type
347        , this.encoding
348      );
349    };
350    FB_proto.toString = function() {
351      return "[object Blob]";
352    };
353    FB_proto.close = function() {
354      this.size = 0;
355      delete this.data;
356    };
357    return FakeBlobBuilder;
358  }(view));
359
360  view.Blob = function(blobParts, options) {
361    var type = options ? (options.type || "") : "";
362    var builder = new BlobBuilder();
363    if (blobParts) {
364      for (var i = 0, len = blobParts.length; i < len; i++) {
365        if (Uint8Array && blobParts[i] instanceof Uint8Array) {
366          builder.append(blobParts[i].buffer);
367        }
368        else {
369          builder.append(blobParts[i]);
370        }
371      }
372    }
373    var blob = builder.getBlob(type);
374    if (!blob.slice && blob.webkitSlice) {
375      blob.slice = blob.webkitSlice;
376    }
377    return blob;
378  };
379
380  var getPrototypeOf = Object.getPrototypeOf || function(object) {
381    return object.__proto__;
382  };
383  view.Blob.prototype = getPrototypeOf(new view.Blob());
384}(typeof self !== "undefined" && self || typeof window !== "undefined" && window || this.content || this));
385
386(function (root, factory) {
387    'use strict';
388    var isElectron = typeof module === 'object' && typeof process !== 'undefined' && process && process.versions && process.versions.electron;
389    if (!isElectron && typeof module === 'object') {
390        module.exports = factory;
391    } else if (typeof define === 'function' && define.amd) {
392        define(function () {
393            return factory;
394        });
395    } else {
396        root.MediumEditor = factory;
397    }
398}(this, function () {
399
400    'use strict';
401
402function MediumEditor(elements, options) {
403    'use strict';
404    return this.init(elements, options);
405}
406
407MediumEditor.extensions = {};
408/*jshint unused: true */
409(function (window) {
410    'use strict';
411
412    function copyInto(overwrite, dest) {
413        var prop,
414            sources = Array.prototype.slice.call(arguments, 2);
415        dest = dest || {};
416        for (var i = 0; i < sources.length; i++) {
417            var source = sources[i];
418            if (source) {
419                for (prop in source) {
420                    if (source.hasOwnProperty(prop) &&
421                        typeof source[prop] !== 'undefined' &&
422                        (overwrite || dest.hasOwnProperty(prop) === false)) {
423                        dest[prop] = source[prop];
424                    }
425                }
426            }
427        }
428        return dest;
429    }
430
431    // https://developer.mozilla.org/en-US/docs/Web/API/Node/contains
432    // Some browsers (including phantom) don't return true for Node.contains(child)
433    // if child is a text node.  Detect these cases here and use a fallback
434    // for calls to Util.isDescendant()
435    var nodeContainsWorksWithTextNodes = false;
436    try {
437        var testParent = document.createElement('div'),
438            testText = document.createTextNode(' ');
439        testParent.appendChild(testText);
440        nodeContainsWorksWithTextNodes = testParent.contains(testText);
441    } catch (exc) {}
442
443    var Util = {
444
445        // http://stackoverflow.com/questions/17907445/how-to-detect-ie11#comment30165888_17907562
446        // by rg89
447        isIE: ((navigator.appName === 'Microsoft Internet Explorer') || ((navigator.appName === 'Netscape') && (new RegExp('Trident/.*rv:([0-9]{1,}[.0-9]{0,})').exec(navigator.userAgent) !== null))),
448
449        isEdge: (/Edge\/\d+/).exec(navigator.userAgent) !== null,
450
451        // if firefox
452        isFF: (navigator.userAgent.toLowerCase().indexOf('firefox') > -1),
453
454        // http://stackoverflow.com/a/11752084/569101
455        isMac: (window.navigator.platform.toUpperCase().indexOf('MAC') >= 0),
456
457        // https://github.com/jashkenas/underscore
458        // Lonely letter MUST USE the uppercase code
459        keyCode: {
460            BACKSPACE: 8,
461            TAB: 9,
462            ENTER: 13,
463            ESCAPE: 27,
464            SPACE: 32,
465            DELETE: 46,
466            K: 75, // K keycode, and not k
467            M: 77,
468            V: 86
469        },
470
471        /**
472         * Returns true if it's metaKey on Mac, or ctrlKey on non-Mac.
473         * See #591
474         */
475        isMetaCtrlKey: function (event) {
476            if ((Util.isMac && event.metaKey) || (!Util.isMac && event.ctrlKey)) {
477                return true;
478            }
479
480            return false;
481        },
482
483        /**
484         * Returns true if the key associated to the event is inside keys array
485         *
486         * @see : https://github.com/jquery/jquery/blob/0705be475092aede1eddae01319ec931fb9c65fc/src/event.js#L473-L484
487         * @see : http://stackoverflow.com/q/4471582/569101
488         */
489        isKey: function (event, keys) {
490            var keyCode = Util.getKeyCode(event);
491
492            // it's not an array let's just compare strings!
493            if (false === Array.isArray(keys)) {
494                return keyCode === keys;
495            }
496
497            if (-1 === keys.indexOf(keyCode)) {
498                return false;
499            }
500
501            return true;
502        },
503
504        getKeyCode: function (event) {
505            var keyCode = event.which;
506
507            // getting the key code from event
508            if (null === keyCode) {
509                keyCode = event.charCode !== null ? event.charCode : event.keyCode;
510            }
511
512            return keyCode;
513        },
514
515        blockContainerElementNames: [
516            // elements our editor generates
517            'p', 'h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'blockquote', 'pre', 'ul', 'li', 'ol',
518            // all other known block elements
519            'address', 'article', 'aside', 'audio', 'canvas', 'dd', 'dl', 'dt', 'fieldset',
520            'figcaption', 'figure', 'footer', 'form', 'header', 'hgroup', 'main', 'nav',
521            'noscript', 'output', 'section', 'video', 'div',
522            'table', 'thead', 'tbody', 'tfoot', 'tr', 'th', 'td'
523        ],
524
525        emptyElementNames: ['br', 'col', 'colgroup', 'hr', 'img', 'input', 'source', 'wbr'],
526
527        extend: function extend(/* dest, source1, source2, ...*/) {
528            var args = [true].concat(Array.prototype.slice.call(arguments));
529            return copyInto.apply(this, args);
530        },
531
532        defaults: function defaults(/*dest, source1, source2, ...*/) {
533            var args = [false].concat(Array.prototype.slice.call(arguments));
534            return copyInto.apply(this, args);
535        },
536
537        /*
538         * Create a link around the provided text nodes which must be adjacent to each other and all be
539         * descendants of the same closest block container. If the preconditions are not met, unexpected
540         * behavior will result.
541         */
542        createLink: function (document, textNodes, href, target) {
543            var anchor = document.createElement('a');
544            Util.moveTextRangeIntoElement(textNodes[0], textNodes[textNodes.length - 1], anchor);
545            anchor.setAttribute('href', href);
546            if (target) {
547                anchor.setAttribute('target', target);
548            }
549            return anchor;
550        },
551
552        /*
553         * Given the provided match in the format {start: 1, end: 2} where start and end are indices into the
554         * textContent of the provided element argument, modify the DOM inside element to ensure that the text
555         * identified by the provided match can be returned as text nodes that contain exactly that text, without
556         * any additional text at the beginning or end of the returned array of adjacent text nodes.
557         *
558         * The only DOM manipulation performed by this function is splitting the text nodes, non-text nodes are
559         * not affected in any way.
560         */
561        findOrCreateMatchingTextNodes: function (document, element, match) {
562            var treeWalker = document.createTreeWalker(element, NodeFilter.SHOW_ALL, null, false),
563                matchedNodes = [],
564                currentTextIndex = 0,
565                startReached = false,
566                currentNode = null,
567                newNode = null;
568
569            while ((currentNode = treeWalker.nextNode()) !== null) {
570                if (currentNode.nodeType > 3) {
571                    continue;
572                } else if (currentNode.nodeType === 3) {
573                    if (!startReached && match.start < (currentTextIndex + currentNode.nodeValue.length)) {
574                        startReached = true;
575                        newNode = Util.splitStartNodeIfNeeded(currentNode, match.start, currentTextIndex);
576                    }
577                    if (startReached) {
578                        Util.splitEndNodeIfNeeded(currentNode, newNode, match.end, currentTextIndex);
579                    }
580                    if (startReached && currentTextIndex === match.end) {
581                        break; // Found the node(s) corresponding to the link. Break out and move on to the next.
582                    } else if (startReached && currentTextIndex > (match.end + 1)) {
583                        throw new Error('PerformLinking overshot the target!'); // should never happen...
584                    }
585
586                    if (startReached) {
587                        matchedNodes.push(newNode || currentNode);
588                    }
589
590                    currentTextIndex += currentNode.nodeValue.length;
591                    if (newNode !== null) {
592                        currentTextIndex += newNode.nodeValue.length;
593                        // Skip the newNode as we'll already have pushed it to the matches
594                        treeWalker.nextNode();
595                    }
596                    newNode = null;
597                } else if (currentNode.tagName.toLowerCase() === 'img') {
598                    if (!startReached && (match.start <= currentTextIndex)) {
599                        startReached = true;
600                    }
601                    if (startReached) {
602                        matchedNodes.push(currentNode);
603                    }
604                }
605            }
606            return matchedNodes;
607        },
608
609        /*
610         * Given the provided text node and text coordinates, split the text node if needed to make it align
611         * precisely with the coordinates.
612         *
613         * This function is intended to be called from Util.findOrCreateMatchingTextNodes.
614         */
615        splitStartNodeIfNeeded: function (currentNode, matchStartIndex, currentTextIndex) {
616            if (matchStartIndex !== currentTextIndex) {
617                return currentNode.splitText(matchStartIndex - currentTextIndex);
618            }
619            return null;
620        },
621
622        /*
623         * Given the provided text node and text coordinates, split the text node if needed to make it align
624         * precisely with the coordinates. The newNode argument should from the result of Util.splitStartNodeIfNeeded,
625         * if that function has been called on the same currentNode.
626         *
627         * This function is intended to be called from Util.findOrCreateMatchingTextNodes.
628         */
629        splitEndNodeIfNeeded: function (currentNode, newNode, matchEndIndex, currentTextIndex) {
630            var textIndexOfEndOfFarthestNode,
631                endSplitPoint;
632            textIndexOfEndOfFarthestNode = currentTextIndex + currentNode.nodeValue.length +
633                    (newNode ? newNode.nodeValue.length : 0) - 1;
634            endSplitPoint = matchEndIndex - currentTextIndex -
635                    (newNode ? currentNode.nodeValue.length : 0);
636            if (textIndexOfEndOfFarthestNode >= matchEndIndex &&
637                    currentTextIndex !== textIndexOfEndOfFarthestNode &&
638                    endSplitPoint !== 0) {
639                (newNode || currentNode).splitText(endSplitPoint);
640            }
641        },
642
643        /*
644        * Take an element, and break up all of its text content into unique pieces such that:
645         * 1) All text content of the elements are in separate blocks. No piece of text content should span
646         *    across multiple blocks. This means no element return by this function should have
647         *    any blocks as children.
648         * 2) The union of the textcontent of all of the elements returned here covers all
649         *    of the text within the element.
650         *
651         *
652         * EXAMPLE:
653         * In the event that we have something like:
654         *
655         * <blockquote>
656         *   <p>Some Text</p>
657         *   <ol>
658         *     <li>List Item 1</li>
659         *     <li>List Item 2</li>
660         *   </ol>
661         * </blockquote>
662         *
663         * This function would return these elements as an array:
664         *   [ <p>Some Text</p>, <li>List Item 1</li>, <li>List Item 2</li> ]
665         *
666         * Since the <blockquote> and <ol> elements contain blocks within them they are not returned.
667         * Since the <p> and <li>'s don't contain block elements and cover all the text content of the
668         * <blockquote> container, they are the elements returned.
669         */
670        splitByBlockElements: function (element) {
671            if (element.nodeType !== 3 && element.nodeType !== 1) {
672                return [];
673            }
674
675            var toRet = [],
676                blockElementQuery = MediumEditor.util.blockContainerElementNames.join(',');
677
678            if (element.nodeType === 3 || element.querySelectorAll(blockElementQuery).length === 0) {
679                return [element];
680            }
681
682            for (var i = 0; i < element.childNodes.length; i++) {
683                var child = element.childNodes[i];
684                if (child.nodeType === 3) {
685                    toRet.push(child);
686                } else if (child.nodeType === 1) {
687                    var blockElements = child.querySelectorAll(blockElementQuery);
688                    if (blockElements.length === 0) {
689                        toRet.push(child);
690                    } else {
691                        toRet = toRet.concat(MediumEditor.util.splitByBlockElements(child));
692                    }
693                }
694            }
695
696            return toRet;
697        },
698
699        // Find the next node in the DOM tree that represents any text that is being
700        // displayed directly next to the targetNode (passed as an argument)
701        // Text that appears directly next to the current node can be:
702        //  - A sibling text node
703        //  - A descendant of a sibling element
704        //  - A sibling text node of an ancestor
705        //  - A descendant of a sibling element of an ancestor
706        findAdjacentTextNodeWithContent: function findAdjacentTextNodeWithContent(rootNode, targetNode, ownerDocument) {
707            var pastTarget = false,
708                nextNode,
709                nodeIterator = ownerDocument.createNodeIterator(rootNode, NodeFilter.SHOW_TEXT, null, false);
710
711            // Use a native NodeIterator to iterate over all the text nodes that are descendants
712            // of the rootNode.  Once past the targetNode, choose the first non-empty text node
713            nextNode = nodeIterator.nextNode();
714            while (nextNode) {
715                if (nextNode === targetNode) {
716                    pastTarget = true;
717                } else if (pastTarget) {
718                    if (nextNode.nodeType === 3 && nextNode.nodeValue && nextNode.nodeValue.trim().length > 0) {
719                        break;
720                    }
721                }
722                nextNode = nodeIterator.nextNode();
723            }
724
725            return nextNode;
726        },
727
728        // Find an element's previous sibling within a medium-editor element
729        // If one doesn't exist, find the closest ancestor's previous sibling
730        findPreviousSibling: function (node) {
731            if (!node || Util.isMediumEditorElement(node)) {
732                return false;
733            }
734
735            var previousSibling = node.previousSibling;
736            while (!previousSibling && !Util.isMediumEditorElement(node.parentNode)) {
737                node = node.parentNode;
738                previousSibling = node.previousSibling;
739            }
740
741            return previousSibling;
742        },
743
744        isDescendant: function isDescendant(parent, child, checkEquality) {
745            if (!parent || !child) {
746                return false;
747            }
748            if (parent === child) {
749                return !!checkEquality;
750            }
751            // If parent is not an element, it can't have any descendants
752            if (parent.nodeType !== 1) {
753                return false;
754            }
755            if (nodeContainsWorksWithTextNodes || child.nodeType !== 3) {
756                return parent.contains(child);
757            }
758            var node = child.parentNode;
759            while (node !== null) {
760                if (node === parent) {
761                    return true;
762                }
763                node = node.parentNode;
764            }
765            return false;
766        },
767
768        // https://github.com/jashkenas/underscore
769        isElement: function isElement(obj) {
770            return !!(obj && obj.nodeType === 1);
771        },
772
773        // https://github.com/jashkenas/underscore
774        throttle: function (func, wait) {
775            var THROTTLE_INTERVAL = 50,
776                context,
777                args,
778                result,
779                timeout = null,
780                previous = 0,
781                later = function () {
782                    previous = Date.now();
783                    timeout = null;
vendor: 4,978 bytes, lines 784-894
784                    result = func.apply(context, args);
785                    if (!timeout) {
786                        context = args = null;
787                    }
788                };
789
790            if (!wait && wait !== 0) {
791                wait = THROTTLE_INTERVAL;
792            }
793
794            return function () {
795                var now = Date.now(),
796                    remaining = wait - (now - previous);
797
798                context = this;
799                args = arguments;
800                if (remaining <= 0 || remaining > wait) {
801                    if (timeout) {
802                        clearTimeout(timeout);
803                        timeout = null;
804                    }
805                    previous = now;
806                    result = func.apply(context, args);
807                    if (!timeout) {
808                        context = args = null;
809                    }
810                } else if (!timeout) {
811                    timeout = setTimeout(later, remaining);
812                }
813                return result;
814            };
815        },
816
817        traverseUp: function (current, testElementFunction) {
818            if (!current) {
819                return false;
820            }
821
822            do {
823                if (current.nodeType === 1) {
824                    if (testElementFunction(current)) {
825                        return current;
826                    }
827                    // do not traverse upwards past the nearest containing editor
828                    if (Util.isMediumEditorElement(current)) {
829                        return false;
830                    }
831                }
832
833                current = current.parentNode;
834            } while (current);
835
836            return false;
837        },
838
839        htmlEntities: function (str) {
840            // converts special characters (like <) into their escaped/encoded values (like &lt;).
841            // This allows you to show to display the string without the browser reading it as HTML.
842            return String(str).replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;').replace(/"/g, '&quot;');
843        },
844
845        // http://stackoverflow.com/questions/6690752/insert-html-at-caret-in-a-contenteditable-div
846        insertHTMLCommand: function (doc, html) {
847            var selection, range, el, fragment, node, lastNode, toReplace,
848                res = false,
849                ecArgs = ['insertHTML', false, html];
850
851            /* Edge's implementation of insertHTML is just buggy right now:
852             * - Doesn't allow leading white space at the beginning of an element
853             * - Found a case when a <font size="2"> tag was inserted when calling alignCenter inside a blockquote
854             *
855             * There are likely other bugs, these are just the ones we found so far.
856             * For now, let's just use the same fallback we did for IE
857             */
858            if (!MediumEditor.util.isEdge && doc.queryCommandSupported('insertHTML')) {
859                try {
860                    return doc.execCommand.apply(doc, ecArgs);
861                } catch (ignore) {}
862            }
863
864            selection = doc.getSelection();
865            if (selection.rangeCount) {
866                range = selection.getRangeAt(0);
867                toReplace = range.commonAncestorContainer;
868
869                // https://github.com/yabwe/medium-editor/issues/748
870                // If the selection is an empty editor element, create a temporary text node inside of the editor
871                // and select it so that we don't delete the editor element
872                if (Util.isMediumEditorElement(toReplace) && !toReplace.firstChild) {
873                    range.selectNode(toReplace.appendChild(doc.createTextNode('')));
874                } else if ((toReplace.nodeType === 3 && range.startOffset === 0 && range.endOffset === toReplace.nodeValue.length) ||
875                        (toReplace.nodeType !== 3 && toReplace.innerHTML === range.toString())) {
876                    // Ensure range covers maximum amount of nodes as possible
877                    // By moving up the DOM and selecting ancestors whose only child is the range
878                    while (!Util.isMediumEditorElement(toReplace) &&
879                            toReplace.parentNode &&
880                            toReplace.parentNode.childNodes.length === 1 &&
881                            !Util.isMediumEditorElement(toReplace.parentNode)) {
882                        toReplace = toReplace.parentNode;
883                    }
884                    range.selectNode(toReplace);
885                }
886                range.deleteContents();
887
888                el = doc.createElement('div');
889                el.innerHTML = html;
890                fragment = doc.createDocumentFragment();
891                while (el.firstChild) {
892                    node = el.firstChild;
893                    lastNode = fragment.appendChild(node);
894                }
895                range.insertNode(fragment);
896
897                // Preserve the selection:
898                if (lastNode) {
899                    range = range.cloneRange();
900                    range.setStartAfter(lastNode);
901                    range.collapse(true);
902                    MediumEditor.selection.selectRange(doc, range);
903                }
904                res = true;
905            }
906
907            // https://github.com/yabwe/medium-editor/issues/992
908            // If we're monitoring calls to execCommand, notify listeners as if a real call had happened
909            if (doc.execCommand.callListeners) {
910                doc.execCommand.callListeners(ecArgs, res);
911            }
912            return res;
913        },
914
915        execFormatBlock: function (doc, tagName) {
916            // Get the top level block element that contains the selection
917            var blockContainer = Util.getTopBlockContainer(MediumEditor.selection.getSelectionStart(doc)),
918                childNodes;
919
920            // Special handling for blockquote
921            if (tagName === 'blockquote') {
922                if (blockContainer) {
923                    childNodes = Array.prototype.slice.call(blockContainer.childNodes);
924                    // Check if the blockquote has a block element as a child (nested blocks)
925                    if (childNodes.some(function (childNode) {
926                        return Util.isBlockContainer(childNode);
927                    })) {
928                        // FF handles blockquote differently on formatBlock
929                        // allowing nesting, we need to use outdent
930                        // https://developer.mozilla.org/en-US/docs/Rich-Text_Editing_in_Mozilla
931                        return doc.execCommand('outdent', false, null);
932                    }
933                }
934
935                // When IE blockquote needs to be called as indent
936                // http://stackoverflow.com/questions/1816223/rich-text-editor-with-blockquote-function/1821777#1821777
937                if (Util.isIE) {
938                    return doc.execCommand('indent', false, tagName);
939                }
940            }
941
942            // If the blockContainer is already the element type being passed in
943            // treat it as 'undo' formatting and just convert it to a <p>
944            if (blockContainer && tagName === blockContainer.nodeName.toLowerCase()) {
945                tagName = 'p';
946            }
947
948            // When IE we need to add <> to heading elements
949            // http://stackoverflow.com/questions/10741831/execcommand-formatblock-headings-in-ie
950            if (Util.isIE) {
951                tagName = '<' + tagName + '>';
952            }
953
954            if (blockContainer) {
955                var blockTagName = blockContainer.nodeName.toLowerCase();
956
957                if (tagName !== blockTagName) {
958                    // Changing list items to something else breaks, remove list item first
959                    if (blockTagName === 'ul') {
960                        doc.execCommand('insertunorderedlist', false);
961                    }
962
963                    if (blockTagName === 'ol') {
964                        doc.execCommand('insertorderedlist', false);
965                    }
966                }
967            }
968
969            // When FF, IE and Edge, we have to handle blockquote node seperately as 'formatblock' does not work.
970            // https://developer.mozilla.org/en-US/docs/Web/API/Document/execCommand#Commands
971            if (blockContainer && blockContainer.nodeName.toLowerCase() === 'blockquote') {
972                // For IE, just use outdent
973                if (Util.isIE && tagName === '<p>') {
974                    return doc.execCommand('outdent', false, tagName);
975                }
976
977                // For Firefox and Edge, make sure there's a nested block element before calling outdent
978                if ((Util.isFF || Util.isEdge) && tagName === 'p') {
979                    childNodes = Array.prototype.slice.call(blockContainer.childNodes);
980                    // If there are some non-block elements we need to wrap everything in a <p> before we outdent
981                    if (childNodes.some(function (childNode) {
982                        return !Util.isBlockContainer(childNode);
983                    })) {
984                        doc.execCommand('formatBlock', false, tagName);
985                    }
986                    return doc.execCommand('outdent', false, tagName);
987                }
988            }
989
990            return doc.execCommand('formatBlock', false, tagName);
991        },
992
993        /**
994         * Set target to blank on the given el element
995         *
996         * TODO: not sure if this should be here
997         *
998         * When creating a link (using core -> createLink) the selection returned by Firefox will be the parent of the created link
999         * 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
1000         * using `anchorUrl` to ensure that we are adding target="_blank" on the good one.
1001         * This isn't a bulletproof solution anyway ..
1002         */
vendor: 4,846 bytes, lines 1003-1127
1003        setTargetBlank: function (el, anchorUrl) {
1004            var i, url = anchorUrl || false;
1005            if (el.nodeName.toLowerCase() === 'a') {
1006                el.target = '_blank';
1007            } else {
1008                el = el.getElementsByTagName('a');
1009
1010                for (i = 0; i < el.length; i += 1) {
1011                    if (false === url || url === el[i].attributes.href.value) {
1012                        el[i].target = '_blank';
1013                    }
1014                }
1015            }
1016        },
1017
1018        /*
1019         * this function is called to explicitly remove the target='_blank' as FF holds on to _blank value even
1020         * after unchecking the checkbox on anchor form
1021         */
1022        removeTargetBlank: function (el, anchorUrl) {
1023            var i;
1024            if (el.nodeName.toLowerCase() === 'a') {
1025                el.removeAttribute('target');
1026            } else {
1027                el = el.getElementsByTagName('a');
1028
1029                for (i = 0; i < el.length; i += 1) {
1030                    if (anchorUrl === el[i].attributes.href.value) {
1031                        el[i].removeAttribute('target');
1032                    }
1033                }
1034            }
1035        },
1036
1037        /*
1038         * this function adds one or several classes on an a element.
1039         * if el parameter is not an a, it will look for a children of el.
1040         * if no a children are found, it will look for the a parent.
1041         */
1042        addClassToAnchors: function (el, buttonClass) {
1043            var classes = buttonClass.split(' '),
1044                i,
1045                j;
1046            if (el.nodeName.toLowerCase() === 'a') {
1047                for (j = 0; j < classes.length; j += 1) {
1048                    el.classList.add(classes[j]);
1049                }
1050            } else {
1051                var aChildren = el.getElementsByTagName('a');
1052                if (aChildren.length === 0) {
1053                    var parentAnchor = Util.getClosestTag(el, 'a');
1054                    el = parentAnchor ? [parentAnchor] : [];
1055                } else {
1056                    el = aChildren;
1057                }
1058                for (i = 0; i < el.length; i += 1) {
1059                    for (j = 0; j < classes.length; j += 1) {
1060                        el[i].classList.add(classes[j]);
1061                    }
1062                }
1063            }
1064        },
1065
1066        isListItem: function (node) {
1067            if (!node) {
1068                return false;
1069            }
1070            if (node.nodeName.toLowerCase() === 'li') {
1071                return true;
1072            }
1073
1074            var parentNode = node.parentNode,
1075                tagName = parentNode.nodeName.toLowerCase();
1076            while (tagName === 'li' || (!Util.isBlockContainer(parentNode) && tagName !== 'div')) {
1077                if (tagName === 'li') {
1078                    return true;
1079                }
1080                parentNode = parentNode.parentNode;
1081                if (parentNode) {
1082                    tagName = parentNode.nodeName.toLowerCase();
1083                } else {
1084                    return false;
1085                }
1086            }
1087            return false;
1088        },
1089
1090        cleanListDOM: function (ownerDocument, element) {
1091            if (element.nodeName.toLowerCase() !== 'li') {
1092                return;
1093            }
1094
1095            var list = element.parentElement;
1096
1097            if (list.parentElement.nodeName.toLowerCase() === 'p') { // yes we need to clean up
1098                Util.unwrap(list.parentElement, ownerDocument);
1099
1100                // move cursor at the end of the text inside the list
1101                // for some unknown reason, the cursor is moved to end of the "visual" line
1102                MediumEditor.selection.moveCursor(ownerDocument, element.firstChild, element.firstChild.textContent.length);
1103            }
1104        },
1105
1106        /* splitDOMTree
1107         *
1108         * Given a root element some descendant element, split the root element
1109         * into its own element containing the descendant element and all elements
1110         * on the left or right side of the descendant ('right' is default)
1111         *
1112         * example:
1113         *
1114         *         <div>
1115         *      /    |   \
1116         *  <span> <span> <span>
1117         *   / \    / \    / \
1118         *  1   2  3   4  5   6
1119         *
1120         *  If I wanted to split this tree given the <div> as the root and "4" as the leaf
1121         *  the result would be (the prime ' marks indicates nodes that are created as clones):
1122         *
1123         *   SPLITTING OFF 'RIGHT' TREE       SPLITTING OFF 'LEFT' TREE
1124         *
1125         *     <div>            <div>'              <div>'      <div>
1126         *      / \              / \                 / \          |
1127         * <span> <span>   <span>' <span>
1127       <span> <span>   <span>
1128         *   / \    |        |      / \           /\     /\       /\
1129         *  1   2   3        4     5   6         1  2   3  4     5  6
1130         *
1131         *  The above example represents splitting off the 'right' or 'left' part of a tree, where
1132         *  the <div>' would be returned as an element not appended to the DOM, and the <div>
1133         *  would remain in place where it was
1134         *
1135        */
1136        splitOffDOMTree: function (rootNode, leafNode, splitLeft) {
1137            var splitOnNode = leafNode,
1138                createdNode = null,
1139                splitRight = !splitLeft;
1140
1141            // loop until we hit the root
1142            while (splitOnNode !== rootNode) {
1143                var currParent = splitOnNode.parentNode,
1144                    newParent = currParent.cloneNode(false),
1145                    targetNode = (splitRight ? splitOnNode : currParent.firstChild),
1146                    appendLast;
1147
1148                // Create a new parent element which is a clone of the current parent
1149                if (createdNode) {
1150                    if (splitRight) {
1151                        // If we're splitting right, add previous created element before siblings
1152                        newParent.appendChild(createdNode);
1153                    } else {
1154                        // If we're splitting left, add previous created element last
1155                        appendLast = createdNode;
1156                    }
1157                }
1158                createdNode = newParent;
1159
1160                while (targetNode) {
1161                    var sibling = targetNode.nextSibling;
1162                    // Special handling for the 'splitNode'
1163                    if (targetNode === splitOnNode) {
1164                        if (!targetNode.hasChildNodes()) {
1165                            targetNode.parentNode.removeChild(targetNode);
1166                        } else {
1167                            // For the node we're splitting on, if it has children, we need to clone it
1168                            // and not just move it
1169                            targetNode = targetNode.cloneNode(false);
1170                        }
1171                        // If the resulting split node has content, add it
1172                        if (targetNode.textContent) {
1173                            createdNode.appendChild(targetNode);
1174                        }
1175
1176                        targetNode = (splitRight ? sibling : null);
1177                    } else {
1178                        // For general case, just remove the element and only
1179                        // add it to the split tree if it contains something
1180                        targetNode.parentNode.removeChild(targetNode);
1181                        if (targetNode.hasChildNodes() || targetNode.textContent) {
1182                            createdNode.appendChild(targetNode);
1183                        }
1184
1185                        targetNode = sibling;
1186                    }
1187                }
1188
1189                // If we had an element we wanted to append at the end, do that now
1190                if (appendLast) {
1191                    createdNode.appendChild(appendLast);
1192                }
1193
1194                splitOnNode = currParent;
1195            }
1196
1197            return createdNode;
1198        },
1199
1200        moveTextRangeIntoElement: function (startNode, endNode, newElement) {
1201            if (!startNode || !endNode) {
1202                return false;
1203            }
1204
1205            var rootNode = Util.findCommonRoot(startNode, endNode);
1206            if (!rootNode) {
1207                return false;
1208            }
1209
1210            if (endNode === startNode) {
1211                var temp = startNode.parentNode,
1212                    sibling = startNode.nextSibling;
1213                temp.removeChild(startNode);
1214                newElement.appendChild(startNode);
1215                if (sibling) {
1216                    temp.insertBefore(newElement, sibling);
1217                } else {
1218                    temp.appendChild(newElement);
1219                }
1220                return newElement.hasChildNodes();
1221            }
1222
1223            // create rootChildren array which includes all the children
1224            // we care about
1225            var rootChildren = [],
1226                firstChild,
1227                lastChild,
1228                nextNode;
1229            for (var i = 0; i < rootNode.childNodes.length; i++) {
1230                nextNode = rootNode.childNodes[i];
vendor: 264,467 bytes, lines 1231-7676
1231                if (!firstChild) {
1232                    if (Util.isDescendant(nextNode, startNode, true)) {
1233                        firstChild = nextNode;
1234                    }
1235                } else {
1236                    if (Util.isDescendant(nextNode, endNode, true)) {
1237                        lastChild = nextNode;
1238                        break;
1239                    } else {
1240                        rootChildren.push(nextNode);
1241                    }
1242                }
1243            }
1244
1245            var afterLast = lastChild.nextSibling,
1246                fragment = rootNode.ownerDocument.createDocumentFragment();
1247
1248            // build up fragment on startNode side of tree
1249            if (firstChild === startNode) {
1250                firstChild.parentNode.removeChild(firstChild);
1251                fragment.appendChild(firstChild);
1252            } else {
1253                fragment.appendChild(Util.splitOffDOMTree(firstChild, startNode));
1254            }
1255
1256            // add any elements between firstChild & lastChild
1257            rootChildren.forEach(function (element) {
1258                element.parentNode.removeChild(element);
1259                fragment.appendChild(element);
1260            });
1261
1262            // build up fragment on endNode side of the tree
1263            if (lastChild === endNode) {
1264                lastChild.parentNode.removeChild(lastChild);
1265                fragment.appendChild(lastChild);
1266            } else {
1267                fragment.appendChild(Util.splitOffDOMTree(lastChild, endNode, true));
1268            }
1269
1270            // Add fragment into passed in element
1271            newElement.appendChild(fragment);
1272
1273            if (lastChild.parentNode === rootNode) {
1274                // If last child is in the root, insert newElement in front of it
1275                rootNode.insertBefore(newElement, lastChild);
1276            } else if (afterLast) {
1277                // If last child was removed, but it had a sibling, insert in front of it
1278                rootNode.insertBefore(newElement, afterLast);
1279            } else {
1280                // lastChild was removed and was the last actual element just append
1281                rootNode.appendChild(newElement);
1282            }
1283
1284            return newElement.hasChildNodes();
1285        },
1286
1287        /* based on http://stackoverflow.com/a/6183069 */
1288        depthOfNode: function (inNode) {
1289            var theDepth = 0,
1290                node = inNode;
1291            while (node.parentNode !== null) {
1292                node = node.parentNode;
1293                theDepth++;
1294            }
1295            return theDepth;
1296        },
1297
1298        findCommonRoot: function (inNode1, inNode2) {
1299            var depth1 = Util.depthOfNode(inNode1),
1300                depth2 = Util.depthOfNode(inNode2),
1301                node1 = inNode1,
1302                node2 = inNode2;
1303
1304            while (depth1 !== depth2) {
1305                if (depth1 > depth2) {
1306                    node1 = node1.parentNode;
1307                    depth1 -= 1;
1308                } else {
1309                    node2 = node2.parentNode;
1310                    depth2 -= 1;
1311                }
1312            }
1313
1314            while (node1 !== node2) {
1315                node1 = node1.parentNode;
1316                node2 = node2.parentNode;
1317            }
1318
1319            return node1;
1320        },
1321        /* END - based on http://stackoverflow.com/a/6183069 */
1322
1323        isElementAtBeginningOfBlock: function (node) {
1324            var textVal,
1325                sibling;
1326            while (!Util.isBlockContainer(node) && !Util.isMediumEditorElement(node)) {
1327                sibling = node;
1328                while (sibling = sibling.previousSibling) {
1329                    textVal = sibling.nodeType === 3 ? sibling.nodeValue : sibling.textContent;
1330                    if (textVal.length > 0) {
1331                        return false;
1332                    }
1333                }
1334                node = node.parentNode;
1335            }
1336            return true;
1337        },
1338
1339        isMediumEditorElement: function (element) {
1340            return element && element.getAttribute && !!element.getAttribute('data-medium-editor-element');
1341        },
1342
1343        getContainerEditorElement: function (element) {
1344            return Util.traverseUp(element, function (node) {
1345                return Util.isMediumEditorElement(node);
1346            });
1347        },
1348
1349        isBlockContainer: function (element) {
1350            return element && element.nodeType !== 3 && !Util.isMediumEditorElement(element) && Util.blockContainerElementNames.indexOf(element.nodeName.toLowerCase()) !== -1;
1351        },
1352
1353        /* Finds the closest ancestor which is a block container element
1354         * If element is within editor element but not within any other block element,
1355         * the editor element is returned
1356         */
1357        getClosestBlockContainer: function (node) {
1358            return Util.traverseUp(node, function (node) {
1359                return Util.isBlockContainer(node) || Util.isMediumEditorElement(node);
1360            });
1361        },
1362
1363        /* Finds highest level ancestor element which is a block container element
1364         * If element is within editor element but not within any other block element,
1365         * the editor element is returned
1366         */
1367        getTopBlockContainer: function (element) {
1368            var topBlock = Util.isBlockContainer(element) ? element : false;
1369            Util.traverseUp(element, function (el) {
1370                if (Util.isBlockContainer(el)) {
1371                    topBlock = el;
1372                }
1373                if (!topBlock && Util.isMediumEditorElement(el)) {
1374                    topBlock = el;
1375                    return true;
1376                }
1377                return false;
1378            });
1379            return topBlock;
1380        },
1381
1382        getFirstSelectableLeafNode: function (element) {
1383            while (element && element.firstChild) {
1384                element = element.firstChild;
1385            }
1386
1387            // We don't want to set the selection to an element that can't have children, this messes up Gecko.
1388            element = Util.traverseUp(element, function (el) {
1389                return Util.emptyElementNames.indexOf(el.nodeName.toLowerCase()) === -1;
1390            });
1391            // Selecting at the beginning of a table doesn't work in PhantomJS.
1392            if (element.nodeName.toLowerCase() === 'table') {
1393                var firstCell = element.querySelector('th, td');
1394                if (firstCell) {
1395                    element = firstCell;
1396                }
1397            }
1398            return element;
1399        },
1400
1401        // TODO: remove getFirstTextNode AND _getFirstTextNode when jumping in 6.0.0 (no code references)
1402        getFirstTextNode: function (element) {
1403            Util.warn('getFirstTextNode is deprecated and will be removed in version 6.0.0');
1404            return Util._getFirstTextNode(element);
1405        },
1406
1407        _getFirstTextNode: function (element) {
1408            if (element.nodeType === 3) {
1409                return element;
1410            }
1411
1412            for (var i = 0; i < element.childNodes.length; i++) {
1413                var textNode = Util._getFirstTextNode(element.childNodes[i]);
1414                if (textNode !== null) {
1415                    return textNode;
1416                }
1417            }
1418            return null;
1419        },
1420
1421        ensureUrlHasProtocol: function (url) {
1422            if (url.indexOf('://') === -1) {
1423                return 'http://' + url;
1424            }
1425            return url;
1426        },
1427
1428        warn: function () {
1429            if (window.console !== undefined && typeof window.console.warn === 'function') {
1430                window.console.warn.apply(window.console, arguments);
1431            }
1432        },
1433
1434        deprecated: function (oldName, newName, version) {
1435            // simple deprecation warning mechanism.
1436            var m = oldName + ' is deprecated, please use ' + newName + ' instead.';
1437            if (version) {
1438                m += ' Will be removed in ' + version;
1439            }
1440            Util.warn(m);
1441        },
1442
1443        deprecatedMethod: function (oldName, newName, args, version) {
1444            // run the replacement and warn when someone calls a deprecated method
1445            Util.deprecated(oldName, newName, version);
1446            if (typeof this[newName] === 'function') {
1447                this[newName].apply(this, args);
1448            }
1449        },
1450
1451        cleanupAttrs: function (el, attrs) {
1452            attrs.forEach(function (attr) {
1453                el.removeAttribute(attr);
1454            });
1455        },
1456
1457        cleanupTags: function (el, tags) {
1458            if (tags.indexOf(el.nodeName.toLowerCase()) !== -1) {
1459                el.parentNode.removeChild(el);
1460            }
1461        },
1462
1463        unwrapTags: function (el, tags) {
1464            if (tags.indexOf(el.nodeName.toLowerCase()) !== -1) {
1465                MediumEditor.util.unwrap(el, document);
1466            }
1467        },
1468
1469        // get the closest parent
1470        getClosestTag: function (el, tag) {
1471            return Util.traverseUp(el, function (element) {
1472                return element.nodeName.toLowerCase() === tag.toLowerCase();
1473            });
1474        },
1475
1476        unwrap: function (el, doc) {
1477            var fragment = doc.createDocumentFragment(),
1478                nodes = Array.prototype.slice.call(el.childNodes);
1479
1480            // cast nodeList to array since appending child
1481            // to a different node will alter length of el.childNodes
1482            for (var i = 0; i < nodes.length; i++) {
1483                fragment.appendChild(nodes[i]);
1484            }
1485
1486            if (fragment.childNodes.length) {
1487                el.parentNode.replaceChild(fragment, el);
1488            } else {
1489                el.parentNode.removeChild(el);
1490            }
1491        },
1492
1493        guid: function () {
1494            function _s4() {
1495                return Math
1496                    .floor((1 + Math.random()) * 0x10000)
1497                    .toString(16)
1498                    .substring(1);
1499            }
1500
1501            return _s4() + _s4() + '-' + _s4() + '-' + _s4() + '-' + _s4() + '-' + _s4() + _s4() + _s4();
1502        }
1503    };
1504
1505    MediumEditor.util = Util;
1506}(window));
1507
1508(function () {
1509    'use strict';
1510
1511    var Extension = function (options) {
1512        MediumEditor.util.extend(this, options);
1513    };
1514
1515    Extension.extend = function (protoProps) {
1516        // magic extender thinger. mostly borrowed from backbone/goog.inherits
1517        // place this function on some thing you want extend-able.
1518        //
1519        // example:
1520        //
1521        //      function Thing(args){
1522        //          this.options = args;
1523        //      }
1524        //
1525        //      Thing.prototype = { foo: "bar" };
1526        //      Thing.extend = extenderify;
1527        //
1528        //      var ThingTwo = Thing.extend({ foo: "baz" });
1529        //
1530        //      var thingOne = new Thing(); // foo === "bar"
1531        //      var thingTwo = new ThingTwo(); // foo === "baz"
1532        //
1533        //      which seems like some simply shallow copy nonsense
1534        //      at first, but a lot more is going on there.
1535        //
1536        //      passing a `constructor` to the extend props
1537        //      will cause the instance to instantiate through that
1538        //      instead of the parent's constructor.
1539
1540        var parent = this,
1541            child;
1542
1543        // The constructor function for the new subclass is either defined by you
1544        // (the "constructor" property in your `extend` definition), or defaulted
1545        // by us to simply call the parent's constructor.
1546
1547        if (protoProps && protoProps.hasOwnProperty('constructor')) {
1548            child = protoProps.constructor;
1549        } else {
1550            child = function () {
1551                return parent.apply(this, arguments);
1552            };
1553        }
1554
1555        // das statics (.extend comes over, so your subclass can have subclasses too)
1556        MediumEditor.util.extend(child, parent);
1557
1558        // Set the prototype chain to inherit from `parent`, without calling
1559        // `parent`'s constructor function.
1560        var Surrogate = function () {
1561            this.constructor = child;
1562        };
1563        Surrogate.prototype = parent.prototype;
1564        child.prototype = new Surrogate();
1565
1566        if (protoProps) {
1567            MediumEditor.util.extend(child.prototype, protoProps);
1568        }
1569
1570        // todo: $super?
1571
1572        return child;
1573    };
1574
1575    Extension.prototype = {
1576        /* init: [function]
1577         *
1578         * Called by MediumEditor during initialization.
1579         * The .base property will already have been set to
1580         * current instance of MediumEditor when this is called.
1581         * All helper methods will exist as well
1582         */
1583        init: function () {},
1584
1585        /* base: [MediumEditor instance]
1586         *
1587         * If not overriden, this will be set to the current instance
1588         * of MediumEditor, before the init method is called
1589         */
1590        base: undefined,
1591
1592        /* name: [string]
1593         *
1594         * 'name' of the extension, used for retrieving the extension.
1595         * If not set, MediumEditor will set this to be the key
1596         * used when passing the extension into MediumEditor via the
1597         * 'extensions' option
1598         */
1599        name: undefined,
1600
1601        /* checkState: [function (node)]
1602         *
1603         * If implemented, this function will be called one or more times
1604         * the state of the editor & toolbar are updated.
1605         * When the state is updated, the editor does the following:
1606         *
1607         * 1) Find the parent node containing the current selection
1608         * 2) Call checkState on the extension, passing the node as an argument
1609         * 3) Get the parent node of the previous node
1610         * 4) Repeat steps #2 and #3 until we move outside the parent contenteditable
1611         */
1612        checkState: undefined,
1613
1614        /* destroy: [function ()]
1615         *
1616         * This method should remove any created html, custom event handlers
1617         * or any other cleanup tasks that should be performed.
1618         * If implemented, this function will be called when MediumEditor's
1619         * destroy method has been called.
1620         */
1621        destroy: undefined,
1622
1623        /* As alternatives to checkState, these functions provide a more structured
1624         * path to updating the state of an extension (usually a button) whenever
1625         * the state of the editor & toolbar are updated.
1626         */
1627
1628        /* queryCommandState: [function ()]
1629         *
1630         * If implemented, this function will be called once on each extension
1631         * when the state of the editor/toolbar is being updated.
1632         *
1633         * If this function returns a non-null value, the extension will
1634         * be ignored as the code climbs the dom tree.
1635         *
1636         * If this function returns true, and the setActive() function is defined
1637         * setActive() will be called
1638         */
1639        queryCommandState: undefined,
1640
1641        /* isActive: [function ()]
1642         *
1643         * If implemented, this function will be called when MediumEditor
1644         * has determined that this extension is 'active' for the current selection.
1645         * This may be called when the editor & toolbar are being updated,
1646         * but only if queryCommandState() or isAlreadyApplied() functions
1647         * are implemented, and when called, return true.
1648         */
1649        isActive: undefined,
1650
1651        /* isAlreadyApplied: [function (node)]
1652         *
1653         * If implemented, this function is similar to checkState() in
1654         * that it will be called repeatedly as MediumEditor moves up
1655         * the DOM to update the editor & toolbar after a state change.
1656         *
1657         * NOTE: This function will NOT be called if checkState() has
1658         * been implemented. This function will NOT be called if
1659         * queryCommandState() is implemented and returns a non-null
1660         * value when called
1661         */
1662        isAlreadyApplied: undefined,
1663
1664        /* setActive: [function ()]
1665         *
1666         * If implemented, this function is called when MediumEditor knows
1667         * that this extension is currently enabled.  Currently, this
1668         * function is called when updating the editor & toolbar, and
1669         * only if queryCommandState() or isAlreadyApplied(node) return
1670         * true when called
1671         */
1672        setActive: undefined,
1673
1674        /* setInactive: [function ()]
1675         *
1676         * If implemented, this function is called when MediumEditor knows
1677         * that this extension is currently disabled.  Curently, this
1678         * is called at the beginning of each state change for
1679         * the editor & toolbar. After calling this, MediumEditor
1680         * will attempt to update the extension, either via checkState()
1681         * or the combination of queryCommandState(), isAlreadyApplied(node),
1682         * isActive(), and setActive()
1683         */
1684        setInactive: undefined,
1685
1686        /* getInteractionElements: [function ()]
1687         *
1688         * If the extension renders any elements that the user can interact with,
1689         * this method should be implemented and return the root element or an array
1690         * containing all of the root elements. MediumEditor will call this function
1691         * during interaction to see if the user clicked on something outside of the editor.
1692         * The elements are used to check if the target element of a click or
1693         * other user event is a descendant of any extension elements.
1694         * This way, the editor can also count user interaction within editor elements as
1695         * interactions with the editor, and thus not trigger 'blur'
1696         */
1697        getInteractionElements: undefined,
1698
1699        /************************ Helpers ************************
1700         * The following are helpers that are either set by MediumEditor
1701         * during initialization, or are helper methods which either
1702         * route calls to the MediumEditor instance or provide common
1703         * functionality for all extensions
1704         *********************************************************/
1705
1706        /* window: [Window]
1707         *
1708         * If not overriden, this will be set to the window object
1709         * to be used by MediumEditor and its extensions.  This is
1710         * passed via the 'contentWindow' option to MediumEditor
1711         * and is the global 'window' object by default
1712         */
1713        'window': undefined,
1714
1715        /* document: [Document]
1716         *
1717         * If not overriden, this will be set to the document object
1718         * to be used by MediumEditor and its extensions. This is
1719         * passed via the 'ownerDocument' optin to MediumEditor
1720         * and is the global 'document' object by default
1721         */
1722        'document': undefined,
1723
1724        /* getEditorElements: [function ()]
1725         *
1726         * Helper function which returns an array containing
1727         * all the contenteditable elements for this instance
1728         * of MediumEditor
1729         */
1730        getEditorElements: function () {
1731            return this.base.elements;
1732        },
1733
1734        /* getEditorId: [function ()]
1735         *
1736         * Helper function which returns a unique identifier
1737         * for this instance of MediumEditor
1738         */
1739        getEditorId: function () {
1740            return this.base.id;
1741        },
1742
1743        /* getEditorOptions: [function (option)]
1744         *
1745         * Helper function which returns the value of an option
1746         * used to initialize this instance of MediumEditor
1747         */
1748        getEditorOption: function (option) {
1749            return this.base.options[option];
1750        }
1751    };
1752
1753    /* List of method names to add to the prototype of Extension
1754     * Each of these methods will be defined as helpers that
1755     * just call directly into the MediumEditor instance.
1756     *
1757     * example for 'on' method:
1758     * Extension.prototype.on = function () {
1759     *     return this.base.on.apply(this.base, arguments);
1760     * }
1761     */
1762    [
1763        // general helpers
1764        'execAction',
1765
1766        // event handling
1767        'on',
1768        'off',
1769        'subscribe',
1770        'trigger'
1771
1772    ].forEach(function (helper) {
1773        Extension.prototype[helper] = function () {
1774            return this.base[helper].apply(this.base, arguments);
1775        };
1776    });
1777
1778    MediumEditor.Extension = Extension;
1779})();
1780
1781(function () {
1782    'use strict';
1783
1784    function filterOnlyParentElements(node) {
1785        if (MediumEditor.util.isBlockContainer(node)) {
1786            return NodeFilter.FILTER_ACCEPT;
1787        } else {
1788            return NodeFilter.FILTER_SKIP;
1789        }
1790    }
1791
1792    var Selection = {
1793        findMatchingSelectionParent: function (testElementFunction, contentWindow) {
1794            var selection = contentWindow.getSelection(),
1795                range,
1796                current;
1797
1798            if (selection.rangeCount === 0) {
1799                return false;
1800            }
1801
1802            range = selection.getRangeAt(0);
1803            current = range.commonAncestorContainer;
1804
1805            return MediumEditor.util.traverseUp(current, testElementFunction);
1806        },
1807
1808        getSelectionElement: function (contentWindow) {
1809            return this.findMatchingSelectionParent(function (el) {
1810                return MediumEditor.util.isMediumEditorElement(el);
1811            }, contentWindow);
1812        },
1813
1814        // http://stackoverflow.com/questions/17678843/cant-restore-selection-after-html-modify-even-if-its-the-same-html
1815        // Tim Down
1816        exportSelection: function (root, doc) {
1817            if (!root) {
1818                return null;
1819            }
1820
1821            var selectionState = null,
1822                selection = doc.getSelection();
1823
1824            if (selection.rangeCount > 0) {
1825                var range = selection.getRangeAt(0),
1826                    preSelectionRange = range.cloneRange(),
1827                    start;
1828
1829                preSelectionRange.selectNodeContents(root);
1830                preSelectionRange.setEnd(range.startContainer, range.startOffset);
1831                start = preSelectionRange.toString().length;
1832
1833                selectionState = {
1834                    start: start,
1835                    end: start + range.toString().length
1836                };
1837
1838                // Check to see if the selection starts with any images
1839                // if so we need to make sure the the beginning of the selection is
1840                // set correctly when importing selection
1841                if (this.doesRangeStartWithImages(range, doc)) {
1842                    selectionState.startsWithImage = true;
1843                }
1844
1845                // Check to see if the selection has any trailing images
1846                // if so, this this means we need to look for them when we import selection
1847                var trailingImageCount = this.getTrailingImageCount(root, selectionState, range.endContainer, range.endOffset);
1848                if (trailingImageCount) {
1849                    selectionState.trailingImageCount = trailingImageCount;
1850                }
1851
1852                // If start = 0 there may still be an empty paragraph before it, but we don't care.
1853                if (start !== 0) {
1854                    var emptyBlocksIndex = this.getIndexRelativeToAdjacentEmptyBlocks(doc, root, range.startContainer, range.startOffset);
1855                    if (emptyBlocksIndex !== -1) {
1856                        selectionState.emptyBlocksIndex = emptyBlocksIndex;
1857                    }
1858                }
1859            }
1860
1861            return selectionState;
1862        },
1863
1864        // http://stackoverflow.com/questions/17678843/cant-restore-selection-after-html-modify-even-if-its-the-same-html
1865        // Tim Down
1866        //
1867        // {object} selectionState - the selection to import
1868        // {DOMElement} root - the root element the selection is being restored inside of
1869        // {Document} doc - the document to use for managing selection
1870        // {boolean} [favorLaterSelectionAnchor] - defaults to false. If true, import the cursor immediately
1871        //      subsequent to an anchor tag if it would otherwise be placed right at the trailing edge inside the
1872        //      anchor. This cursor positioning, even though visually equivalent to the user, can affect behavior
1873        //      in MS IE.
1874        importSelection: function (selectionState, root, doc, favorLaterSelectionAnchor) {
1875            if (!selectionState || !root) {
1876                return;
1877            }
1878
1879            var range = doc.createRange();
1880            range.setStart(root, 0);
1881            range.collapse(true);
1882
1883            var node = root,
1884                nodeStack = [],
1885                charIndex = 0,
1886                foundStart = false,
1887                foundEnd = false,
1888                trailingImageCount = 0,
1889                stop = false,
1890                nextCharIndex,
1891                allowRangeToStartAtEndOfNode = false,
1892                lastTextNode = null;
1893
1894            // When importing selection, the start of the selection may lie at the end of an element
1895            // or at the beginning of an element.  Since visually there is no difference between these 2
1896            // we will try to move the selection to the beginning of an element since this is generally
1897            // what users will expect and it's a more predictable behavior.
1898            //
1899            // However, there are some specific cases when we don't want to do this:
1900            //  1) We're attempting to move the cursor outside of the end of an anchor [favorLaterSelectionAnchor = true]
1901            //  2) The selection starts with an image, which is special since an image doesn't have any 'content'
1902            //     as far as selection and ranges are concerned
1903            //  3) The selection starts after a specified number of empty block elements (selectionState.emptyBlocksIndex)
1904            //
1905            // For these cases, we want the selection to start at a very specific location, so we should NOT
1906            // automatically move the cursor to the beginning of the first actual chunk of text
1907            if (favorLaterSelectionAnchor || selectionState.startsWithImage || typeof selectionState.emptyBlocksIndex !== 'undefined') {
1908                allowRangeToStartAtEndOfNode = true;
1909            }
1910
1911            while (!stop && node) {
1912                // Only iterate over elements and text nodes
1913                if (node.nodeType > 3) {
1914                    node = nodeStack.pop();
1915                    continue;
1916                }
1917
1918                // If we hit a text node, we need to add the amount of characters to the overall count
1919                if (node.nodeType === 3 && !foundEnd) {
1920                    nextCharIndex = charIndex + node.length;
1921                    // Check if we're at or beyond the start of the selection we're importing
1922                    if (!foundStart && selectionState.start >= charIndex && selectionState.start <= nextCharIndex) {
1923                        // NOTE: We only want to allow a selection to start at the END of an element if
1924                        //  allowRangeToStartAtEndOfNode is true
1925                        if (allowRangeToStartAtEndOfNode || selectionState.start < nextCharIndex) {
1926                            range.setStart(node, selectionState.start - charIndex);
1927                            foundStart = true;
1928                        }
1929                        // We're at the end of a text node where the selection could start but we shouldn't
1930                        // make the selection start here because allowRangeToStartAtEndOfNode is false.
1931                        // However, we should keep a reference to this node in case there aren't any more
1932                        // text nodes after this, so that we have somewhere to import the selection to
1933                        else {
1934                            lastTextNode = node;
1935                        }
1936                    }
1937                    // We've found the start of the selection, check if we're at or beyond the end of the selection we're importing
1938                    if (foundStart && selectionState.end >= charIndex && selectionState.end <= nextCharIndex) {
1939                        if (!selectionState.trailingImageCount) {
1940                            range.setEnd(node, selectionState.end - charIndex);
1941                            stop = true;
1942                        } else {
1943                            foundEnd = true;
1944                        }
1945                    }
1946                    charIndex = nextCharIndex;
1947                } else {
1948                    if (selectionState.trailingImageCount && foundEnd) {
1949                        if (node.nodeName.toLowerCase() === 'img') {
1950                            trailingImageCount++;
1951                        }
1952                        if (trailingImageCount === selectionState.trailingImageCount) {
1953                            // Find which index the image is in its parent's children
1954                            var endIndex = 0;
1955                            while (node.parentNode.childNodes[endIndex] !== node) {
1956                                endIndex++;
1957                            }
1958                            range.setEnd(node.parentNode, endIndex + 1);
1959                            stop = true;
1960                        }
1961                    }
1962
1963                    if (!stop && node.nodeType === 1) {
1964                        // this is an element
1965                        // add all its children to the stack
1966                        var i = node.childNodes.length - 1;
1967                        while (i >= 0) {
1968                            nodeStack.push(node.childNodes[i]);
1969                            i -= 1;
1970                        }
1971                    }
1972                }
1973
1974                if (!stop) {
1975                    node = nodeStack.pop();
1976                }
1977            }
1978
1979            // If we've gone through the entire text but didn't find the beginning of a text node
1980            // to make the selection start at, we should fall back to starting the selection
1981            // at the END of the last text node we found
1982            if (!foundStart && lastTextNode) {
1983                range.setStart(lastTextNode, lastTextNode.length);
1984                range.setEnd(lastTextNode, lastTextNode.length);
1985            }
1986
1987            if (typeof selectionState.emptyBlocksIndex !== 'undefined') {
1988                range = this.importSelectionMoveCursorPastBlocks(doc, root, selectionState.emptyBlocksIndex, range);
1989            }
1990
1991            // If the selection is right at the ending edge of a link, put it outside the anchor tag instead of inside.
1992            if (favorLaterSelectionAnchor) {
1993                range = this.importSelectionMoveCursorPastAnchor(selectionState, range);
1994            }
1995
1996            this.selectRange(doc, range);
1997        },
1998
1999        // Utility method called from importSelection only
2000        importSelectionMoveCursorPastAnchor: function (selectionState, range) {
2001            var nodeInsideAnchorTagFunction = function (node) {
2002                return node.nodeName.toLowerCase() === 'a';
2003            };
2004            if (selectionState.start === selectionState.end &&
2005                    range.startContainer.nodeType === 3 &&
2006                    range.startOffset === range.startContainer.nodeValue.length &&
2007                    MediumEditor.util.traverseUp(range.startContainer, nodeInsideAnchorTagFunction)) {
2008                var prevNode = range.startContainer,
2009                    currentNode = range.startContainer.parentNode;
2010                while (currentNode !== null && currentNode.nodeName.toLowerCase() !== 'a') {
2011                    if (currentNode.childNodes[currentNode.childNodes.length - 1] !== prevNode) {
2012                        currentNode = null;
2013                    } else {
2014                        prevNode = currentNode;
2015                        currentNode = currentNode.parentNode;
2016                    }
2017                }
2018                if (currentNode !== null && currentNode.nodeName.toLowerCase() === 'a') {
2019                    var currentNodeIndex = null;
2020                    for (var i = 0; currentNodeIndex === null && i < currentNode.parentNode.childNodes.length; i++) {
2021                        if (currentNode.parentNode.childNodes[i] === currentNode) {
2022                            currentNodeIndex = i;
2023                        }
2024                    }
2025                    range.setStart(currentNode.parentNode, currentNodeIndex + 1);
2026                    range.collapse(true);
2027                }
2028            }
2029            return range;
2030        },
2031
2032        // Uses the emptyBlocksIndex calculated by getIndexRelativeToAdjacentEmptyBlocks
2033        // to move the cursor back to the start of the correct paragraph
2034        importSelectionMoveCursorPastBlocks: function (doc, root, index, range) {
2035            var treeWalker = doc.createTreeWalker(root, NodeFilter.SHOW_ELEMENT, filterOnlyParentElements, false),
2036                startContainer = range.startContainer,
2037                startBlock,
2038                targetNode,
2039                currIndex = 0;
2040            index = index || 1; // If index is 0, we still want to move to the next block
2041
2042            // Chrome counts newlines and spaces that separate block elements as actual elements.
2043            // If the selection is inside one of these text nodes, and it has a previous sibling
2044            // which is a block element, we want the treewalker to start at the previous sibling
2045            // and NOT at the parent of the textnode
2046            if (startContainer.nodeType === 3 && MediumEditor.util.isBlockContainer(startContainer.previousSibling)) {
2047                startBlock = startContainer.previousSibling;
2048            } else {
2049                startBlock = MediumEditor.util.getClosestBlockContainer(startContainer);
2050            }
2051
2052            // Skip over empty blocks until we hit the block we want the selection to be in
2053            while (treeWalker.nextNode()) {
2054                if (!targetNode) {
2055                    // Loop through all blocks until we hit the starting block element
2056                    if (startBlock === treeWalker.currentNode) {
2057                        targetNode = treeWalker.currentNode;
2058                    }
2059                } else {
2060                    targetNode = treeWalker.currentNode;
2061                    currIndex++;
2062                    // We hit the target index, bail
2063                    if (currIndex === index) {
2064                        break;
2065                    }
2066                    // If we find a non-empty block, ignore the emptyBlocksIndex and just put selection here
2067                    if (targetNode.textContent.length > 0) {
2068                        break;
2069                    }
2070                }
2071            }
2072
2073            if (!targetNode) {
2074                targetNode = startBlock;
2075            }
2076
2077            // We're selecting a high-level block node, so make sure the cursor gets moved into the deepest
2078            // element at the beginning of the block
2079            range.setStart(MediumEditor.util.getFirstSelectableLeafNode(targetNode), 0);
2080
2081            return range;
2082        },
2083
2084        // Returns -1 unless the cursor is at the beginning of a paragraph/block
2085        // If the paragraph/block is preceeded by empty paragraphs/block (with no text)
2086        // it will return the number of empty paragraphs before the cursor.
2087        // Otherwise, it will return 0, which indicates the cursor is at the beginning
2088        // of a paragraph/block, and not at the end of the paragraph/block before it
2089        getIndexRelativeToAdjacentEmptyBlocks: function (doc, root, cursorContainer, cursorOffset) {
2090            // If there is text in front of the cursor, that means there isn't only empty blocks before it
2091            if (cursorContainer.textContent.length > 0 && cursorOffset > 0) {
2092                return -1;
2093            }
2094
2095            // Check if the block that contains the cursor has any other text in front of the cursor
2096            var node = cursorContainer;
2097            if (node.nodeType !== 3) {
2098                node = cursorContainer.childNodes[cursorOffset];
2099            }
2100            if (node) {
2101                // The element isn't at the beginning of a block, so it has content before it
2102                if (!MediumEditor.util.isElementAtBeginningOfBlock(node)) {
2103                    return -1;
2104                }
2105
2106                var previousSibling = MediumEditor.util.findPreviousSibling(node);
2107                // If there is no previous sibling, this is the first text element in the editor
2108                if (!previousSibling) {
2109                    return -1;
2110                }
2111                // If the previous sibling has text, then there are no empty blocks before this
2112                else if (previousSibling.nodeValue) {
2113                    return -1;
2114                }
2115            }
2116
2117            // Walk over block elements, counting number of empty blocks between last piece of text
2118            // and the block the cursor is in
2119            var closestBlock = MediumEditor.util.getClosestBlockContainer(cursorContainer),
2120                treeWalker = doc.createTreeWalker(root, NodeFilter.SHOW_ELEMENT, filterOnlyParentElements, false),
2121                emptyBlocksCount = 0;
2122            while (treeWalker.nextNode()) {
2123                var blockIsEmpty = treeWalker.currentNode.textContent === '';
2124                if (blockIsEmpty || emptyBlocksCount > 0) {
2125                    emptyBlocksCount += 1;
2126                }
2127                if (treeWalker.currentNode === closestBlock) {
2128                    return emptyBlocksCount;
2129                }
2130                if (!blockIsEmpty) {
2131                    emptyBlocksCount = 0;
2132                }
2133            }
2134
2135            return emptyBlocksCount;
2136        },
2137
2138        // Returns true if the selection range begins with an image tag
2139        // Returns false if the range starts with any non empty text nodes
2140        doesRangeStartWithImages: function (range, doc) {
2141            if (range.startOffset !== 0 || range.startContainer.nodeType !== 1) {
2142                return false;
2143            }
2144
2145            if (range.startContainer.nodeName.toLowerCase() === 'img') {
2146                return true;
2147            }
2148
2149            var img = range.startContainer.querySelector('img');
2150            if (!img) {
2151                return false;
2152            }
2153
2154            var treeWalker = doc.createTreeWalker(range.startContainer, NodeFilter.SHOW_ALL, null, false);
2155            while (treeWalker.nextNode()) {
2156                var next = treeWalker.currentNode;
2157                // If we hit the image, then there isn't any text before the image so
2158                // the image is at the beginning of the range
2159                if (next === img) {
2160                    break;
2161                }
2162                // If we haven't hit the iamge, but found text that contains content
2163                // then the range doesn't start with an image
2164                if (next.nodeValue) {
2165                    return false;
2166                }
2167            }
2168
2169            return true;
2170        },
2171
2172        getTrailingImageCount: function (root, selectionState, endContainer, endOffset) {
2173            // If the endOffset of a range is 0, the endContainer doesn't contain images
2174            // If the endContainer is a text node, there are no trailing images
2175            if (endOffset === 0 || endContainer.nodeType !== 1) {
2176                return 0;
2177            }
2178
2179            // If the endContainer isn't an image, and doesn't have an image descendants
2180            // there are no trailing images
2181            if (endContainer.nodeName.toLowerCase() !== 'img' && !endContainer.querySelector('img')) {
2182                return 0;
2183            }
2184
2185            var lastNode = endContainer.childNodes[endOffset - 1];
2186            while (lastNode.hasChildNodes()) {
2187                lastNode = lastNode.lastChild;
2188            }
2189
2190            var node = root,
2191                nodeStack = [],
2192                charIndex = 0,
2193                foundStart = false,
2194                foundEnd = false,
2195                stop = false,
2196                nextCharIndex,
2197                trailingImages = 0;
2198
2199            while (!stop && node) {
2200                // Only iterate over elements and text nodes
2201                if (node.nodeType > 3) {
2202                    node = nodeStack.pop();
2203                    continue;
2204                }
2205
2206                if (node.nodeType === 3 && !foundEnd) {
2207                    trailingImages = 0;
2208                    nextCharIndex = charIndex + node.length;
2209                    if (!foundStart && selectionState.start >= charIndex && selectionState.start <= nextCharIndex) {
2210                        foundStart = true;
2211                    }
2212                    if (foundStart && selectionState.end >= charIndex && selectionState.end <= nextCharIndex) {
2213                        foundEnd = true;
2214                    }
2215                    charIndex = nextCharIndex;
2216                } else {
2217                    if (node.nodeName.toLowerCase() === 'img') {
2218                        trailingImages++;
2219                    }
2220
2221                    if (node === lastNode) {
2222                        stop = true;
2223                    } else if (node.nodeType === 1) {
2224                        // this is an element
2225                        // add all its children to the stack
2226                        var i = node.childNodes.length - 1;
2227                        while (i >= 0) {
2228                            nodeStack.push(node.childNodes[i]);
2229                            i -= 1;
2230                        }
2231                    }
2232                }
2233
2234                if (!stop) {
2235                    node = nodeStack.pop();
2236                }
2237            }
2238
2239            return trailingImages;
2240        },
2241
2242        // determine if the current selection contains any 'content'
2243        // content being any non-white space text or an image
2244        selectionContainsContent: function (doc) {
2245            var sel = doc.getSelection();
2246
2247            // collapsed selection or selection withour range doesn't contain content
2248            if (!sel || sel.isCollapsed || !sel.rangeCount) {
2249                return false;
2250            }
2251
2252            // if toString() contains any text, the selection contains some content
2253            if (sel.toString().trim() !== '') {
2254                return true;
2255            }
2256
2257            // if selection contains only image(s), it will return empty for toString()
2258            // so check for an image manually
2259            var selectionNode = this.getSelectedParentElement(sel.getRangeAt(0));
2260            if (selectionNode) {
2261                if (selectionNode.nodeName.toLowerCase() === 'img' ||
2262                    (selectionNode.nodeType === 1 && selectionNode.querySelector('img'))) {
2263                    return true;
2264                }
2265            }
2266
2267            return false;
2268        },
2269
2270        selectionInContentEditableFalse: function (contentWindow) {
2271            // determine if the current selection is exclusively inside
2272            // a contenteditable="false", though treat the case of an
2273            // explicit contenteditable="true" inside a "false" as false.
2274            var sawtrue,
2275                sawfalse = this.findMatchingSelectionParent(function (el) {
2276                    var ce = el && el.getAttribute('contenteditable');
2277                    if (ce === 'true') {
2278                        sawtrue = true;
2279                    }
2280                    return el.nodeName !== '#text' && ce === 'false';
2281                }, contentWindow);
2282
2283            return !sawtrue && sawfalse;
2284        },
2285
2286        // http://stackoverflow.com/questions/4176923/html-of-selected-text
2287        // by Tim Down
2288        getSelectionHtml: function getSelectionHtml(doc) {
2289            var i,
2290                html = '',
2291                sel = doc.getSelection(),
2292                len,
2293                container;
2294            if (sel.rangeCount) {
2295                container = doc.createElement('div');
2296                for (i = 0, len = sel.rangeCount; i < len; i += 1) {
2297                    container.appendChild(sel.getRangeAt(i).cloneContents());
2298                }
2299                html = container.innerHTML;
2300            }
2301            return html;
2302        },
2303
2304        /**
2305         *  Find the caret position within an element irrespective of any inline tags it may contain.
2306         *
2307         *  @param {DOMElement} An element containing the cursor to find offsets relative to.
2308         *  @param {Range} A Range representing cursor position. Will window.getSelection if none is passed.
2309         *  @return {Object} 'left' and 'right' attributes contain offsets from begining and end of Element
2310         */
2311        getCaretOffsets: function getCaretOffsets(element, range) {
2312            var preCaretRange, postCaretRange;
2313
2314            if (!range) {
2315                range = window.getSelection().getRangeAt(0);
2316            }
2317
2318            preCaretRange = range.cloneRange();
2319            postCaretRange = range.cloneRange();
2320
2321            preCaretRange.selectNodeContents(element);
2322            preCaretRange.setEnd(range.endContainer, range.endOffset);
2323
2324            postCaretRange.selectNodeContents(element);
2325            postCaretRange.setStart(range.endContainer, range.endOffset);
2326
2327            return {
2328                left: preCaretRange.toString().length,
2329                right: postCaretRange.toString().length
2330            };
2331        },
2332
2333        // http://stackoverflow.com/questions/15867542/range-object-get-selection-parent-node-chrome-vs-firefox
2334        rangeSelectsSingleNode: function (range) {
2335            var startNode = range.startContainer;
2336            return startNode === range.endContainer &&
2337                startNode.hasChildNodes() &&
2338                range.endOffset === range.startOffset + 1;
2339        },
2340
2341        getSelectedParentElement: function (range) {
2342            if (!range) {
2343                return null;
2344            }
2345
2346            // Selection encompasses a single element
2347            if (this.rangeSelectsSingleNode(range) && range.startContainer.childNodes[range.startOffset].nodeType !== 3) {
2348                return range.startContainer.childNodes[range.startOffset];
2349            }
2350
2351            // Selection range starts inside a text node, so get its parent
2352            if (range.startContainer.nodeType === 3) {
2353                return range.startContainer.parentNode;
2354            }
2355
2356            // Selection starts inside an element
2357            return range.startContainer;
2358        },
2359
2360        getSelectedElements: function (doc) {
2361            var selection = doc.getSelection(),
2362                range,
2363                toRet,
2364                currNode;
2365
2366            if (!selection.rangeCount || selection.isCollapsed || !selection.getRangeAt(0).commonAncestorContainer) {
2367                return [];
2368            }
2369
2370            range = selection.getRangeAt(0);
2371
2372            if (range.commonAncestorContainer.nodeType === 3) {
2373                toRet = [];
2374                currNode = range.commonAncestorContainer;
2375                while (currNode.parentNode && currNode.parentNode.childNodes.length === 1) {
2376                    toRet.push(currNode.parentNode);
2377                    currNode = currNode.parentNode;
2378                }
2379
2380                return toRet;
2381            }
2382
2383            return [].filter.call(range.commonAncestorContainer.getElementsByTagName('*'), function (el) {
2384                return (typeof selection.containsNode === 'function') ? selection.containsNode(el, true) : true;
2385            });
2386        },
2387
2388        selectNode: function (node, doc) {
2389            var range = doc.createRange();
2390            range.selectNodeContents(node);
2391            this.selectRange(doc, range);
2392        },
2393
2394        select: function (doc, startNode, startOffset, endNode, endOffset) {
2395            var range = doc.createRange();
2396            range.setStart(startNode, startOffset);
2397            if (endNode) {
2398                range.setEnd(endNode, endOffset);
2399            } else {
2400                range.collapse(true);
2401            }
2402            this.selectRange(doc, range);
2403            return range;
2404        },
2405
2406        /**
2407         *  Clear the current highlighted selection and set the caret to the start or the end of that prior selection, defaults to end.
2408         *
2409         *  @param {DomDocument} doc            Current document
2410         *  @param {boolean} moveCursorToStart  A boolean representing whether or not to set the caret to the beginning of the prior selection.
2411         */
2412        clearSelection: function (doc, moveCursorToStart) {
2413            if (moveCursorToStart) {
2414                doc.getSelection().collapseToStart();
2415            } else {
2416                doc.getSelection().collapseToEnd();
2417            }
2418        },
2419
2420        /**
2421         * Move cursor to the given node with the given offset.
2422         *
2423         * @param  {DomDocument} doc     Current document
2424         * @param  {DomElement}  node    Element where to jump
2425         * @param  {integer}     offset  Where in the element should we jump, 0 by default
2426         */
2427        moveCursor: function (doc, node, offset) {
2428            this.select(doc, node, offset);
2429        },
2430
2431        getSelectionRange: function (ownerDocument) {
2432            var selection = ownerDocument.getSelection();
2433            if (selection.rangeCount === 0) {
2434                return null;
2435            }
2436            return selection.getRangeAt(0);
2437        },
2438
2439        selectRange: function (ownerDocument, range) {
2440            var selection = ownerDocument.getSelection();
2441
2442            selection.removeAllRanges();
2443            selection.addRange(range);
2444        },
2445
2446        // http://stackoverflow.com/questions/1197401/how-can-i-get-the-element-the-caret-is-in-with-javascript-when-using-contentedi
2447        // by You
2448        getSelectionStart: function (ownerDocument) {
2449            var node = ownerDocument.getSelection().anchorNode,
2450                startNode = (node && node.nodeType === 3 ? node.parentNode : node);
2451
2452            return startNode;
2453        }
2454    };
2455
2456    MediumEditor.selection = Selection;
2457}());
2458
2459(function () {
2460    'use strict';
2461
2462    function isElementDescendantOfExtension(extensions, element) {
2463        return extensions.some(function (extension) {
2464            if (typeof extension.getInteractionElements !== 'function') {
2465                return false;
2466            }
2467
2468            var extensionElements = extension.getInteractionElements();
2469            if (!extensionElements) {
2470                return false;
2471            }
2472
2473            if (!Array.isArray(extensionElements)) {
2474                extensionElements = [extensionElements];
2475            }
2476            return extensionElements.some(function (el) {
2477                return MediumEditor.util.isDescendant(el, element, true);
2478            });
2479        });
2480    }
2481
2482    var Events = function (instance) {
2483        this.base = instance;
2484        this.options = this.base.options;
2485        this.events = [];
2486        this.disabledEvents = {};
2487        this.customEvents = {};
2488        this.listeners = {};
2489    };
2490
2491    Events.prototype = {
2492        InputEventOnContenteditableSupported: !MediumEditor.util.isIE && !MediumEditor.util.isEdge,
2493
2494        // Helpers for event handling
2495
2496        attachDOMEvent: function (targets, event, listener, useCapture) {
2497            var win = this.base.options.contentWindow,
2498                doc = this.base.options.ownerDocument;
2499
2500            targets = MediumEditor.util.isElement(targets) || [win, doc].indexOf(targets) > -1 ? [targets] : targets;
2501
2502            Array.prototype.forEach.call(targets, function (target) {
2503                target.addEventListener(event, listener, useCapture);
2504                this.events.push([target, event, listener, useCapture]);
2505            }.bind(this));
2506        },
2507
2508        detachDOMEvent: function (targets, event, listener, useCapture) {
2509            var index, e,
2510                win = this.base.options.contentWindow,
2511                doc = this.base.options.ownerDocument;
2512
2513            if (targets !== null) {
2514                targets = MediumEditor.util.isElement(targets) || [win, doc].indexOf(targets) > -1 ? [targets] : targets;
2515
2516                Array.prototype.forEach.call(targets, function (target) {
2517                    index = this.indexOfListener(target, event, listener, useCapture);
2518                    if (index !== -1) {
2519                        e = this.events.splice(index, 1)[0];
2520                        e[0].removeEventListener(e[1], e[2], e[3]);
2521                    }
2522                }.bind(this));
2523            }
2524        },
2525
2526        indexOfListener: function (target, event, listener, useCapture) {
2527            var i, n, item;
2528            for (i = 0, n = this.events.length; i < n; i = i + 1) {
2529                item = this.events[i];
2530                if (item[0] === target && item[1] === event && item[2] === listener && item[3] === useCapture) {
2531                    return i;
2532                }
2533            }
2534            return -1;
2535        },
2536
2537        detachAllDOMEvents: function () {
2538            var e = this.events.pop();
2539            while (e) {
2540                e[0].removeEventListener(e[1], e[2], e[3]);
2541                e = this.events.pop();
2542            }
2543        },
2544
2545        detachAllEventsFromElement: function (element) {
2546            var filtered = this.events.filter(function (e) {
2547                return e && e[0].getAttribute && e[0].getAttribute('medium-editor-index') === element.getAttribute('medium-editor-index');
2548            });
2549
2550            for (var i = 0, len = filtered.length; i < len; i++) {
2551                var e = filtered[i];
2552                this.detachDOMEvent(e[0], e[1], e[2], e[3]);
2553            }
2554        },
2555
2556        // Attach all existing handlers to a new element
2557        attachAllEventsToElement: function (element) {
2558            if (this.listeners['editableInput']) {
2559                this.contentCache[element.getAttribute('medium-editor-index')] = element.innerHTML;
2560            }
2561
2562            if (this.eventsCache) {
2563                this.eventsCache.forEach(function (e) {
2564                    this.attachDOMEvent(element, e['name'], e['handler'].bind(this));
2565                }, this);
2566            }
2567        },
2568
2569        enableCustomEvent: function (event) {
2570            if (this.disabledEvents[event] !== undefined) {
2571                delete this.disabledEvents[event];
2572            }
2573        },
2574
2575        disableCustomEvent: function (event) {
2576            this.disabledEvents[event] = true;
2577        },
2578
2579        // custom events
2580        attachCustomEvent: function (event, listener) {
2581            this.setupListener(event);
2582            if (!this.customEvents[event]) {
2583                this.customEvents[event] = [];
2584            }
2585            this.customEvents[event].push(listener);
2586        },
2587
2588        detachCustomEvent: function (event, listener) {
2589            var index = this.indexOfCustomListener(event, listener);
2590            if (index !== -1) {
2591                this.customEvents[event].splice(index, 1);
2592                // TODO: If array is empty, should detach internal listeners via destroyListener()
2593            }
2594        },
2595
2596        indexOfCustomListener: function (event, listener) {
2597            if (!this.customEvents[event] || !this.customEvents[event].length) {
2598                return -1;
2599            }
2600
2601            return this.customEvents[event].indexOf(listener);
2602        },
2603
2604        detachAllCustomEvents: function () {
2605            this.customEvents = {};
2606            // TODO: Should detach internal listeners here via destroyListener()
2607        },
2608
2609        triggerCustomEvent: function (name, data, editable) {
2610            if (this.customEvents[name] && !this.disabledEvents[name]) {
2611                this.customEvents[name].forEach(function (listener) {
2612                    listener(data, editable);
2613                });
2614            }
2615        },
2616
2617        // Cleaning up
2618
2619        destroy: function () {
2620            this.detachAllDOMEvents();
2621            this.detachAllCustomEvents();
2622            this.detachExecCommand();
2623
2624            if (this.base.elements) {
2625                this.base.elements.forEach(function (element) {
2626                    element.removeAttribute('data-medium-focused');
2627                });
2628            }
2629        },
2630
2631        // Listening to calls to document.execCommand
2632
2633        // Attach a listener to be notified when document.execCommand is called
2634        attachToExecCommand: function () {
2635            if (this.execCommandListener) {
2636                return;
2637            }
2638
2639            // Store an instance of the listener so:
2640            // 1) We only attach to execCommand once
2641            // 2) We can remove the listener later
2642            this.execCommandListener = function (execInfo) {
2643                this.handleDocumentExecCommand(execInfo);
2644            }.bind(this);
2645
2646            // Ensure that execCommand has been wrapped correctly
2647            this.wrapExecCommand();
2648
2649            // Add listener to list of execCommand listeners
2650            this.options.ownerDocument.execCommand.listeners.push(this.execCommandListener);
2651        },
2652
2653        // Remove our listener for calls to document.execCommand
2654        detachExecCommand: function () {
2655            var doc = this.options.ownerDocument;
2656            if (!this.execCommandListener || !doc.execCommand.listeners) {
2657                return;
2658            }
2659
2660            // Find the index of this listener in the array of listeners so it can be removed
2661            var index = doc.execCommand.listeners.indexOf(this.execCommandListener);
2662            if (index !== -1) {
2663                doc.execCommand.listeners.splice(index, 1);
2664            }
2665
2666            // If the list of listeners is now empty, put execCommand back to its original state
2667            if (!doc.execCommand.listeners.length) {
2668                this.unwrapExecCommand();
2669            }
2670        },
2671
2672        // Wrap document.execCommand in a custom method so we can listen to calls to it
2673        wrapExecCommand: function () {
2674            var doc = this.options.ownerDocument;
2675
2676            // Ensure all instance of MediumEditor only wrap execCommand once
2677            if (doc.execCommand.listeners) {
2678                return;
2679            }
2680
2681            // Helper method to call all listeners to execCommand
2682            var callListeners = function (args, result) {
2683                if (doc.execCommand.listeners) {
2684                    doc.execCommand.listeners.forEach(function (listener) {
2685                        listener({
2686                            command: args[0],
2687                            value: args[2],
2688                            args: args,
2689                            result: result
2690                        });
2691                    });
2692                }
2693            },
2694
2695                // Create a wrapper method for execCommand which will:
2696                // 1) Call document.execCommand with the correct arguments
2697                // 2) Loop through any listeners and notify them that execCommand was called
2698                //    passing extra info on the call
2699                // 3) Return the result
2700                wrapper = function () {
2701                    var result = doc.execCommand.orig.apply(this, arguments);
2702
2703                    if (!doc.execCommand.listeners) {
2704                        return result;
2705                    }
2706
2707                    var args = Array.prototype.slice.call(arguments);
2708                    callListeners(args, result);
2709
2710                    return result;
2711                };
2712
2713            // Store a reference to the original execCommand
2714            wrapper.orig = doc.execCommand;
2715
2716            // Attach an array for storing listeners
2717            wrapper.listeners = [];
2718
2719            // Helper for notifying listeners
2720            wrapper.callListeners = callListeners;
2721
2722            // Overwrite execCommand
2723            doc.execCommand = wrapper;
2724        },
2725
2726        // Revert document.execCommand back to its original self
2727        unwrapExecCommand: function () {
2728            var doc = this.options.ownerDocument;
2729            if (!doc.execCommand.orig) {
2730                return;
2731            }
2732
2733            // Use the reference to the original execCommand to revert back
2734            doc.execCommand = doc.execCommand.orig;
2735        },
2736
2737        // Listening to browser events to emit events medium-editor cares about
2738        setupListener: function (name) {
2739            if (this.listeners[name]) {
2740                return;
2741            }
2742
2743            switch (name) {
2744                case 'externalInteraction':
2745                    // Detecting when user has interacted with elements outside of MediumEditor
2746                    this.attachDOMEvent(this.options.ownerDocument.body, 'mousedown', this.handleBodyMousedown.bind(this), true);
2747                    this.attachDOMEvent(this.options.ownerDocument.body, 'click', this.handleBodyClick.bind(this), true);
2748                    this.attachDOMEvent(this.options.ownerDocument.body, 'focus', this.handleBodyFocus.bind(this), true);
2749                    break;
2750                case 'blur':
2751                    // Detecting when focus is lost
2752                    this.setupListener('externalInteraction');
2753                    break;
2754                case 'focus':
2755                    // Detecting when focus moves into some part of MediumEditor
2756                    this.setupListener('externalInteraction');
2757                    break;
2758                case 'editableInput':
2759                    // setup cache for knowing when the content has changed
2760                    this.contentCache = {};
2761                    this.base.elements.forEach(function (element) {
2762                        this.contentCache[element.getAttribute('medium-editor-index')] = element.innerHTML;
2763                    }, this);
2764
2765                    // Attach to the 'oninput' event, handled correctly by most browsers
2766                    if (this.InputEventOnContenteditableSupported) {
2767                        this.attachToEachElement('input', this.handleInput);
2768                    }
2769
2770                    // For browsers which don't support the input event on contenteditable (IE)
2771                    // we'll attach to 'selectionchange' on the document and 'keypress' on the editables
2772                    if (!this.InputEventOnContenteditableSupported) {
2773                        this.setupListener('editableKeypress');
2774                        this.keypressUpdateInput = true;
2775                        this.attachDOMEvent(document, 'selectionchange', this.handleDocumentSelectionChange.bind(this));
2776                        // Listen to calls to execCommand
2777                        this.attachToExecCommand();
2778                    }
2779                    break;
2780                case 'editableClick':
2781                    // Detecting click in the contenteditables
2782                    this.attachToEachElement('click', this.handleClick);
2783                    break;
2784                case 'editableBlur':
2785                    // Detecting blur in the contenteditables
2786                    this.attachToEachElement('blur', this.handleBlur);
2787                    break;
2788                case 'editableKeypress':
2789                    // Detecting keypress in the contenteditables
2790                    this.attachToEachElement('keypress', this.handleKeypress);
2791                    break;
2792                case 'editableKeyup':
2793                    // Detecting keyup in the contenteditables
2794                    this.attachToEachElement('keyup', this.handleKeyup);
2795                    break;
2796                case 'editableKeydown':
2797                    // Detecting keydown on the contenteditables
2798                    this.attachToEachElement('keydown', this.handleKeydown);
2799                    break;
2800                case 'editableKeydownSpace':
2801                    // Detecting keydown for SPACE on the contenteditables
2802                    this.setupListener('editableKeydown');
2803                    break;
2804                case 'editableKeydownEnter':
2805                    // Detecting keydown for ENTER on the contenteditables
2806                    this.setupListener('editableKeydown');
2807                    break;
2808                case 'editableKeydownTab':
2809                    // Detecting keydown for TAB on the contenteditable
2810                    this.setupListener('editableKeydown');
2811                    break;
2812                case 'editableKeydownDelete':
2813                    // Detecting keydown for DELETE/BACKSPACE on the contenteditables
2814                    this.setupListener('editableKeydown');
2815                    break;
2816                case 'editableMouseover':
2817                    // Detecting mouseover on the contenteditables
2818                    this.attachToEachElement('mouseover', this.handleMouseover);
2819                    break;
2820                case 'editableDrag':
2821                    // Detecting dragover and dragleave on the contenteditables
2822                    this.attachToEachElement('dragover', this.handleDragging);
2823                    this.attachToEachElement('dragleave', this.handleDragging);
2824                    break;
2825                case 'editableDrop':
2826                    // Detecting drop on the contenteditables
2827                    this.attachToEachElement('drop', this.handleDrop);
2828                    break;
2829                // TODO: We need to have a custom 'paste' event separate from 'editablePaste'
2830                // Need to think about the way to introduce this without breaking folks
2831                case 'editablePaste':
2832                    // Detecting paste on the contenteditables
2833                    this.attachToEachElement('paste', this.handlePaste);
2834                    break;
2835            }
2836            this.listeners[name] = true;
2837        },
2838
2839        attachToEachElement: function (name, handler) {
2840            // build our internal cache to know which element got already what handler attached
2841            if (!this.eventsCache) {
2842                this.eventsCache = [];
2843            }
2844
2845            this.base.elements.forEach(function (element) {
2846                this.attachDOMEvent(element, name, handler.bind(this));
2847            }, this);
2848
2849            this.eventsCache.push({ 'name': name, 'handler': handler });
2850        },
2851
2852        cleanupElement: function (element) {
2853            var index = element.getAttribute('medium-editor-index');
2854            if (index) {
2855                this.detachAllEventsFromElement(element);
2856                if (this.contentCache) {
2857                    delete this.contentCache[index];
2858                }
2859            }
2860        },
2861
2862        focusElement: function (element) {
2863            element.focus();
2864            this.updateFocus(element, { target: element, type: 'focus' });
2865        },
2866
2867        updateFocus: function (target, eventObj) {
2868            var hadFocus = this.base.getFocusedElement(),
2869                toFocus;
2870
2871            // For clicks, we need to know if the mousedown that caused the click happened inside the existing focused element
2872            // or one of the extension elements.  If so, we don't want to focus another element
2873            if (hadFocus &&
2874                eventObj.type === 'click' &&
2875                this.lastMousedownTarget &&
2876                (MediumEditor.util.isDescendant(hadFocus, this.lastMousedownTarget, true) ||
2877                    isElementDescendantOfExtension(this.base.extensions, this.lastMousedownTarget))) {
2878                toFocus = hadFocus;
2879            }
2880
2881            if (!toFocus) {
2882                this.base.elements.some(function (element) {
2883                    // If the target is part of an editor element, this is the element getting focus
2884                    if (!toFocus && (MediumEditor.util.isDescendant(element, target, true))) {
2885                        toFocus = element;
2886                    }
2887
2888                    // bail if we found an element that's getting focus
2889                    return !!toFocus;
2890                }, this);
2891            }
2892
2893            // Check if the target is external (not part of the editor, toolbar, or any other extension)
2894            var externalEvent = !MediumEditor.util.isDescendant(hadFocus, target, true) &&
2895                !isElementDescendantOfExtension(this.base.extensions, target);
2896
2897            if (toFocus !== hadFocus) {
2898                // If element has focus, and focus is going outside of editor
2899                // Don't blur focused element if clicking on editor, toolbar, or anchorpreview
2900                if (hadFocus && externalEvent) {
2901                    // Trigger blur on the editable that has lost focus
2902                    hadFocus.removeAttribute('data-medium-focused');
2903                    this.triggerCustomEvent('blur', eventObj, hadFocus);
2904                }
2905
2906                // If focus is going into an editor element
2907                if (toFocus) {
2908                    // Trigger focus on the editable that now has focus
2909                    toFocus.setAttribute('data-medium-focused', true);
2910                    this.triggerCustomEvent('focus', eventObj, toFocus);
2911                }
2912            }
2913
2914            if (externalEvent) {
2915                this.triggerCustomEvent('externalInteraction', eventObj);
2916            }
2917        },
2918
2919        updateInput: function (target, eventObj) {
2920            if (!this.contentCache) {
2921                return;
2922            }
2923            // An event triggered which signifies that the user may have changed someting
2924            // Look in our cache of input for the contenteditables to see if something changed
2925            var index = target.getAttribute('medium-editor-index'),
2926                html = target.innerHTML;
2927
2928            if (html !== this.contentCache[index]) {
2929                // The content has changed since the last time we checked, fire the event
2930                this.triggerCustomEvent('editableInput', eventObj, target);
2931            }
2932            this.contentCache[index] = html;
2933        },
2934
2935        handleDocumentSelectionChange: function (event) {
2936            // When selectionchange fires, target and current target are set
2937            // to document, since this is where the event is handled
2938            // However, currentTarget will have an 'activeElement' property
2939            // which will point to whatever element has focus.
2940            if (event.currentTarget && event.currentTarget.activeElement) {
2941                var activeElement = event.currentTarget.activeElement,
2942                    currentTarget;
2943                // We can look at the 'activeElement' to determine if the selectionchange has
2944                // happened within a contenteditable owned by this instance of MediumEditor
2945                this.base.elements.some(function (element) {
2946                    if (MediumEditor.util.isDescendant(element, activeElement, true)) {
2947                        currentTarget = element;
2948                        return true;
2949                    }
2950                    return false;
2951                }, this);
2952
2953                // We know selectionchange fired within one of our contenteditables
2954                if (currentTarget) {
2955                    this.updateInput(currentTarget, { target: activeElement, currentTarget: currentTarget });
2956                }
2957            }
2958        },
2959
2960        handleDocumentExecCommand: function () {
2961            // document.execCommand has been called
2962            // If one of our contenteditables currently has focus, we should
2963            // attempt to trigger the 'editableInput' event
2964            var target = this.base.getFocusedElement();
2965            if (target) {
2966                this.updateInput(target, { target: target, currentTarget: target });
2967            }
2968        },
2969
2970        handleBodyClick: function (event) {
2971            this.updateFocus(event.target, event);
2972        },
2973
2974        handleBodyFocus: function (event) {
2975            this.updateFocus(event.target, event);
2976        },
2977
2978        handleBodyMousedown: function (event) {
2979            this.lastMousedownTarget = event.target;
2980        },
2981
2982        handleInput: function (event) {
2983            this.updateInput(event.currentTarget, event);
2984        },
2985
2986        handleClick: function (event) {
2987            this.triggerCustomEvent('editableClick', event, event.currentTarget);
2988        },
2989
2990        handleBlur: function (event) {
2991            this.triggerCustomEvent('editableBlur', event, event.currentTarget);
2992        },
2993
2994        handleKeypress: function (event) {
2995            this.triggerCustomEvent('editableKeypress', event, event.currentTarget);
2996
2997            // If we're doing manual detection of the editableInput event we need
2998            // to check for input changes during 'keypress'
2999            if (this.keypressUpdateInput) {
3000                var eventObj = { target: event.target, currentTarget: event.currentTarget };
3001
3002                // In IE, we need to let the rest of the event stack complete before we detect
3003                // changes to input, so using setTimeout here
3004                setTimeout(function () {
3005                    this.updateInput(eventObj.currentTarget, eventObj);
3006                }.bind(this), 0);
3007            }
3008        },
3009
3010        handleKeyup: function (event) {
3011            this.triggerCustomEvent('editableKeyup', event, event.currentTarget);
3012        },
3013
3014        handleMouseover: function (event) {
3015            this.triggerCustomEvent('editableMouseover', event, event.currentTarget);
3016        },
3017
3018        handleDragging: function (event) {
3019            this.triggerCustomEvent('editableDrag', event, event.currentTarget);
3020        },
3021
3022        handleDrop: function (event) {
3023            this.triggerCustomEvent('editableDrop', event, event.currentTarget);
3024        },
3025
3026        handlePaste: function (event) {
3027            this.triggerCustomEvent('editablePaste', event, event.currentTarget);
3028        },
3029
3030        handleKeydown: function (event) {
3031
3032            this.triggerCustomEvent('editableKeydown', event, event.currentTarget);
3033
3034            if (MediumEditor.util.isKey(event, MediumEditor.util.keyCode.SPACE)) {
3035                return this.triggerCustomEvent('editableKeydownSpace', event, event.currentTarget);
3036            }
3037
3038            if (MediumEditor.util.isKey(event, MediumEditor.util.keyCode.ENTER) || (event.ctrlKey && MediumEditor.util.isKey(event, MediumEditor.util.keyCode.M))) {
3039                return this.triggerCustomEvent('editableKeydownEnter', event, event.currentTarget);
3040            }
3041
3042            if (MediumEditor.util.isKey(event, MediumEditor.util.keyCode.TAB)) {
3043                return this.triggerCustomEvent('editableKeydownTab', event, event.currentTarget);
3044            }
3045
3046            if (MediumEditor.util.isKey(event, [MediumEditor.util.keyCode.DELETE, MediumEditor.util.keyCode.BACKSPACE])) {
3047                return this.triggerCustomEvent('editableKeydownDelete', event, event.currentTarget);
3048            }
3049        }
3050    };
3051
3052    MediumEditor.Events = Events;
3053}());
3054
3055(function () {
3056    'use strict';
3057
3058    var Button = MediumEditor.Extension.extend({
3059
3060        /* Button Options */
3061
3062        /* action: [string]
3063         * The action argument to pass to MediumEditor.execAction()
3064         * when the button is clicked
3065         */
3066        action: undefined,
3067
3068        /* aria: [string]
3069         * The value to add as the aria-label attribute of the button
3070         * element displayed in the toolbar.
3071         * This is also used as the tooltip for the button
3072         */
3073        aria: undefined,
3074
3075        /* tagNames: [Array]
3076         * NOTE: This is not used if useQueryState is set to true.
3077         *
3078         * Array of element tag names that would indicate that this
3079         * button has already been applied. If this action has already
3080         * been applied, the button will be displayed as 'active' in the toolbar
3081         *
3082         * Example:
3083         * For 'bold', if the text is ever within a <b> or <strong>
3084         * tag that indicates the text is already bold. So the array
3085         * of tagNames for bold would be: ['b', 'strong']
3086         */
3087        tagNames: undefined,
3088
3089        /* style: [Object]
3090         * NOTE: This is not used if useQueryState is set to true.
3091         *
3092         * A pair of css property & value(s) that indicate that this
3093         * button has already been applied. If this action has already
3094         * been applied, the button will be displayed as 'active' in the toolbar
3095         * Properties of the object:
3096         *   prop [String]: name of the css property
3097         *   value [String]: value(s) of the css property
3098         *                   multiple values can be separated by a '|'
3099         *
3100         * Example:
3101         * For 'bold', if the text is ever within an element with a 'font-weight'
3102         * style property set to '700' or 'bold', that indicates the text
3103         * is already bold.  So the style object for bold would be:
3104         * { prop: 'font-weight', value: '700|bold' }
3105         */
3106        style: undefined,
3107
3108        /* useQueryState: [boolean]
3109         * Enables/disables whether this button should use the built-in
3110         * document.queryCommandState() method to determine whether
3111         * the action has already been applied.  If the action has already
3112         * been applied, the button will be displayed as 'active' in the toolbar
3113         *
3114         * Example:
3115         * For 'bold', if this is set to true, the code will call:
3116         * document.queryCommandState('bold') which will return true if the
3117         * browser thinks the text is already bold, and false otherwise
3118         */
3119        useQueryState: undefined,
3120
3121        /* contentDefault: [string]
3122         * Default innerHTML to put inside the button
3123         */
3124        contentDefault: undefined,
3125
3126        /* contentFA: [string]
3127         * The innerHTML to use for the content of the button
3128         * if the `buttonLabels` option for MediumEditor is set to 'fontawesome'
3129         */
3130        contentFA: undefined,
3131
3132        /* classList: [Array]
3133         * An array of classNames (strings) to be added to the button
3134         */
3135        classList: undefined,
3136
3137        /* attrs: [object]
3138         * A set of key-value pairs to add to the button as custom attributes
3139         */
3140        attrs: undefined,
3141
3142        // The button constructor can optionally accept the name of a built-in button
3143        // (ie 'bold', 'italic', etc.)
3144        // When the name of a button is passed, it will initialize itself with the
3145        // configuration for that button
3146        constructor: function (options) {
3147            if (Button.isBuiltInButton(options)) {
3148                MediumEditor.Extension.call(this, this.defaults[options]);
3149            } else {
3150                MediumEditor.Extension.call(this, options);
3151            }
3152        },
3153
3154        init: function () {
3155            MediumEditor.Extension.prototype.init.apply(this, arguments);
3156
3157            this.button = this.createButton();
3158            this.on(this.button, 'click', this.handleClick.bind(this));
3159        },
3160
3161        /* getButton: [function ()]
3162         *
3163         * If implemented, this function will be called when
3164         * the toolbar is being created.  The DOM Element returned
3165         * by this function will be appended to the toolbar along
3166         * with any other buttons.
3167         */
3168        getButton: function () {
3169            return this.button;
3170        },
3171
3172        getAction: function () {
3173            return (typeof this.action === 'function') ? this.action(this.base.options) : this.action;
3174        },
3175
3176        getAria: function () {
3177            return (typeof this.aria === 'function') ? this.aria(this.base.options) : this.aria;
3178        },
3179
3180        getTagNames: function () {
3181            return (typeof this.tagNames === 'function') ? this.tagNames(this.base.options) : this.tagNames;
3182        },
3183
3184        createButton: function () {
3185            var button = this.document.createElement('button'),
3186                content = this.contentDefault,
3187                ariaLabel = this.getAria(),
3188                buttonLabels = this.getEditorOption('buttonLabels');
3189            // Add class names
3190            button.classList.add('medium-editor-action');
3191            button.classList.add('medium-editor-action-' + this.name);
3192            if (this.classList) {
3193                this.classList.forEach(function (className) {
3194                    button.classList.add(className);
3195                });
3196            }
3197
3198            // Add attributes
3199            button.setAttribute('data-action', this.getAction());
3200            if (ariaLabel) {
3201                button.setAttribute('title', ariaLabel);
3202                button.setAttribute('aria-label', ariaLabel);
3203            }
3204            if (this.attrs) {
3205                Object.keys(this.attrs).forEach(function (attr) {
3206                    button.setAttribute(attr, this.attrs[attr]);
3207                }, this);
3208            }
3209
3210            if (buttonLabels === 'fontawesome' && this.contentFA) {
3211                content = this.contentFA;
3212            }
3213            button.innerHTML = content;
3214            return button;
3215        },
3216
3217        handleClick: function (event) {
3218            event.preventDefault();
3219            event.stopPropagation();
3220
3221            var action = this.getAction();
3222
3223            if (action) {
3224                this.execAction(action);
3225            }
3226        },
3227
3228        isActive: function () {
3229            return this.button.classList.contains(this.getEditorOption('activeButtonClass'));
3230        },
3231
3232        setInactive: function () {
3233            this.button.classList.remove(this.getEditorOption('activeButtonClass'));
3234            delete this.knownState;
3235        },
3236
3237        setActive: function () {
3238            this.button.classList.add(this.getEditorOption('activeButtonClass'));
3239            delete this.knownState;
3240        },
3241
3242        queryCommandState: function () {
3243            var queryState = null;
3244            if (this.useQueryState) {
3245                queryState = this.base.queryCommandState(this.getAction());
3246            }
3247            return queryState;
3248        },
3249
3250        isAlreadyApplied: function (node) {
3251            var isMatch = false,
3252                tagNames = this.getTagNames(),
3253                styleVals,
3254                computedStyle;
3255
3256            if (this.knownState === false || this.knownState === true) {
3257                return this.knownState;
3258            }
3259
3260            if (tagNames && tagNames.length > 0) {
3261                isMatch = tagNames.indexOf(node.nodeName.toLowerCase()) !== -1;
3262            }
3263
3264            if (!isMatch && this.style) {
3265                styleVals = this.style.value.split('|');
3266                computedStyle = this.window.getComputedStyle(node, null).getPropertyValue(this.style.prop);
3267                styleVals.forEach(function (val) {
3268                    if (!this.knownState) {
3269                        isMatch = (computedStyle.indexOf(val) !== -1);
3270                        // text-decoration is not inherited by default
3271                        // so if the computed style for text-decoration doesn't match
3272                        // don't write to knownState so we can fallback to other checks
3273                        if (isMatch || this.style.prop !== 'text-decoration') {
3274                            this.knownState = isMatch;
3275                        }
3276                    }
3277                }, this);
3278            }
3279
3280            return isMatch;
3281        }
3282    });
3283
3284    Button.isBuiltInButton = function (name) {
3285        return (typeof name === 'string') && MediumEditor.extensions.button.prototype.defaults.hasOwnProperty(name);
3286    };
3287
3288    MediumEditor.extensions.button = Button;
3289}());
3290
3291(function () {
3292    'use strict';
3293
3294    /* MediumEditor.extensions.button.defaults: [Object]
3295     * Set of default config options for all of the built-in MediumEditor buttons
3296     */
3297    MediumEditor.extensions.button.prototype.defaults = {
3298        'bold': {
3299            name: 'bold',
3300            action: 'bold',
3301            aria: 'bold',
3302            tagNames: ['b', 'strong'],
3303            style: {
3304                prop: 'font-weight',
3305                value: '700|bold'
3306            },
3307            useQueryState: true,
3308            contentDefault: '<b>B</b>',
3309            contentFA: '<i class="fa fa-bold"></i>'
3310        },
3311        'italic': {
3312            name: 'italic',
3313            action: 'italic',
3314            aria: 'italic',
3315            tagNames: ['i', 'em'],
3316            style: {
3317                prop: 'font-style',
3318                value: 'italic'
3319            },
3320            useQueryState: true,
3321            contentDefault: '<b><i>I</i></b>',
3322            contentFA: '<i class="fa fa-italic"></i>'
3323        },
3324        'underline': {
3325            name: 'underline',
3326            action: 'underline',
3327            aria: 'underline',
3328            tagNames: ['u'],
3329            style: {
3330                prop: 'text-decoration',
3331                value: 'underline'
3332            },
3333            useQueryState: true,
3334            contentDefault: '<b><u>U</u></b>',
3335            contentFA: '<i class="fa fa-underline"></i>'
3336        },
3337        'strikethrough': {
3338            name: 'strikethrough',
3339            action: 'strikethrough',
3340            aria: 'strike through',
3341            tagNames: ['strike'],
3342            style: {
3343                prop: 'text-decoration',
3344                value: 'line-through'
3345            },
3346            useQueryState: true,
3347            contentDefault: '<s>A</s>',
3348            contentFA: '<i class="fa fa-strikethrough"></i>'
3349        },
3350        'superscript': {
3351            name: 'superscript',
3352            action: 'superscript',
3353            aria: 'superscript',
3354            tagNames: ['sup'],
3355            /* firefox doesn't behave the way we want it to, so we CAN'T use queryCommandState for superscript
3356               https://github.com/guardian/scribe/blob/master/BROWSERINCONSISTENCIES.md#documentquerycommandstate */
3357            // useQueryState: true
3358            contentDefault: '<b>x<sup>1</sup></b>',
3359            contentFA: '<i class="fa fa-superscript"></i>'
3360        },
3361        'subscript': {
3362            name: 'subscript',
3363            action: 'subscript',
3364            aria: 'subscript',
3365            tagNames: ['sub'],
3366            /* firefox doesn't behave the way we want it to, so we CAN'T use queryCommandState for subscript
3367               https://github.com/guardian/scribe/blob/master/BROWSERINCONSISTENCIES.md#documentquerycommandstate */
3368            // useQueryState: true
3369            contentDefault: '<b>x<sub>1</sub></b>',
3370            contentFA: '<i class="fa fa-subscript"></i>'
3371        },
3372        'image': {
3373            name: 'image',
3374            action: 'image',
3375            aria: 'image',
3376            tagNames: ['img'],
3377            contentDefault: '<b>image</b>',
3378            contentFA: '<i class="fa fa-picture-o"></i>'
3379        },
3380        'html': {
3381            name: 'html',
3382            action: 'html',
3383            aria: 'evaluate html',
3384            tagNames: ['iframe', 'object'],
3385            contentDefault: '<b>html</b>',
3386            contentFA: '<i class="fa fa-code"></i>'
3387        },
3388        'orderedlist': {
3389            name: 'orderedlist',
3390            action: 'insertorderedlist',
3391            aria: 'ordered list',
3392            tagNames: ['ol'],
3393            useQueryState: true,
3394            contentDefault: '<b>1.</b>',
3395            contentFA: '<i class="fa fa-list-ol"></i>'
3396        },
3397        'unorderedlist': {
3398            name: 'unorderedlist',
3399            action: 'insertunorderedlist',
3400            aria: 'unordered list',
3401            tagNames: ['ul'],
3402            useQueryState: true,
3403            contentDefault: '<b>&bull;</b>',
3404            contentFA: '<i class="fa fa-list-ul"></i>'
3405        },
3406        'indent': {
3407            name: 'indent',
3408            action: 'indent',
3409            aria: 'indent',
3410            tagNames: [],
3411            contentDefault: '<b>&rarr;</b>',
3412            contentFA: '<i class="fa fa-indent"></i>'
3413        },
3414        'outdent': {
3415            name: 'outdent',
3416            action: 'outdent',
3417            aria: 'outdent',
3418            tagNames: [],
3419            contentDefault: '<b>&larr;</b>',
3420            contentFA: '<i class="fa fa-outdent"></i>'
3421        },
3422        'justifyCenter': {
3423            name: 'justifyCenter',
3424            action: 'justifyCenter',
3425            aria: 'center justify',
3426            tagNames: [],
3427            style: {
3428                prop: 'text-align',
3429                value: 'center'
3430            },
3431            contentDefault: '<b>C</b>',
3432            contentFA: '<i class="fa fa-align-center"></i>'
3433        },
3434        'justifyFull': {
3435            name: 'justifyFull',
3436            action: 'justifyFull',
3437            aria: 'full justify',
3438            tagNames: [],
3439            style: {
3440                prop: 'text-align',
3441                value: 'justify'
3442            },
3443            contentDefault: '<b>J</b>',
3444            contentFA: '<i class="fa fa-align-justify"></i>'
3445        },
3446        'justifyLeft': {
3447            name: 'justifyLeft',
3448            action: 'justifyLeft',
3449            aria: 'left justify',
3450            tagNames: [],
3451            style: {
3452                prop: 'text-align',
3453                value: 'left'
3454            },
3455            contentDefault: '<b>L</b>',
3456            contentFA: '<i class="fa fa-align-left"></i>'
3457        },
3458        'justifyRight': {
3459            name: 'justifyRight',
3460            action: 'justifyRight',
3461            aria: 'right justify',
3462            tagNames: [],
3463            style: {
3464                prop: 'text-align',
3465                value: 'right'
3466            },
3467            contentDefault: '<b>R</b>',
3468            contentFA: '<i class="fa fa-align-right"></i>'
3469        },
3470        // Known inline elements that are not removed, or not removed consistantly across browsers:
3471        // <span>, <label>, <br>
3472        'removeFormat': {
3473            name: 'removeFormat',
3474            aria: 'remove formatting',
3475            action: 'removeFormat',
3476            contentDefault: '<b>X</b>',
3477            contentFA: '<i class="fa fa-eraser"></i>'
3478        },
3479
3480        /***** Buttons for appending block elements (append-<element> action) *****/
3481
3482        'quote': {
3483            name: 'quote',
3484            action: 'append-blockquote',
3485            aria: 'blockquote',
3486            tagNames: ['blockquote'],
3487            contentDefault: '<b>&ldquo;</b>',
3488            contentFA: '<i class="fa fa-quote-right"></i>'
3489        },
3490        'pre': {
3491            name: 'pre',
3492            action: 'append-pre',
3493            aria: 'preformatted text',
3494            tagNames: ['pre'],
3495            contentDefault: '<b>0101</b>',
3496            contentFA: '<i class="fa fa-code fa-lg"></i>'
3497        },
3498        'h1': {
3499            name: 'h1',
3500            action: 'append-h1',
3501            aria: 'header type one',
3502            tagNames: ['h1'],
3503            contentDefault: '<b>H1</b>',
3504            contentFA: '<i class="fa fa-header"><sup>1</sup>'
3505        },
3506        'h2': {
3507            name: 'h2',
3508            action: 'append-h2',
3509            aria: 'header type two',
3510            tagNames: ['h2'],
3511            contentDefault: '<b>H2</b>',
3512            contentFA: '<i class="fa fa-header"><sup>2</sup>'
3513        },
3514        'h3': {
3515            name: 'h3',
3516            action: 'append-h3',
3517            aria: 'header type three',
3518            tagNames: ['h3'],
3519            contentDefault: '<b>H3</b>',
3520            contentFA: '<i class="fa fa-header"><sup>3</sup>'
3521        },
3522        'h4': {
3523            name: 'h4',
3524            action: 'append-h4',
3525            aria: 'header type four',
3526            tagNames: ['h4'],
3527            contentDefault: '<b>H4</b>',
3528            contentFA: '<i class="fa fa-header"><sup>4</sup>'
3529        },
3530        'h5': {
3531            name: 'h5',
3532            action: 'append-h5',
3533            aria: 'header type five',
3534            tagNames: ['h5'],
3535            contentDefault: '<b>H5</b>',
3536            contentFA: '<i class="fa fa-header"><sup>5</sup>'
3537        },
3538        'h6': {
3539            name: 'h6',
3540            action: 'append-h6',
3541            aria: 'header type six',
3542            tagNames: ['h6'],
3543            contentDefault: '<b>H6</b>',
3544            contentFA: '<i class="fa fa-header"><sup>6</sup>'
3545        }
3546    };
3547
3548})();
3549
3550(function () {
3551    'use strict';
3552
3553    /* Base functionality for an extension which will display
3554     * a 'form' inside the toolbar
3555     */
3556    var FormExtension = MediumEditor.extensions.button.extend({
3557
3558        init: function () {
3559            MediumEditor.extensions.button.prototype.init.apply(this, arguments);
3560        },
3561
3562        // default labels for the form buttons
3563        formSaveLabel: '&#10003;',
3564        formCloseLabel: '&times;',
3565
3566        /* activeClass: [string]
3567         * set class which added to shown form
3568         */
3569        activeClass: 'medium-editor-toolbar-form-active',
3570
3571        /* hasForm: [boolean]
3572         *
3573         * Setting this to true will cause getForm() to be called
3574         * when the toolbar is created, so the form can be appended
3575         * inside the toolbar container
3576         */
3577        hasForm: true,
3578
3579        /* getForm: [function ()]
3580         *
3581         * When hasForm is true, this function must be implemented
3582         * and return a DOM Element which will be appended to
3583         * the toolbar container. The form should start hidden, and
3584         * the extension can choose when to hide/show it
3585         */
3586        getForm: function () {},
3587
3588        /* isDisplayed: [function ()]
3589         *
3590         * This function should return true/false reflecting
3591         * whether the form is currently displayed
3592         */
3593        isDisplayed: function () {
3594            if (this.hasForm) {
3595                return this.getForm().classList.contains(this.activeClass);
3596            }
3597            return false;
3598        },
3599
3600        /* hideForm: [function ()]
3601         *
3602         * This function should show the form element inside
3603         * the toolbar container
3604         */
3605        showForm: function () {
3606            if (this.hasForm) {
3607                this.getForm().classList.add(this.activeClass);
3608            }
3609        },
3610
3611        /* hideForm: [function ()]
3612         *
3613         * This function should hide the form element inside
3614         * the toolbar container
3615         */
3616        hideForm: function () {
3617            if (this.hasForm) {
3618                this.getForm().classList.remove(this.activeClass);
3619            }
3620        },
3621
3622        /************************ Helpers ************************
3623         * The following are helpers that are either set by MediumEditor
3624         * during initialization, or are helper methods which either
3625         * route calls to the MediumEditor instance or provide common
3626         * functionality for all form extensions
3627         *********************************************************/
3628
3629        /* showToolbarDefaultActions: [function ()]
3630         *
3631         * Helper method which will turn back the toolbar after canceling
3632         * the customized form
3633         */
3634        showToolbarDefaultActions: function () {
3635            var toolbar = this.base.getExtensionByName('toolbar');
3636            if (toolbar) {
3637                toolbar.showToolbarDefaultActions();
3638            }
3639        },
3640
3641        /* hideToolbarDefaultActions: [function ()]
3642         *
3643         * Helper function which will hide the default contents of the
3644         * toolbar, but leave the toolbar container in the same state
3645         * to allow a form to display its custom contents inside the toolbar
3646         */
3647        hideToolbarDefaultActions: function () {
3648            var toolbar = this.base.getExtensionByName('toolbar');
3649            if (toolbar) {
3650                toolbar.hideToolbarDefaultActions();
3651            }
3652        },
3653
3654        /* setToolbarPosition: [function ()]
3655         *
3656         * Helper function which will update the size and position
3657         * of the toolbar based on the toolbar content and the current
3658         * position of the user's selection
3659         */
3660        setToolbarPosition: function () {
3661            var toolbar = this.base.getExtensionByName('toolbar');
3662            if (toolbar) {
3663                toolbar.setToolbarPosition();
3664            }
3665        }
3666    });
3667
3668    MediumEditor.extensions.form = FormExtension;
3669})();
3670(function () {
3671    'use strict';
3672
3673    var AnchorForm = MediumEditor.extensions.form.extend({
3674        /* Anchor Form Options */
3675
3676        /* customClassOption: [string]  (previously options.anchorButton + options.anchorButtonClass)
3677         * Custom class name the user can optionally have added to their created links (ie 'button').
3678         * If passed as a non-empty string, a checkbox will be displayed allowing the user to choose
3679         * whether to have the class added to the created link or not.
3680         */
3681        customClassOption: null,
3682
3683        /* customClassOptionText: [string]
3684         * text to be shown in the checkbox when the __customClassOption__ is being used.
3685         */
3686        customClassOptionText: 'Button',
3687
3688        /* linkValidation: [boolean]  (previously options.checkLinkFormat)
3689         * enables/disables check for common URL protocols on anchor links.
3690         */
3691        linkValidation: false,
3692
3693        /* placeholderText: [string]  (previously options.anchorInputPlaceholder)
3694         * text to be shown as placeholder of the anchor input.
3695         */
3696        placeholderText: 'Paste or type a link',
3697
3698        /* targetCheckbox: [boolean]  (previously options.anchorTarget)
3699         * enables/disables displaying a "Open in new window" checkbox, which when checked
3700         * changes the `target` attribute of the created link.
3701         */
3702        targetCheckbox: false,
3703
3704        /* targetCheckboxText: [string]  (previously options.anchorInputCheckboxLabel)
3705         * text to be shown in the checkbox enabled via the __targetCheckbox__ option.
3706         */
3707        targetCheckboxText: 'Open in new window',
3708
3709        // Options for the Button base class
3710        name: 'anchor',
3711        action: 'createLink',
3712        aria: 'link',
3713        tagNames: ['a'],
3714        contentDefault: '<b>#</b>',
3715        contentFA: '<i class="fa fa-link"></i>',
3716
3717        init: function () {
3718            MediumEditor.extensions.form.prototype.init.apply(this, arguments);
3719
3720            this.subscribe('editableKeydown', this.handleKeydown.bind(this));
3721        },
3722
3723        // Called when the button the toolbar is clicked
3724        // Overrides ButtonExtension.handleClick
3725        handleClick: function (event) {
3726            event.preventDefault();
3727            event.stopPropagation();
3728
3729            var range = MediumEditor.selection.getSelectionRange(this.document);
3730
3731            if (range.startContainer.nodeName.toLowerCase() === 'a' ||
3732                range.endContainer.nodeName.toLowerCase() === 'a' ||
3733                MediumEditor.util.getClosestTag(MediumEditor.selection.getSelectedParentElement(range), 'a')) {
3734                return this.execAction('unlink');
3735            }
3736
3737            if (!this.isDisplayed()) {
3738                this.showForm();
3739            }
3740
3741            return false;
3742        },
3743
3744        // Called when user hits the defined shortcut (CTRL / COMMAND + K)
3745        handleKeydown: function (event) {
3746            if (MediumEditor.util.isKey(event, MediumEditor.util.keyCode.K) && MediumEditor.util.isMetaCtrlKey(event) && !event.shiftKey) {
3747                this.handleClick(event);
3748            }
3749        },
3750
3751        // Called by medium-editor to append form to the toolbar
3752        getForm: function () {
3753            if (!this.form) {
3754                this.form = this.createForm();
3755            }
3756            return this.form;
3757        },
3758
3759        getTemplate: function () {
3760            var template = [
3761                '<input type="text" class="medium-editor-toolbar-input" placeholder="', this.placeholderText, '">'
3762            ];
3763
3764            template.push(
3765                '<a href="#" class="medium-editor-toolbar-save">',
3766                this.getEditorOption('buttonLabels') === 'fontawesome' ? '<i class="fa fa-check"></i>' : this.formSaveLabel,
3767                '</a>'
3768            );
3769
3770            template.push('<a href="#" class="medium-editor-toolbar-close">',
3771                this.getEditorOption('buttonLabels') === 'fontawesome' ? '<i class="fa fa-times"></i>' : this.formCloseLabel,
3772                '</a>');
3773
3774            // both of these options are slightly moot with the ability to
3775            // override the various form buildup/serialize functions.
3776
3777            if (this.targetCheckbox) {
3778                // fixme: ideally, this targetCheckboxText would be a formLabel too,
3779                // figure out how to deprecate? also consider `fa-` icon default implcations.
3780                template.push(
3781                    '<div class="medium-editor-toolbar-form-row">',
3782                    '<input type="checkbox" class="medium-editor-toolbar-anchor-target" id="medium-editor-toolbar-anchor-target-field-' + this.getEditorId() + '">',
3783                    '<label for="medium-editor-toolbar-anchor-target-field-' + this.getEditorId() + '">',
3784                    this.targetCheckboxText,
3785                    '</label>',
3786                    '</div>'
3787                );
3788            }
3789
3790            if (this.customClassOption) {
3791                // fixme: expose this `Button` text as a formLabel property, too
3792                // and provide similar access to a `fa-` icon default.
3793                template.push(
3794                    '<div class="medium-editor-toolbar-form-row">',
3795                    '<input type="checkbox" class="medium-editor-toolbar-anchor-button">',
3796                    '<label>',
3797                    this.customClassOptionText,
3798                    '</label>',
3799                    '</div>'
3800                );
3801            }
3802
3803            return template.join('');
3804
3805        },
3806
3807        // Used by medium-editor when the default toolbar is to be displayed
3808        isDisplayed: function () {
3809            return MediumEditor.extensions.form.prototype.isDisplayed.apply(this);
3810        },
3811
3812        hideForm: function () {
3813            MediumEditor.extensions.form.prototype.hideForm.apply(this);
3814            this.getInput().value = '';
3815        },
3816
3817        showForm: function (opts) {
3818            var input = this.getInput(),
3819                targetCheckbox = this.getAnchorTargetCheckbox(),
3820                buttonCheckbox = this.getAnchorButtonCheckbox();
3821
3822            opts = opts || { value: '' };
3823            // TODO: This is for backwards compatability
3824            // We don't need to support the 'string' argument in 6.0.0
3825            if (typeof opts === 'string') {
3826                opts = {
3827                    value: opts
3828                };
3829            }
3830
3831            this.base.saveSelection();
3832            this.hideToolbarDefaultActions();
3833            MediumEditor.extensions.form.prototype.showForm.apply(this);
3834            this.setToolbarPosition();
3835
3836            input.value = opts.value;
3837            input.focus();
3838
3839            // If we have a target checkbox, we want it to be checked/unchecked
3840            // based on whether the existing link has target=_blank
3841            if (targetCheckbox) {
3842                targetCheckbox.checked = opts.target === '_blank';
3843            }
3844
3845            // If we have a custom class checkbox, we want it to be checked/unchecked
3846            // based on whether an existing link already has the class
3847            if (buttonCheckbox) {
3848                var classList = opts.buttonClass ? opts.buttonClass.split(' ') : [];
3849                buttonCheckbox.checked = (classList.indexOf(this.customClassOption) !== -1);
3850            }
3851        },
3852
3853        // Called by core when tearing down medium-editor (destroy)
3854        destroy: function () {
3855            if (!this.form) {
3856                return false;
3857            }
3858
3859            if (this.form.parentNode) {
3860                this.form.parentNode.removeChild(this.form);
3861            }
3862
3863            delete this.form;
3864        },
3865
3866        // core methods
3867
3868        getFormOpts: function () {
3869            // no notion of private functions? wanted `_getFormOpts`
3870            var targetCheckbox = this.getAnchorTargetCheckbox(),
3871                buttonCheckbox = this.getAnchorButtonCheckbox(),
3872                opts = {
3873                    value: this.getInput().value.trim()
3874                };
3875
3876            if (this.linkValidation) {
3877                opts.value = this.checkLinkFormat(opts.value);
3878            }
3879
3880            opts.target = '_self';
3881            if (targetCheckbox && targetCheckbox.checked) {
3882                opts.target = '_blank';
3883            }
3884
3885            if (buttonCheckbox && buttonCheckbox.checked) {
3886                opts.buttonClass = this.customClassOption;
3887            }
3888
3889            return opts;
3890        },
3891
3892        doFormSave: function () {
3893            var opts = this.getFormOpts();
3894            this.completeFormSave(opts);
3895        },
3896
3897        completeFormSave: function (opts) {
3898            this.base.restoreSelection();
3899            this.execAction(this.action, opts);
3900            this.base.checkSelection();
3901        },
3902
3903        ensureEncodedUri: function (str) {
3904            return str === decodeURI(str) ? encodeURI(str) : str;
3905        },
3906
3907        ensureEncodedUriComponent: function (str) {
3908            return str === decodeURIComponent(str) ? encodeURIComponent(str) : str;
3909        },
3910
3911        ensureEncodedParam: function (param) {
3912            var split = param.split('='),
3913                key = split[0],
3914                val = split[1];
3915
3916            return key + (val === undefined ? '' : '=' + this.ensureEncodedUriComponent(val));
3917        },
3918
3919        ensureEncodedQuery: function (queryString) {
3920            return queryString.split('&').map(this.ensureEncodedParam.bind(this)).join('&');
3921        },
3922
3923        checkLinkFormat: function (value) {
3924            // Matches any alphabetical characters followed by ://
3925            // Matches protocol relative "//"
3926            // Matches common external protocols "mailto:" "tel:" "maps:"
3927            // Matches relative hash link, begins with "#"
3928            var urlSchemeRegex = /^([a-z]+:)?\/\/|^(mailto|tel|maps):|^\#/i,
3929                hasScheme = urlSchemeRegex.test(value),
3930                scheme = '',
3931                // telRegex is a regex for checking if the string is a telephone number
3932                telRegex = /^\+?\s?\(?(?:\d\s?\-?\)?){3,20}$/,
3933                urlParts = value.match(/^(.*?)(?:\?(.*?))?(?:#(.*))?$/),
3934                path = urlParts[1],
3935                query = urlParts[2],
3936                fragment = urlParts[3];
3937
3938            if (telRegex.test(value)) {
3939                return 'tel:' + value;
3940            }
3941
3942            if (!hasScheme) {
3943                var host = path.split('/')[0];
3944                // if the host part of the path looks like a hostname
3945                if (host.match(/.+(\.|:).+/) || host === 'localhost') {
3946                    scheme = 'http://';
3947                }
3948            }
3949
3950            return scheme +
3951                // Ensure path is encoded
3952                this.ensureEncodedUri(path) +
3953                // Ensure query is encoded
3954                (query === undefined ? '' : '?' + this.ensureEncodedQuery(query)) +
3955                // Include fragment unencoded as encodeUriComponent is too
3956                // heavy handed for the many characters allowed in a fragment
3957                (fragment === undefined ? '' : '#' + fragment);
3958        },
3959
3960        doFormCancel: function () {
3961            this.base.restoreSelection();
3962            this.base.checkSelection();
3963        },
3964
3965        // form creation and event handling
3966        attachFormEvents: function (form) {
3967            var close = form.querySelector('.medium-editor-toolbar-close'),
3968                save = form.querySelector('.medium-editor-toolbar-save'),
3969                input = form.querySelector('.medium-editor-toolbar-input');
3970
3971            // Handle clicks on the form itself
3972            this.on(form, 'click', this.handleFormClick.bind(this));
3973
3974            // Handle typing in the textbox
3975            this.on(input, 'keyup', this.handleTextboxKeyup.bind(this));
3976
3977            // Handle close button clicks
3978            this.on(close, 'click', this.handleCloseClick.bind(this));
3979
3980            // Handle save button clicks (capture)
3981            this.on(save, 'click', this.handleSaveClick.bind(this), true);
3982
3983        },
3984
3985        createForm: function () {
3986            var doc = this.document,
3987                form = doc.createElement('div');
3988
3989            // Anchor Form (div)
3990            form.className = 'medium-editor-toolbar-form';
3991            form.id = 'medium-editor-toolbar-form-anchor-' + this.getEditorId();
3992            form.innerHTML = this.getTemplate();
3993            this.attachFormEvents(form);
3994
3995            return form;
3996        },
3997
3998        getInput: function () {
3999            return this.getForm().querySelector('input.medium-editor-toolbar-input');
4000        },
4001
4002        getAnchorTargetCheckbox: function () {
4003            return this.getForm().querySelector('.medium-editor-toolbar-anchor-target');
4004        },
4005
4006        getAnchorButtonCheckbox: function () {
4007            return this.getForm().querySelector('.medium-editor-toolbar-anchor-button');
4008        },
4009
4010        handleTextboxKeyup: function (event) {
4011            // For ENTER -> create the anchor
4012            if (event.keyCode === MediumEditor.util.keyCode.ENTER) {
4013                event.preventDefault();
4014                this.doFormSave();
4015                return;
4016            }
4017
4018            // For ESCAPE -> close the form
4019            if (event.keyCode === MediumEditor.util.keyCode.ESCAPE) {
4020                event.preventDefault();
4021                this.doFormCancel();
4022            }
4023        },
4024
4025        handleFormClick: function (event) {
4026            // make sure not to hide form when clicking inside the form
4027            event.stopPropagation();
4028        },
4029
4030        handleSaveClick: function (event) {
4031            // Clicking Save -> create the anchor
4032            event.preventDefault();
4033            this.doFormSave();
4034        },
4035
4036        handleCloseClick: function (event) {
4037            // Click Close -> close the form
4038            event.preventDefault();
4039            this.doFormCancel();
4040        }
4041    });
4042
4043    MediumEditor.extensions.anchor = AnchorForm;
4044}());
4045
4046(function () {
4047    'use strict';
4048
4049    var AnchorPreview = MediumEditor.Extension.extend({
4050        name: 'anchor-preview',
4051
4052        // Anchor Preview Options
4053
4054        /* hideDelay: [number]  (previously options.anchorPreviewHideDelay)
4055         * time in milliseconds to show the anchor tag preview after the mouse has left the anchor tag.
4056         */
4057        hideDelay: 500,
4058
4059        /* previewValueSelector: [string]
4060         * the default selector to locate where to put the activeAnchor value in the preview
4061         */
4062        previewValueSelector: 'a',
4063
4064        /* showWhenToolbarIsVisible: [boolean]
4065         * determines whether the anchor tag preview shows up when the toolbar is visible
4066         */
4067        showWhenToolbarIsVisible: false,
4068
4069        /* showOnEmptyLinks: [boolean]
4070        * determines whether the anchor tag preview shows up on links with href="" or href="#something"
4071        */
4072        showOnEmptyLinks: true,
4073
4074        init: function () {
4075            this.anchorPreview = this.createPreview();
4076
4077            this.getEditorOption('elementsContainer').appendChild(this.anchorPreview);
4078
4079            this.attachToEditables();
4080        },
4081
4082        getInteractionElements: function () {
4083            return this.getPreviewElement();
4084        },
4085
4086        // TODO: Remove this function in 6.0.0
4087        getPreviewElement: function () {
4088            return this.anchorPreview;
4089        },
4090
4091        createPreview: function () {
4092            var el = this.document.createElement('div');
4093
4094            el.id = 'medium-editor-anchor-preview-' + this.getEditorId();
4095            el.className = 'medium-editor-anchor-preview';
4096            el.innerHTML = this.getTemplate();
4097
4098            this.on(el, 'click', this.handleClick.bind(this));
4099
4100            return el;
4101        },
4102
4103        getTemplate: function () {
4104            return '<div class="medium-editor-toolbar-anchor-preview" id="medium-editor-toolbar-anchor-preview">' +
4105                '    <a class="medium-editor-toolbar-anchor-preview-inner"></a>' +
4106                '</div>';
4107        },
4108
4109        destroy: function () {
4110            if (this.anchorPreview) {
4111                if (this.anchorPreview.parentNode) {
4112                    this.anchorPreview.parentNode.removeChild(this.anchorPreview);
4113                }
4114                delete this.anchorPreview;
4115            }
4116        },
4117
4118        hidePreview: function () {
4119            if (this.anchorPreview) {
4120                this.anchorPreview.classList.remove('medium-editor-anchor-preview-active');
4121            }
4122            this.activeAnchor = null;
4123        },
4124
4125        showPreview: function (anchorEl) {
4126            if (this.anchorPreview.classList.contains('medium-editor-anchor-preview-active') ||
4127                    anchorEl.getAttribute('data-disable-preview')) {
4128                return true;
4129            }
4130
4131            if (this.previewValueSelector) {
4132                this.anchorPreview.querySelector(this.previewValueSelector).textContent = anchorEl.attributes.href.value;
4133                this.anchorPreview.querySelector(this.previewValueSelector).href = anchorEl.attributes.href.value;
4134            }
4135
4136            this.anchorPreview.classList.add('medium-toolbar-arrow-over');
4137            this.anchorPreview.classList.remove('medium-toolbar-arrow-under');
4138
4139            if (!this.anchorPreview.classList.contains('medium-editor-anchor-preview-active')) {
4140                this.anchorPreview.classList.add('medium-editor-anchor-preview-active');
4141            }
4142
4143            this.activeAnchor = anchorEl;
4144
4145            this.positionPreview();
4146            this.attachPreviewHandlers();
4147
4148            return this;
4149        },
4150
4151        positionPreview: function (activeAnchor) {
4152            activeAnchor = activeAnchor || this.activeAnchor;
4153            var containerWidth = this.window.innerWidth,
4154                buttonHeight = this.anchorPreview.offsetHeight,
4155                boundary = activeAnchor.getBoundingClientRect(),
4156                diffLeft = this.diffLeft,
4157                diffTop = this.diffTop,
4158                elementsContainer = this.getEditorOption('elementsContainer'),
4159                elementsContainerAbsolute = ['absolute', 'fixed'].indexOf(window.getComputedStyle(elementsContainer).getPropertyValue('position')) > -1,
4160                relativeBoundary = {},
4161                halfOffsetWidth, defaultLeft, middleBoundary, elementsContainerBoundary, top;
4162
4163            halfOffsetWidth = this.anchorPreview.offsetWidth / 2;
4164            var toolbarExtension = this.base.getExtensionByName('toolbar');
4165            if (toolbarExtension) {
4166                diffLeft = toolbarExtension.diffLeft;
4167                diffTop = toolbarExtension.diffTop;
4168            }
4169            defaultLeft = diffLeft - halfOffsetWidth;
4170
4171            // If container element is absolute / fixed, recalculate boundaries to be relative to the container
4172            if (elementsContainerAbsolute) {
4173                elementsContainerBoundary = elementsContainer.getBoundingClientRect();
4174                ['top', 'left'].forEach(function (key) {
4175                    relativeBoundary[key] = boundary[key] - elementsContainerBoundary[key];
4176                });
4177
4178                relativeBoundary.width = boundary.width;
4179                relativeBoundary.height = boundary.height;
4180                boundary = relativeBoundary;
4181
4182                containerWidth = elementsContainerBoundary.width;
4183
4184                // Adjust top position according to container scroll position
4185                top = elementsContainer.scrollTop;
4186            } else {
4187                // Adjust top position according to window scroll position
4188                top = this.window.pageYOffset;
4189            }
4190
4191            middleBoundary = boundary.left + boundary.width / 2;
4192            top += buttonHeight + boundary.top + boundary.height - diffTop - this.anchorPreview.offsetHeight;
4193
4194            this.anchorPreview.style.top = Math.round(top) + 'px';
4195            this.anchorPreview.style.right = 'initial';
4196            if (middleBoundary < halfOffsetWidth) {
4197                this.anchorPreview.style.left = defaultLeft + halfOffsetWidth + 'px';
4198                this.anchorPreview.style.right = 'initial';
4199            } else if ((containerWidth - middleBoundary) < halfOffsetWidth) {
4200                this.anchorPreview.style.left = 'auto';
4201                this.anchorPreview.style.right = 0;
4202            } else {
4203                this.anchorPreview.style.left = defaultLeft + middleBoundary + 'px';
4204                this.anchorPreview.style.right = 'initial';
4205            }
4206        },
4207
4208        attachToEditables: function () {
4209            this.subscribe('editableMouseover', this.handleEditableMouseover.bind(this));
4210            this.subscribe('positionedToolbar', this.handlePositionedToolbar.bind(this));
4211        },
4212
4213        handlePositionedToolbar: function () {
4214            // If the toolbar is visible and positioned, we don't need to hide the preview
4215            // when showWhenToolbarIsVisible is true
4216            if (!this.showWhenToolbarIsVisible) {
4217                this.hidePreview();
4218            }
4219        },
4220
4221        handleClick: function (event) {
4222            var anchorExtension = this.base.getExtensionByName('anchor'),
4223                activeAnchor = this.activeAnchor;
4224
4225            if (anchorExtension && activeAnchor) {
4226                event.preventDefault();
4227
4228                this.base.selectElement(this.activeAnchor);
4229
4230                // Using setTimeout + delay because:
4231                // We may actually be displaying the anchor form, which should be controlled by delay
4232                this.base.delay(function () {
4233                    if (activeAnchor) {
4234                        var opts = {
4235                            value: activeAnchor.attributes.href.value,
4236                            target: activeAnchor.getAttribute('target'),
4237                            buttonClass: activeAnchor.getAttribute('class')
4238                        };
4239                        anchorExtension.showForm(opts);
4240                        activeAnchor = null;
4241                    }
4242                }.bind(this));
4243            }
4244
4245            this.hidePreview();
4246        },
4247
4248        handleAnchorMouseout: function () {
4249            this.anchorToPreview = null;
4250            this.off(this.activeAnchor, 'mouseout', this.instanceHandleAnchorMouseout);
4251            this.instanceHandleAnchorMouseout = null;
4252        },
4253
4254        handleEditableMouseover: function (event) {
4255            var target = MediumEditor.util.getClosestTag(event.target, 'a');
4256
4257            if (false === target) {
4258                return;
4259            }
4260
4261            // Detect empty href attributes
4262            // The browser will make href="" or href="#top"
4263            // into absolute urls when accessed as event.target.href, so check the html
4264            if (!this.showOnEmptyLinks &&
4265                (!/href=["']\S+["']/.test(target.outerHTML) || /href=["']#\S+["']/.test(target.outerHTML))) {
4266                return true;
4267            }
4268
4269            // only show when toolbar is not present
4270            var toolbar = this.base.getExtensionByName('toolbar');
4271            if (!this.showWhenToolbarIsVisible && toolbar && toolbar.isDisplayed && toolbar.isDisplayed()) {
4272                return true;
4273            }
4274
4275            // detach handler for other anchor in case we hovered multiple anchors quickly
4276            if (this.activeAnchor && this.activeAnchor !== target) {
4277                this.detachPreviewHandlers();
4278            }
4279
4280            this.anchorToPreview = target;
4281
4282            this.instanceHandleAnchorMouseout = this.handleAnchorMouseout.bind(this);
4283            this.on(this.anchorToPreview, 'mouseout', this.instanceHandleAnchorMouseout);
4284            // Using setTimeout + delay because:
4285            // - We're going to show the anchor preview according to the configured delay
4286            //   if the mouse has not left the anchor tag in that time
4287            this.base.delay(function () {
4288                if (this.anchorToPreview) {
4289                    this.showPreview(this.anchorToPreview);
4290                }
4291            }.bind(this));
4292        },
4293
4294        handlePreviewMouseover: function () {
4295            this.lastOver = (new Date()).getTime();
4296            this.hovering = true;
4297        },
4298
4299        handlePreviewMouseout: function (event) {
4300            if (!event.relatedTarget || !/anchor-preview/.test(event.relatedTarget.className)) {
4301                this.hovering = false;
4302            }
4303        },
4304
4305        updatePreview: function () {
4306            if (this.hovering) {
4307                return true;
4308            }
4309            var durr = (new Date()).getTime() - this.lastOver;
4310            if (durr > this.hideDelay) {
4311                // hide the preview 1/2 second after mouse leaves the link
4312                this.detachPreviewHandlers();
4313            }
4314        },
4315
4316        detachPreviewHandlers: function () {
4317            // cleanup
4318            clearInterval(this.intervalTimer);
4319            if (this.instanceHandlePreviewMouseover) {
4320                this.off(this.anchorPreview, 'mouseover', this.instanceHandlePreviewMouseover);
4321                this.off(this.anchorPreview, 'mouseout', this.instanceHandlePreviewMouseout);
4322                if (this.activeAnchor) {
4323                    this.off(this.activeAnchor, 'mouseover', this.instanceHandlePreviewMouseover);
4324                    this.off(this.activeAnchor, 'mouseout', this.instanceHandlePreviewMouseout);
4325                }
4326            }
4327
4328            this.hidePreview();
4329
4330            this.hovering = this.instanceHandlePreviewMouseover = this.instanceHandlePreviewMouseout = null;
4331        },
4332
4333        // TODO: break up method and extract out handlers
4334        attachPreviewHandlers: function () {
4335            this.lastOver = (new Date()).getTime();
4336            this.hovering = true;
4337
4338            this.instanceHandlePreviewMouseover = this.handlePreviewMouseover.bind(this);
4339            this.instanceHandlePreviewMouseout = this.handlePreviewMouseout.bind(this);
4340
4341            this.intervalTimer = setInterval(this.updatePreview.bind(this), 200);
4342
4343            this.on(this.anchorPreview, 'mouseover', this.instanceHandlePreviewMouseover);
4344            this.on(this.anchorPreview, 'mouseout', this.instanceHandlePreviewMouseout);
4345            this.on(this.activeAnchor, 'mouseover', this.instanceHandlePreviewMouseover);
4346            this.on(this.activeAnchor, 'mouseout', this.instanceHandlePreviewMouseout);
4347        }
4348    });
4349
4350    MediumEditor.extensions.anchorPreview = AnchorPreview;
4351}());
4352
4353(function () {
4354    'use strict';
4355
4356    var WHITESPACE_CHARS,
4357        KNOWN_TLDS_FRAGMENT,
4358        LINK_REGEXP_TEXT,
4359        KNOWN_TLDS_REGEXP,
4360        LINK_REGEXP;
4361
4362    WHITESPACE_CHARS = [' ', '\t', '\n', '\r', '\u00A0', '\u2000', '\u2001', '\u2002', '\u2003',
4363                                    '\u2028', '\u2029'];
4364    KNOWN_TLDS_FRAGMENT = 'com|net|org|edu|gov|mil|aero|asia|biz|cat|coop|info|int|jobs|mobi|museum|name|post|pro|tel|travel|' +
4365        '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|' +
4366        '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|' +
4367        '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|' +
4368        '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|' +
4369        '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|' +
4370        '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|' +
4371        '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';
4372
4373    LINK_REGEXP_TEXT =
4374        '(' +
4375        // Version of Gruber URL Regexp optimized for JS: http://stackoverflow.com/a/17733640
4376        '((?:(https?://|ftps?://|nntp://)|www\\d{0,3}[.]|[a-z0-9.\\-]+[.](' + KNOWN_TLDS_FRAGMENT + ')\\\/)\\S+(?:[^\\s`!\\[\\]{};:\'\".,?\u00AB\u00BB\u201C\u201D\u2018\u2019]))' +
4377        // Addition to above Regexp to support bare domains/one level subdomains with common non-i18n TLDs and without www prefix:
4378        ')|(([a-z0-9\\-]+\\.)?[a-z0-9\\-]+\\.(' + KNOWN_TLDS_FRAGMENT + '))';
4379
4380    KNOWN_TLDS_REGEXP = new RegExp('^(' + KNOWN_TLDS_FRAGMENT + ')$', 'i');
4381
4382    LINK_REGEXP = new RegExp(LINK_REGEXP_TEXT, 'gi');
4383
4384    function nodeIsNotInsideAnchorTag(node) {
4385        return !MediumEditor.util.getClosestTag(node, 'a');
4386    }
4387
4388    var AutoLink = MediumEditor.Extension.extend({
4389        init: function () {
4390            MediumEditor.Extension.prototype.init.apply(this, arguments);
4391
4392            this.disableEventHandling = false;
4393            this.subscribe('editableKeypress', this.onKeypress.bind(this));
4394            this.subscribe('editableBlur', this.onBlur.bind(this));
4395            // MS IE has it's own auto-URL detect feature but ours is better in some ways. Be consistent.
4396            this.document.execCommand('AutoUrlDetect', false, false);
4397        },
4398
4399        isLastInstance: function () {
4400            var activeInstances = 0;
4401            for (var i = 0; i < this.window._mediumEditors.length; i++) {
4402                var editor = this.window._mediumEditors[i];
4403                if (editor !== null && editor.getExtensionByName('autoLink') !== undefined) {
4404                    activeInstances++;
4405                }
4406            }
4407            return activeInstances === 1;
4408        },
4409
4410        destroy: function () {
4411            // Turn AutoUrlDetect back on
4412            if (this.document.queryCommandSupported('AutoUrlDetect') && this.isLastInstance()) {
4413                this.document.execCommand('AutoUrlDetect', false, true);
4414            }
4415        },
4416
4417        onBlur: function (blurEvent, editable) {
4418            this.performLinking(editable);
4419        },
4420
4421        onKeypress: function (keyPressEvent) {
4422            if (this.disableEventHandling) {
4423                return;
4424            }
4425
4426            if (MediumEditor.util.isKey(keyPressEvent, [MediumEditor.util.keyCode.SPACE, MediumEditor.util.keyCode.ENTER])) {
4427                clearTimeout(this.performLinkingTimeout);
4428                // Saving/restoring the selection in the middle of a keypress doesn't work well...
4429                this.performLinkingTimeout = setTimeout(function () {
4430                    try {
4431                        var sel = this.base.exportSelection();
4432                        if (this.performLinking(keyPressEvent.target)) {
4433                            // pass true for favorLaterSelectionAnchor - this is needed for links at the end of a
4434                            // paragraph in MS IE, or MS IE causes the link to be deleted right after adding it.
4435                            this.base.importSelection(sel, true);
4436                        }
4437                    } catch (e) {
4438                        if (window.console) {
4439                            window.console.error('Failed to perform linking', e);
4440                        }
4441                        this.disableEventHandling = true;
4442                    }
4443                }.bind(this), 0);
4444            }
4445        },
4446
4447        performLinking: function (contenteditable) {
4448            /*
4449            Perform linking on blockElement basis, blockElements are HTML elements with text content and without
4450            child element.
4451
4452            Example:
4453            - HTML content
4454            <blockquote>
4455              <p>link.</p>
4456              <p>my</p>
4457            </blockquote>
4458
4459            - blockElements
4460            [<p>link.</p>, <p>my</p>]
4461
4462            otherwise the detection can wrongly find the end of one paragraph and the beginning of another paragraph
4463            to constitute a link, such as a paragraph ending "link." and the next paragraph beginning with "my" is
4464            interpreted into "link.my" and the code tries to create a link across blockElements - which doesn't work
4465            and is terrible.
4466            (Medium deletes the spaces/returns between P tags so the textContent ends up without paragraph spacing)
4467            */
4468            var blockElements = MediumEditor.util.splitByBlockElements(contenteditable),
4469                documentModified = false;
4470            if (blockElements.length === 0) {
4471                blockElements = [contenteditable];
4472            }
4473            for (var i = 0; i < blockElements.length; i++) {
4474                documentModified = this.removeObsoleteAutoLinkSpans(blockElements[i]) || documentModified;
4475                documentModified = this.performLinkingWithinElement(blockElements[i]) || documentModified;
4476            }
4477            this.base.events.updateInput(contenteditable, { target: contenteditable, currentTarget: contenteditable });
4478            return documentModified;
4479        },
4480
4481        removeObsoleteAutoLinkSpans: function (element) {
4482            if (!element || element.nodeType === 3) {
4483                return false;
4484            }
4485
4486            var spans = element.querySelectorAll('span[data-auto-link="true"]'),
4487                documentModified = false;
4488
4489            for (var i = 0; i < spans.length; i++) {
4490                var textContent = spans[i].textContent;
4491                if (textContent.indexOf('://') === -1) {
4492                    textContent = MediumEditor.util.ensureUrlHasProtocol(textContent);
4493                }
4494                if (spans[i].getAttribute('data-href') !== textContent && nodeIsNotInsideAnchorTag(spans[i])) {
4495                    documentModified = true;
4496                    var trimmedTextContent = textContent.replace(/\s+$/, '');
4497                    if (spans[i].getAttribute('data-href') === trimmedTextContent) {
4498                        var charactersTrimmed = textContent.length - trimmedTextContent.length,
4499                            subtree = MediumEditor.util.splitOffDOMTree(spans[i], this.splitTextBeforeEnd(spans[i], charactersTrimmed));
4500                        spans[i].parentNode.insertBefore(subtree, spans[i].nextSibling);
4501                    } else {
4502                        // Some editing has happened to the span, so just remove it entirely. The user can put it back
4503                        // around just the href content if they need to prevent it from linking
4504                        MediumEditor.util.unwrap(spans[i], this.document);
4505                    }
4506                }
4507            }
4508            return documentModified;
4509        },
4510
4511        splitTextBeforeEnd: function (element, characterCount) {
4512            var treeWalker = this.document.createTreeWalker(element, NodeFilter.SHOW_TEXT, null, false),
4513                lastChildNotExhausted = true;
4514
4515            // Start the tree walker at the last descendant of the span
4516            while (lastChildNotExhausted) {
4517                lastChildNotExhausted = treeWalker.lastChild() !== null;
4518            }
4519
4520            var currentNode,
4521                currentNodeValue,
4522                previousNode;
4523            while (characterCount > 0 && previousNode !== null) {
4524                currentNode = treeWalker.currentNode;
4525                currentNodeValue = currentNode.nodeValue;
4526                if (currentNodeValue.length > characterCount) {
4527                    previousNode = currentNode.splitText(currentNodeValue.length - characterCount);
4528                    characterCount = 0;
4529                } else {
4530                    previousNode = treeWalker.previousNode();
4531                    characterCount -= currentNodeValue.length;
4532                }
4533            }
4534            return previousNode;
4535        },
4536
4537        performLinkingWithinElement: function (element) {
4538            var matches = this.findLinkableText(element),
4539                linkCreated = false;
4540
4541            for (var matchIndex = 0; matchIndex < matches.length; matchIndex++) {
4542                var matchingTextNodes = MediumEditor.util.findOrCreateMatchingTextNodes(this.document, element,
4543                        matches[matchIndex]);
4544                if (this.shouldNotLink(matchingTextNodes)) {
4545                    continue;
4546                }
4547                this.createAutoLink(matchingTextNodes, matches[matchIndex].href);
4548            }
4549            return linkCreated;
4550        },
4551
4552        shouldNotLink: function (textNodes) {
4553            var shouldNotLink = false;
4554            for (var i = 0; i < textNodes.length && shouldNotLink === false; i++) {
4555                // Do not link if the text node is either inside an anchor or inside span[data-auto-link]
4556                shouldNotLink = !!MediumEditor.util.traverseUp(textNodes[i], function (node) {
4557                    return node.nodeName.toLowerCase() === 'a' ||
4558                        (node.getAttribute && node.getAttribute('data-auto-link') === 'true');
4559                });
4560            }
4561            return shouldNotLink;
4562        },
4563
4564        findLinkableText: function (contenteditable) {
4565            var textContent = contenteditable.textContent,
4566                match = null,
4567                matches = [];
4568
4569            while ((match = LINK_REGEXP.exec(textContent)) !== null) {
4570                var matchOk = true,
4571                    matchEnd = match.index + match[0].length;
4572                // If the regexp detected something as a link that has text immediately preceding/following it, bail out.
4573                matchOk = (match.index === 0 || WHITESPACE_CHARS.indexOf(textContent[match.index - 1]) !== -1) &&
4574                    (matchEnd === textContent.length || WHITESPACE_CHARS.indexOf(textContent[matchEnd]) !== -1);
4575                // If the regexp detected a bare domain that doesn't use one of our expected TLDs, bail out.
4576                matchOk = matchOk && (match[0].indexOf('/') !== -1 ||
4577                    KNOWN_TLDS_REGEXP.test(match[0].split('.').pop().split('?').shift()));
4578
4579                if (matchOk) {
4580                    matches.push({
4581                        href: match[0],
4582                        start: match.index,
4583                        end: matchEnd
4584                    });
4585                }
4586            }
4587            return matches;
4588        },
4589
4590        createAutoLink: function (textNodes, href) {
4591            href = MediumEditor.util.ensureUrlHasProtocol(href);
4592            var anchor = MediumEditor.util.createLink(this.document, textNodes, href, this.getEditorOption('targetBlank') ? '_blank' : null),
4593                span = this.document.createElement('span');
4594            span.setAttribute('data-auto-link', 'true');
4595            span.setAttribute('data-href', href);
4596            anchor.insertBefore(span, anchor.firstChild);
4597            while (anchor.childNodes.length > 1) {
4598                span.appendChild(anchor.childNodes[1]);
4599            }
4600        }
4601
4602    });
4603
4604    MediumEditor.extensions.autoLink = AutoLink;
4605}());
4606
4607(function () {
4608    'use strict';
4609
4610    var CLASS_DRAG_OVER = 'medium-editor-dragover';
4611
4612    function clearClassNames(element) {
4613        var editable = MediumEditor.util.getContainerEditorElement(element),
4614            existing = Array.prototype.slice.call(editable.parentElement.querySelectorAll('.' + CLASS_DRAG_OVER));
4615
4616        existing.forEach(function (el) {
4617            el.classList.remove(CLASS_DRAG_OVER);
4618        });
4619    }
4620
4621    var FileDragging = MediumEditor.Extension.extend({
4622        name: 'fileDragging',
4623
4624        allowedTypes: ['image'],
4625
4626        init: function () {
4627            MediumEditor.Extension.prototype.init.apply(this, arguments);
4628
4629            this.subscribe('editableDrag', this.handleDrag.bind(this));
4630            this.subscribe('editableDrop', this.handleDrop.bind(this));
4631        },
4632
4633        handleDrag: function (event) {
4634            event.preventDefault();
4635            event.dataTransfer.dropEffect = 'copy';
4636
4637            var target = event.target.classList ? event.target : event.target.parentElement;
4638
4639            // Ensure the class gets removed from anything that had it before
4640            clearClassNames(target);
4641
4642            if (event.type === 'dragover') {
4643                target.classList.add(CLASS_DRAG_OVER);
4644            }
4645        },
4646
4647        handleDrop: function (event) {
4648            // Prevent file from opening in the current window
4649            event.preventDefault();
4650            event.stopPropagation();
4651            // Select the dropping target, and set the selection to the end of the target
4652            // https://github.com/yabwe/medium-editor/issues/980
4653            this.base.selectElement(event.target);
4654            var selection = this.base.exportSelection();
4655            selection.start = selection.end;
4656            this.base.importSelection(selection);
4657            // IE9 does not support the File API, so prevent file from opening in the window
4658            // but also don't try to actually get the file
4659            if (event.dataTransfer.files) {
4660                Array.prototype.slice.call(event.dataTransfer.files).forEach(function (file) {
4661                    if (this.isAllowedFile(file)) {
4662                        if (file.type.match('image')) {
4663                            this.insertImageFile(file);
4664                        }
4665                    }
4666                }, this);
4667            }
4668
4669            // Make sure we remove our class from everything
4670            clearClassNames(event.target);
4671        },
4672
4673        isAllowedFile: function (file) {
4674            return this.allowedTypes.some(function (fileType) {
4675                return !!file.type.match(fileType);
4676            });
4677        },
4678
4679        insertImageFile: function (file) {
4680            if (typeof FileReader !== 'function') {
4681                return;
4682            }
4683            var fileReader = new FileReader();
4684            fileReader.readAsDataURL(file);
4685
4686            // attach the onload event handler, makes it easier to listen in with jasmine
4687            fileReader.addEventListener('load', function (e) {
4688                var addImageElement = this.document.createElement('img');
4689                addImageElement.src = e.target.result;
4690                MediumEditor.util.insertHTMLCommand(this.document, addImageElement.outerHTML);
4691            }.bind(this));
4692        }
4693    });
4694
4695    MediumEditor.extensions.fileDragging = FileDragging;
4696}());
4697
4698(function () {
4699    'use strict';
4700
4701    var KeyboardCommands = MediumEditor.Extension.extend({
4702        name: 'keyboard-commands',
4703
4704        /* KeyboardCommands Options */
4705
4706        /* commands: [Array]
4707         * Array of objects describing each command and the combination of keys that will trigger it
4708         * Required for each object:
4709         *   command [String] (argument passed to editor.execAction())
4710         *   key [String] (keyboard character that triggers this command)
4711         *   meta [boolean] (whether the ctrl/meta key has to be active or inactive)
4712         *   shift [boolean] (whether the shift key has to be active or inactive)
4713         *   alt [boolean] (whether the alt key has to be active or inactive)
4714         */
4715        commands: [
4716            {
4717                command: 'bold',
4718                key: 'B',
4719                meta: true,
4720                shift: false,
4721                alt: false
4722            },
4723            {
4724                command: 'italic',
4725                key: 'I',
4726                meta: true,
4727                shift: false,
4728                alt: false
4729            },
4730            {
4731                command: 'underline',
4732                key: 'U',
4733                meta: true,
4734                shift: false,
4735                alt: false
4736            }
4737        ],
4738
4739        init: function () {
4740            MediumEditor.Extension.prototype.init.apply(this, arguments);
4741
4742            this.subscribe('editableKeydown', this.handleKeydown.bind(this));
4743            this.keys = {};
4744            this.commands.forEach(function (command) {
4745                var keyCode = command.key.charCodeAt(0);
4746                if (!this.keys[keyCode]) {
4747                    this.keys[keyCode] = [];
4748                }
4749                this.keys[keyCode].push(command);
4750            }, this);
4751        },
4752
4753        handleKeydown: function (event) {
4754            var keyCode = MediumEditor.util.getKeyCode(event);
4755            if (!this.keys[keyCode]) {
4756                return;
4757            }
4758
4759            var isMeta = MediumEditor.util.isMetaCtrlKey(event),
4760                isShift = !!event.shiftKey,
4761                isAlt = !!event.altKey;
4762
4763            this.keys[keyCode].forEach(function (data) {
4764                if (data.meta === isMeta &&
4765                    data.shift === isShift &&
4766                    (data.alt === isAlt ||
4767                     undefined === data.alt)) { // TODO deprecated: remove check for undefined === data.alt when jumping to 6.0.0
4768                    event.preventDefault();
4769                    event.stopPropagation();
4770
4771                    // command can be a function to execute
4772                    if (typeof data.command === 'function') {
4773                        data.command.apply(this);
4774                    }
4775                    // command can be false so the shortcut is just disabled
4776                    else if (false !== data.command) {
4777                        this.execAction(data.command);
4778                    }
4779                }
4780            }, this);
4781        }
4782    });
4783
4784    MediumEditor.extensions.keyboardCommands = KeyboardCommands;
4785}());
4786
4787(function () {
4788    'use strict';
4789
4790    var FontNameForm = MediumEditor.extensions.form.extend({
4791
4792        name: 'fontname',
4793        action: 'fontName',
4794        aria: 'change font name',
4795        contentDefault: '&#xB1;', // ±
4796        contentFA: '<i class="fa fa-font"></i>',
4797
4798        fonts: ['', 'Arial', 'Verdana', 'Times New Roman'],
4799
4800        init: function () {
4801            MediumEditor.extensions.form.prototype.init.apply(this, arguments);
4802        },
4803
4804        // Called when the button the toolbar is clicked
4805        // Overrides ButtonExtension.handleClick
4806        handleClick: function (event) {
4807            event.preventDefault();
4808            event.stopPropagation();
4809
4810            if (!this.isDisplayed()) {
4811                // Get FontName of current selection (convert to string since IE returns this as number)
4812                var fontName = this.document.queryCommandValue('fontName') + '';
4813                this.showForm(fontName);
4814            }
4815
4816            return false;
4817        },
4818
4819        // Called by medium-editor to append form to the toolbar
4820        getForm: function () {
4821            if (!this.form) {
4822                this.form = this.createForm();
4823            }
4824            return this.form;
4825        },
4826
4827        // Used by medium-editor when the default toolbar is to be displayed
4828        isDisplayed: function () {
4829            return this.getForm().style.display === 'block';
4830        },
4831
4832        hideForm: function () {
4833            this.getForm().style.display = 'none';
4834            this.getSelect().value = '';
4835        },
4836
4837        showForm: function (fontName) {
4838            var select = this.getSelect();
4839
4840            this.base.saveSelection();
4841            this.hideToolbarDefaultActions();
4842            this.getForm().style.display = 'block';
4843            this.setToolbarPosition();
4844
4845            select.value = fontName || '';
4846            select.focus();
4847        },
4848
4849        // Called by core when tearing down medium-editor (destroy)
4850        destroy: function () {
4851            if (!this.form) {
4852                return false;
4853            }
4854
4855            if (this.form.parentNode) {
4856                this.form.parentNode.removeChild(this.form);
4857            }
4858
4859            delete this.form;
4860        },
4861
4862        // core methods
4863
4864        doFormSave: function () {
4865            this.base.restoreSelection();
4866            this.base.checkSelection();
4867        },
4868
4869        doFormCancel: function () {
4870            this.base.restoreSelection();
4871            this.clearFontName();
4872            this.base.checkSelection();
4873        },
4874
4875        // form creation and event handling
4876        createForm: function () {
4877            var doc = this.document,
4878                form = doc.createElement('div'),
4879                select = doc.createElement('select'),
4880                close = doc.createElement('a'),
4881                save = doc.createElement('a'),
4882                option;
4883
4884            // Font Name Form (div)
4885            form.className = 'medium-editor-toolbar-form';
4886            form.id = 'medium-editor-toolbar-form-fontname-' + this.getEditorId();
4887
4888            // Handle clicks on the form itself
4889            this.on(form, 'click', this.handleFormClick.bind(this));
4890
4891            // Add font names
4892            for (var i = 0; i<this.fonts.length; i++) {
4893                option = doc.createElement('option');
4894                option.innerHTML = this.fonts[i];
4895                option.value = this.fonts[i];
4896                select.appendChild(option);
4897            }
4898
4899            select.className = 'medium-editor-toolbar-select';
4900            form.appendChild(select);
4901
4902            // Handle typing in the textbox
4903            this.on(select, 'change', this.handleFontChange.bind(this));
4904
4905            // Add save buton
4906            save.setAttribute('href', '#');
4907            save.className = 'medium-editor-toobar-save';
4908            save.innerHTML = this.getEditorOption('buttonLabels') === 'fontawesome' ?
4909                             '<i class="fa fa-check"></i>' :
4910                             '&#10003;';
4911            form.appendChild(save);
4912
4913            // Handle save button clicks (capture)
4914            this.on(save, 'click', this.handleSaveClick.bind(this), true);
4915
4916            // Add close button
4917            close.setAttribute('href', '#');
4918            close.className = 'medium-editor-toobar-close';
4919            close.innerHTML = this.getEditorOption('buttonLabels') === 'fontawesome' ?
4920                              '<i class="fa fa-times"></i>' :
4921                              '&times;';
4922            form.appendChild(close);
4923
4924            // Handle close button clicks
4925            this.on(close, 'click', this.handleCloseClick.bind(this));
4926
4927            return form;
4928        },
4929
4930        getSelect: function () {
4931            return this.getForm().querySelector('select.medium-editor-toolbar-select');
4932        },
4933
4934        clearFontName: function () {
4935            MediumEditor.selection.getSelectedElements(this.document).forEach(function (el) {
4936                if (el.nodeName.toLowerCase() === 'font' && el.hasAttribute('face')) {
4937                    el.removeAttribute('face');
4938                }
4939            });
4940        },
4941
4942        handleFontChange: function () {
4943            var font = this.getSelect().value;
4944            if (font === '') {
4945                this.clearFontName();
4946            } else {
4947                this.execAction('fontName', { value: font });
4948            }
4949        },
4950
4951        handleFormClick: function (event) {
4952            // make sure not to hide form when clicking inside the form
4953            event.stopPropagation();
4954        },
4955
4956        handleSaveClick: function (event) {
4957            // Clicking Save -> create the font size
4958            event.preventDefault();
4959            this.doFormSave();
4960        },
4961
4962        handleCloseClick: function (event) {
4963            // Click Close -> close the form
4964            event.preventDefault();
4965            this.doFormCancel();
4966        }
4967    });
4968
4969    MediumEditor.extensions.fontName = FontNameForm;
4970}());
4971
4972(function () {
4973    'use strict';
4974
4975    var FontSizeForm = MediumEditor.extensions.form.extend({
4976
4977        name: 'fontsize',
4978        action: 'fontSize',
4979        aria: 'increase/decrease font size',
4980        contentDefault: '&#xB1;', // ±
4981        contentFA: '<i class="fa fa-text-height"></i>',
4982
4983        init: function () {
4984            MediumEditor.extensions.form.prototype.init.apply(this, arguments);
4985        },
4986
4987        // Called when the button the toolbar is clicked
4988        // Overrides ButtonExtension.handleClick
4989        handleClick: function (event) {
4990            event.preventDefault();
4991            event.stopPropagation();
4992
4993            if (!this.isDisplayed()) {
4994                // Get fontsize of current selection (convert to string since IE returns this as number)
4995                var fontSize = this.document.queryCommandValue('fontSize') + '';
4996                this.showForm(fontSize);
4997            }
4998
4999            return false;
5000        },
5001
5002        // Called by medium-editor to append form to the toolbar
5003        getForm: function () {
5004            if (!this.form) {
5005                this.form = this.createForm();
5006            }
5007            return this.form;
5008        },
5009
5010        // Used by medium-editor when the default toolbar is to be displayed
5011        isDisplayed: function () {
5012            return this.getForm().style.display === 'block';
5013        },
5014
5015        hideForm: function () {
5016            this.getForm().style.display = 'none';
5017            this.getInput().value = '';
5018        },
5019
5020        showForm: function (fontSize) {
5021            var input = this.getInput();
5022
5023            this.base.saveSelection();
5024            this.hideToolbarDefaultActions();
5025            this.getForm().style.display = 'block';
5026            this.setToolbarPosition();
5027
5028            input.value = fontSize || '';
5029            input.focus();
5030        },
5031
5032        // Called by core when tearing down medium-editor (destroy)
5033        destroy: function () {
5034            if (!this.form) {
5035                return false;
5036            }
5037
5038            if (this.form.parentNode) {
5039                this.form.parentNode.removeChild(this.form);
5040            }
5041
5042            delete this.form;
5043        },
5044
5045        // core methods
5046
5047        doFormSave: function () {
5048            this.base.restoreSelection();
5049            this.base.checkSelection();
5050        },
5051
5052        doFormCancel: function () {
5053            this.base.restoreSelection();
5054            this.clearFontSize();
5055            this.base.checkSelection();
5056        },
5057
5058        // form creation and event handling
5059        createForm: function () {
5060            var doc = this.document,
5061                form = doc.createElement('div'),
5062                input = doc.createElement('input'),
5063                close = doc.createElement('a'),
5064                save = doc.createElement('a');
5065
5066            // Font Size Form (div)
5067            form.className = 'medium-editor-toolbar-form';
5068            form.id = 'medium-editor-toolbar-form-fontsize-' + this.getEditorId();
5069
5070            // Handle clicks on the form itself
5071            this.on(form, 'click', this.handleFormClick.bind(this));
5072
5073            // Add font size slider
5074            input.setAttribute('type', 'range');
5075            input.setAttribute('min', '1');
5076            input.setAttribute('max', '7');
5077            input.className = 'medium-editor-toolbar-input';
5078            form.appendChild(input);
5079
5080            // Handle typing in the textbox
5081            this.on(input, 'change', this.handleSliderChange.bind(this));
5082
5083            // Add save buton
5084            save.setAttribute('href', '#');
5085            save.className = 'medium-editor-toobar-save';
5086            save.innerHTML = this.getEditorOption('buttonLabels') === 'fontawesome' ?
5087                             '<i class="fa fa-check"></i>' :
5088                             '&#10003;';
5089            form.appendChild(save);
5090
5091            // Handle save button clicks (capture)
5092            this.on(save, 'click', this.handleSaveClick.bind(this), true);
5093
5094            // Add close button
5095            close.setAttribute('href', '#');
5096            close.className = 'medium-editor-toobar-close';
5097            close.innerHTML = this.getEditorOption('buttonLabels') === 'fontawesome' ?
5098                              '<i class="fa fa-times"></i>' :
5099                              '&times;';
5100            form.appendChild(close);
5101
5102            // Handle close button clicks
5103            this.on(close, 'click', this.handleCloseClick.bind(this));
5104
5105            return form;
5106        },
5107
5108        getInput: function () {
5109            return this.getForm().querySelector('input.medium-editor-toolbar-input');
5110        },
5111
5112        clearFontSize: function () {
5113            MediumEditor.selection.getSelectedElements(this.document).forEach(function (el) {
5114                if (el.nodeName.toLowerCase() === 'font' && el.hasAttribute('size')) {
5115                    el.removeAttribute('size');
5116                }
5117            });
5118        },
5119
5120        handleSliderChange: function () {
5121            var size = this.getInput().value;
5122            if (size === '4') {
5123                this.clearFontSize();
5124            } else {
5125                this.execAction('fontSize', { value: size });
5126            }
5127        },
5128
5129        handleFormClick: function (event) {
5130            // make sure not to hide form when clicking inside the form
5131            event.stopPropagation();
5132        },
5133
5134        handleSaveClick: function (event) {
5135            // Clicking Save -> create the font size
5136            event.preventDefault();
5137            this.doFormSave();
5138        },
5139
5140        handleCloseClick: function (event) {
5141            // Click Close -> close the form
5142            event.preventDefault();
5143            this.doFormCancel();
5144        }
5145    });
5146
5147    MediumEditor.extensions.fontSize = FontSizeForm;
5148}());
5149(function () {
5150    'use strict';
5151
5152    /* Helpers and internal variables that don't need to be members of actual paste handler */
5153
5154    var pasteBinDefaultContent = '%ME_PASTEBIN%',
5155        lastRange = null,
5156        keyboardPasteEditable = null,
5157        stopProp = function (event) {
5158            event.stopPropagation();
5159        };
5160
5161    /*jslint regexp: true*/
5162    /*
5163        jslint does not allow character negation, because the negation
5164        will not match any unicode characters. In the regexes in this
5165        block, negation is used specifically to match the end of an html
5166        tag, and in fact unicode characters *should* be allowed.
5167    */
5168    function createReplacements() {
5169        return [
5170            // Remove anything but the contents within the BODY element
5171            [new RegExp(/^[\s\S]*<body[^>]*>\s*|\s*<\/body[^>]*>[\s\S]*$/g), ''],
5172
5173            // cleanup comments added by Chrome when pasting html
5174            [new RegExp(/<!--StartFragment-->|<!--EndFragment-->/g), ''],
5175
5176            // Trailing BR elements
5177            [new RegExp(/<br>$/i), ''],
5178
5179            // replace two bogus tags that begin pastes from google docs
5180            [new RegExp(/<[^>]*docs-internal-guid[^>]*>/gi), ''],
5181            [new RegExp(/<\/b>(<br[^>]*>)?$/gi), ''],
5182
5183             // un-html spaces and newlines inserted by OS X
5184            [new RegExp(/<span class="Apple-converted-space">\s+<\/span>/g), ' '],
5185            [new RegExp(/<br class="Apple-interchange-newline">/g), '<br>'],
5186
5187            // replace google docs italics+bold with a span to be replaced once the html is inserted
5188            [new RegExp(/<span[^>]*(font-style:italic;font-weight:(bold|700)|font-weight:(bold|700);font-style:italic)[^>]*>/gi), '<span class="replace-with italic bold">'],
5189
5190            // replace google docs italics with a span to be replaced once the html is inserted
5191            [new RegExp(/<span[^>]*font-style:italic[^>]*>/gi), '<span class="replace-with italic">'],
5192
5193            //[replace google docs bolds with a span to be replaced once the html is inserted
5194            [new RegExp(/<span[^>]*font-weight:(bold|700)[^>]*>/gi), '<span class="replace-with bold">'],
5195
5196             // replace manually entered b/i/a tags with real ones
5197            [new RegExp(/&lt;(\/?)(i|b|a)&gt;/gi), '<$1$2>'],
5198
5199             // replace manually a tags with real ones, converting smart-quotes from google docs
5200            [new RegExp(/&lt;a(?:(?!href).)+href=(?:&quot;|&rdquo;|&ldquo;|"|“|”)(((?!&quot;|&rdquo;|&ldquo;|"|“|”).)*)(?:&quot;|&rdquo;|&ldquo;|"|“|”)(?:(?!&gt;).)*&gt;/gi), '<a href="$1">'],
5201
5202            // Newlines between paragraphs in html have no syntactic value,
5203            // but then have a tendency to accidentally become additional paragraphs down the line
5204            [new RegExp(/<\/p>\n+/gi), '</p>'],
5205            [new RegExp(/\n+<p/gi), '<p'],
5206
5207            // Microsoft Word makes these odd tags, like <o:p></o:p>
5208            [new RegExp(/<\/?o:[a-z]*>/gi), ''],
5209
5210            // Microsoft Word adds some special elements around list items
5211            [new RegExp(/<!\[if !supportLists\]>(((?!<!).)*)<!\[endif]\>/gi), '$1']
5212        ];
5213    }
5214    /*jslint regexp: false*/
5215
5216    /**
5217     * Gets various content types out of the Clipboard API. It will also get the
5218     * plain text using older IE and WebKit API.
5219     *
5220     * @param {event} event Event fired on paste.
5221     * @param {win} reference to window
5222     * @param {doc} reference to document
5223     * @return {Object} Object with mime types and data for those mime types.
5224     */
5225    function getClipboardContent(event, win, doc) {
5226        var dataTransfer = event.clipboardData || win.clipboardData || doc.dataTransfer,
5227            data = {};
5228
5229        if (!dataTransfer) {
5230            return data;
5231        }
5232
5233        // Use old WebKit/IE API
5234        if (dataTransfer.getData) {
5235            var legacyText = dataTransfer.getData('Text');
5236            if (legacyText && legacyText.length > 0) {
5237                data['text/plain'] = legacyText;
5238            }
5239        }
5240
5241        if (dataTransfer.types) {
5242            for (var i = 0; i < dataTransfer.types.length; i++) {
5243                var contentType = dataTransfer.types[i];
5244                data[contentType] = dataTransfer.getData(contentType);
5245            }
5246        }
5247
5248        return data;
5249    }
5250
5251    var PasteHandler = MediumEditor.Extension.extend({
5252        /* Paste Options */
5253
5254        /* forcePlainText: [boolean]
5255         * Forces pasting as plain text.
5256         */
5257        forcePlainText: true,
5258
5259        /* cleanPastedHTML: [boolean]
5260         * cleans pasted content from different sources, like google docs etc.
5261         */
5262        cleanPastedHTML: false,
5263
5264        /* preCleanReplacements: [Array]
5265         * custom pairs (2 element arrays) of RegExp and replacement text to use during past when
5266         * __forcePlainText__ or __cleanPastedHTML__ are `true` OR when calling `cleanPaste(text)` helper method.
5267         * These replacements are executed before any medium editor defined replacements.
5268         */
5269        preCleanReplacements: [],
5270
5271        /* cleanReplacements: [Array]
5272         * custom pairs (2 element arrays) of RegExp and replacement text to use during paste when
5273         * __forcePlainText__ or __cleanPastedHTML__ are `true` OR when calling `cleanPaste(text)` helper method.
5274         * These replacements are executed after any medium editor defined replacements.
5275         */
5276        cleanReplacements: [],
5277
5278        /* cleanAttrs:: [Array]
5279         * list of element attributes to remove during paste when __cleanPastedHTML__ is `true` or when
5280         * calling `cleanPaste(text)` or `pasteHTML(html, options)` helper methods.
5281         */
5282        cleanAttrs: ['class', 'style', 'dir'],
5283
5284        /* cleanTags: [Array]
5285         * list of element tag names to remove during paste when __cleanPastedHTML__ is `true` or when
5286         * calling `cleanPaste(text)` or `pasteHTML(html, options)` helper methods.
5287         */
5288        cleanTags: ['meta'],
5289
5290        /* unwrapTags: [Array]
5291         * list of element tag names to unwrap (remove the element tag but retain its child elements)
5292         * during paste when __cleanPastedHTML__ is `true` or when
5293         * calling `cleanPaste(text)` or `pasteHTML(html, options)` helper methods.
5294         */
5295        unwrapTags: [],
5296
5297        init: function () {
5298            MediumEditor.Extension.prototype.init.apply(this, arguments);
5299
5300            if (this.forcePlainText || this.cleanPastedHTML) {
5301                this.subscribe('editableKeydown', this.handleKeydown.bind(this));
5302                // We need access to the full event data in paste
5303                // so we can't use the editablePaste event here
5304                this.getEditorElements().forEach(function (element) {
5305                    this.on(element, 'paste', this.handlePaste.bind(this));
5306                }, this);
5307                this.subscribe('addElement', this.handleAddElement.bind(this));
5308            }
5309        },
5310
5311        handleAddElement: function (event, editable) {
5312            this.on(editable, 'paste', this.handlePaste.bind(this));
5313        },
5314
5315        destroy: function () {
5316            // Make sure pastebin is destroyed in case it's still around for some reason
5317            if (this.forcePlainText || this.cleanPastedHTML) {
5318                this.removePasteBin();
5319            }
5320        },
5321
5322        handlePaste: function (event, editable) {
5323            if (event.defaultPrevented) {
5324                return;
5325            }
5326
5327            var clipboardContent = getClipboardContent(event, this.window, this.document),
5328                pastedHTML = clipboardContent['text/html'],
5329                pastedPlain = clipboardContent['text/plain'];
5330
5331            if (this.window.clipboardData && event.clipboardData === undefined && !pastedHTML) {
5332                // If window.clipboardData exists, but event.clipboardData doesn't exist,
5333                // we're probably in IE. IE only has two possibilities for clipboard
5334                // data format: 'Text' and 'URL'.
5335                //
5336                // For IE, we'll fallback to 'Text' for text/html
5337                pastedHTML = pastedPlain;
5338            }
5339
5340            if (pastedHTML || pastedPlain) {
5341                event.preventDefault();
5342
5343                this.doPaste(pastedHTML, pastedPlain, editable);
5344            }
5345        },
5346
5347        doPaste: function (pastedHTML, pastedPlain, editable) {
5348            var paragraphs,
5349                html = '',
5350                p;
5351
5352            if (this.cleanPastedHTML && pastedHTML) {
5353                return this.cleanPaste(pastedHTML);
5354            }
5355
5356            if (!(this.getEditorOption('disableReturn') || (editable && editable.getAttribute('data-disable-return')))) {
5357                paragraphs = pastedPlain.split(/[\r\n]+/g);
5358                // If there are no \r\n in data, don't wrap in <p>
5359                if (paragraphs.length > 1) {
5360                    for (p = 0; p < paragraphs.length; p += 1) {
5361                        if (paragraphs[p] !== '') {
5362                            html += '<p>' + MediumEditor.util.htmlEntities(paragraphs[p]) + '</p>';
5363                        }
5364                    }
5365                } else {
5366                    html = MediumEditor.util.htmlEntities(paragraphs[0]);
5367                }
5368            } else {
5369                html = MediumEditor.util.htmlEntities(pastedPlain);
5370            }
5371            MediumEditor.util.insertHTMLCommand(this.document, html);
5372        },
5373
5374        handlePasteBinPaste: function (event) {
5375            if (event.defaultPrevented) {
5376                this.removePasteBin();
5377                return;
5378            }
5379
5380            var clipboardContent = getClipboardContent(event, this.window, this.document),
5381                pastedHTML = clipboardContent['text/html'],
5382                pastedPlain = clipboardContent['text/plain'],
5383                editable = keyboardPasteEditable;
5384
5385            // If we have valid html already, or we're not in cleanPastedHTML mode
5386            // we can ignore the paste bin and just paste now
5387            if (!this.cleanPastedHTML || pastedHTML) {
5388                event.preventDefault();
5389                this.removePasteBin();
5390                this.doPaste(pastedHTML, pastedPlain, editable);
5391
5392                // The event handling code listens for paste on the editable element
5393                // in order to trigger the editablePaste event.  Since this paste event
5394                // is happening on the pastebin, the event handling code never knows about it
5395                // So, we have to trigger editablePaste manually
5396                this.trigger('editablePaste', { currentTarget: editable, target: editable }, editable);
5397                return;
5398            }
5399
5400            // We need to look at the paste bin, so do a setTimeout to let the paste
5401            // fall through into the paste bin
5402            setTimeout(function () {
5403                // Only look for HTML if we're in cleanPastedHTML mode
5404                if (this.cleanPastedHTML) {
5405                    // If clipboard didn't have HTML, try the paste bin
5406                    pastedHTML = this.getPasteBinHtml();
5407                }
5408
5409                // If we needed the paste bin, we're done with it now, remove it
5410                this.removePasteBin();
5411
5412                // Handle the paste with the html from the paste bin
5413                this.doPaste(pastedHTML, pastedPlain, editable);
5414
5415                // The event handling code listens for paste on the editable element
5416                // in order to trigger the editablePaste event.  Since this paste event
5417                // is happening on the pastebin, the event handling code never knows about it
5418                // So, we have to trigger editablePaste manually
5419                this.trigger('editablePaste', { currentTarget: editable, target: editable }, editable);
5420            }.bind(this), 0);
5421        },
5422
5423        handleKeydown: function (event, editable) {
5424            // if it's not Ctrl+V, do nothing
5425            if (!(MediumEditor.util.isKey(event, MediumEditor.util.keyCode.V) && MediumEditor.util.isMetaCtrlKey(event))) {
5426                return;
5427            }
5428
5429            event.stopImmediatePropagation();
5430
5431            this.removePasteBin();
5432            this.createPasteBin(editable);
5433        },
5434
5435        createPasteBin: function (editable) {
5436            var rects,
5437                range = MediumEditor.selection.getSelectionRange(this.document),
5438                top = this.window.pageYOffset;
5439
5440            keyboardPasteEditable = editable;
5441
5442            if (range) {
5443                rects = range.getClientRects();
5444
5445                // on empty line, rects is empty so we grab information from the first container of the range
5446                if (rects.length) {
5447                    top += rects[0].top;
5448                } else {
5449                    top += range.startContainer.getBoundingClientRect().top;
5450                }
5451            }
5452
5453            lastRange = range;
5454
5455            var pasteBinElm = this.document.createElement('div');
5456            pasteBinElm.id = this.pasteBinId = 'medium-editor-pastebin-' + (+Date.now());
5457            pasteBinElm.setAttribute('style', 'border: 1px red solid; position: absolute; top: ' + top + 'px; width: 10px; height: 10px; overflow: hidden; opacity: 0');
5458            pasteBinElm.setAttribute('contentEditable', true);
5459            pasteBinElm.innerHTML = pasteBinDefaultContent;
5460
5461            this.document.body.appendChild(pasteBinElm);
5462
5463            // avoid .focus() to stop other event (actually the paste event)
5464            this.on(pasteBinElm, 'focus', stopProp);
5465            this.on(pasteBinElm, 'focusin', stopProp);
5466            this.on(pasteBinElm, 'focusout', stopProp);
5467
5468            pasteBinElm.focus();
5469
5470            MediumEditor.selection.selectNode(pasteBinElm, this.document);
5471
5472            if (!this.boundHandlePaste) {
5473                this.boundHandlePaste = this.handlePasteBinPaste.bind(this);
5474            }
5475
5476            this.on(pasteBinElm, 'paste', this.boundHandlePaste);
5477        },
5478
5479        removePasteBin: function () {
5480            if (null !== lastRange) {
5481                MediumEditor.selection.selectRange(this.document, lastRange);
5482                lastRange = null;
5483            }
5484
5485            if (null !== keyboardPasteEditable) {
5486                keyboardPasteEditable = null;
5487            }
5488
5489            var pasteBinElm = this.getPasteBin();
5490            if (!pasteBinElm) {
5491                return;
5492            }
5493
5494            if (pasteBinElm) {
5495                this.off(pasteBinElm, 'focus', stopProp);
5496                this.off(pasteBinElm, 'focusin', stopProp);
5497                this.off(pasteBinElm, 'focusout', stopProp);
5498                this.off(pasteBinElm, 'paste', this.boundHandlePaste);
5499                pasteBinElm.parentElement.removeChild(pasteBinElm);
5500            }
5501        },
5502
5503        getPasteBin: function () {
5504            return this.document.getElementById(this.pasteBinId);
5505        },
5506
5507        getPasteBinHtml: function () {
5508            var pasteBinElm = this.getPasteBin();
5509
5510            if (!pasteBinElm) {
5511                return false;
5512            }
5513
5514            // WebKit has a nice bug where it clones the paste bin if you paste from for example notepad
5515            // so we need to force plain text mode in this case
5516            if (pasteBinElm.firstChild && pasteBinElm.firstChild.id === 'mcepastebin') {
5517                return false;
5518            }
5519
5520            var pasteBinHtml = pasteBinElm.innerHTML;
5521
5522            // If paste bin is empty try using plain text mode
5523            // since that is better than nothing right
5524            if (!pasteBinHtml || pasteBinHtml === pasteBinDefaultContent) {
5525                return false;
5526            }
5527
5528            return pasteBinHtml;
5529        },
5530
5531        cleanPaste: function (text) {
5532            var i, elList, tmp, workEl,
5533                multiline = /<p|<br|<div/.test(text),
5534                replacements = [].concat(
5535                    this.preCleanReplacements || [],
5536                    createReplacements(),
5537                    this.cleanReplacements || []);
5538
5539            for (i = 0; i < replacements.length; i += 1) {
5540                text = text.replace(replacements[i][0], replacements[i][1]);
5541            }
5542
5543            if (!multiline) {
5544                return this.pasteHTML(text);
5545            }
5546
5547            // create a temporary div to cleanup block elements
5548            tmp = this.document.createElement('div');
5549
5550            // double br's aren't converted to p tags, but we want paragraphs.
5551            tmp.innerHTML = '<p>' + text.split('<br><br>').join('</p><p>') + '</p>';
5552
5553            // block element cleanup
5554            elList = tmp.querySelectorAll('a,p,div,br');
5555            for (i = 0; i < elList.length; i += 1) {
5556                workEl = elList[i];
5557
5558                // Microsoft Word replaces some spaces with newlines.
5559                // While newlines between block elements are meaningless, newlines within
5560                // elements are sometimes actually spaces.
5561                workEl.innerHTML = workEl.innerHTML.replace(/\n/gi, ' ');
5562
5563                switch (workEl.nodeName.toLowerCase()) {
5564                    case 'p':
5565                    case 'div':
5566                        this.filterCommonBlocks(workEl);
5567                        break;
5568                    case 'br':
5569                        this.filterLineBreak(workEl);
5570                        break;
5571                }
5572            }
5573
5574            this.pasteHTML(tmp.innerHTML);
5575        },
5576
5577        pasteHTML: function (html, options) {
5578            options = MediumEditor.util.defaults({}, options, {
5579                cleanAttrs: this.cleanAttrs,
5580                cleanTags: this.cleanTags,
5581                unwrapTags: this.unwrapTags
5582            });
5583
5584            var elList, workEl, i, fragmentBody, pasteBlock = this.document.createDocumentFragment();
5585
5586            pasteBlock.appendChild(this.document.createElement('body'));
5587
5588            fragmentBody = pasteBlock.querySelector('body');
5589            fragmentBody.innerHTML = html;
5590
5591            this.cleanupSpans(fragmentBody);
5592
5593            elList = fragmentBody.querySelectorAll('*');
5594            for (i = 0; i < elList.length; i += 1) {
5595                workEl = elList[i];
5596
5597                if ('a' === workEl.nodeName.toLowerCase() && this.getEditorOption('targetBlank')) {
5598                    MediumEditor.util.setTargetBlank(workEl);
5599                }
5600
5601                MediumEditor.util.cleanupAttrs(workEl, options.cleanAttrs);
5602                MediumEditor.util.cleanupTags(workEl, options.cleanTags);
5603                MediumEditor.util.unwrapTags(workEl, options.unwrapTags);
5604            }
5605
5606            MediumEditor.util.insertHTMLCommand(this.document, fragmentBody.innerHTML.replace(/&nbsp;/g, ' '));
5607        },
5608
5609        // TODO (6.0): Make this an internal helper instead of member of paste handler
5610        isCommonBlock: function (el) {
5611            return (el && (el.nodeName.toLowerCase() === 'p' || el.nodeName.toLowerCase() === 'div'));
5612        },
5613
5614        // TODO (6.0): Make this an internal helper instead of member of paste handler
5615        filterCommonBlocks: function (el) {
5616            if (/^\s*$/.test(el.textContent) && el.parentNode) {
5617                el.parentNode.removeChild(el);
5618            }
5619        },
5620
5621        // TODO (6.0): Make this an internal helper instead of member of paste handler
5622        filterLineBreak: function (el) {
5623            if (this.isCommonBlock(el.previousElementSibling)) {
5624                // remove stray br's following common block elements
5625                this.removeWithParent(el);
5626            } else if (this.isCommonBlock(el.parentNode) && (el.parentNode.firstChild === el || el.parentNode.lastChild === el)) {
5627                // remove br's just inside open or close tags of a div/p
5628                this.removeWithParent(el);
5629            } else if (el.parentNode && el.parentNode.childElementCount === 1 && el.parentNode.textContent === '') {
5630                // and br's that are the only child of elements other than div/p
5631                this.removeWithParent(el);
5632            }
5633        },
5634
5635        // TODO (6.0): Make this an internal helper instead of member of paste handler
5636        // remove an element, including its parent, if it is the only element within its parent
5637        removeWithParent: function (el) {
5638            if (el && el.parentNode) {
5639                if (el.parentNode.parentNode && el.parentNode.childElementCount === 1) {
5640                    el.parentNode.parentNode.removeChild(el.parentNode);
5641                } else {
5642                    el.parentNode.removeChild(el);
5643                }
5644            }
5645        },
5646
5647        // TODO (6.0): Make this an internal helper instead of member of paste handler
5648        cleanupSpans: function (containerEl) {
5649            var i,
5650                el,
5651                newEl,
5652                spans = containerEl.querySelectorAll('.replace-with'),
5653                isCEF = function (el) {
5654                    return (el && el.nodeName !== '#text' && el.getAttribute('contenteditable') === 'false');
5655                };
5656
5657            for (i = 0; i < spans.length; i += 1) {
5658                el = spans[i];
5659                newEl = this.document.createElement(el.classList.contains('bold') ? 'b' : 'i');
5660
5661                if (el.classList.contains('bold') && el.classList.contains('italic')) {
5662                    // add an i tag as well if this has both italics and bold
5663                    newEl.innerHTML = '<i>' + el.innerHTML + '</i>';
5664                } else {
5665                    newEl.innerHTML = el.innerHTML;
5666                }
5667                el.parentNode.replaceChild(newEl, el);
5668            }
5669
5670            spans = containerEl.querySelectorAll('span');
5671            for (i = 0; i < spans.length; i += 1) {
5672                el = spans[i];
5673
5674                // bail if span is in contenteditable = false
5675                if (MediumEditor.util.traverseUp(el, isCEF)) {
5676                    return false;
5677                }
5678
5679                // remove empty spans, replace others with their contents
5680                MediumEditor.util.unwrap(el, this.document);
5681            }
5682        }
5683    });
5684
5685    MediumEditor.extensions.paste = PasteHandler;
5686}());
5687
5688(function () {
5689    'use strict';
5690
5691    var Placeholder = MediumEditor.Extension.extend({
5692        name: 'placeholder',
5693
5694        /* Placeholder Options */
5695
5696        /* text: [string]
5697         * Text to display in the placeholder
5698         */
5699        text: 'Type your text',
5700
5701        /* hideOnClick: [boolean]
5702         * Should we hide the placeholder on click (true) or when user starts typing (false)
5703         */
5704        hideOnClick: true,
5705
5706        init: function () {
5707            MediumEditor.Extension.prototype.init.apply(this, arguments);
5708
5709            this.initPlaceholders();
5710            this.attachEventHandlers();
5711        },
5712
5713        initPlaceholders: function () {
5714            this.getEditorElements().forEach(this.initElement, this);
5715        },
5716
5717        handleAddElement: function (event, editable) {
5718            this.initElement(editable);
5719        },
5720
5721        initElement: function (el) {
5722            if (!el.getAttribute('data-placeholder')) {
5723                el.setAttribute('data-placeholder', this.text);
5724            }
5725            this.updatePlaceholder(el);
5726        },
5727
5728        destroy: function () {
5729            this.getEditorElements().forEach(this.cleanupElement, this);
5730        },
5731
5732        handleRemoveElement: function (event, editable) {
5733            this.cleanupElement(editable);
5734        },
5735
5736        cleanupElement: function (el) {
5737            if (el.getAttribute('data-placeholder') === this.text) {
5738                el.removeAttribute('data-placeholder');
5739            }
5740        },
5741
5742        showPlaceholder: function (el) {
5743            if (el) {
5744                // https://github.com/yabwe/medium-editor/issues/234
5745                // In firefox, styling the placeholder with an absolutely positioned
5746                // pseudo element causes the cursor to appear in a bad location
5747                // when the element is completely empty, so apply a different class to
5748                // style it with a relatively positioned pseudo element
5749                if (MediumEditor.util.isFF && el.childNodes.length === 0) {
5750                    el.classList.add('medium-editor-placeholder-relative');
5751                    el.classList.remove('medium-editor-placeholder');
5752                } else {
5753                    el.classList.add('medium-editor-placeholder');
5754                    el.classList.remove('medium-editor-placeholder-relative');
5755                }
5756            }
5757        },
5758
5759        hidePlaceholder: function (el) {
5760            if (el) {
5761                el.classList.remove('medium-editor-placeholder');
5762                el.classList.remove('medium-editor-placeholder-relative');
5763            }
5764        },
5765
5766        updatePlaceholder: function (el, dontShow) {
5767            // If the element has content, hide the placeholder
5768            if (el.querySelector('img, blockquote, ul, ol, table') || (el.textContent.replace(/^\s+|\s+$/g, '') !== '')) {
5769                return this.hidePlaceholder(el);
5770            }
5771
5772            if (!dontShow) {
5773                this.showPlaceholder(el);
5774            }
5775        },
5776
5777        attachEventHandlers: function () {
5778            if (this.hideOnClick) {
5779                // For the 'hideOnClick' option, the placeholder should always be hidden on focus
5780                this.subscribe('focus', this.handleFocus.bind(this));
5781            }
5782
5783            // If the editor has content, it should always hide the placeholder
5784            this.subscribe('editableInput', this.handleInput.bind(this));
5785
5786            // When the editor loses focus, check if the placeholder should be visible
5787            this.subscribe('blur', this.handleBlur.bind(this));
5788
5789            // Need to know when elements are added/removed from the editor
5790            this.subscribe('addElement', this.handleAddElement.bind(this));
5791            this.subscribe('removeElement', this.handleRemoveElement.bind(this));
5792        },
5793
5794        handleInput: function (event, element) {
5795            // If the placeholder should be hidden on focus and the
5796            // element has focus, don't show the placeholder
5797            var dontShow = this.hideOnClick && (element === this.base.getFocusedElement());
5798
5799            // Editor's content has changed, check if the placeholder should be hidden
5800            this.updatePlaceholder(element, dontShow);
5801        },
5802
5803        handleFocus: function (event, element) {
5804            // Editor has focus, hide the placeholder
5805            this.hidePlaceholder(element);
5806        },
5807
5808        handleBlur: function (event, element) {
5809            // Editor has lost focus, check if the placeholder should be shown
5810            this.updatePlaceholder(element);
5811        }
5812    });
5813
5814    MediumEditor.extensions.placeholder = Placeholder;
5815}());
5816
5817(function () {
5818    'use strict';
5819
5820    var Toolbar = MediumEditor.Extension.extend({
5821        name: 'toolbar',
5822
5823        /* Toolbar Options */
5824
5825        /* align: ['left'|'center'|'right']
5826         * When the __static__ option is true, this aligns the static toolbar
5827         * relative to the medium-editor element.
5828         */
5829        align: 'center',
5830
5831        /* allowMultiParagraphSelection: [boolean]
5832         * enables/disables whether the toolbar should be displayed when
5833         * selecting multiple paragraphs/block elements
5834         */
5835        allowMultiParagraphSelection: true,
5836
5837        /* buttons: [Array]
5838         * the names of the set of buttons to display on the toolbar.
5839         */
5840        buttons: ['bold', 'italic', 'underline', 'anchor', 'h2', 'h3', 'quote'],
5841
5842        /* diffLeft: [Number]
5843         * value in pixels to be added to the X axis positioning of the toolbar.
5844         */
5845        diffLeft: 0,
5846
5847        /* diffTop: [Number]
5848         * value in pixels to be added to the Y axis positioning of the toolbar.
5849         */
5850        diffTop: -10,
5851
5852        /* firstButtonClass: [string]
5853         * CSS class added to the first button in the toolbar.
5854         */
5855        firstButtonClass: 'medium-editor-button-first',
5856
5857        /* lastButtonClass: [string]
5858         * CSS class added to the last button in the toolbar.
5859         */
5860        lastButtonClass: 'medium-editor-button-last',
5861
5862        /* standardizeSelectionStart: [boolean]
5863         * enables/disables standardizing how the beginning of a range is decided
5864         * between browsers whenever the selected text is analyzed for updating toolbar buttons status.
5865         */
5866        standardizeSelectionStart: false,
5867
5868        /* static: [boolean]
5869         * enable/disable the toolbar always displaying in the same location
5870         * relative to the medium-editor element.
5871         */
5872        static: false,
5873
5874        /* sticky: [boolean]
5875         * When the __static__ option is true, this enables/disables the toolbar
5876         * "sticking" to the viewport and staying visible on the screen while
5877         * the page scrolls.
5878         */
5879        sticky: false,
5880
5881        /* stickyTopOffset: [Number]
5882         * Value in pixel of the top offset above the toolbar
5883         */
5884        stickyTopOffset: 0,
5885
5886        /* updateOnEmptySelection: [boolean]
5887         * When the __static__ option is true, this enables/disables updating
5888         * the state of the toolbar buttons even when the selection is collapsed
5889         * (there is no selection, just a cursor).
5890         */
5891        updateOnEmptySelection: false,
5892
5893        /* relativeContainer: [node]
5894         * appending the toolbar to a given node instead of body
5895         */
5896        relativeContainer: null,
5897
5898        init: function () {
5899            MediumEditor.Extension.prototype.init.apply(this, arguments);
5900
5901            this.initThrottledMethods();
5902
5903            if (!this.relativeContainer) {
5904                this.getEditorOption('elementsContainer').appendChild(this.getToolbarElement());
5905            } else {
5906                this.relativeContainer.appendChild(this.getToolbarElement());
5907            }
5908        },
5909
5910        // Helper method to execute method for every extension, but ignoring the toolbar extension
5911        forEachExtension: function (iterator, context) {
5912            return this.base.extensions.forEach(function (command) {
5913                if (command === this) {
5914                    return;
5915                }
5916                return iterator.apply(context || this, arguments);
5917            }, this);
5918        },
5919
5920        // Toolbar creation/deletion
5921
5922        createToolbar: function () {
5923            var toolbar = this.document.createElement('div');
5924
5925            toolbar.id = 'medium-editor-toolbar-' + this.getEditorId();
5926            toolbar.className = 'medium-editor-toolbar';
5927
5928            if (this.static) {
5929                toolbar.className += ' static-toolbar';
5930            } else if (this.relativeContainer) {
5931                toolbar.className += ' medium-editor-relative-toolbar';
5932            } else {
5933                toolbar.className += ' medium-editor-stalker-toolbar';
5934            }
5935
5936            toolbar.appendChild(this.createToolbarButtons());
5937
5938            // Add any forms that extensions may have
5939            this.forEachExtension(function (extension) {
5940                if (extension.hasForm) {
5941                    toolbar.appendChild(extension.getForm());
5942                }
5943            });
5944
5945            this.attachEventHandlers();
5946
5947            return toolbar;
5948        },
5949
5950        createToolbarButtons: function () {
5951            var ul = this.document.createElement('ul'),
5952                li,
5953                btn,
5954                buttons,
5955                extension,
5956                buttonName,
5957                buttonOpts;
5958
5959            ul.id = 'medium-editor-toolbar-actions' + this.getEditorId();
5960            ul.className = 'medium-editor-toolbar-actions';
5961            ul.style.display = 'block';
5962
5963            this.buttons.forEach(function (button) {
5964                if (typeof button === 'string') {
5965                    buttonName = button;
5966                    buttonOpts = null;
5967                } else {
5968                    buttonName = button.name;
5969                    buttonOpts = button;
5970                }
5971
5972                // If the button already exists as an extension, it'll be returned
5973                // othwerise it'll create the default built-in button
5974                extension = this.base.addBuiltInExtension(buttonName, buttonOpts);
5975
5976                if (extension && typeof extension.getButton === 'function') {
5977                    btn = extension.getButton(this.base);
5978                    li = this.document.createElement('li');
5979                    if (MediumEditor.util.isElement(btn)) {
5980                        li.appendChild(btn);
5981                    } else {
5982                        li.innerHTML = btn;
5983                    }
5984                    ul.appendChild(li);
5985                }
5986            }, this);
5987
5988            buttons = ul.querySelectorAll('button');
5989            if (buttons.length > 0) {
5990                buttons[0].classList.add(this.firstButtonClass);
5991                buttons[buttons.length - 1].classList.add(this.lastButtonClass);
5992            }
5993
5994            return ul;
5995        },
5996
5997        destroy: function () {
5998            if (this.toolbar) {
5999                if (this.toolbar.parentNode) {
6000                    this.toolbar.parentNode.removeChild(this.toolbar);
6001                }
6002                delete this.toolbar;
6003            }
6004        },
6005
6006        // Toolbar accessors
6007
6008        getInteractionElements: function () {
6009            return this.getToolbarElement();
6010        },
6011
6012        getToolbarElement: function () {
6013            if (!this.toolbar) {
6014                this.toolbar = this.createToolbar();
6015            }
6016
6017            return this.toolbar;
6018        },
6019
6020        getToolbarActionsElement: function () {
6021            return this.getToolbarElement().querySelector('.medium-editor-toolbar-actions');
6022        },
6023
6024        // Toolbar event handlers
6025
6026        initThrottledMethods: function () {
6027            // throttledPositionToolbar is throttled because:
6028            // - It will be called when the browser is resizing, which can fire many times very quickly
6029            // - For some event (like resize) a slight lag in UI responsiveness is OK and provides performance benefits
6030            this.throttledPositionToolbar = MediumEditor.util.throttle(function () {
6031                if (this.base.isActive) {
6032                    this.positionToolbarIfShown();
6033                }
6034            }.bind(this));
6035        },
6036
6037        attachEventHandlers: function () {
6038            // MediumEditor custom events for when user beings and ends interaction with a contenteditable and its elements
6039            this.subscribe('blur', this.handleBlur.bind(this));
6040            this.subscribe('focus', this.handleFocus.bind(this));
6041
6042            // Updating the state of the toolbar as things change
6043            this.subscribe('editableClick', this.handleEditableClick.bind(this));
6044            this.subscribe('editableKeyup', this.handleEditableKeyup.bind(this));
6045
6046            // Handle mouseup on document for updating the selection in the toolbar
6047            this.on(this.document.documentElement, 'mouseup', this.handleDocumentMouseup.bind(this));
6048
6049            // Add a scroll event for sticky toolbar
6050            if (this.static && this.sticky) {
6051                // On scroll (capture), re-position the toolbar
6052                this.on(this.window, 'scroll', this.handleWindowScroll.bind(this), true);
6053            }
6054
6055            // On resize, re-position the toolbar
6056            this.on(this.window, 'resize', this.handleWindowResize.bind(this));
6057        },
6058
6059        handleWindowScroll: function () {
6060            this.positionToolbarIfShown();
6061        },
6062
6063        handleWindowResize: function () {
6064            this.throttledPositionToolbar();
6065        },
6066
6067        handleDocumentMouseup: function (event) {
6068            // Do not trigger checkState when mouseup fires over the toolbar
6069            if (event &&
6070                    event.target &&
6071                    MediumEditor.util.isDescendant(this.getToolbarElement(), event.target)) {
6072                return false;
6073            }
6074            this.checkState();
6075        },
6076
6077        handleEditableClick: function () {
6078            // Delay the call to checkState to handle bug where selection is empty
6079            // immediately after clicking inside a pre-existing selection
6080            setTimeout(function () {
6081                this.checkState();
6082            }.bind(this), 0);
6083        },
6084
6085        handleEditableKeyup: function () {
6086            this.checkState();
6087        },
6088
6089        handleBlur: function () {
6090            // Kill any previously delayed calls to hide the toolbar
6091            clearTimeout(this.hideTimeout);
6092
6093            // Blur may fire even if we have a selection, so we want to prevent any delayed showToolbar
6094            // calls from happening in this specific case
6095            clearTimeout(this.delayShowTimeout);
6096
6097            // Delay the call to hideToolbar to handle bug with multiple editors on the page at once
6098            this.hideTimeout = setTimeout(function () {
6099                this.hideToolbar();
6100            }.bind(this), 1);
6101        },
6102
6103        handleFocus: function () {
6104            this.checkState();
6105        },
6106
6107        // Hiding/showing toolbar
6108
6109        isDisplayed: function () {
6110            return this.getToolbarElement().classList.contains('medium-editor-toolbar-active');
6111        },
6112
6113        showToolbar: function () {
6114            clearTimeout(this.hideTimeout);
6115            if (!this.isDisplayed()) {
6116                this.getToolbarElement().classList.add('medium-editor-toolbar-active');
6117                this.trigger('showToolbar', {}, this.base.getFocusedElement());
6118            }
6119        },
6120
6121        hideToolbar: function () {
6122            if (this.isDisplayed()) {
6123                this.getToolbarElement().classList.remove('medium-editor-toolbar-active');
6124                this.trigger('hideToolbar', {}, this.base.getFocusedElement());
6125            }
6126        },
6127
6128        isToolbarDefaultActionsDisplayed: function () {
6129            return this.getToolbarActionsElement().style.display === 'block';
6130        },
6131
6132        hideToolbarDefaultActions: function () {
6133            if (this.isToolbarDefaultActionsDisplayed()) {
6134                this.getToolbarActionsElement().style.display = 'none';
6135            }
6136        },
6137
6138        showToolbarDefaultActions: function () {
6139            this.hideExtensionForms();
6140
6141            if (!this.isToolbarDefaultActionsDisplayed()) {
6142                this.getToolbarActionsElement().style.display = 'block';
6143            }
6144
6145            // Using setTimeout + options.delay because:
6146            // We will actually be displaying the toolbar, which should be controlled by options.delay
6147            this.delayShowTimeout = this.base.delay(function () {
6148                this.showToolbar();
6149            }.bind(this));
6150        },
6151
6152        hideExtensionForms: function () {
6153            // Hide all extension forms
6154            this.forEachExtension(function (extension) {
6155                if (extension.hasForm && extension.isDisplayed()) {
6156                    extension.hideForm();
6157                }
6158            });
6159        },
6160
6161        // Responding to changes in user selection
6162
6163        // Checks for existance of multiple block elements in the current selection
6164        multipleBlockElementsSelected: function () {
6165            var regexEmptyHTMLTags = /<[^\/>][^>]*><\/[^>]+>/gim, // http://stackoverflow.com/questions/3129738/remove-empty-tags-using-regex
6166                regexBlockElements = new RegExp('<(' + MediumEditor.util.blockContainerElementNames.join('|') + ')[^>]*>', 'g'),
6167                selectionHTML = MediumEditor.selection.getSelectionHtml(this.document).replace(regexEmptyHTMLTags, ''), // Filter out empty blocks from selection
6168                hasMultiParagraphs = selectionHTML.match(regexBlockElements); // Find how many block elements are within the html
6169
6170            return !!hasMultiParagraphs && hasMultiParagraphs.length > 1;
6171        },
6172
6173        modifySelection: function () {
6174            var selection = this.window.getSelection(),
6175                selectionRange = selection.getRangeAt(0);
6176
6177            /*
6178            * In firefox, there are cases (ie doubleclick of a word) where the selectionRange start
6179            * will be at the very end of an element.  In other browsers, the selectionRange start
6180            * would instead be at the very beginning of an element that actually has content.
6181            * example:
6182            *   <span>foo</span><span>bar</span>
6183            *
6184            * If the text 'bar' is selected, most browsers will have the selectionRange start at the beginning
6185            * of the 'bar' span.  However, there are cases where firefox will have the selectionRange start
6186            * at the end of the 'foo' span.  The contenteditable behavior will be ok, but if there are any
6187            * properties on the 'bar' span, they won't be reflected accurately in the toolbar
6188            * (ie 'Bold' button wouldn't be active)
6189            *
6190            * So, for cases where the selectionRange start is at the end of an element/node, find the next
6191            * adjacent text node that actually has content in it, and move the selectionRange start there.
6192            */
6193            if (this.standardizeSelectionStart &&
6194                    selectionRange.startContainer.nodeValue &&
6195                    (selectionRange.startOffset === selectionRange.startContainer.nodeValue.length)) {
6196                var adjacentNode = MediumEditor.util.findAdjacentTextNodeWithContent(MediumEditor.selection.getSelectionElement(this.window), selectionRange.startContainer, this.document);
6197                if (adjacentNode) {
6198                    var offset = 0;
6199                    while (adjacentNode.nodeValue.substr(offset, 1).trim().length === 0) {
6200                        offset = offset + 1;
6201                    }
6202                    selectionRange = MediumEditor.selection.select(this.document, adjacentNode, offset,
6203                        selectionRange.endContainer, selectionRange.endOffset);
6204                }
6205            }
6206        },
6207
6208        checkState: function () {
6209            if (this.base.preventSelectionUpdates) {
6210                return;
6211            }
6212
6213            // If no editable has focus OR selection is inside contenteditable = false
6214            // hide toolbar
6215            if (!this.base.getFocusedElement() ||
6216                    MediumEditor.selection.selectionInContentEditableFalse(this.window)) {
6217                return this.hideToolbar();
6218            }
6219
6220            // If there's no selection element, selection element doesn't belong to this editor
6221            // or toolbar is disabled for this selection element
6222            // hide toolbar
6223            var selectionElement = MediumEditor.selection.getSelectionElement(this.window);
6224            if (!selectionElement ||
6225                    this.getEditorElements().indexOf(selectionElement) === -1 ||
6226                    selectionElement.getAttribute('data-disable-toolbar')) {
6227                return this.hideToolbar();
6228            }
6229
6230            // Now we know there's a focused editable with a selection
6231
6232            // If the updateOnEmptySelection option is true, show the toolbar
6233            if (this.updateOnEmptySelection && this.static) {
6234                return this.showAndUpdateToolbar();
6235            }
6236
6237            // If we don't have a 'valid' selection -> hide toolbar
6238            if (!MediumEditor.selection.selectionContainsContent(this.document) ||
6239                (this.allowMultiParagraphSelection === false && this.multipleBlockElementsSelected())) {
6240                return this.hideToolbar();
6241            }
6242
6243            this.showAndUpdateToolbar();
6244        },
6245
6246        // Updating the toolbar
6247
6248        showAndUpdateToolbar: function () {
6249            this.modifySelection();
6250            this.setToolbarButtonStates();
6251            this.trigger('positionToolbar', {}, this.base.getFocusedElement());
6252            this.showToolbarDefaultActions();
6253            this.setToolbarPosition();
6254        },
6255
6256        setToolbarButtonStates: function () {
6257            this.forEachExtension(function (extension) {
6258                if (typeof extension.isActive === 'function' &&
6259                    typeof extension.setInactive === 'function') {
6260                    extension.setInactive();
6261                }
6262            });
6263
6264            this.checkActiveButtons();
6265        },
6266
6267        checkActiveButtons: function () {
6268            var manualStateChecks = [],
6269                queryState = null,
6270                selectionRange = MediumEditor.selection.getSelectionRange(this.document),
6271                parentNode,
6272                updateExtensionState = function (extension) {
6273                    if (typeof extension.checkState === 'function') {
6274                        extension.checkState(parentNode);
6275                    } else if (typeof extension.isActive === 'function' &&
6276                               typeof extension.isAlreadyApplied === 'function' &&
6277                               typeof extension.setActive === 'function') {
6278                        if (!extension.isActive() && extension.isAlreadyApplied(parentNode)) {
6279                            extension.setActive();
6280                        }
6281                    }
6282                };
6283
6284            if (!selectionRange) {
6285                return;
6286            }
6287
6288            // Loop through all extensions
6289            this.forEachExtension(function (extension) {
6290                // For those extensions where we can use document.queryCommandState(), do so
6291                if (typeof extension.queryCommandState === 'function') {
6292                    queryState = extension.queryCommandState();
6293                    // If queryCommandState returns a valid value, we can trust the browser
6294                    // and don't need to do our manual checks
6295                    if (queryState !== null) {
6296                        if (queryState && typeof extension.setActive === 'function') {
6297                            extension.setActive();
6298                        }
6299                        return;
6300                    }
6301                }
6302                // We can't use queryCommandState for this extension, so add to manualStateChecks
6303                manualStateChecks.push(extension);
6304            });
6305
6306            parentNode = MediumEditor.selection.getSelectedParentElement(selectionRange);
6307
6308            // Make sure the selection parent isn't outside of the contenteditable
6309            if (!this.getEditorElements().some(function (element) {
6310                    return MediumEditor.util.isDescendant(element, parentNode, true);
6311                })) {
6312                return;
6313            }
6314
6315            // Climb up the DOM and do manual checks for whether a certain extension is currently enabled for this node
6316            while (parentNode) {
6317                manualStateChecks.forEach(updateExtensionState);
6318
6319                // we can abort the search upwards if we leave the contentEditable element
6320                if (MediumEditor.util.isMediumEditorElement(parentNode)) {
6321                    break;
6322                }
6323                parentNode = parentNode.parentNode;
6324            }
6325        },
6326
6327        // Positioning toolbar
6328
6329        positionToolbarIfShown: function () {
6330            if (this.isDisplayed()) {
6331                this.setToolbarPosition();
6332            }
6333        },
6334
6335        setToolbarPosition: function () {
6336            var container = this.base.getFocusedElement(),
6337                selection = this.window.getSelection();
6338
6339            // If there isn't a valid selection, bail
6340            if (!container) {
6341                return this;
6342            }
6343
6344            if (this.static || !selection.isCollapsed) {
6345                this.showToolbar();
6346
6347                // we don't need any absolute positioning if relativeContainer is set
6348                if (!this.relativeContainer) {
6349                    if (this.static) {
6350                        this.positionStaticToolbar(container);
6351                    } else {
6352                        this.positionToolbar(selection);
6353                    }
6354                }
6355
6356                this.trigger('positionedToolbar', {}, this.base.getFocusedElement());
6357            }
6358        },
6359
6360        positionStaticToolbar: function (container) {
6361            // position the toolbar at left 0, so we can get the real width of the toolbar
6362            this.getToolbarElement().style.left = '0';
6363
6364            // document.documentElement for IE 9
6365            var scrollTop = (this.document.documentElement && this.document.documentElement.scrollTop) || this.document.body.scrollTop,
6366                windowWidth = this.window.innerWidth,
6367                toolbarElement = this.getToolbarElement(),
6368                containerRect = container.getBoundingClientRect(),
6369                containerTop = containerRect.top + scrollTop,
6370                containerCenter = (containerRect.left + (containerRect.width / 2)),
6371                toolbarHeight = toolbarElement.offsetHeight,
6372                toolbarWidth = toolbarElement.offsetWidth,
6373                halfOffsetWidth = toolbarWidth / 2,
6374                targetLeft;
6375
6376            if (this.sticky) {
6377                // If it's beyond the height of the editor, position it at the bottom of the editor
6378                if (scrollTop > (containerTop + container.offsetHeight - toolbarHeight - this.stickyTopOffset)) {
6379                    toolbarElement.style.top = (containerTop + container.offsetHeight - toolbarHeight) + 'px';
6380                    toolbarElement.classList.remove('medium-editor-sticky-toolbar');
6381                // Stick the toolbar to the top of the window
6382                } else if (scrollTop > (containerTop - toolbarHeight - this.stickyTopOffset)) {
6383                    toolbarElement.classList.add('medium-editor-sticky-toolbar');
6384                    toolbarElement.style.top = this.stickyTopOffset + 'px';
6385                // Normal static toolbar position
6386                } else {
6387                    toolbarElement.classList.remove('medium-editor-sticky-toolbar');
6388                    toolbarElement.style.top = containerTop - toolbarHeight + 'px';
6389                }
6390            } else {
6391                toolbarElement.style.top = containerTop - toolbarHeight + 'px';
6392            }
6393
6394            switch (this.align) {
6395                case 'left':
6396                    targetLeft = containerRect.left;
6397                    break;
6398
6399                case 'right':
6400                    targetLeft = containerRect.right - toolbarWidth;
6401                    break;
6402
6403                case 'center':
6404                    targetLeft = containerCenter - halfOffsetWidth;
6405                    break;
6406            }
6407
6408            if (targetLeft < 0) {
6409                targetLeft = 0;
6410            } else if ((targetLeft + toolbarWidth) > windowWidth) {
6411                targetLeft = (windowWidth - Math.ceil(toolbarWidth) - 1);
6412            }
6413
6414            toolbarElement.style.left = targetLeft + 'px';
6415        },
6416
6417        positionToolbar: function (selection) {
6418            // position the toolbar at left 0, so we can get the real width of the toolbar
6419            this.getToolbarElement().style.left = '0';
6420            this.getToolbarElement().style.right = 'initial';
6421
6422            var range = selection.getRangeAt(0),
6423                boundary = range.getBoundingClientRect();
6424
6425            // Handle selections with just images
6426            if (!boundary || ((boundary.height === 0 && boundary.width === 0) && range.startContainer === range.endContainer)) {
6427                // If there's a nested image, use that for the bounding rectangle
6428                if (range.startContainer.nodeType === 1 && range.startContainer.querySelector('img')) {
6429                    boundary = range.startContainer.querySelector('img').getBoundingClientRect();
6430                } else {
6431                    boundary = range.startContainer.getBoundingClientRect();
6432                }
6433            }
6434
6435            var containerWidth = this.window.innerWidth,
6436                toolbarElement = this.getToolbarElement(),
6437                toolbarHeight = toolbarElement.offsetHeight,
6438                toolbarWidth = toolbarElement.offsetWidth,
6439                halfOffsetWidth = toolbarWidth / 2,
6440                buttonHeight = 50,
6441                defaultLeft = this.diffLeft - halfOffsetWidth,
6442                elementsContainer = this.getEditorOption('elementsContainer'),
6443                elementsContainerAbsolute = ['absolute', 'fixed'].indexOf(window.getComputedStyle(elementsContainer).getPropertyValue('position')) > -1,
6444                positions = {},
6445                relativeBoundary = {},
6446                middleBoundary, elementsContainerBoundary;
6447
6448            // If container element is absolute / fixed, recalculate boundaries to be relative to the container
6449            if (elementsContainerAbsolute) {
6450                elementsContainerBoundary = elementsContainer.getBoundingClientRect();
6451                ['top', 'left'].forEach(function (key) {
6452                    relativeBoundary[key] = boundary[key] - elementsContainerBoundary[key];
6453                });
6454
6455                relativeBoundary.width = boundary.width;
6456                relativeBoundary.height = boundary.height;
6457                boundary = relativeBoundary;
6458
6459                containerWidth = elementsContainerBoundary.width;
6460
6461                // Adjust top position according to container scroll position
6462                positions.top = elementsContainer.scrollTop;
6463            } else {
6464                // Adjust top position according to window scroll position
6465                positions.top = this.window.pageYOffset;
6466            }
6467
6468            middleBoundary = boundary.left + boundary.width / 2;
6469            positions.top += boundary.top - toolbarHeight;
6470
6471            if (boundary.top < buttonHeight) {
6472                toolbarElement.classList.add('medium-toolbar-arrow-over');
6473                toolbarElement.classList.remove('medium-toolbar-arrow-under');
6474                positions.top += buttonHeight + boundary.height - this.diffTop;
6475            } else {
6476                toolbarElement.classList.add('medium-toolbar-arrow-under');
6477                toolbarElement.classList.remove('medium-toolbar-arrow-over');
6478                positions.top += this.diffTop;
6479            }
6480
6481            if (middleBoundary < halfOffsetWidth) {
6482                positions.left = defaultLeft + halfOffsetWidth;
6483                positions.right = 'initial';
6484            } else if ((containerWidth - middleBoundary) < halfOffsetWidth) {
6485                positions.left = 'auto';
6486                positions.right = 0;
6487            } else {
6488                positions.left = defaultLeft + middleBoundary;
6489                positions.right = 'initial';
6490            }
6491
6492            ['top', 'left', 'right'].forEach(function (key) {
6493                toolbarElement.style[key] = positions[key] + (isNaN(positions[key]) ? '' : 'px');
6494            });
6495        }
6496    });
6497
6498    MediumEditor.extensions.toolbar = Toolbar;
6499}());
6500
6501(function () {
6502    'use strict';
6503
6504    var ImageDragging = MediumEditor.Extension.extend({
6505        init: function () {
6506            MediumEditor.Extension.prototype.init.apply(this, arguments);
6507
6508            this.subscribe('editableDrag', this.handleDrag.bind(this));
6509            this.subscribe('editableDrop', this.handleDrop.bind(this));
6510        },
6511
6512        handleDrag: function (event) {
6513            var className = 'medium-editor-dragover';
6514            event.preventDefault();
6515            event.dataTransfer.dropEffect = 'copy';
6516
6517            if (event.type === 'dragover') {
6518                event.target.classList.add(className);
6519            } else if (event.type === 'dragleave') {
6520                event.target.classList.remove(className);
6521            }
6522        },
6523
6524        handleDrop: function (event) {
6525            var className = 'medium-editor-dragover',
6526                files;
6527            event.preventDefault();
6528            event.stopPropagation();
6529
6530            // IE9 does not support the File API, so prevent file from opening in a new window
6531            // but also don't try to actually get the file
6532            if (event.dataTransfer.files) {
6533                files = Array.prototype.slice.call(event.dataTransfer.files, 0);
6534                files.some(function (file) {
6535                    if (file.type.match('image')) {
6536                        var fileReader, id;
6537                        fileReader = new FileReader();
6538                        fileReader.readAsDataURL(file);
6539
6540                        id = 'medium-img-' + (+new Date());
6541                        MediumEditor.util.insertHTMLCommand(this.document, '<img class="medium-editor-image-loading" id="' + id + '" />');
6542
6543                        fileReader.onload = function () {
6544                            var img = this.document.getElementById(id);
6545                            if (img) {
6546                                img.removeAttribute('id');
6547                                img.removeAttribute('class');
6548                                img.src = fileReader.result;
6549                            }
6550                        }.bind(this);
6551                    }
6552                }.bind(this));
6553            }
6554            event.target.classList.remove(className);
6555        }
6556    });
6557
6558    MediumEditor.extensions.imageDragging = ImageDragging;
6559}());
6560
6561(function () {
6562    'use strict';
6563
6564    // Event handlers that shouldn't be exposed externally
6565
6566    function handleDisableExtraSpaces(event) {
6567        var node = MediumEditor.selection.getSelectionStart(this.options.ownerDocument),
6568            textContent = node.textContent,
6569            caretPositions = MediumEditor.selection.getCaretOffsets(node);
6570
6571        if ((textContent[caretPositions.left - 1] === undefined) || (textContent[caretPositions.left - 1].trim() === '') || (textContent[caretPositions.left] !== undefined && textContent[caretPositions.left].trim() === '')) {
6572            event.preventDefault();
6573        }
6574    }
6575
6576    function handleDisabledEnterKeydown(event, element) {
6577        if (this.options.disableReturn || element.getAttribute('data-disable-return')) {
6578            event.preventDefault();
6579        } else if (this.options.disableDoubleReturn || element.getAttribute('data-disable-double-return')) {
6580            var node = MediumEditor.selection.getSelectionStart(this.options.ownerDocument);
6581
6582            // if current text selection is empty OR previous sibling text is empty OR it is not a list
6583            if ((node && node.textContent.trim() === '' && node.nodeName.toLowerCase() !== 'li') ||
6584                (node.previousElementSibling && node.previousElementSibling.nodeName.toLowerCase() !== 'br' &&
6585                 node.previousElementSibling.textContent.trim() === '')) {
6586                event.preventDefault();
6587            }
6588        }
6589    }
6590
6591    function handleTabKeydown(event) {
6592        // Override tab only for pre nodes
6593        var node = MediumEditor.selection.getSelectionStart(this.options.ownerDocument),
6594            tag = node && node.nodeName.toLowerCase();
6595
6596        if (tag === 'pre') {
6597            event.preventDefault();
6598            MediumEditor.util.insertHTMLCommand(this.options.ownerDocument, '    ');
6599        }
6600
6601        // Tab to indent list structures!
6602        if (MediumEditor.util.isListItem(node)) {
6603            event.preventDefault();
6604
6605            // If Shift is down, outdent, otherwise indent
6606            if (event.shiftKey) {
6607                this.options.ownerDocument.execCommand('outdent', false, null);
6608            } else {
6609                this.options.ownerDocument.execCommand('indent', false, null);
6610            }
6611        }
6612    }
6613
6614    function handleBlockDeleteKeydowns(event) {
6615        var p, node = MediumEditor.selection.getSelectionStart(this.options.ownerDocument),
6616            tagName = node.nodeName.toLowerCase(),
6617            isEmpty = /^(\s+|<br\/?>)?$/i,
6618            isHeader = /h\d/i;
6619
6620        if (MediumEditor.util.isKey(event, [MediumEditor.util.keyCode.BACKSPACE, MediumEditor.util.keyCode.ENTER]) &&
6621                // has a preceeding sibling
6622                node.previousElementSibling &&
6623                // in a header
6624                isHeader.test(tagName) &&
6625                // at the very end of the block
6626                MediumEditor.selection.getCaretOffsets(node).left === 0) {
6627            if (MediumEditor.util.isKey(event, MediumEditor.util.keyCode.BACKSPACE) && isEmpty.test(node.previousElementSibling.innerHTML)) {
6628                // backspacing the begining of a header into an empty previous element will
6629                // change the tagName of the current node to prevent one
6630                // instead delete previous node and cancel the event.
6631                node.previousElementSibling.parentNode.removeChild(node.previousElementSibling);
6632                event.preventDefault();
6633            } else if (!this.options.disableDoubleReturn && MediumEditor.util.isKey(event, MediumEditor.util.keyCode.ENTER)) {
6634                // hitting return in the begining of a header will create empty header elements before the current one
6635                // instead, make "<p><br></p>" element, which are what happens if you hit return in an empty paragraph
6636                p = this.options.ownerDocument.createElement('p');
6637                p.innerHTML = '<br>';
6638                node.previousElementSibling.parentNode.insertBefore(p, node);
6639                event.preventDefault();
6640            }
6641        } else if (MediumEditor.util.isKey(event, MediumEditor.util.keyCode.DELETE) &&
6642                    // between two sibling elements
6643                    node.nextElementSibling &&
6644                    node.previousElementSibling &&
6645                    // not in a header
6646                    !isHeader.test(tagName) &&
6647                    // in an empty tag
6648                    isEmpty.test(node.innerHTML) &&
6649                    // when the next tag *is* a header
6650                    isHeader.test(node.nextElementSibling.nodeName.toLowerCase())) {
6651            // hitting delete in an empty element preceding a header, ex:
6652            //  <p>[CURSOR]</p><h1>Header</h1>
6653            // Will cause the h1 to become a paragraph.
6654            // Instead, delete the paragraph node and move the cursor to the begining of the h1
6655
6656            // remove node and move cursor to start of header
6657            MediumEditor.selection.moveCursor(this.options.ownerDocument, node.nextElementSibling);
6658
6659            node.previousElementSibling.parentNode.removeChild(node);
6660
6661            event.preventDefault();
6662        } else if (MediumEditor.util.isKey(event, MediumEditor.util.keyCode.BACKSPACE) &&
6663                tagName === 'li' &&
6664                // hitting backspace inside an empty li
6665                isEmpty.test(node.innerHTML) &&
6666                // is first element (no preceeding siblings)
6667                !node.previousElementSibling &&
6668                // parent also does not have a sibling
6669                !node.parentElement.previousElementSibling &&
6670                // is not the only li in a list
6671                node.nextElementSibling &&
6672                node.nextElementSibling.nodeName.toLowerCase() === 'li') {
6673            // backspacing in an empty first list element in the first list (with more elements) ex:
6674            //  <ul><li>[CURSOR]</li><li>List Item 2</li></ul>
6675            // will remove the first <li> but add some extra element before (varies based on browser)
6676            // Instead, this will:
6677            // 1) remove the list element
6678            // 2) create a paragraph before the list
6679            // 3) move the cursor into the paragraph
6680
6681            // create a paragraph before the list
6682            p = this.options.ownerDocument.createElement('p');
6683            p.innerHTML = '<br>';
6684            node.parentElement.parentElement.insertBefore(p, node.parentElement);
6685
6686            // move the cursor into the new paragraph
6687            MediumEditor.selection.moveCursor(this.options.ownerDocument, p);
6688
6689            // remove the list element
6690            node.parentElement.removeChild(node);
6691
6692            event.preventDefault();
6693        } else if (MediumEditor.util.isKey(event, MediumEditor.util.keyCode.BACKSPACE) &&
6694                (MediumEditor.util.getClosestTag(node, 'blockquote') !== false) &&
6695                MediumEditor.selection.getCaretOffsets(node).left === 0) {
6696
6697            // when cursor is at the begining of the element and the element is <blockquote>
6698            // then pressing backspace key should change the <blockquote> to a <p> tag
6699            event.preventDefault();
6700            MediumEditor.util.execFormatBlock(this.options.ownerDocument, 'p');
6701        } else if (MediumEditor.util.isKey(event, MediumEditor.util.keyCode.ENTER) &&
6702                (MediumEditor.util.getClosestTag(node, 'blockquote') !== false) &&
6703                MediumEditor.selection.getCaretOffsets(node).right === 0) {
6704
6705            // when cursor is at the end of <blockquote>,
6706            // then pressing enter key should create <p> tag, not <blockquote>
6707            p = this.options.ownerDocument.createElement('p');
6708            p.innerHTML = '<br>';
6709            node.parentElement.insertBefore(p, node.nextSibling);
6710
6711            // move the cursor into the new paragraph
6712            MediumEditor.selection.moveCursor(this.options.ownerDocument, p);
6713
6714            event.preventDefault();
6715        } else if (MediumEditor.util.isKey(event, MediumEditor.util.keyCode.BACKSPACE) &&
6716                MediumEditor.util.isMediumEditorElement(node.parentElement) &&
6717                !node.previousElementSibling &&
6718                node.nextElementSibling &&
6719                isEmpty.test(node.innerHTML)) {
6720
6721            // when cursor is in the first element, it's empty and user presses backspace,
6722            // do delete action instead to get rid of the first element and move caret to 2nd
6723            event.preventDefault();
6724            MediumEditor.selection.moveCursor(this.options.ownerDocument, node.nextSibling);
6725            node.parentElement.removeChild(node);
6726        }
6727    }
6728
6729    function handleKeyup(event) {
6730        var node = MediumEditor.selection.getSelectionStart(this.options.ownerDocument),
6731            tagName;
6732
6733        if (!node) {
6734            return;
6735        }
6736
6737        // https://github.com/yabwe/medium-editor/issues/994
6738        // Firefox thrown an error when calling `formatBlock` on an empty editable blockContainer that's not a <div>
6739        if (MediumEditor.util.isMediumEditorElement(node) && node.children.length === 0 && !MediumEditor.util.isBlockContainer(node)) {
6740            this.options.ownerDocument.execCommand('formatBlock', false, 'p');
6741        }
6742
6743        // https://github.com/yabwe/medium-editor/issues/834
6744        // https://github.com/yabwe/medium-editor/pull/382
6745        // Don't call format block if this is a block element (ie h1, figCaption, etc.)
6746        if (MediumEditor.util.isKey(event, MediumEditor.util.keyCode.ENTER) &&
6747            !MediumEditor.util.isListItem(node) &&
6748            !MediumEditor.util.isBlockContainer(node)) {
6749
6750            tagName = node.nodeName.toLowerCase();
6751            // For anchor tags, unlink
6752            if (tagName === 'a') {
6753                this.options.ownerDocument.execCommand('unlink', false, null);
6754            } else if (!event.shiftKey && !event.ctrlKey) {
6755                this.options.ownerDocument.execCommand('formatBlock', false, 'p');
6756            }
6757        }
6758
6759        if (MediumEditor.util.isKey(event, MediumEditor.util.keyCode.ENTER) && node.nodeName.toLowerCase() === 'div') {
6760            this.options.ownerDocument.execCommand('formatBlock', false, 'p');
6761        }
6762    }
6763
6764    function handleEditableInput(event, editable) {
6765        var textarea = editable.parentNode.querySelector('textarea[medium-editor-textarea-id="' + editable.getAttribute('medium-editor-textarea-id') + '"]');
6766        if (textarea) {
6767            var index = this.elements.indexOf(editable);
6768            if (index !== -1) {
6769                textarea.value = this.getContent(index);
6770            }
6771        }
6772    }
6773
6774    // Internal helper methods which shouldn't be exposed externally
6775
6776    function addToEditors(win) {
6777        if (!win._mediumEditors) {
6778            // To avoid breaking users who are assuming that the unique id on
6779            // medium-editor elements will start at 1, inserting a 'null' in the
6780            // array so the unique-id can always map to the index of the editor instance
6781            win._mediumEditors = [null];
6782        }
6783
6784        // If this already has a unique id, re-use it
6785        if (!this.id) {
6786            this.id = win._mediumEditors.length;
6787        }
6788
6789        win._mediumEditors[this.id] = this;
6790    }
6791
6792    function removeFromEditors(win) {
6793        if (!win._mediumEditors || !win._mediumEditors[this.id]) {
6794            return;
6795        }
6796
6797        /* Setting the instance to null in the array instead of deleting it allows:
6798         * 1) Each instance to preserve its own unique-id, even after being destroyed
6799         *    and initialized again
6800         * 2) The unique-id to always correspond to an index in the array of medium-editor
6801         *    instances. Thus, we will be able to look at a contenteditable, and determine
6802         *    which instance it belongs to, by indexing into the global array.
6803         */
6804        win._mediumEditors[this.id] = null;
6805    }
6806
6807    function createElementsArray(selector, doc, filterEditorElements) {
6808        var elements = [];
6809
6810        if (!selector) {
6811            selector = [];
6812        }
6813        // If string, use as query selector
6814        if (typeof selector === 'string') {
6815            selector = doc.querySelectorAll(selector);
6816        }
6817        // If element, put into array
6818        if (MediumEditor.util.isElement(selector)) {
6819            selector = [selector];
6820        }
6821
6822        if (filterEditorElements) {
6823            // Remove elements that have already been initialized by the editor
6824            // selecotr might not be an array (ie NodeList) so use for loop
6825            for (var i = 0; i < selector.length; i++) {
6826                var el = selector[i];
6827                if (MediumEditor.util.isElement(el) &&
6828                    !el.getAttribute('data-medium-editor-element') &&
6829                    !el.getAttribute('medium-editor-textarea-id')) {
6830                    elements.push(el);
6831                }
6832            }
6833        } else {
6834            // Convert NodeList (or other array like object) into an array
6835            elements = Array.prototype.slice.apply(selector);
6836        }
6837
6838        return elements;
6839    }
6840
6841    function cleanupTextareaElement(element) {
6842        var textarea = element.parentNode.querySelector('textarea[medium-editor-textarea-id="' + element.getAttribute('medium-editor-textarea-id') + '"]');
6843        if (textarea) {
6844            // Un-hide the textarea
6845            textarea.classList.remove('medium-editor-hidden');
6846            textarea.removeAttribute('medium-editor-textarea-id');
6847        }
6848        if (element.parentNode) {
6849            element.parentNode.removeChild(element);
6850        }
6851    }
6852
6853    function setExtensionDefaults(extension, defaults) {
6854        Object.keys(defaults).forEach(function (prop) {
6855            if (extension[prop] === undefined) {
6856                extension[prop] = defaults[prop];
6857            }
6858        });
6859        return extension;
6860    }
6861
6862    function initExtension(extension, name, instance) {
6863        var extensionDefaults = {
6864            'window': instance.options.contentWindow,
6865            'document': instance.options.ownerDocument,
6866            'base': instance
6867        };
6868
6869        // Add default options into the extension
6870        extension = setExtensionDefaults(extension, extensionDefaults);
6871
6872        // Call init on the extension
6873        if (typeof extension.init === 'function') {
6874            extension.init();
6875        }
6876
6877        // Set extension name (if not already set)
6878        if (!extension.name) {
6879            extension.name = name;
6880        }
6881        return extension;
6882    }
6883
6884    function isToolbarEnabled() {
6885        return this.options.toolbar !== false;
6886    }
6887
6888    function isAnchorPreviewEnabled() {
6889        // If toolbar is disabled, don't add
6890        if (!isToolbarEnabled.call(this)) {
6891            return false;
6892        }
6893
6894        return this.options.anchorPreview !== false;
6895    }
6896
6897    function isPlaceholderEnabled() {
6898        return this.options.placeholder !== false;
6899    }
6900
6901    function isAutoLinkEnabled() {
6902        return this.options.autoLink !== false;
6903    }
6904
6905    function isImageDraggingEnabled() {
6906        return this.options.imageDragging !== false;
6907    }
6908
6909    function isKeyboardCommandsEnabled() {
6910        return this.options.keyboardCommands !== false;
6911    }
6912
6913    function shouldUseFileDraggingExtension() {
6914        // Since the file-dragging extension replaces the image-dragging extension,
6915        // we need to check if the user passed an overrided image-dragging extension.
6916        // If they have, to avoid breaking users, we won't use file-dragging extension.
6917        return !this.options.extensions['imageDragging'];
6918    }
6919
6920    function createContentEditable(textarea) {
6921        var div = this.options.ownerDocument.createElement('div'),
6922            now = Date.now(),
6923            uniqueId = 'medium-editor-' + now,
6924            atts = textarea.attributes;
6925
6926        // Some browsers can move pretty fast, since we're using a timestamp
6927        // to make a unique-id, ensure that the id is actually unique on the page
6928        while (this.options.ownerDocument.getElementById(uniqueId)) {
6929            now++;
6930            uniqueId = 'medium-editor-' + now;
6931        }
6932
6933        div.className = textarea.className;
6934        div.id = uniqueId;
6935        div.innerHTML = textarea.value;
6936
6937        textarea.setAttribute('medium-editor-textarea-id', uniqueId);
6938
6939        // re-create all attributes from the textearea to the new created div
6940        for (var i = 0, n = atts.length; i < n; i++) {
6941            // do not re-create existing attributes
6942            if (!div.hasAttribute(atts[i].nodeName)) {
6943                div.setAttribute(atts[i].nodeName, atts[i].nodeValue);
6944            }
6945        }
6946
6947        // If textarea has a form, listen for reset on the form to clear
6948        // the content of the created div
6949        if (textarea.form) {
6950            this.on(textarea.form, 'reset', function (event) {
6951                if (!event.defaultPrevented) {
6952                    this.resetContent(this.options.ownerDocument.getElementById(uniqueId));
6953                }
6954            }.bind(this));
6955        }
6956
6957        textarea.classList.add('medium-editor-hidden');
6958        textarea.parentNode.insertBefore(
6959            div,
6960            textarea
6961        );
6962
6963        return div;
6964    }
6965
6966    function initElement(element, editorId) {
6967        if (!element.getAttribute('data-medium-editor-element')) {
6968            if (element.nodeName.toLowerCase() === 'textarea') {
6969                element = createContentEditable.call(this, element);
6970
6971                // Make sure we only attach to editableInput once for <textarea> elements
6972                if (!this.instanceHandleEditableInput) {
6973                    this.instanceHandleEditableInput = handleEditableInput.bind(this);
6974                    this.subscribe('editableInput', this.instanceHandleEditableInput);
6975                }
6976            }
6977
6978            if (!this.options.disableEditing && !element.getAttribute('data-disable-editing')) {
6979                element.setAttribute('contentEditable', true);
6980                element.setAttribute('spellcheck', this.options.spellcheck);
6981            }
6982
6983            // Make sure we only attach to editableKeydownEnter once for disable-return options
6984            if (!this.instanceHandleEditableKeydownEnter) {
6985                if (element.getAttribute('data-disable-return') || element.getAttribute('data-disable-double-return')) {
6986                    this.instanceHandleEditableKeydownEnter = handleDisabledEnterKeydown.bind(this);
6987                    this.subscribe('editableKeydownEnter', this.instanceHandleEditableKeydownEnter);
6988                }
6989            }
6990
6991            // if we're not disabling return, add a handler to help handle cleanup
6992            // for certain cases when enter is pressed
6993            if (!this.options.disableReturn && !element.getAttribute('data-disable-return')) {
6994                this.on(element, 'keyup', handleKeyup.bind(this));
6995            }
6996
6997            var elementId = MediumEditor.util.guid();
6998
6999            element.setAttribute('data-medium-editor-element', true);
7000            element.classList.add('medium-editor-element');
7001            element.setAttribute('role', 'textbox');
7002            element.setAttribute('aria-multiline', true);
7003            element.setAttribute('data-medium-editor-editor-index', editorId);
7004            // TODO: Merge data-medium-editor-element and medium-editor-index attributes for 6.0.0
7005            // medium-editor-index is not named correctly anymore and can be re-purposed to signify
7006            // whether the element has been initialized or not
7007            element.setAttribute('medium-editor-index', elementId);
7008            initialContent[elementId] = element.innerHTML;
7009
7010            this.events.attachAllEventsToElement(element);
7011        }
7012
7013        return element;
7014    }
7015
7016    function attachHandlers() {
7017        // attach to tabs
7018        this.subscribe('editableKeydownTab', handleTabKeydown.bind(this));
7019
7020        // Bind keys which can create or destroy a block element: backspace, delete, return
7021        this.subscribe('editableKeydownDelete', handleBlockDeleteKeydowns.bind(this));
7022        this.subscribe('editableKeydownEnter', handleBlockDeleteKeydowns.bind(this));
7023
7024        // Bind double space event
7025        if (this.options.disableExtraSpaces) {
7026            this.subscribe('editableKeydownSpace', handleDisableExtraSpaces.bind(this));
7027        }
7028
7029        // Make sure we only attach to editableKeydownEnter once for disable-return options
7030        if (!this.instanceHandleEditableKeydownEnter) {
7031            // disabling return or double return
7032            if (this.options.disableReturn || this.options.disableDoubleReturn) {
7033                this.instanceHandleEditableKeydownEnter = handleDisabledEnterKeydown.bind(this);
7034                this.subscribe('editableKeydownEnter', this.instanceHandleEditableKeydownEnter);
7035            }
7036        }
7037    }
7038
7039    function initExtensions() {
7040
7041        this.extensions = [];
7042
7043        // Passed in extensions
7044        Object.keys(this.options.extensions).forEach(function (name) {
7045            // Always save the toolbar extension for last
7046            if (name !== 'toolbar' && this.options.extensions[name]) {
7047                this.extensions.push(initExtension(this.options.extensions[name], name, this));
7048            }
7049        }, this);
7050
7051        // 4 Cases for imageDragging + fileDragging extensons:
7052        //
7053        // 1. ImageDragging ON + No Custom Image Dragging Extension:
7054        //    * Use fileDragging extension (default options)
7055        // 2. ImageDragging OFF + No Custom Image Dragging Extension:
7056        //    * Use fileDragging extension w/ images turned off
7057        // 3. ImageDragging ON + Custom Image Dragging Extension:
7058        //    * Don't use fileDragging (could interfere with custom image dragging extension)
7059        // 4. ImageDragging OFF + Custom Image Dragging:
7060        //    * Don't use fileDragging (could interfere with custom image dragging extension)
7061        if (shouldUseFileDraggingExtension.call(this)) {
7062            var opts = this.options.fileDragging;
7063            if (!opts) {
7064                opts = {};
7065
7066                // Image is in the 'allowedTypes' list by default.
7067                // If imageDragging is off override the 'allowedTypes' list with an empty one
7068                if (!isImageDraggingEnabled.call(this)) {
7069                    opts.allowedTypes = [];
7070                }
7071            }
7072            this.addBuiltInExtension('fileDragging', opts);
7073        }
7074
7075        // Built-in extensions
7076        var builtIns = {
7077            paste: true,
7078            'anchor-preview': isAnchorPreviewEnabled.call(this),
7079            autoLink: isAutoLinkEnabled.call(this),
7080            keyboardCommands: isKeyboardCommandsEnabled.call(this),
7081            placeholder: isPlaceholderEnabled.call(this)
7082        };
7083        Object.keys(builtIns).forEach(function (name) {
7084            if (builtIns[name]) {
7085                this.addBuiltInExtension(name);
7086            }
7087        }, this);
7088
7089        // Users can pass in a custom toolbar extension
7090        // so check for that first and if it's not present
7091        // just create the default toolbar
7092        var toolbarExtension = this.options.extensions['toolbar'];
7093        if (!toolbarExtension && isToolbarEnabled.call(this)) {
7094            // Backwards compatability
7095            var toolbarOptions = MediumEditor.util.extend({}, this.options.toolbar, {
7096                allowMultiParagraphSelection: this.options.allowMultiParagraphSelection // deprecated
7097            });
7098            toolbarExtension = new MediumEditor.extensions.toolbar(toolbarOptions);
7099        }
7100
7101        // If the toolbar is not disabled, so we actually have an extension
7102        // initialize it and add it to the extensions array
7103        if (toolbarExtension) {
7104            this.extensions.push(initExtension(toolbarExtension, 'toolbar', this));
7105        }
7106    }
7107
7108    function mergeOptions(defaults, options) {
7109        var deprecatedProperties = [
7110            ['allowMultiParagraphSelection', 'toolbar.allowMultiParagraphSelection']
7111        ];
7112        // warn about using deprecated properties
7113        if (options) {
7114            deprecatedProperties.forEach(function (pair) {
7115                if (options.hasOwnProperty(pair[0]) && options[pair[0]] !== undefined) {
7116                    MediumEditor.util.deprecated(pair[0], pair[1], 'v6.0.0');
7117                }
7118            });
7119        }
7120
7121        return MediumEditor.util.defaults({}, options, defaults);
7122    }
7123
7124    function execActionInternal(action, opts) {
7125        /*jslint regexp: true*/
7126        var appendAction = /^append-(.+)$/gi,
7127            justifyAction = /justify([A-Za-z]*)$/g, /* Detecting if is justifyCenter|Right|Left */
7128            match,
7129            cmdValueArgument;
7130        /*jslint regexp: false*/
7131
7132        // Actions starting with 'append-' should attempt to format a block of text ('formatBlock') using a specific
7133        // type of block element (ie append-blockquote, append-h1, append-pre, etc.)
7134        match = appendAction.exec(action);
7135        if (match) {
7136            return MediumEditor.util.execFormatBlock(this.options.ownerDocument, match[1]);
7137        }
7138
7139        if (action === 'fontSize') {
7140            // TODO: Deprecate support for opts.size in 6.0.0
7141            if (opts.size) {
7142                MediumEditor.util.deprecated('.size option for fontSize command', '.value', '6.0.0');
7143            }
7144            cmdValueArgument = opts.value || opts.size;
7145            return this.options.ownerDocument.execCommand('fontSize', false, cmdValueArgument);
7146        }
7147
7148        if (action === 'fontName') {
7149            // TODO: Deprecate support for opts.name in 6.0.0
7150            if (opts.name) {
7151                MediumEditor.util.deprecated('.name option for fontName command', '.value', '6.0.0');
7152            }
7153            cmdValueArgument = opts.value || opts.name;
7154            return this.options.ownerDocument.execCommand('fontName', false, cmdValueArgument);
7155        }
7156
7157        if (action === 'createLink') {
7158            return this.createLink(opts);
7159        }
7160
7161        if (action === 'image') {
7162            var src = this.options.contentWindow.getSelection().toString().trim();
7163            return this.options.ownerDocument.execCommand('insertImage', false, src);
7164        }
7165
7166        if (action === 'html') {
7167            var html = this.options.contentWindow.getSelection().toString().trim();
7168            return MediumEditor.util.insertHTMLCommand(this.options.ownerDocument, html);
7169        }
7170
7171        /* Issue: https://github.com/yabwe/medium-editor/issues/595
7172         * If the action is to justify the text */
7173        if (justifyAction.exec(action)) {
7174            var result = this.options.ownerDocument.execCommand(action, false, null),
7175                parentNode = MediumEditor.selection.getSelectedParentElement(MediumEditor.selection.getSelectionRange(this.options.ownerDocument));
7176            if (parentNode) {
7177                cleanupJustifyDivFragments.call(this, MediumEditor.util.getTopBlockContainer(parentNode));
7178            }
7179
7180            return result;
7181        }
7182
7183        cmdValueArgument = opts && opts.value;
7184        return this.options.ownerDocument.execCommand(action, false, cmdValueArgument);
7185    }
7186
7187    /* If we've just justified text within a container block
7188     * Chrome may have removed <br> elements and instead wrapped lines in <div> elements
7189     * with a text-align property.  If so, we want to fix this
7190     */
7191    function cleanupJustifyDivFragments(blockContainer) {
7192        if (!blockContainer) {
7193            return;
7194        }
7195
7196        var textAlign,
7197            childDivs = Array.prototype.slice.call(blockContainer.childNodes).filter(function (element) {
7198                var isDiv = element.nodeName.toLowerCase() === 'div';
7199                if (isDiv && !textAlign) {
7200                    textAlign = element.style.textAlign;
7201                }
7202                return isDiv;
7203            });
7204
7205        /* If we found child <div> elements with text-align style attributes
7206         * we should fix this by:
7207         *
7208         * 1) Unwrapping each <div> which has a text-align style
7209         * 2) Insert a <br> element after each set of 'unwrapped' div children
7210         * 3) Set the text-align style of the parent block element
7211         */
7212        if (childDivs.length) {
7213            // Since we're mucking with the HTML, preserve selection
7214            this.saveSelection();
7215            childDivs.forEach(function (div) {
7216                if (div.style.textAlign === textAlign) {
7217                    var lastChild = div.lastChild;
7218                    if (lastChild) {
7219                        // Instead of a div, extract the child elements and add a <br>
7220                        MediumEditor.util.unwrap(div, this.options.ownerDocument);
7221                        var br = this.options.ownerDocument.createElement('BR');
7222                        lastChild.parentNode.insertBefore(br, lastChild.nextSibling);
7223                    }
7224                }
7225            }, this);
7226            blockContainer.style.textAlign = textAlign;
7227            // We're done, so restore selection
7228            this.restoreSelection();
7229        }
7230    }
7231
7232    var initialContent = {};
7233
7234    MediumEditor.prototype = {
7235        // NOT DOCUMENTED - exposed for backwards compatability
7236        init: function (elements, options) {
7237            this.options = mergeOptions.call(this, this.defaults, options);
7238            this.origElements = elements;
7239
7240            if (!this.options.elementsContainer) {
7241                this.options.elementsContainer = this.options.ownerDocument.body;
7242            }
7243
7244            return this.setup();
7245        },
7246
7247        setup: function () {
7248            if (this.isActive) {
7249                return;
7250            }
7251
7252            addToEditors.call(this, this.options.contentWindow);
7253            this.events = new MediumEditor.Events(this);
7254            this.elements = [];
7255
7256            // Call initialization helpers
7257            initExtensions.call(this);
7258            attachHandlers.call(this);
7259
7260            this.addElements(this.origElements);
7261
7262            this.isActive = true;
7263        },
7264
7265        destroy: function () {
7266            if (!this.isActive) {
7267                return;
7268            }
7269
7270            this.isActive = false;
7271
7272            this.extensions.forEach(function (extension) {
7273                if (typeof extension.destroy === 'function') {
7274                    extension.destroy();
7275                }
7276            }, this);
7277
7278            this.events.destroy();
7279
7280            this.elements.forEach(function (element, i) {
7281                // Call the getContent to set innerHTML, extensions can do some cleanup
7282                element.innerHTML = this.getContent(i);
7283
7284                // cleanup extra added attributes
7285                element.removeAttribute('contentEditable');
7286                element.removeAttribute('spellcheck');
7287                element.removeAttribute('data-medium-editor-element');
7288                element.classList.remove('medium-editor-element');
7289                element.removeAttribute('role');
7290                element.removeAttribute('aria-multiline');
7291                element.removeAttribute('medium-editor-index');
7292                element.removeAttribute('data-medium-editor-editor-index');
7293
7294                // Remove any elements created for textareas
7295                if (element.getAttribute('medium-editor-textarea-id')) {
7296                    cleanupTextareaElement(element);
7297                }
7298            }, this);
7299            this.elements = [];
7300            this.instanceHandleEditableKeydownEnter = null;
7301            this.instanceHandleEditableInput = null;
7302
7303            removeFromEditors.call(this, this.options.contentWindow);
7304        },
7305
7306        on: function (target, event, listener, useCapture) {
7307            this.events.attachDOMEvent(target, event, listener, useCapture);
7308
7309            return this;
7310        },
7311
7312        off: function (target, event, listener, useCapture) {
7313            this.events.detachDOMEvent(target, event, listener, useCapture);
7314
7315            return this;
7316        },
7317
7318        subscribe: function (event, listener) {
7319            this.events.attachCustomEvent(event, listener);
7320
7321            return this;
7322        },
7323
7324        unsubscribe: function (event, listener) {
7325            this.events.detachCustomEvent(event, listener);
7326
7327            return this;
7328        },
7329
7330        trigger: function (name, data, editable) {
7331            this.events.triggerCustomEvent(name, data, editable);
7332
7333            return this;
7334        },
7335
7336        delay: function (fn) {
7337            var self = this;
7338            return setTimeout(function () {
7339                if (self.isActive) {
7340                    fn();
7341                }
7342            }, this.options.delay);
7343        },
7344
7345        serialize: function () {
7346            var i,
7347                elementid,
7348                content = {},
7349                len = this.elements.length;
7350
7351            for (i = 0; i < len; i += 1) {
7352                elementid = (this.elements[i].id !== '') ? this.elements[i].id : 'element-' + i;
7353                content[elementid] = {
7354                    value: this.getContent(i)
7355                };
7356            }
7357            return content;
7358        },
7359
7360        getExtensionByName: function (name) {
7361            var extension;
7362            if (this.extensions && this.extensions.length) {
7363                this.extensions.some(function (ext) {
7364                    if (ext.name === name) {
7365                        extension = ext;
7366                        return true;
7367                    }
7368                    return false;
7369                });
7370            }
7371            return extension;
7372        },
7373
7374        /**
7375         * NOT DOCUMENTED - exposed as a helper for other extensions to use
7376         */
7377        addBuiltInExtension: function (name, opts) {
7378            var extension = this.getExtensionByName(name),
7379                merged;
7380            if (extension) {
7381                return extension;
7382            }
7383
7384            switch (name) {
7385                case 'anchor':
7386                    merged = MediumEditor.util.extend({}, this.options.anchor, opts);
7387                    extension = new MediumEditor.extensions.anchor(merged);
7388                    break;
7389                case 'anchor-preview':
7390                    extension = new MediumEditor.extensions.anchorPreview(this.options.anchorPreview);
7391                    break;
7392                case 'autoLink':
7393                    extension = new MediumEditor.extensions.autoLink();
7394                    break;
7395                case 'fileDragging':
7396                    extension = new MediumEditor.extensions.fileDragging(opts);
7397                    break;
7398                case 'fontname':
7399                    extension = new MediumEditor.extensions.fontName(this.options.fontName);
7400                    break;
7401                case 'fontsize':
7402                    extension = new MediumEditor.extensions.fontSize(opts);
7403                    break;
7404                case 'keyboardCommands':
7405                    extension = new MediumEditor.extensions.keyboardCommands(this.options.keyboardCommands);
7406                    break;
7407                case 'paste':
7408                    extension = new MediumEditor.extensions.paste(this.options.paste);
7409                    break;
7410                case 'placeholder':
7411                    extension = new MediumEditor.extensions.placeholder(this.options.placeholder);
7412                    break;
7413                default:
7414                    // All of the built-in buttons for MediumEditor are extensions
7415                    // so check to see if the extension we're creating is a built-in button
7416                    if (MediumEditor.extensions.button.isBuiltInButton(name)) {
7417                        if (opts) {
7418                            merged = MediumEditor.util.defaults({}, opts, MediumEditor.extensions.button.prototype.defaults[name]);
7419                            extension = new MediumEditor.extensions.button(merged);
7420                        } else {
7421                            extension = new MediumEditor.extensions.button(name);
7422                        }
7423                    }
7424            }
7425
7426            if (extension) {
7427                this.extensions.push(initExtension(extension, name, this));
7428            }
7429
7430            return extension;
7431        },
7432
7433        stopSelectionUpdates: function () {
7434            this.preventSelectionUpdates = true;
7435        },
7436
7437        startSelectionUpdates: function () {
7438            this.preventSelectionUpdates = false;
7439        },
7440
7441        checkSelection: function () {
7442            var toolbar = this.getExtensionByName('toolbar');
7443            if (toolbar) {
7444                toolbar.checkState();
7445            }
7446            return this;
7447        },
7448
7449        // Wrapper around document.queryCommandState for checking whether an action has already
7450        // been applied to the current selection
7451        queryCommandState: function (action) {
7452            var fullAction = /^full-(.+)$/gi,
7453                match,
7454                queryState = null;
7455
7456            // Actions starting with 'full-' need to be modified since this is a medium-editor concept
7457            match = fullAction.exec(action);
7458            if (match) {
7459                action = match[1];
7460            }
7461
7462            try {
7463                queryState = this.options.ownerDocument.queryCommandState(action);
7464            } catch (exc) {
7465                queryState = null;
7466            }
7467
7468            return queryState;
7469        },
7470
7471        execAction: function (action, opts) {
7472            /*jslint regexp: true*/
7473            var fullAction = /^full-(.+)$/gi,
7474                match,
7475                result;
7476            /*jslint regexp: false*/
7477
7478            // Actions starting with 'full-' should be applied to to the entire contents of the editable element
7479            // (ie full-bold, full-append-pre, etc.)
7480            match = fullAction.exec(action);
7481            if (match) {
7482                // Store the current selection to be restored after applying the action
7483                this.saveSelection();
7484                // Select all of the contents before calling the action
7485                this.selectAllContents();
7486                result = execActionInternal.call(this, match[1], opts);
7487                // Restore the previous selection
7488                this.restoreSelection();
7489            } else {
7490                result = execActionInternal.call(this, action, opts);
7491            }
7492
7493            // do some DOM clean-up for known browser issues after the action
7494            if (action === 'insertunorderedlist' || action === 'insertorderedlist') {
7495                MediumEditor.util.cleanListDOM(this.options.ownerDocument, this.getSelectedParentElement());
7496            }
7497
7498            this.checkSelection();
7499            return result;
7500        },
7501
7502        getSelectedParentElement: function (range) {
7503            if (range === undefined) {
7504                range = this.options.contentWindow.getSelection().getRangeAt(0);
7505            }
7506            return MediumEditor.selection.getSelectedParentElement(range);
7507        },
7508
7509        selectAllContents: function () {
7510            var currNode = MediumEditor.selection.getSelectionElement(this.options.contentWindow);
7511
7512            if (currNode) {
7513                // Move to the lowest descendant node that still selects all of the contents
7514                while (currNode.children.length === 1) {
7515                    currNode = currNode.children[0];
7516                }
7517
7518                this.selectElement(currNode);
7519            }
7520        },
7521
7522        selectElement: function (element) {
7523            MediumEditor.selection.selectNode(element, this.options.ownerDocument);
7524
7525            var selElement = MediumEditor.selection.getSelectionElement(this.options.contentWindow);
7526            if (selElement) {
7527                this.events.focusElement(selElement);
7528            }
7529        },
7530
7531        getFocusedElement: function () {
7532            var focused;
7533            this.elements.some(function (element) {
7534                // Find the element that has focus
7535                if (!focused && element.getAttribute('data-medium-focused')) {
7536                    focused = element;
7537                }
7538
7539                // bail if we found the element that had focus
7540                return !!focused;
7541            }, this);
7542
7543            return focused;
7544        },
7545
7546        // Export the state of the selection in respect to one of this
7547        // instance of MediumEditor's elements
7548        exportSelection: function () {
7549            var selectionElement = MediumEditor.selection.getSelectionElement(this.options.contentWindow),
7550                editableElementIndex = this.elements.indexOf(selectionElement),
7551                selectionState = null;
7552
7553            if (editableElementIndex >= 0) {
7554                selectionState = MediumEditor.selection.exportSelection(selectionElement, this.options.ownerDocument);
7555            }
7556
7557            if (selectionState !== null && editableElementIndex !== 0) {
7558                selectionState.editableElementIndex = editableElementIndex;
7559            }
7560
7561            return selectionState;
7562        },
7563
7564        saveSelection: function () {
7565            this.selectionState = this.exportSelection();
7566        },
7567
7568        // Restore a selection based on a selectionState returned by a call
7569        // to MediumEditor.exportSelection
7570        importSelection: function (selectionState, favorLaterSelectionAnchor) {
7571            if (!selectionState) {
7572                return;
7573            }
7574
7575            var editableElement = this.elements[selectionState.editableElementIndex || 0];
7576            MediumEditor.selection.importSelection(selectionState, editableElement, this.options.ownerDocument, favorLaterSelectionAnchor);
7577        },
7578
7579        restoreSelection: function () {
7580            this.importSelection(this.selectionState);
7581        },
7582
7583        createLink: function (opts) {
7584            var currentEditor = MediumEditor.selection.getSelectionElement(this.options.contentWindow),
7585                customEvent = {},
7586                targetUrl;
7587
7588            // Make sure the selection is within an element this editor is tracking
7589            if (this.elements.indexOf(currentEditor) === -1) {
7590                return;
7591            }
7592
7593            try {
7594                this.events.disableCustomEvent('editableInput');
7595                // TODO: Deprecate support for opts.url in 6.0.0
7596                if (opts.url) {
7597                    MediumEditor.util.deprecated('.url option for createLink', '.value', '6.0.0');
7598                }
7599                targetUrl = opts.url || opts.value;
7600                if (targetUrl && targetUrl.trim().length > 0) {
7601                    var currentSelection = this.options.contentWindow.getSelection();
7602                    if (currentSelection) {
7603                        var currRange = currentSelection.getRangeAt(0),
7604                            commonAncestorContainer = currRange.commonAncestorContainer,
7605                            exportedSelection,
7606                            startContainerParentElement,
7607                            endContainerParentElement,
7608                            textNodes;
7609
7610                        // If the selection is contained within a single text node
7611                        // and the selection starts at the beginning of the text node,
7612                        // MSIE still says the startContainer is the parent of the text node.
7613                        // If the selection is contained within a single text node, we
7614                        // want to just use the default browser 'createLink', so we need
7615                        // to account for this case and adjust the commonAncestorContainer accordingly
7616                        if (currRange.endContainer.nodeType === 3 &&
7617                            currRange.startContainer.nodeType !== 3 &&
7618                            currRange.startOffset === 0 &&
7619                            currRange.startContainer.firstChild === currRange.endContainer) {
7620                            commonAncestorContainer = currRange.endContainer;
7621                        }
7622
7623                        startContainerParentElement = MediumEditor.util.getClosestBlockContainer(currRange.startContainer);
7624                        endContainerParentElement = MediumEditor.util.getClosestBlockContainer(currRange.endContainer);
7625
7626                        // If the selection is not contained within a single text node
7627                        // but the selection is contained within the same block element
7628                        // we want to make sure we create a single link, and not multiple links
7629                        // which can happen with the built in browser functionality
7630                        if (commonAncestorContainer.nodeType !== 3 && commonAncestorContainer.textContent.length !== 0 && startContainerParentElement === endContainerParentElement) {
7631                            var parentElement = (startContainerParentElement || currentEditor),
7632                                fragment = this.options.ownerDocument.createDocumentFragment();
7633
7634                            // since we are going to create a link from an extracted text,
7635                            // be sure that if we are updating a link, we won't let an empty link behind (see #754)
7636                            // (Workaroung for Chrome)
7637                            this.execAction('unlink');
7638
7639                            exportedSelection = this.exportSelection();
7640                            fragment.appendChild(parentElement.cloneNode(true));
7641
7642                            if (currentEditor === parentElement) {
7643                                // We have to avoid the editor itself being wiped out when it's the only block element,
7644                                // as our reference inside this.elements gets detached from the page when insertHTML runs.
7645                                // If we just use [parentElement, 0] and [parentElement, parentElement.childNodes.length]
7646                                // as the range boundaries, this happens whenever parentElement === currentEditor.
7647                                // The tradeoff to this workaround is that a orphaned tag can sometimes be left behind at
7648                                // the end of the editor's content.
7649                                // In Gecko:
7650                                // as an empty <strong></strong> if parentElement.lastChild is a <strong> tag.
7651                                // In WebKit:
7652                                // an invented <br /> tag at the end in the same situation
7653                                MediumEditor.selection.select(
7654                                    this.options.ownerDocument,
7655                                    parentElement.firstChild,
7656                                    0,
7657                                    parentElement.lastChild,
7658                                    parentElement.lastChild.nodeType === 3 ?
7659                                    parentElement.lastChild.nodeValue.length : parentElement.lastChild.childNodes.length
7660                                );
7661                            } else {
7662                                MediumEditor.selection.select(
7663                                    this.options.ownerDocument,
7664                                    parentElement,
7665                                    0,
7666                                    parentElement,
7667                                    parentElement.childNodes.length
7668                                );
7669                            }
7670
7671                            var modifiedExportedSelection = this.exportSelection();
7672
7673                            textNodes = MediumEditor.util.findOrCreateMatchingTextNodes(
7674                                this.options.ownerDocument,
7675                                fragment,
7676                                {
7677                                    start: exportedSelection.start - modifiedExportedSelection.start,
7678                                    end: exportedSelection.end - modifiedExportedSelection.start,
7679                                    editableElementIndex: exportedSelection.editableElementIndex
7680                                }
7681                            );
7682                            // If textNodes are not present, when changing link on images
7683                            // ex: <a><img src="http://image.test.com"></a>, change fragment to currRange.startContainer
7684                            // and set textNodes array to [imageElement, imageElement]
7685                            if (textNodes.length === 0) {
7686                                fragment = this.options.ownerDocument.createDocumentFragment();
7687                                fragment.appendChild(commonAncestorContainer.cloneNode(true));
7688                                textNodes = [fragment.firstChild.firstChild, fragment.firstChild.lastChild];
7689                            }
7690
7691                            // Creates the link in the document fragment
7692                            MediumEditor.util.createLink(this.options.ownerDocument, textNodes, targetUrl.trim());
7693
7694                            // Chrome trims the leading whitespaces when inserting HTML, which messes up restoring the selection.
7695                            var leadingWhitespacesCount = (fragment.firstChild.innerHTML.match(/^\s+/) || [''])[0].length;
7696
7697                            // Now move the created link back into the original document in a way to preserve undo/redo history
7698                            MediumEditor.util.insertHTMLCommand(this.options.ownerDocument, fragment.firstChild.innerHTML.replace(/^\s+/, ''));
7699                            exportedSelection.start -= leadingWhitespacesCount;
7700                            exportedSelection.end -= leadingWhitespacesCount;
7701
7702                            this.importSelection(exportedSelection);
7703                        } else {
7704                            this.options.ownerDocument.execCommand('createLink', false, targetUrl);
7705                        }
7706
7707                        if (this.options.targetBlank || opts.target === '_blank') {
7708                            MediumEditor.util.setTargetBlank(MediumEditor.selection.getSelectionStart(this.options.ownerDocument), targetUrl);
7709                        } else {
7710                            MediumEditor.util.removeTargetBlank(MediumEditor.selection.getSelectionStart(this.options.ownerDocument), targetUrl);
7711                        }
7712
7713                        if (opts.buttonClass) {
7714                            MediumEditor.util.addClassToAnchors(MediumEditor.selection.getSelectionStart(this.options.ownerDocument), opts.buttonClass);
7715                        }
7716                    }
7717                }
7718                // Fire input event for backwards compatibility if anyone was listening directly to the DOM input event
7719                if (this.options.targetBlank || opts.target === '_blank' || opts.buttonClass) {
7720                    customEvent = this.options.ownerDocument.createEvent('HTMLEvents');
7721                    customEvent.initEvent('input', true, true, this.options.contentWindow);
7722                    for (var i = 0, len = this.elements.length; i < len; i += 1) {
7723                        this.elements[i].dispatchEvent(customEvent);
7724                    }
7725                }
7726            } finally {
7727                this.events.enableCustomEvent('editableInput');
7728            }
7729            // Fire our custom editableInput event
7730            this.events.triggerCustomEvent('editableInput', customEvent, currentEditor);
7731        },
7732
7733        cleanPaste: function (text) {
7734            this.getExtensionByName('paste').cleanPaste(text);
7735        },
7736
7737        pasteHTML: function (html, options) {
7738            this.getExtensionByName('paste').pasteHTML(html, options);
7739        },
7740
7741        setContent: function (html, index) {
7742            index = index || 0;
7743
7744            if (this.elements[index]) {
7745                var target = this.elements[index];
7746
7747                this.extensions.forEach(function (extension) {
7748                    if (typeof extension.setContent === 'function') {
7749                        html = extension.setContent(html);
7750                    }
7751                }, this);
7752
7753                target.innerHTML = html;
7754
7755                this.checkContentChanged(target);
7756            }
7757        },
7758
7759        getContent: function (index) {
7760            index = index || 0;
7761
7762            if (this.elements[index]) {
7763                var html = this.elements[index].innerHTML.trim();
7764
7765                this.extensions.forEach(function (extension) {
7766                    if (typeof extension.getContent === 'function') {
7767                        html = extension.getContent(html);
7768                    }
7769                }, this);
7770
7771                return html;
7772            }
7773
7774            return null;
7775        },
7776
7777        checkContentChanged: function (editable) {
7778            editable = editable || MediumEditor.selection.getSelectionElement(this.options.contentWindow);
7779            this.events.updateInput(editable, { target: editable, currentTarget: editable });
7780        },
7781
7782        resetContent: function (element) {
7783            // For all elements that exist in the this.elements array, we can assume:
7784            // - Its initial content has been set in the initialContent object
7785            // - It has a medium-editor-index attribute which is the key value in the initialContent object
7786
7787            if (element) {
7788                var index = this.elements.indexOf(element);
7789                if (index !== -1) {
7790                    this.setContent(initialContent[element.getAttribute('medium-editor-index')], index);
7791                }
7792                return;
7793            }
7794
vendor: 4,141 bytes, lines 7795-7895
7795            this.elements.forEach(function (el, idx) {
7796                this.setContent(initialContent[el.getAttribute('medium-editor-index')], idx);
7797            }, this);
7798        },
7799
7800        addElements: function (selector) {
7801            // Convert elements into an array
7802            var elements = createElementsArray(selector, this.options.ownerDocument, true);
7803
7804            // Do we have elements to add now?
7805            if (elements.length === 0) {
7806                return false;
7807            }
7808
7809            elements.forEach(function (element) {
7810                // Initialize all new elements (we check that in those functions don't worry)
7811                element = initElement.call(this, element, this.id);
7812
7813                // Add new elements to our internal elements array
7814                this.elements.push(element);
7815
7816                this.setContent(element.innerHTML, this.elements.length - 1);
7817
7818                // Trigger event so extensions can know when an element has been added
7819                this.trigger('addElement', { target: element, currentTarget: element }, element);
7820            }, this);
7821        },
7822
7823        removeElements: function (selector) {
7824            // Convert elements into an array
7825            var elements = createElementsArray(selector, this.options.ownerDocument),
7826                toRemove = elements.map(function (el) {
7827                    // For textareas, make sure we're looking at the editor div and not the textarea itself
7828                    if (el.getAttribute('medium-editor-textarea-id') && el.parentNode) {
7829                        return el.parentNode.querySelector('div[medium-editor-textarea-id="' + el.getAttribute('medium-editor-textarea-id') + '"]');
7830                    } else {
7831                        return el;
7832                    }
7833                });
7834
7835            this.elements = this.elements.filter(function (element) {
7836                // If this is an element we want to remove
7837                if (toRemove.indexOf(element) !== -1) {
7838                    this.events.cleanupElement(element);
7839                    if (element.getAttribute('medium-editor-textarea-id')) {
7840                        cleanupTextareaElement(element);
7841                    }
7842                    // Trigger event so extensions can clean-up elements that are being removed
7843                    this.trigger('removeElement', { target: element, currentTarget: element }, element);
7844                    return false;
7845                }
7846                return true;
7847            }, this);
7848        }
7849    };
7850
7851    MediumEditor.getEditorFromElement = function (element) {
7852        var index = element.getAttribute('data-medium-editor-editor-index'),
7853            win = element && element.ownerDocument && (element.ownerDocument.defaultView || element.ownerDocument.parentWindow);
7854        if (win && win._mediumEditors && win._mediumEditors[index]) {
7855            return win._mediumEditors[index];
7856        }
7857        return null;
7858    };
7859}());
7860
7861(function () {
7862    // summary: The default options hash used by the Editor
7863
7864    MediumEditor.prototype.defaults = {
7865        activeButtonClass: 'medium-editor-button-active',
7866        buttonLabels: false,
7867        delay: 0,
7868        disableReturn: false,
7869        disableDoubleReturn: false,
7870        disableExtraSpaces: false,
7871        disableEditing: false,
7872        autoLink: false,
7873        elementsContainer: false,
7874        contentWindow: window,
7875        ownerDocument: document,
7876        targetBlank: false,
7877        extensions: {},
7878        spellcheck: true
7879    };
7880})();
7881
7882MediumEditor.parseVersionString = function (release) {
7883    var split = release.split('-'),
7884        version = split[0].split('.'),
7885        preRelease = (split.length > 1) ? split[1] : '';
7886    return {
7887        major: parseInt(version[0], 10),
7888        minor: parseInt(version[1], 10),
7889        revision: parseInt(version[2], 10),
7890        preRelease: preRelease,
7891        toString: function () {
7892            return [version[0], version[1], version[2]].join('.') + (preRelease ? '-' + preRelease : '');
7893        }
7894    };
7895};
7896
7897MediumEditor.version = MediumEditor.parseVersionString.call(this, ({
7898    // grunt-bump looks for this:
7899    'version': '5.23.1'
7900}).version);
7901
7902    return MediumEditor;
7903}()));

Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.